Je stocke des mots de passe dans le trousseau iOS et les récupère plus tard pour implémenter une fonction "se souvenir de moi" (connexion automatique) sur mon application.
J'ai implémenté mon propre wrapper autour des fonctions Security.framework
(SecItemCopyMatching()
, etc.), et cela fonctionnait comme un charme jusqu'à iOS 12.
Maintenant, je teste que mon application ne rompt pas avec le prochain iOS 13, et voilà:
SecItemCopyMatching()
renvoie toujours .errSecItemNotFound
... même si j'ai déjà enregistré les données que j'interroge.
Mon wrapper est une classe avec des propriétés statiques pour fournir facilement les valeurs de kSecAttrService
et kSecAttrAccount
lors de l'assemblage des dictionnaires de requête:
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"
// ...
Je insère le mot de passe dans le trousseau avec un code comme celui-ci:
/*
- 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?()
}
... et plus tard, je récupère le mot de passe avec:
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)
}
}
}
}
(Je ne pense pas que l'une des entrées des dictionnaires que j'utilise pour l'un ou l'autre des appels ait une valeur inappropriée ... mais peut-être que je manque quelque chose qui vient de se "faire passer" jusqu'à maintenant)
J'ai mis en place n référentiel avec un projet fonctionnel (Xcode 11 beta) qui illustre le problème.
Le stockage du mot de passe réussit toujours; Le chargement du mot de passe:
.errSecItemNotFound
Sur Xcode 11 - iOS 13.MISE À JOUR: Je ne peux pas reproduire le problème sur l'appareil, seulement sur Simulator. Sur l'appareil, le mot de passe enregistré est récupéré avec succès. Il s'agit peut-être d'un bogue ou d'une limitation du simulateur iOS 13 et/ou du SDK iOS 13 pour la plate-forme x86.
MISE À JOUR 2: Si quelqu'un propose une approche alternative qui contourne en quelque sorte le problème (que ce soit par conception ou en profitant d'une certaine surveillance d'Apple), Je vais l'accepter comme réponse.
Nous avons eu le même problème lors de la génération d'une paire de clés - fonctionne très bien sur les appareils, mais sur le simulateur iOS 13 et supérieur, il ne peut pas trouver la clé lorsque nous essayons de la récupérer plus tard.
La solution est dans Apple: https://developer.Apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_keychain
Lorsque vous générez vous-même des clés, comme décrit dans Génération de nouvelles clés cryptographiques, vous pouvez les stocker dans le trousseau en tant que partie implicite de ce processus. Si vous obtenez une clé par un autre moyen, vous pouvez toujours la stocker dans le trousseau.
En bref, après avoir créé une clé avec SecKeyCreateRandomKey
, vous devez enregistrer cette clé dans le trousseau à l'aide de SecItemAdd
:
var error: Unmanaged<CFError>?
guard let key = SecKeyCreateRandomKey(createKeyQuery as CFDictionary, &error) else {
// An error occured.
return
}
let saveKeyQuery: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag,
kSecValueRef as String: key
]
let status = SecItemAdd(saveKeyQuery as CFDictionary, nil)
guard status == errSecSuccess else {
// An error occured.
return
}
// Success!
Concernant le problème dans kSecClassGenericPassword
, j'essayais de comprendre quel était le problème et j'ai trouvé une solution pour cela.
Fondamentalement, il semble que Apple corrigeait un problème avec kSecAttrAccessControl
, donc sous iOS version 13, vous ajoutez un objet keyChain avec kSecAttrAccessControl
sans identité biométrique et au-dessus d'iOS 13 qui ne fonctionne plus dans un simulateur.
Donc, la solution est quand vous voulez chiffrer l'objet keyChain avec biométrique, vous devez ajouter kSecAttrAccessControl
à votre requête, mais si vous n'avez pas besoin de chiffrer par biométrique, vous devez ajouter seulement kSecAttrAccessible
c'est la bonne façon de les faire.
Requête pour chiffrer biométrique:
guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlocked,
userPresence,
nil) else {
// failed to create accessControl
return
}
var attributes: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
kSecAttrService: "Your service",
kSecAttrAccount: "Your account",
kSecValueData: "data",
kSecAttrAccessControl: accessControl]
Requête pour KeyChain régulier (sans biométrique):
var attributes: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
kSecAttrService: "Your service",
kSecAttrAccount: "Your account",
kSecValueData: "data",
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]