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

iOS端删除重建对称密钥后,ChaChaPoly密封盒解密失败求助

解决ChaChaPoly密钥重建不一致导致的解密认证失败问题

嘿,我来帮你排查这个问题!你遇到的authenticationFailure错误本质上是因为重新登录后生成的密钥和之前的完全不一样,导致ChaChaPoly无法验证加密数据的完整性(毕竟ChaChaPoly的seal操作会附带一个认证标签,密钥不对的话标签验证直接失败)。

先看你当前的密钥生成逻辑,这里有个明显的问题:你提到“先对密码进行SHA256哈希,提取摘要的数据表示后,用SymmetricKey(data: password)生成密钥”——这里的password如果是原密码的Data,那你等于白做了SHA256哈希!这正是密钥不一致的根源。

问题分析

ChaChaPoly要求密钥必须是**256位(32字节)**的长度。如果直接用原密码的Data生成密钥:

  • 如果原密码长度不足32字节,SymmetricKey(data:)不会自动填充,反而可能直接使用这个短数据作为密钥(但ChaChaPoly不接受非32字节的密钥,这时候其实你的密钥生成已经有问题了,只是你用了try?没捕获错误);
  • 即使密码长度刚好32字节,直接用原密码生成密钥也不安全,而且如果密码转Data时编码不一致(比如某次用UTF-8,某次用UTF-16),也会导致密钥不同。

而你做的SHA256哈希正好能生成32字节的固定长度摘要,这才是生成合规ChaChaPoly密钥的正确原料,但你却没用到它!

正确的密钥生成流程

重新梳理密钥生成逻辑,确保每次登录都能从相同密码生成完全一致的密钥:

import CryptoKit

// 1. 将密码转为统一编码的Data(必须始终用同一种编码,比如UTF-8)
guard let passwordData = passwordString.data(using: .utf8) else {
    fatalError("密码转Data失败")
}

// 2. 计算SHA256哈希,得到32字节的摘要
let passwordHash = SHA256.hash(data: passwordData)
let hashData = Data(passwordHash)

// 3. 用哈希后的Data生成ChaChaPoly合规的256位密钥
let symmetricKey = SymmetricKey(data: hashData)

确保密钥存储/读取的正确性

你提到把密钥存在钥匙串里,注意SymmetricKey不能直接存储,需要先转换成原始字节Data,读取时再还原:

存储密钥到钥匙串

func saveKey(_ key: SymmetricKey, account: String) throws {
    // 提取密钥的原始字节
    let keyData = key.withUnsafeBytes { Data(Array($0)) }
    
    // 钥匙串存储逻辑(这里用示例代码,你可以替换成自己的钥匙串封装)
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecValueData as String: keyData,
        kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
    ]
    
    // 删除旧密钥(如果存在)
    SecItemDelete(query as CFDictionary)
    
    // 添加新密钥
    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw NSError(domain: "KeychainError", code: Int(status), userInfo: nil)
    }
}

从钥匙串读取密钥

func readKey(account: String) throws -> SymmetricKey? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecReturnData as String: kCFBooleanTrue!,
        kSecMatchLimit as String: kSecMatchLimitOne
    ]
    
    var data: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &data)
    
    guard status == errSecSuccess, let keyData = data as? Data else {
        return nil
    }
    
    // 验证数据长度必须是32字节(ChaChaPoly密钥要求)
    guard keyData.count == 32 else {
        throw CryptoKitError.incorrectKeySize
    }
    
    return SymmetricKey(data: keyData)
}

修正加密/解密代码(去掉try!,改用安全的错误处理)

不要用try!强制解包,否则遇到错误直接崩溃,改用do-catch捕获错误,方便调试:

加密代码

func encrypt(rawData: Data, account: String) throws -> Data {
    guard let key = try readKey(account: account) else {
        throw NSError(domain: "EncryptionError", code: 0, userInfo: ["message": "密钥不存在"])
    }
    
    let sealedBox = try ChaChaPoly.seal(rawData, using: key)
    return sealedBox.combined
}

解密代码

func decrypt(encryptedData: Data, account: String) throws -> Data {
    guard let key = try readKey(account: account) else {
        throw NSError(domain: "DecryptionError", code: 0, userInfo: ["message": "密钥不存在"])
    }
    
    let sealedBox = try ChaChaPoly.SealedBox(combined: encryptedData)
    return try ChaChaPoly.open(sealedBox, using: key)
}

额外注意事项

  1. 密码编码一致性:每次把密码转Data时必须用同一种编码(比如utf8),不能随意切换,否则哈希结果会不同;
  2. 避免密码哈希的重复操作:不要在生成密钥时多次哈希密码,确保每次都是“原密码→SHA256→密钥”的流程;
  3. 钥匙串存储的访问权限:根据你的应用需求设置合适的kSecAttrAccessible属性,确保密钥的安全性。

按照这个流程修改后,重新登录时用相同密码生成的密钥就会和之前完全一致,解密认证失败的问题应该就能解决了!

内容的提问来源于stack exchange,提问作者Sir Sirae

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 08:00:41