В 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, нужно выполнить несколько условий:
- Все приложения должны иметь один и тот же bundle seed ID.
(картинка отсюда) - Все приложения должны иметь полный bundle ID (no wildcard — без групповых ID, без * в конце).
- Все приложения должны иметь доступ к той security-group, в которую пишутся данные. Для этого нужно в Entitlements.plist для каждого приложения создать список групп. Он может выглядеть следующим образом:
Как видим, тут есть поле keychain-access-groups. В нем есть 2 значения. Рассмотрим их по очереди:
- $(AppIdentifierPrefix) — вместо этого значения при компиляции подставится bundle seed ID. Как мы помним, он должен быть одинаковым для всех приложений, которые хотят иметь общий доступ к keychain.
- $(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
.
На этом экране можно выделить какой-нибудь ключ и удалить его, или же удалить все ключи сразу.
Для использования инструмента необходимо следующее:
- Соответствующий provisioning profile с bundle seed ID равным bundle seed ID приложения, которое сохраняет данные в keychain;
- Группа, прописанная в ZGKConfig.h, должна совпадать с группой редактируемого приложения.
Работающий проект можно взять ОТСЮДА.
ВЫВОД:
- Keychain — это отличное место для хранения конфиденциальной информации. Все, что там хранится, зашифровано.
- Keychain, судя по всему, организован в виде базы данных, и для получения данных оттуда или записи в него нужно формировать запросы, схожие с SQL.
- Через keychain можно организовывать общий доступ к паролям, покупкам и другим конфиденциальным данным, используя keychain-security-group.
Материал предоставлен компанией Softeq Development, FLLC
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: