Текст книги "iOS. Приемы программирования"
Автор книги: Вандад Нахавандипур
Жанр: Зарубежная компьютерная литература, Зарубежная литература
сообщить о неприемлемом содержимом
Текущая страница: 32 (всего у книги 59 страниц)
Разделы 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. Значением этого ключа должна быть группа доступа, которую вы найдете в разделе Entitlements (Разрешения) вашего профиля инициализации. Об этом мы говорили во введении к данной главе.
ОбсуждениеНесколько приложений с одного и того же портала разработки могут совместно использовать область связки ключей. Чтобы не усложнять этот пример, в данном разделе рассмотрим взаимодействие всего двух приложений, но описанные методы применимы для любого количества программ.
Чтобы два приложения могли совместно использовать область связки ключей, должны выполняться следующие требования.
1. Оба приложения должны быть подписаны с помощью профиля инициализации, взятого с одного и того же портала для разработки под iOS.
2. Оба приложения должны иметь в своем профиле инициализации один и тот же групповой идентификатор (Group ID). Обычно это идентификатор команды (Team ID), выбираемый Apple. Рекомендую не менять этот групповой идентификатор при создании собственных профилей инициализации.
3. Первое приложение, сохраняющее значение в связке ключей, должно задавать для сохраняемого элемента связки ключей атрибут kSecAttrAccessGroup. Эта группа доступа должна совпадать с той, которая указана в вашем профиле инициализации. Прочитайте во введении к этой главе, как извлекать это значение из ваших профилей инициализации.
4. Значение, сохраненное в связке ключей, должно быть сохранено с таким атрибутом kSecAttrService, значение которого известно обоим взаимодействующим приложениям. Обычно это идентификатор пакета того приложения, которое сохранило значение в связке ключей. Если вы сами написали оба приложения, то знаете этот идентификатор пакета. Таким образом, в другом вашем приложении можете считать это значение, предоставив идентификатор пакета первого приложения вышеупомянутому ключу.
5. Оба приложения должны обладать идентификацией подписи кода. Это файл настроек. plist, содержимое которого в точности совпадает с информацией из секции Entitlements (Разрешения) вашего профиля инициализации. Затем потребуется указать путь к этому файлу в разделе Code Signing Entitlements (Разрешения для подписи кода) в настройках сборки. Вскоре мы обсудим этот вопрос подробнее.
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 {
NSLog(@"Failed to store the value with code: %ld", (long)status);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Код начинается с запрашивания связки ключей для нахождения имеющегося элемента с заданным ключом, именем службы и группой доступа к связке ключей. Если такое значение существует, мы удаляем его из связки ключей. Мы делаем это лишь для того, чтобы позже можно было успешно добавить новое значение. Функция SecItemAdd завершится с ошибкой, если вы попытаетесь перезаписать ею имеющееся значение. Поэтому мы удаляем имеющееся значение (если оно существует) и записываем новое. Вы также можете попытаться найти имеющееся значение, обновить его (при наличии такого значения), а если оно отсутствует – записать новое значение. Второй подход более сложен, а поскольку мы выполняем демонстрационное упражнение, обойдемся первым примером.
Однако прежде, чем вы сможете запустить это приложение, необходимо задать в коде разрешения, связанные с подписыванием. Чтобы задать элементы, регламентирующие подписывание кода, выполните следующие шаги.
1. Воспользуйтесь приемами, изученными во введении к этой главе, нужными для извлечения разрешений, находящихся в вашем профиле инициализации.
2. Создайте в вашем приложении новый. plist-файл и назовите его Entitlements.plist. Вставьте содержимое файла с разрешениями из профиля инициализации – в точности как они есть – в ваш файл Entitlements.plist и сохраните.
3. Перейдите в настройки сборки и найдите разрешения подписывания кода (Code Signing Entitlements). Задайте для этого раздела значение $(TARGET_NAME)/Entitlements.plist. Оно означает, что Xcode должна найти файл Entitlements.plist в целевом каталоге, имеющем такое имя.
Опишу причины, по которым нам требуется это значение. Если создать проект под названием MyProject, Xcode создаст корневой каталог (SCROOT) под названием MyProject. В нем среда создаст еще один подкаталог, который также будет называться MyProject, а уже в этом каталоге будет находиться ваш исходный код. В каталоге SCROOT (то есть в вышестоящем каталоге под названием MyProject) будет создан еще один подкаталог под названием MyProjectTests. В этом подкаталоге будут модульные тесты, интеграционные тесты и тесты пользовательского интерфейса. Под этой структурой вы найдете ваш файл Entitlements.plist (он находится по адресу MyProject/MyProject/Entitlements.plist). Программа поиска разрешений подписывания кода ищет указанный. plist-файл в каталоге SRCROOT, поэтому, если вы предоставите ей значение MyProject/MyProject/Entitlements.plist, это ее вполне устроит! $(TARGET_NAME) в Xcode – это переменная, разрешаемая в имя вашей цели, которое по умолчанию совпадает с именем вашего проекта. Следовательно, в случае с MyProject значение $(TARGET_NAME)/Entitlements.plist будет разрешаться в MyProject/Entitlements.plist.
4. Соберите приложение и убедитесь, что все работает правильно.
Если вы получите сообщение об ошибке примерно следующего содержания:
error: The data couldn't be read because it isn't in the correct format[8]8
«Ошибка: данные не могут быть считаны, так как имеют неверный формат». – Примеч. пер.
[Закрыть]
это означает, что ваши разрешения записаны в неверном формате. Распространенная ошибка, которую совершают многие iOS-программисты, связана с заполнением файла с разрешениями следующим образом:
<plist version="1.0">
<key>Entitlements</key>
<dict>
<key>application-identifier</key>
<string>F3FU372W5M.com.pixolity.ios.cookbook.SecurityApp</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>
Обратите внимание: этот файл с разрешениями оформлен неправильно, так как в верхней его части содержится висячий ключ Entitlements. Этот ключ потребуется удалить, чтобы ваш файл имел следующий вид:
<plist version="1.0">
<dict>
<key>application-identifier</key>
<string>F3FU372W5M.com.pixolity.ios.cookbook.SecurityApp</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>
Итак, разработка записывающего приложения закончена. Вплотную займемся приложением, которое считывает данные. Это два совершенно самостоятельных подписанных приложения, каждое из которых обладает собственным профилем инициализации:
#import «AppDelegate.h»
#import <Security/Security.h>
@implementation AppDelegate
– (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *key = @"Full Name";
/* Это не ID пакета того приложения, которое записывало информацию
в связку ключей. Это и не ID пакета данного приложения. Идентификатор
пакета данного приложения – com.pixolity.ios.cookbook.SecondSecurityApp */
NSString *service = @"com.pixolity.ios.cookbook.SecurityApp";
NSString *accessGroup = @"F3FU372W5M.*";
NSDictionary *queryDictionary = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccessGroup: accessGroup,
(__bridge id)kSecAttrAccount: key,
(__bridge id)kSecReturnData: (__bridge id)kCFBooleanTrue,
};
CFDataRef data = NULL;
OSStatus found =
SecItemCopyMatching((__bridge CFDictionaryRef)queryDictionary,
(CFTypeRef *)&data);
if (found == errSecSuccess){
NSString *value = [[NSString alloc]
initWithData:(__bridge_transfer NSData *)data
encoding: NSUTF8StringEncoding];
NSLog(@"Value = %@", value);
} else {
NSLog(@"Failed to read the value with error = %ld", (long)found);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Может потребоваться время, чтобы привыкнуть к тому, как работает связка ключей. Не волнуйтесь, если все не заработает с пол-оборота. Просто внимательно изучите все инструкции, данные в этой главе (особенно в ее введении), чтобы четко понять, что такое группа доступа к связке ключей и как она связана с разрешениями вашего приложения.
См. такжеРазделы 8.0 и 8.2.
8.7. Запись и считывание информации связки ключей из iCloud
Постановка задачиТребуется сохранить информацию в связке ключей, а также обеспечить хранение этой информации в пользовательской связке ключей, расположенной в облаке iCloud. Так пользователь сможет получать доступ к этой связке с любых имеющихся у него устройств с iOS.
РешениеПри добавлении вашего элемента к связке ключей с помощью функции SecItemAdd добавьте ключ kSecAttrSynchronizable в словарь, передаваемый этой функции. В качестве значения этого ключа передайте kCFBooleanTrue.
ОбсуждениеКогда элементы хранятся в связке ключей с ключом kSecAttrSynchronizable, имеющим значение kCFBooleanTrue, такие элементы сохраняются и в той пользовательской связке ключей, которая расположена в облаке iCloud. Таким образом, эти элементы будут доступны на всех пользовательских устройствах, если пользователь зашел в них под учетной записью для работы с iCloud. Если вы просто хотите считать значение, которое (как вам точно известно) синхронизировано с пользовательской связкой ключей из iCloud, необходимо указать вышеупомянутый ключ, а также задать для этого ключа значение kCFBooleanTrue. В таком случае iOS сможет получить требуемое значение из облака, если еще не сделала этого.
Пример, который мы рассмотрим в этом разделе, на 99 % совпадает с кодом, изученным в разделе 8.6. Разница заключается в следующем: когда мы сохраняем значение в связке ключей или пытаемся его оттуда считать, то указываем в нашем словаре ключ kSecAttrSynchronizable и задаем для этого ключа значение kCFBooleanTrue. Итак, давайте для начала рассмотрим, как сохранить значение в связке ключей:
#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,
(__bridge id)kSecAttrSynchronizable: (__bridge id)kCFBooleanTrue
};
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,
(__bridge id)kSecAttrSynchronizable: (__bridge id)kCFBooleanTrue
};
CFTypeRef result = NULL;
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)secItem, &result);
if (status == errSecSuccess){
NSLog(@"Successfully stored the value");
} else {
NSLog(@"Failed to store the value with code: %ld", (long)status);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Пожалуйста, внимательно изучите примечания из раздела 8.6. На данном этапе вы уже должны знать, что группа доступа, упоминаемая во всех приведенных примерах, у каждого разработчика будет называться по-своему. Обычно это командный идентификатор, генерируемый на портале Apple для разработки под iOS для каждого разработчика и обеспечивающий доступ к коду для его команды. Необходимо изменить это значение в ваших примерах и убедиться, что оно совпадает именно с вашим командным идентификатором.
Ранее был приведен код приложения, сохраняющего значения в связке ключей, расположенной в облаке iCloud. Теперь остается написать приложение, считывающее эти данные:
#import «AppDelegate.h»
#import <Security/Security.h>
@implementation AppDelegate
– (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSString *key = @"Full Name";
/* Это не ID пакета того приложения, которое записывало информацию
в связку ключей в iCloud. Это и не ID пакета данного приложения.
Идентификатор пакета данного приложения – com.pixolity.ios.cookbook.SecondSecurityApp*/
NSString *service = @"com.pixolity.ios.cookbook.SecurityApp";
NSString *accessGroup = @"F3FU372W5M.*";
NSDictionary *queryDictionary = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccessGroup: accessGroup,
(__bridge id)kSecAttrAccount: key,
(__bridge id)kSecReturnData: (__bridge id)kCFBooleanTrue,
(__bridge id)kSecAttrSynchronizable: (__bridge id)kCFBooleanTrue
};
CFDataRef data = NULL;
OSStatus found =
SecItemCopyMatching((__bridge CFDictionaryRef)queryDictionary,
(CFTypeRef *)&data);
if (found == errSecSuccess){
NSString *value = [[NSString alloc]
initWithData:(__bridge_transfer NSData *)data
encoding: NSUTF8StringEncoding];
NSLog(@"Value = %@", value);
} else {
NSLog(@"Failed to read the value with error = %ld", (long)found);
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Необходимо отметить еще несколько деталей, связанных с работой со связкой ключей в iCloud.
• В такой связке ключей можно хранить только пароли.
• Связка ключей iCloud работает где угодно – это означает, что она появляется сразу на нескольких устройствах, принадлежащих одному пользователю iCloud. Если вы запишете элемент в связку ключей iCloud, он будет синхронизирован на всех устройствах этого пользователя. Аналогично если вы удалите элемент, то он удалится со всех устройств пользователя, связанных с iCloud. Поэтому в данном случае нужно проявлять особую осторожность.
Также следует отметить, что все остальные приемы, изученные нами в этой главе (например, обновление имеющегося элемента связки ключей – см. раздел 8.4), работают и со связкой ключей, расположенной в iCloud.
Правообладателям!
Это произведение, предположительно, находится в статусе 'public domain'. Если это не так и размещение материала нарушает чьи-либо права, то сообщите нам об этом.