iOS. Приемы программирования Нахавандипур Вандад
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: keyToSearchFor,
};
Затем сделаем запрос к этому словарю и проверим, находится ли интересующий нас элемент в связке ключей:
OSStatus found = SecItemCopyMatching((__bridge CFDictionaryRef)query,
NULL);
Можно и не проверять наличие значения перед тем, как его обновлять. Вполне допустимо просто попытаться обновить значение. Если же его не существует, функция SecItemUpdate вернет значение errSecItemNotFound. Выбор заключается в том, проводить ли поиск в связке ключей самостоятельно или перепоручить эту задачу SecItemUpdate.
Если эта функция вернет значение errSecSuccess, вы будете знать, что интересовавшее вас значение уже обновлено. Обратите внимание: в качестве второго параметра мы передали NULL. Дело в том, что мы не собираемся получать из связки ключей старое значение. Мы просто хотим определить, существует ли значение, а сделать это можем, только проверив возвращаемое значение функции. Если возвращаемое значение равно errSecSuccess, делаем вывод, что значение уже было сохранено и может быть обновлено. Обновлять значение мы будем вот так:
NSData *newData = [@"Mark Tremonti"
dataUsingEncoding: NSUTF8StringEncoding];
NSDictionary *update = @{
(__bridge id)kSecValueData: newData,
};
OSStatus updated = SecItemUpdate((__bridge CFDictionaryRef)query,
(__bridge CFDictionaryRef)update);
if (updated == errSecSuccess){
NSLog(@"Successfully updated the existing value");
} else {
NSLog(@"Failed to update the value. Error = %ld", (long)updated);
}
Обновляющий словарь, который мы передаем функции SecItemUpdate в качестве второго параметра, может содержать больше ключей чем один ключ kSecValueData, использованный в нашем примере. На самом деле этот словарь может содержать обновления для любого имеющегося элемента. Например, если вы хотите добавить комментарий к имеющемуся значению (комментарий — это строка), то можете выполнить обновление следующим образом:
#import «AppDelegate.h»
#import <Security/Security.h>
@implementation AppDelegate
— (void) readExistingValue{
NSString *keyToSearchFor = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: keyToSearchFor,
(__bridge id)kSecReturnAttributes: (__bridge id)kCFBooleanTrue,
};
CFDictionaryRef cfAttributes = NULL;
OSStatus found = SecItemCopyMatching((__bridge CFDictionaryRef)query,
(CFTypeRef *)&cfAttributes);
if (found == errSecSuccess){
NSDictionary *attributes =
(__bridge_transfer NSDictionary *)cfAttributes;
NSString *comments = attributes[(__bridge id)kSecAttrComment];
NSLog(@"Comments = %@", comments);
} else {
NSLog(@"Error happened with code: %ld", (long)found);
}
}
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *keyToSearchFor = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: keyToSearchFor,
};
OSStatus found = SecItemCopyMatching((__bridge CFDictionaryRef)query,
NULL);
if (found == errSecSuccess){
NSData *newData = [@"Mark Tremonti"
dataUsingEncoding: NSUTF8StringEncoding];
NSDictionary *update = @{
(__bridge id)kSecValueData: newData,
(__bridge id)kSecAttrComment: @"My Comments",
};
OSStatus updated = SecItemUpdate((__bridge CFDictionaryRef)query,
(__bridge CFDictionaryRef)update);
if (updated == errSecSuccess){
[self readExistingValue];
} else {
NSLog(@"Failed to update the value. Error = %ld", (long)updated);
}
} else {
NSLog(@"Error happened with code: %ld", (long)found);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
В этом примере важнее всего отметить, что мы включили в обновляющий словарь ключ kSecAttrComment. Как только обновление будет выполнено, мы считаем комментарий с помощью того самого метода считывания, который изучили в разделе 8.3.
См. также
Разделы 8.2 и 8.3.
8.5. Удаление значений из связки ключей
Постановка задачи
Требуется удалить элемент из связки ключей.
Решение
Воспользуйтесь функцией SecItemDelete.
Обсуждение
В разделе 8.2 мы научились сохранять значения в связке ключей. Для удаления этих значений потребуется использовать функцию SecItemDelete. Эта функция принимает всего один параметр: словарь типа CFDictionaryRef. Можно взять обычный словарь и преобразовать его в экземпляр CFDictionaryRef с помощью мостика, как мы поступали в других разделах этой главы. Словарь, передаваемый этому методу, должен содержать следующие ключи:
• kSecClass — тип элемента, который вы собираетесь удалить, например kSecClassGenericPassword;
• kSecAttrService — сервис, к которому привязан элемент. Сохраняя элемент, вы подбираете для него сервис, и этот же сервис вы должны указать здесь. Так, в предыдущих примерах мы задавали в качестве значения этого ключа идентификатор пакета нашего приложения. Если вы поступали так же, то просто задайте идентификатор пакета приложения в качестве значения этого ключа;
• kSecAttrAccount — здесь указывается ключ, который должен быть удален.
Если вы выполнили все указания, приведенные в разделе 8.2, то на данном этапе связка ключей имеет обобщенный пароль (kSecClassGenericPassword) с именем сервиса (kSecAttrService), равным идентификатору пакета приложения, а также имеет ключ (kSecAttrAccount), равный Full Name. Вот что нужно сделать, чтобы удалить этот ключ:
#import «AppDelegate.h»
#import <Security/Security.h>
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *key = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: key
};
OSStatus foundExisting =
SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL);
if (foundExisting == errSecSuccess){
OSStatus deleted = SecItemDelete((__bridge CFDictionaryRef)query);
if (deleted == errSecSuccess){
NSLog(@"Successfully deleted the item");
} else {
NSLog(@"Failed to delete the item.");
}
} else {
NSLog(@"Did not find the existing value.");
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
После запуска этой программы (предполагается, что вы выполнили все инструкции из раздела 8.2) вы должны увидеть на консоли NSLog, соответствующий успешному удалению. В противном случае вы в любой момент можете считать значение функции SecItemDelete и узнать, почему возникла проблема.
См. также
Раздел 8.2.
8.6. Совместное использование данных из связки ключей в нескольких приложениях
Постановка задачи
Требуется, чтобы хранилищем данных связки ключей могли пользоваться два ваших приложения.
Решение
Сохраняя данные вашей связки ключей, укажите ключ kSecAttrAccessGroup в словаре, передаваемом функции SecItemAdd. Значением этого ключа должна быть группа доступа, которую вы найдете в разделе Enh2ments (Разрешения) вашего профиля инициализации. Об этом мы говорили во введении к данной главе.
Обсуждение
Несколько приложений с одного и того же портала разработки могут совместно использовать область связки ключей. Чтобы не усложнять этот пример, в данном разделе рассмотрим взаимодействие всего двух приложений, но описанные методы применимы для любого количества программ.
Чтобы два приложения могли совместно использовать область связки ключей, должны выполняться следующие требования.
1. Оба приложения должны быть подписаны с помощью профиля инициализации, взятого с одного и того же портала для разработки под iOS.
2. Оба приложения должны иметь в своем профиле инициализации один и тот же групповой идентификатор (Group ID). Обычно это идентификатор команды (Team ID), выбираемый Apple. Рекомендую не менять этот групповой идентификатор при создании собственных профилей инициализации.
3. Первое приложение, сохраняющее значение в связке ключей, должно задавать для сохраняемого элемента связки ключей атрибут kSecAttrAccessGroup. Эта группа доступа должна совпадать с той, которая указана в вашем профиле инициализации. Прочитайте во введении к этой главе, как извлекать это значение из ваших профилей инициализации.
4. Значение, сохраненное в связке ключей, должно быть сохранено с таким атрибутом kSecAttrService, значение которого известно обоим взаимодействующим приложениям. Обычно это идентификатор пакета того приложения, которое сохранило значение в связке ключей. Если вы сами написали оба приложения, то знаете этот идентификатор пакета. Таким образом, в другом вашем приложении можете считать это значение, предоставив идентификатор пакета первого приложения вышеупомянутому ключу.
5. Оба приложения должны обладать идентификацией подписи кода. Это файл настроек. plist, содержимое которого в точности совпадает с информацией из секции Enh2ments (Разрешения) вашего профиля инициализации. Затем потребуется указать путь к этому файлу в разделе Code Signing Enh2ments (Разрешения для подписи кода) в настройках сборки. Вскоре мы обсудим этот вопрос подробнее.
6. Хотя приложение и подписано профилями инициализации, в которых указаны разрешения (подробнее об этом говорится во введении к данной главе), вам все равно потребуется явно сообщить Xcode об этих разрешениях. Вся информация о разрешениях — это обычный файл. plist, содержимое которого очень напоминает тот файл разрешений, вывод которого я продемонстрировал во введении к этой главе.
<plist version="1.0">
<dict>
<key>application-identifier</key>
<string>F3FU372W5M.com.pixolity.ios.cookbook.SecondSecurityApp</string>
<key>com.apple.developer.default-data-protection</key>
<string>NSFileProtectionComplete</string>
<key>get-task-allow</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>F3FU372W5M.*</string>
</array>
</dict>
</plist>
Обратите внимание на ключ keychain-access-groups. Значение этого ключа указывает группу связки ключей, к которой имеет доступ данное приложение, а именно: F3FU372W5M.*. Вам потребуется найти в разделе «Разрешения» свою группу доступа к связке ключей и использовать ее в примерах кода в данном разделе. Мы напишем два приложения. Первое будет заносить в связку ключей информацию, касающуюся группы доступа к связке ключей, а второе будет считывать эту информацию. Приложения будут иметь разные идентификаторы пакетов и, в принципе, будут двумя совершенно разными программами. Однако они смогут совместно использовать определенную область в связке ключей.
Группа доступа является общим идентификатором моей команды и соответствует группе, имеющей доступ к нашей общей связке ключей. Разумеется, в вашем случае это значение будет другим. Воспользуйтесь приемами, изученными в разделе 8.0 этой главы, чтобы извлечь разрешения из ваших профилей инициализации.
Для первого из наших iOS-приложений я задам следующие настройки. Вы должны заменить их собственными.
• Идентификатор пакета com.pixolity.ios.cookbook.SecurityApp.
Группа доступа к связке ключей F3FU372W5M.*.
Профиль инициализации, специально созданный для идентификатора пакета данного приложения.
А вот мои настройки для второго приложения — того, которое будет считывать из связки ключей значения, сохраненные там первым приложением.
• Идентификатор пакета com.pixolity.ios.cookbook.SecurityApp.
Группа доступа к связке ключей F3FU372W5M.*.
Профиль инициализации, специально созданный для идентификатора пакета данного приложения, но отличающийся от аналогичного профиля, созданного для первого приложения.
Самая важная деталь, отличающая первое приложение (сохраняющее связку ключей) от второго (считывающего цепочку ключей), — это идентификаторы пакетов. Первое приложение будет использовать свой идентификатор пакета для сохранения значения в связке ключей, а второе — использовать идентификатор пакета первого приложения для считывания того самого значения из связки ключей. Код очень напоминает тот, что мы написали в разделе 8.2. Разница заключается в том, что новый код будет указывать группу доступа к связке ключей при сохранении данных в этой связке ключей:
#import «AppDelegate.h»
#import <Security/Security.h>
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *key = @"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSString *accessGroup = @"F3FU372W5M.*";
/* Сначала удаляем имеющееся значение, если оно существует. Этого можно
и не делать, но SecItemAdd завершится с ошибкой, если имеющееся
значение окажется в связке ключей */
NSDictionary *queryDictionary = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccessGroup: accessGroup,
(__bridge id)kSecAttrAccount: key,
};
SecItemDelete((__bridge CFDictionaryRef)queryDictionary);
/* Затем запишем новое значение в связку ключей */
NSString *value = @"Steve Jobs";
NSData *valueData = [value dataUsingEncoding: NSUTF8StringEncoding];
NSDictionary *secItem = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccessGroup: accessGroup,
(__bridge id)kSecAttrAccount: key,
(__bridge id)kSecValueData: valueData,
};
CFTypeRef result = NULL;
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)secItem, &result);
if (status == errSecSuccess){
NSLog(@"Successfully stored the value");
} else {