web-dev-qa-db-ja.com

iOS 13にアップグレードした後、キーチェーンクエリが常にerrSecItemNotFoundを返す

パスワードをiOSキーチェーンに保存し、後でそれらを取得して、「記憶」(自動ログイン)機能をアプリに実装します。

_Security.framework_関数(SecItemCopyMatching()など)の周りに独自のラッパーを実装しましたが、iOS 12までは魅力のように機能していました。

今私は私のアプリが来たるiOS 13で壊れないことをテストしています、そして驚いたことに:

SecItemCopyMatching()は常に_.errSecItemNotFound_を返します

...クエリを実行するデータを以前に保存している場合でも。

私のラッパーは静的プロパティを持つクラスで、クエリ辞書を組み立てるときにkSecAttrServicekSecAttrAccountの値を便利に提供します。

_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" 

// ...
_

次のようなコードを使用して、キーチェーンにパスワードをinsertingしています。

_/* 
  - 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)
            }
        }
    }
}
_

(どちらの呼び出しにも使用する辞書のエントリに不適切な値があるとは思わない...しかし、たぶん今まで「パスを取得」したことのないものが見当たらない)

私は aレポジトリ を設定して、問題を実証する作業プロジェクト(Xcode 11ベータ)を設定しました。

パスワードの保存は常に成功します。パスワードの読み込み:

  • Xcode 10-iOS 12(およびそれ以前)では成功
  • Xcode 11の_.errSecItemNotFound_での失敗-iOS 13。

UPDATE:デバイスで問題を再現できません。シミュレータのみです。デバイスでは、保存されているパスワードが正常に取得されます。おそらくこれは、x86プラットフォーム用のiOS 13シミュレータやiOS 13 SDKのバグまたは制限です。

更新2:誰かが問題を何らかの方法で回避する別のアプローチを考え出した場合(設計によるか、Appleによる監視の利点を利用するか)、回答としてお受けいたします。

31
Nicolas Miari

キーペアの生成時に同じ問題が発生しました-デバイスでは問題なく動作しますが、iOS 13以降のシミュレータでは、後で取得しようとしたときにキーを見つけることができません。

解決策はApple documentation: https://developer.Apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_keychain にあります

「新しい暗号鍵の生成」で説明されているように、自分で鍵を生成する場合、そのプロセスの暗黙的な部分としてキーチェーンにそれらを格納できます。他の方法でキーを取得した場合でも、キーチェーンに保存できます。

つまり、SecKeyCreateRandomKeyでキーを作成した後、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!
0

kSecClassGenericPasswordの問題に関して、私は問題が何であるかを理解しようとしていましたが、その解決策を見つけました。

基本的にAppleはkSecAttrAccessControlの問題を修正していたため、iOSバージョン13より下では、生体認証IDのないkSecAttrAccessControlを使用してkeyChainオブジェクトを追加し、iOS 13以降ではシミュレータで動作しなくなりました。

したがって、その解決策は、keyChainオブジェクトを生体認証で暗号化する場合にkSecAttrAccessControlをクエリに追加する必要がありますが、生体認証で暗号化する必要がない場合は、kSecAttrAccessibleのみを追加する必要があります。これは、これらを行う正しい方法です。

生体認証暗号化のクエリ:

_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]
_

通常のKeyChainのクエリ(生体認証なし):

_var attributes: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                               kSecAttrService: "Your service",
                                               kSecAttrAccount: "Your account",
                                               kSecValueData: "data",
                                               kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]
_
0
Sagi Shmuel