Запрос цепочки для ключей всегда возвращает errSecItemNotFound после обновления до iOS 13
Я храню пароли в связке ключей iOS, а затем извлекаю их для реализации функции "запомнить меня" (автоматический вход в систему) в моем приложении.
Я реализовал свою собственную оболочку для функций Security.framework
(SecItemCopyMatching()
и т.д.), И до iOS 12 она работала как чудо.
Сейчас я проверяю, что мое приложение не ломается с готовящейся к выпуску iOS 13, и вот:
SecItemCopyMatching()
всегда возвращает .errSecItemNotFound
... хотя ранее я уже хранил данные, которые запрашиваю.
Моя оболочка - это класс со статическими свойствами для удобного предоставления значений kSecAttrService
и kSecAttrAccount
при сборке словарей запросов:
class LocalCredentialStore {
private static let serviceName: String = {
guard let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String else {
return "Unknown App"
}
return name
}()
private static let accountName = "Login Password"
// ...
Я вставляю пароль в связку ключей с помощью следующего кода:
/*
- NOTE: protectWithPasscode is currently always FALSE, so the password
can later be retrieved programmatically, i.e. without user interaction.
*/
static func storePassword(_ password: String, protectWithPasscode: Bool, completion: (() -> Void)? = nil, failure: ((Error) -> Void)? = nil) {
// Encode payload:
guard let dataToStore = password.data(using: .utf8) else {
failure?(NSError(localizedDescription: ""))
return
}
// DELETE any previous entry:
self.deleteStoredPassword()
// INSERT new value:
let protection: CFTypeRef = protectWithPasscode ? kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly : kSecAttrAccessibleWhenUnlocked
let flags: SecAccessControlCreateFlags = protectWithPasscode ? .userPresence : []
guard let accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
protection,
flags,
nil) else {
failure?(NSError(localizedDescription: ""))
return
}
let insertQuery: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessControl: accessControl,
kSecValueData: dataToStore,
kSecUseAuthenticationUI: kSecUseAuthenticationUIAllow,
kSecAttrService: serviceName, // These two values identify the entry;
kSecAttrAccount: accountName // together they become the primary key in the Database.
]
let resultCode = SecItemAdd(insertQuery as CFDictionary, nil)
guard resultCode == errSecSuccess else {
failure?(NSError(localizedDescription: ""))
return
}
completion?()
}
... и позже я получаю пароль с помощью:
static func loadPassword(completion: @escaping ((String?) -> Void)) {
// [1] Perform search on background thread:
DispatchQueue.global().async {
let selectQuery: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: serviceName,
kSecAttrAccount: accountName,
kSecReturnData: true,
kSecUseOperationPrompt: "Please authenticate"
]
var extractedData: CFTypeRef?
let result = SecItemCopyMatching(selectQuery, &extractedData)
// [2] Rendez-vous with the caller on the main thread:
DispatchQueue.main.async {
switch result {
case errSecSuccess:
guard let data = extractedData as? Data, let password = String(data: data, encoding: .utf8) else {
return completion(nil)
}
completion(password) // < SUCCESS
case errSecUserCanceled:
completion(nil)
case errSecAuthFailed:
completion(nil)
case errSecItemNotFound:
completion(nil)
default:
completion(nil)
}
}
}
}
(я не думаю, что какие-либо записи в словарях, которые я использую для обоих вызовов, имеют неуместное значение... но, возможно, я упускаю что-то, что только что "получило пропуск" до сих пор)
Я создал репозиторий с работающим проектом (бета-версия Xcode 11), который демонстрирует проблему.
Хранение пароля всегда успешно; Загрузка пароля:
- Успешно в Xcode 10 - iOS 12 (и более ранних версиях), но
- Сбой с
.errSecItemNotFound
на Xcode 11 - iOS 13.
ОБНОВЛЕНИЕ: Я не могу воспроизвести проблему на устройстве, только симулятор. На устройстве сохраненный пароль успешно восстановлен.
Возможно, это ошибка или ограничение в iOS 13 Simulator и/или iOS 13 SDK для платформы x86.
ОБНОВЛЕНИЕ 2: Если кто-то придумает альтернативный подход, который каким-то образом решит проблему (будь то по замыслу или с помощью некоторого контроля со стороны Apple), я приму его в качестве ответа.
Ответы
Ответ 1
У меня была похожая проблема, когда я получал errSecItemNotFound
с любым действием, связанным с цепочкой для ключей, но только на симуляторе. На реальном устройстве это было прекрасно, я тестировал последние X-коды (бета, GM, стабильный) на разных симуляторах, и те, которые доставляли мне трудности, были iOS 13.
Проблема заключалась в том, что я использовал kSecClassKey
в атрибуте запроса kSecClass
, но без "обязательных" значений (посмотрите, какие классы идут с какими значениями здесь) для генерации первичного ключа:
kSecAttrApplicationLabel
kSecAttrApplicationTag
kSecAttrKeyType
kSecAttrKeySizeInBits
kSecAttrEffectiveKeySize
И что помогло - это выбрать kSecClassGenericPassword
для kSecClass
и предоставить "необходимые" значения для генерации первичного ключа:
kSecAttrAccount
kSecAttrService
См. здесь, чтобы узнать больше о типах kSecClass и других атрибутах.
Я пришел к такому выводу, запустив новый проект iOS 13 и скопировав оболочку Keychain, которая использовалась в нашем приложении, как и ожидалось, что это не сработало, поэтому я нашел это прекрасное руководство по использованию цепочки для ключей здесь и опробовал их обертку, которая не удивила, и затем построчно сравнил мою реализацию с их.
Надеюсь это поможет.
Ответ 2
Благодаря предложению @Edvinas в его ответе выше я смог выяснить, в чем дело.
Как он предполагает, я скачал класс-обертку Keychain, используемый в этом репозитории Github (проект 28), и заменил свой код вызовами основного класса, и вот - это сработало.
Затем я добавил консольные журналы, чтобы сравнить словари запросов, используемые в оболочке брелка для хранения/извлечения пароля (т.е. аргументы для SecItemAdd()
и SecItemCopyMatching
), с теми, которые я использовал с помощью. Было несколько отличий:
- Оболочка использует Swift Dictionary (
[String, Any]
), а мой код использует NSDictionary
(я должен обновить это. Уже 2019!).
- Оболочка использует идентификатор пакета для значения
kSecAttrService
, которое я использовал CFBundleName
. Это не должно быть проблемой, но мое имя пакета содержит японские символы...
- Оболочка использует значения
CFBoolean
для kSecReturnData
, я использовал булевы Swift.
- Оболочка использует
kSecAttrGeneric
в дополнение к kSecAttrAccount
и kSecAttrService
, мой код использует только последние два.
- Оболочка кодирует значения
kSecAttrGeneric
и kSecAttrAccount
как Data
, мой код хранил значения непосредственно как String
.
- Мой словарь вставок использует
kSecAttrAccessControl
и kSecUseAuthenticationUI
, а оболочка - нет (он использует kSecAttrAccessible
с настраиваемыми значениями. В моем случае, я считаю, kSecAttrAccessibleWhenUnlocked
применяется).
- Мой поисковый словарь использует
kSecUseOperationPrompt
, обертка не
- Оболочка указывает
kSecMatchLimit
на значение kSecMatchLimitOne
, мой код - нет.
(Пункты 6 и 7 на самом деле не нужны, потому что , хотя я сначала разработал свой класс с учетом биометрической аутентификации, в настоящее время я не использую его.)
... и т.д..
Я сопоставил свои словари с словарями обертки и, наконец, получил запрос на копирование для успешного выполнения. Затем я удалил разные предметы, пока не смог определить причину. Оказывается, что:
- Мне не нужны
kSecAttrGeneric
(только kSecAttrService
и kSecAttrAccount
, как указано в ответе @Edvinas).
- Мне не нужно кодировать данные в значение
kSecAttrAccount
(это может быть хорошей идеей, но в моем случае это нарушит ранее сохраненные данные и усложнит миграцию).
- Оказывается, что
kSecMatchLimit
тоже не нужен (возможно, потому что мой код приводит к сохранению/сопоставлению уникального значения?), Но я предполагаю, что я добавлю его просто для безопасности (не похоже, что это нарушит обратную совместимость).
- Быстрые логические значения, например
kSecReturnData
отлично работает. Присваивание целого числа 1
нарушает его (хотя это то, как значение регистрируется в консоли).
- (Японское) имя пакета в качестве значения для
kSecService
тоже подойдет.
... и т.д..
Итак, в конце концов, я:
- Удалил
kSecUseAuthenticationUI
из словаря вставок и заменил его на kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked
.
- Удален
kSecUseAuthenticationUI
из словаря вставок.
- Удалено
kSecUseOperationPrompt
из словаря копирования.
... и теперь мой код работает. Мне придется проверить, загружает ли это пароли, сохраненные с использованием старого кода на реальных устройствах (в противном случае мои пользователи потеряют свои сохраненные пароли при следующем обновлении).
Итак, это мой последний рабочий код:
import Foundation
import Security
/**
Provides keychain-based support for secure, local storage and retrieval of the
user password.
*/
class LocalCredentialStore {
private static let serviceName: String = {
guard let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String else {
return "Unknown App"
}
return name
}()
private static let accountName = "Login Password"
/**
Returns 'true' if successfully deleted, or no password was stored to begin
with; In case of anomalous result 'false' is returned.
*/
@discardableResult static func deleteStoredPassword() -> Bool {
let deleteQuery: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
kSecAttrService: serviceName,
kSecAttrAccount: accountName,
kSecReturnData: false
]
let result = SecItemDelete(deleteQuery as CFDictionary)
switch result {
case errSecSuccess, errSecItemNotFound:
return true
default:
return false
}
}
/**
If a password is already stored, it is silently overwritten.
*/
static func storePassword(_ password: String, protectWithPasscode: Bool, completion: (() -> Void)? = nil, failure: ((Error) -> Void)? = nil) {
// Encode payload:
guard let dataToStore = password.data(using: .utf8) else {
failure?(NSError(localizedDescription: ""))
return
}
// DELETE any previous entry:
self.deleteStoredPassword()
// INSERT new value:
let insertQuery: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
kSecValueData: dataToStore,
kSecAttrService: serviceName, // These two values identify the entry;
kSecAttrAccount: accountName // together they become the primary key in the Database.
]
let resultCode = SecItemAdd(insertQuery as CFDictionary, nil)
guard resultCode == errSecSuccess else {
failure?(NSError(localizedDescription: ""))
return
}
completion?()
}
/**
If a password is stored and can be retrieved successfully, it is passed back as the argument of
'completion'; otherwise, 'nil' is passed.
Completion handler is always executed on themain thread.
*/
static func loadPassword(completion: @escaping ((String?) -> Void)) {
// [1] Perform search on background thread:
DispatchQueue.global().async {
let selectQuery: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
kSecAttrService: serviceName,
kSecAttrAccount: accountName,
kSecMatchLimit: kSecMatchLimitOne,
kSecReturnData: true
]
var extractedData: CFTypeRef?
let result = SecItemCopyMatching(selectQuery, &extractedData)
// [2] Rendez-vous with the caller on the main thread:
DispatchQueue.main.async {
switch result {
case errSecSuccess:
guard let data = extractedData as? Data, let password = String(data: data, encoding: .utf8) else {
return completion(nil)
}
completion(password)
case errSecUserCanceled:
completion(nil)
case errSecAuthFailed:
completion(nil)
case errSecItemNotFound:
completion(nil)
default:
completion(nil)
}
}
}
}
}
Заключительные слова мудрости: Если у вас нет веской причины , а не, просто возьмите Обертку для ключей, которую @Edvinas упомянул в своем ответе (это хранилище, проект 28)) и двигаться дальше!