Не так давно столкнулся с необходимостью использования Keychain-а для обеспечения дополнительной защиты при входе в приложение.
Я нашел много хороших статей по этой теме, но в основном там описываются сторонние фреймворки, упрощающие жизнь, а было интересно посмотреть, как работать с API напрямую. В этой статье я попытался объединить документацию Apple с практикой на простом примере.
Начнем с небольших определений
Keychain зашифрованная база данных, куда сохраняются небольшие объемы пользовательской информации (см. документацию Apple).
Общая схема работы продемонстрирована на рисунке.
Keychain API Services в свои очередь являются часть фреймворка Security, но его рассмотрение требует отдельной статьи.
Добавление элемента
let keychainItemQuery = [ kSecValueData: pass.data(using: .utf8)!, kSecClass: kSecClassGenericPassword ] as CFDictionary let status = SecItemAdd(keychainItemQuery, nil) print("Operation finished with status: \(status)")
Выше приведен пример сохранения пароля в Keychain.
Рассмотрим функцию SecItemAdd подробнее.
func SecItemAdd(_ attributes: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
На вход подается объект класса CFDictionary, который в свою очередь является ссылкой на неизменяемый объект словаря.
Что в это словарь входит? На самом деле его состав зависит от решаемой задачи здесь же мы просто сохраняем простой пароль, давайте разберем этот простейший запрос.
Итак, kSecClass этот ключ используется для значений хранимых элементов, список их можно посмотреть тут, мы же выбрали стандартный пароль.
kSecValueData ключ, использующийся для передачи данных элемента.
На этом обязательные ключи заканчиваются, далее идут опциональные. Список таких параметров доступен в документации.
Возвращаемое значение типа OSStatus определяет результат операции сохранения/изменения/удаления, у него так же есть масса значений.
Получение элемента
Для получения элемента из Keychain-а используется метод SecItemCopyMatching.
Формируем запрос в виде словаря, где содержится искомый пароль.
let keychainItem = [ kSecValueData: pass.data(using: .utf8)!, kSecClass: kSecClassGenericPassword, kSecReturnAttributes: true, kSecReturnData: true ] as CFDictionary var ref: AnyObject? let status = SecItemCopyMatching(keychainItem, &ref) if let result = ref as? NSDictionary, let passwordData = result[kSecValueData] as? Data { print("Operation finished with status: \(status)") print(result) let str = String(decoding: passwordData, as: UTF8.self) print(str) }
Посмотрим логи:
Operation finished with status: 0
{
accc = "<SecAccessControlRef: ak>";
acct = "";
agrp = "xxx.com.maximenko.xxx";
cdat = "2020-11-19 20:39:43 +0000";
mdat = "2020-11-19 20:39:43 +0000";
musr = {length = 0, bytes = 0x};
pdmn = ak;
persistref = {length = 0, bytes = 0x};
sha1 = {length = 20, bytes = 0xxxxxxxxxxxxxxxxxxxxxxx};
svce = "";
sync = 0;
tomb = 0;
"v_Data" = {length = 3, bytes = 0x4b656b};
}
Kek
Как мы видим, возвращен 0, символизирующий успешный результат поиска и выведен весь список атрибутов, полученных из API (Access group параметр затерт на всякий случай:)). Эти атрибуты подробно описаны тут.
Значение по ключу kSecValueData нас собственно тут интересует, успешно разворачиваем его в строку, далее выведенную терминал.
Обновление элемента
Для этого есть метод SecItemUpdate.
На вход подается 2 CFDictionary словаря в первом информация об обновляемом элементе, во втором та информация, на которую надо будет заменить старую.
let query = [ kSecClass: kSecClassGenericPassword,] as CFDictionarylet updateFields = [ kSecValueData: pass.data(using: .utf8)!] as CFDictionarylet status = SecItemUpdate(query, updateFields)
На примере простого пароля в первом запросе указываем что именно требуется найти для замены, во втором то ключ-значение, которое мы обновим в найденном элементе.
Удаление элемента
Для удаления используем SecItemDelete.
У него на входе один параметр словарь c информацией об удаляемом элементе, который надо найти.
Возвращает статус выполнения операции типа OSStatus.
let query = [ kSecClass: kSecClassGenericPassword, kSecValueData: pass.data(using: .utf8)!] as CFDictionarylet res = SecItemDelete(query)
Подведение итогов
В данной статье рассматривается работа с Keychain-ом на примере нескольких основных методов. Если увидите какие-то неточности, ошибки или просто хотите более подробно обсудить тему, пишите в комментарии или Telegram (skipperprivate).
P.S.
Если будет интересно, можно отдельную статью посвятить работе с более сложными сущностями вроде Интернет-пароля, с большим количеством полей и т.д., или обсудить подобное в комментариях.