В iOS (начиная с версии 2.0) и в MacOS (начиная c версии 8.6) появилась возможность хранить пароли, приватные ключи, сертификаты и прочие конфеденциальные данные в особом защищенном месте. Это место называется Keychain (Связка ключей).

Так для чего же можно использовать Keychain? Рассмотрим пару случаев.

Допустим, у вас есть бесплатная версия приложения и ее можно обновить до платной с помощью покупки внутри приложения (in-app purchase). Где хранить маркер покупки? В UserDefaults? На устройстве это просто текстовый файл, который открывается блокнотом на взломанном устройстве и в который можно записать все что угодно (так легко подбираются «читы» к игрушкам, своеобразный ArtMoney). Значит, UserDefaults не подходит. А подходит keychain. В него так просто не влезешь.

Или, допустим, вы выпустили несколько хорошо продающихся приложений и все они используют один сервер с одним и тем же логином и паролем. Как сделать так, чтобы пароль хранился в одном, доступном всем вашим приложениям, месте? Все приложения в iOS запускаются в своем sandbox («песочнице»). Соответственно, просто создать общую память нельзя. Но это можно сделать с помощью keychain. В версии 3.2 появились группы доступа keychain-access-groups.

Теперь подробнее о keychain.

Если просто сесть читать про keychain на apple.com, то создается впечатление, что они чего-то там сильно намудрили. Сделали что-то простое чем-то очень сложным. Что нам надо? Всего-то записать логин и пароль. Лучше даже в виде словарика: ключ — логин, значение — пароль. Идеально что-то вроде следующего варианта:

-(void)savePassword:(NSString*)pass forUserName:(NSString*)name;

Но там нет такого и подавно. Вместо этого — 10 страниц описаний и кода. Но если вчитаться в эти 10 страниц, становится немного понятно, что же там на самом деле. А на самом деле keychain — это база данных. Т.е. попросту таблица с записями. И в ней много столбцов с непонятными универсальными названиями. Следовательно, чтобы прочитать что-нибудь из этой базы, нужно сформировать запрос к ней. Вот пример запроса из Keychain Services Programming Guide:

         // Set up the keychain search dictionary:

genericPasswordQuery = [[NSMutableDictionary alloc] init];
// This keychain item is a generic password.
[genericPasswordQuery setObject:(id)kSecClassGenericPassword
forKey:(id)kSecClass];
// The kSecAttrGeneric attribute is used to store a unique string that is used
// to easily identify and find this keychain item. The string is first
// converted to an NSData object:
NSData *keychainItemID = [NSData dataWithBytes:kKeychainItemIdentifier
length:strlen((const char *)kKeychainItemIdentifier)];
[genericPasswordQuery setObject:keychainItemID forKey:(id)kSecAttrGeneric];
// Return the attributes of the first match only:
[genericPasswordQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
// Return the attributes of the keychain item (the password is
// acquired in the secItemFormatToDictionary: method):
[genericPasswordQuery setObject:(id)kCFBooleanTrue
forKey:(id)kSecReturnAttributes];
//Initialize the dictionary used to hold return data from the keychain:
NSMutableDictionary *outDictionary = nil;
// If the keychain item exists, return the attributes of the item:
keychainErr = SecItemCopyMatching((CFDictionaryRef)genericPasswordQuery,
(CFTypeRef *)&outDictionary);
if (keychainErr == noErr) {

В этом запросе из keychain, доступного данному приложению, выбираются записи с типом kSecClassGenericPassword, у которых в поле kSecAttrGeneric записано значение строки identifier. Далее в запросе задаются параметры самого запроса — размер возвращаемой выборки (kSecMatchLimit) и в каком формате возвращать поля из таблицы в результате запроса (kSecReturnAttributes). Далее, как видим, в функцию SecItemCopyMatching передается запрос и указатель на структуру, куда нужно записывать результаты. Т.е. в общем это похоже на обычный запрос к базе данных.

Данные в keychain записываются и выбираются в виде item. Item — это, по сути, запись в базе. Записи могут быть разных типов, и, в зависимости от типа, у записи меняется набор полей, которые она может хранить (в продолжение аналогии с базой данных — изменяется набор столбцов в таблице). Записи могут быть следующих типов:

  •  kSecClassGenericPassword;
  •  kSecClassInternetPassword;
  •  kSecClassCertificate;
  •  kSecClassKey;
  •  kSecClassIdentity;

Для каждого класса меняется набор полей, например для kSecClassGenericPassword это будут поля:

  •  kSecAttrAccessible;
  •  kSecAttrAccessGroup;
  •  kSecAttrCreationDate;
  •  kSecAttrModificationDate;
  •  kSecAttrDescription;
  •  kSecAttrComment;
  •  kSecAttrCreator;
  •  kSecAttrType;
  •  kSecAttrLabel;
  •  kSecAttrIsInvisible;
  •  kSecAttrIsNegative;
  •  kSecAttrAccount;
  •  kSecAttrService;
  •  kSecAttrGeneric.

О типах данных для этих полей и о том, что можно в них хранить, можно прочитать здесь.

Обычно для запроса к keychain достаточно следующих атрибутов:

  • Класс записи (kSecClassGenericPassword, kSecClassInternetPassword...), которую нужно искать.

  • Один или два атрибута, которые нужно искать, — например kSecAttrLabel или kSecAttrService.

  • Тип возвращаемого значения (например dictionary или persistent reference).

Например, если нужно найти в keychain пароль для App Store-аккаунта с именем пользователя “ImaUser”, то можно использовать следующий запрос для SecItemCopyMatching:

Тип ключа Ключ Значение
Item class kSecClass kSecClassGenericPassword
Attribute kSecAttrAccount "ImaUser"
Attribute kSecAttrService "Apple Store"
Search attribute kSecMatchCaseInsensitive kCFBooleanTrue
Return type kSecReturnData kCFBooleanTrue

Этот запрос вернет только пароль. Если нужны остальные атрибуты этой записи, например дата создания, то нужно добавить ключ kSecReturnAttributes со значением kCFBooleanTrue.

Возникает вопрос: в каком классе что хранить? Я все время применяю kSecClassGenericPassword. Возможно, для хранения сертификатов и других вещей понадобятся и другие классы.

А теперь поговорим о группах доступа. Как уже я писал выше, группы доступа можно использовать для совместного доступа нескольких приложений к одной и той же записи в keychain. Например, один и тот же пароль для доступа к аккаунту на сервере или приложения, которые шарят между собой покупки. Или, например, есть две версии приложения — платная и бесплатная. Бесплатная версия предоставляет только ограниченный набор функционала, а в платной доступен полный набор. Если установлена платная, то бесплатная тоже должна предоставлять полный набор функционала. Это можно сделать через keychain. Для того, чтобы иметь общие записи в keychain, нужно выполнить несколько условий:

  1. Все приложения должны иметь один и тот же bundle seed ID.



    (картинка отсюда)
  2. Все приложения должны иметь полный bundle ID (no wildcard — без групповых ID, без * в конце).
  3. Все приложения должны иметь доступ к той security-group, в которую пишутся данные. Для этого нужно в Entitlements.plist для каждого приложения создать список групп. Он может выглядеть следующим образом:

Как видим, тут есть поле keychain-access-groups. В нем есть 2 значения. Рассмотрим их по очереди:

  1. $(AppIdentifierPrefix) — вместо этого значения при компиляции подставится bundle seed ID. Как мы помним, он должен быть одинаковым для всех приложений, которые хотят иметь общий доступ к keychain.
  2. $(CFBundleIdentifier) — идентификатор приложения, обычно в виде reversed DNS notation (com.yourcompany.yourappname).

Таким образом, у нас есть две группы. Первая в списке группа будет уникальной для приложения — она же является группой по умолчанию. Т.е если при записи в keychain мы не укажем группу (kSecAttrAccessGroup), то будет использована первая группа из файла Entitlements.plist (имя файла задается в настройках билда). Вторую группу com.someApp можно использовать как общую, и, соответственно, при записи в keychain нужно указать ее — так, как это сделано в следующем примере запроса:

NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass,

kSecAttrAccount,
kSecAttrService,
kSecAttrAccessGroup, nil] autorelease];
NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword,
username,
serviceName,
securityGroup, nil] autorelease];
NSMutableDictionary *query = [[[NSMutableDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];

Далее рассмотрим утилиту для отладки  keychain — она будет отображать содержимое keychain для заданной группы. С ее помощью можно просматривать содержимое и удалять записи.

 

На первом экране выводится список групп, который задается в файлике ZGKConfig.h.

После выбора группы программа вычитывает содержимое keychain, доступное из этой группы, и отображает его в виде таблицы ключ-значение. Программа считает, что все элементы (item) в этой группе принадлежат классу genericPassword. В качестве ключа используется атрибут kSecAttrAccount.

На этом экране можно выделить какой-нибудь ключ и удалить его, или же удалить все ключи сразу.

Для использования инструмента необходимо следующее:

  1. Соответствующий provisioning profile с bundle seed ID равным bundle seed ID приложения, которое сохраняет данные в keychain;
  2. Группа, прописанная в ZGKConfig.h, должна совпадать с группой редактируемого приложения.

Работающий проект можно взять ОТСЮДА.

ВЫВОД:

  1. Keychain — это отличное место для хранения конфиденциальной информации. Все, что там хранится, зашифровано.
  2. Keychain, судя по всему, организован в виде базы данных, и для получения данных оттуда или записи в него нужно формировать запросы, схожие с SQL.
  3. Через keychain можно организовывать общий доступ к паролям, покупкам и другим конфиденциальным данным, используя keychain-security-group.

Материал предоставлен компанией Softeq Development, FLLC

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *