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
相关产品推荐
相关产品推荐

