You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS Secure Enclave ECIES解密失败(err -69)生产环境求助

生产环境iOS Secure Enclave ECIES解密失败问题排查与解决思路

错误信息

decrypt: decrypted data fail: Optional(Error Domain=NSOSStatusErrorDomain Code=-50 "ECIES: Failed to aes-gcm decrypt data (err -69)" UserInfo={numberOfErrorsDeep=0, NSDescription=ECIES: Failed to aes-gcm decrypt data (err -69)}), DataLength: 113

相关代码

static func getPrivateKey(keyName: String) -> (key: SecKey?, errorMsg: String?) {
    var key: SecKey?
    key = loadKey(name: keyName)
    if let key = key { return (key, nil) }
    let keyInfo = makeAndStoreKey(keyName: keyName)
    if let key = keyInfo.key { return (key, nil) }
    return (nil, keyInfo.errorMsg)
}

static func makeAndStoreKey(keyName: String, requiresBiometry: Bool = false) -> (key: SecKey?, errorMsg: String?) {
    removeKey(name: keyName)
    let flags: SecAccessControlCreateFlags
    if #available(iOS 11.3, *) {
        flags = requiresBiometry ?
        [.privateKeyUsage, .biometryCurrentSet] : .privateKeyUsage
    } else {
        flags = requiresBiometry ?
        [.privateKeyUsage, .touchIDCurrentSet] : .privateKeyUsage
    }
    let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                    kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
                                                    flags,
                                                    nil)
    guard let tag = keyName.data(using: .utf8), let access = access else {
        return (nil, Crypto.Err.accessOrTagFail.errorDescription)
    }
    let attributes: [CFString: Any] = [
        kSecAttrKeyType: kSecAttrKeyTypeEC,
        kSecAttrKeySizeInBits: 256,
        kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
        kSecPrivateKeyAttrs: [
            kSecAttrIsPermanent: true,
            kSecAttrApplicationTag: tag,
            kSecAttrAccessControl: access
        ]
    ]
    var error: Unmanaged<CFError>?
    guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
        return (nil, "SecKeyCreateRandomKeyFail: \((error?.takeRetainedValue() as? Error)?.localizedDescription ?? "")")
    }
    return (privateKey, nil)
}

static func loadKey(name: String) -> SecKey? {
    guard let tag = name.data(using: .utf8) else { return nil }
    let query: [CFString: Any] = [
        kSecClass: kSecClassKey,
        kSecAttrApplicationTag: tag,
        kSecAttrKeyType: kSecAttrKeyTypeEC,
        kSecReturnRef: true
    ]
    var item: CFTypeRef?
    let status = SecItemCopyMatching(query as CFDictionary, &item)
    guard status == errSecSuccess, let item = item else { return nil }
    return item as! SecKey
}

static func removeKey(name: String) {
    guard let tag = name.data(using: .utf8) else { return }
    let query: [CFString: Any] = [
        kSecClass: kSecClassKey,
        kSecAttrApplicationTag: tag
    ]
    SecItemDelete(query as CFDictionary)
}

static func encrypt(data: Data, keyName: String) -> Data? {
    var tmpData: Data?
    var errMsg: String?
    defer {
        if tmpData == nil {
            self.updateDeviceDecryptFail()
            self.errorCallBack?(.masterKey(state: .deviceEncryptErr(errMsg: errMsg)))
        }
    }
    guard #available(iOS 11.0, *) else { return nil }
    let keyInfo = getPrivateKey(keyName: keyName)
    guard let key = keyInfo.key else {
        errMsg = "encrypt: \(Crypto.Err.getPrivateKeyFail.errorDescription ?? "") \(keyInfo.errorMsg ?? "")"
        return nil
    }
    guard let publicKey = SecKeyCopyPublicKey(key) else {
        errMsg = "encrypt: \(Crypto.Err.getPublicKeyFail.errorDescription ?? "")"
        return nil
    }
    let algorithm: SecKeyAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
    guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, algorithm) else {
        errMsg = "encrypt: \(Crypto.Err.isAlgorithmSupportedFail.errorDescription ?? "")"
        return nil
    }
    var error: Unmanaged<CFError>?
    let cipherData = SecKeyCreateEncryptedData(publicKey,
                                                algorithm,
                                                data as CFData,
                                                &error) as Data?
    guard let cipherData = cipherData else {
        errMsg = "encrypt: \(Crypto.Err.encryptedDataFail(error: (error?.takeRetainedValue() as? Error)).errorDescription ?? "")"
        return nil
    }
    tmpData = cipherData
    return cipherData
}

static func decrypt(encryptedData: Data, keyName: String) -> Data? {
    var tmpData: Data?
    var errMsg: String?
    defer { if tmpData == nil { self.errorCallBack?(.masterKey(state: .deviceDecryptErr(errMsg: errMsg))) } }
    guard #available(iOS 11.0, *) else { return nil }
    let keyInfo = getPrivateKey(keyName: keyName)
    guard let key = keyInfo.key else {
        errMsg = "decrypt: \(Crypto.Err.getPrivateKeyFail.errorDescription ?? "") \(keyInfo.errorMsg ?? "")"
        return nil
    }
    let algorithm: SecKeyAlgorithm = .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
    guard SecKeyIsAlgorithmSupported(key, .decrypt, algorithm) else {
        errMsg = "decrypt: \(Crypto.Err.isAlgorithmSupportedFail.errorDescription ?? "")"
        return nil
    }
    var error: Unmanaged<CFError>?
    let data = SecKeyCreateDecryptedData(key,
                                            algorithm,
                                            encryptedData as CFData,
                                            &error) as Data?
    guard let data = data else {
        errMsg = "decrypt: \(Crypto.Err.decryptedDataFail(error: (error?.takeRetainedValue() as? Error)).errorDescription ?? ""), DataLength: \(encryptedData.count)"
        return nil
    }
    tmpData = data
    return data
}

解决思路

  • 修复密钥覆盖逻辑:

    • 修改getPrivateKey方法,当loadKey失败时直接返回错误,而非自动创建新密钥。原有逻辑会导致旧密钥丢失,用新密钥解密旧数据必然失败。
    • 给loadKey的查询参数添加kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,确保只加载Secure Enclave内的密钥,避免跨环境密钥不兼容。
  • 调整密钥访问权限:

    • 评估业务场景后,将kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly替换为kSecAttrAccessibleWhenUnlockedThisDeviceOnly,避免设备重启后首次解锁前无法访问密钥的问题。
    • 移除.biometryCurrentSet/.touchIDCurrentSet标记,或添加生物识别变更后的密钥恢复逻辑。当用户更新生物识别信息时,带该标记的密钥会被锁定,导致解密失败。
  • 确保数据与算法一致性:

    • 加密和解密严格使用同一算法.eciesEncryptionCofactorVariableIVX963SHA256AESGCM,避免因算法参数差异导致认证失败。
    • 对加密数据添加SHA-256哈希校验,解密前先验证数据完整性,排除数据传输/存储过程中被篡改或截断的可能。
  • 优化错误日志与处理:

    • 在loadKey中记录SecItemCopyMatching的具体返回状态(如errSecItemNotFound、errSecAuthFailed),精准定位密钥加载失败原因。
    • 针对NSOSStatusErrorDomain的-50、-69错误码添加分支处理:-69通常对应AES-GCM认证失败,可直接提示用户密钥不匹配或数据被篡改。

内容的提问来源于stack exchange,提问作者saber claus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 21:55:00