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

使用CryptoSwift实现AES加解密遇问题及存储格式咨询

解决CryptoSwift加解密失败及密钥存储、Firebase存储格式问题

1. 修复解密函数还原明文的问题

你的解密流程存在两处核心错误,同时密钥生成逻辑有遗漏,导致无法还原明文:

错误点及修复:

  • 密文解析错误:解密时直接使用cipherText.bytes会将hex字符串的每个字符转为UInt8,而非解析hex对应的二进制数据,需改为[UInt8](hex: cipherText)
  • 结果转换错误:解密后的UInt8数组是明文的二进制数据,应转成UTF-8字符串,而非hex字符串
  • Salt未保存:PKCS5.PBKDF2生成密钥时依赖salt,你当前每次生成随机salt但未保存,导致后续生成的密钥不一致,必须将salt与密钥绑定存储(比如存到钥匙串或Firebase用户文档)

修正后的加密解密及密钥生成代码:

class EncryptionService: EncryptionServicing {
    
    private var keychainService = KeychainService()
    
    // 修改为返回密钥和salt,需同时存储两者
    func generateKey() throws -> (key: String, salt: String)  {
        print("[EncryptionService] 🔑 Generate key called")
        do {
            if let userId = UserService.shared.userInfo?.userId {
                let userIdBytes: [UInt8] = Array(userId.utf8)
                // 生成随机salt(推荐长度至少8字节,增强安全性)
                let salt: [UInt8] = (1...8).map( {_ in UInt8.random(in: 0...255)} )

                let key = try PKCS5.PBKDF2(
                    password: userIdBytes,
                    salt: salt,
                    iterations: 4096,
                    keyLength: 32, /* AES-256 */
                    variant: .sha2(.sha256)
                ).calculate()

                return (key.toHexString(), salt.toHexString())
            }
        }
        catch {
            print(error)
        }
        throw NSError(domain: "EncryptionError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to generate key"])
    }
    
    func encrypt(clearText: String, aesKey: String) throws -> (cipherText: String, iv: String) {
        print("[EncryptionService] ✅ Encrypt called")
        do {
            let iv = AES.randomIV(AES.blockSize)
            let keyArray = [UInt8](hex: aesKey)
            
            let aes = try AES(key: keyArray, blockMode: CBC(iv: iv), padding: .pkcs7)
            let cipherTextBytes = try aes.encrypt(Array(clearText.utf8))
            
            // 改用Base64存储,节省空间
            guard let cipherTextBase64 = cipherTextBytes.toBase64(), let ivBase64 = iv.toBase64() else {
                throw NSError(domain: "EncryptionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to convert to Base64"])
            }
            return (cipherTextBase64, ivBase64)
        }
        catch {
            print(error)
            throw error
        }
    }

    func decrypt(cipherText: String, iv: String, aesKey: String) throws -> String {
        print("[EncryptionService] ✅ Decryption called")
        do {
            print("Ciphertext: \(cipherText)")
            print("Iv: \(iv)")
            print("aesKey: \(aesKey)")
            
            let keyArray = [UInt8](hex: aesKey)
            // 解析Base64格式的IV和密文
            guard let ivBytes = [UInt8](base64: iv), let cipherTextBytes = [UInt8](base64: cipherText) else {
                throw NSError(domain: "DecryptionError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid Base64 data"])
            }
            
            let aes = try AES(key: keyArray, blockMode: CBC(iv: ivBytes), padding: .pkcs7)
            
            print("AES Key size: \(aes.keySize)")
            
            let decryptedBytes = try aes.decrypt(cipherTextBytes)
            // 将解密后的二进制转成UTF-8字符串
            guard let decryptedText = String(bytes: decryptedBytes, encoding: .utf8) else {
                throw NSError(domain: "DecryptionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to convert bytes to string"])
            }
            return decryptedText
        } catch {
            print(error)
            throw error
        }
    }
}

ViewModel调整:

需保存salt并在需要时重新生成密钥,或者直接存储密钥(推荐存储密钥二进制到钥匙串):

@Published var dummyClearText: String = "Hello World!"
@Published var dummyCipherText: String = ""
@Published var dummyIv: String = ""
@Published var dummyDecryptedText: String = ""
@Published var aesKey: String = ""
@Published var salt: String = ""

// 生成密钥并存储
func generateAndStoreKey() {
    do {
        let result = try EncryptionService().generateKey()
        self.aesKey = result.key
        self.salt = result.salt
        // 这里将key和salt存到钥匙串,示例为简化直接保存
        // try keychainService.saveKey(data: Data(hex: result.key), service: "diary-key")
        // try keychainService.saveKey(data: Data(hex: result.salt), service: "diary-salt")
    } catch {
        print(error)
    }
}

func generateCipherText() {
    do {
        let result = try EncryptionService().encrypt(clearText: self.dummyClearText, aesKey: self.aesKey)
        self.dummyCipherText = result.cipherText
        self.dummyIv = result.iv
    }
    catch {
        print(error)
    }
}

func generateClearText() {
    do {
        let result = try EncryptionService().decrypt(cipherText: self.dummyCipherText, iv: self.dummyIv, aesKey: self.aesKey)
        self.dummyDecryptedText = result
    }
    catch {
        print(error)
    }
}

2. 密钥存储到钥匙串的格式

  • 优先存储二进制Data:不要存储hex或Base64字符串,直接将密钥的[UInt8]转成Data(bytes: keyArray),存储到钥匙串。这样既节省空间,又避免字符串解析的潜在错误。
  • 示例存储逻辑:
    // 存储密钥
    func saveKeyToKeychain(key: [UInt8], service: String) throws {
        let data = Data(bytes: key)
        // 调用钥匙串存储方法(需实现KeychainService的对应逻辑)
        try keychainService.save(data: data, service: service, account: "user-diary-key")
    }
    // 获取密钥
    func getKeyFromKeychain(service: String) throws -> [UInt8] {
        let data = try keychainService.read(service: service, account: "user-diary-key")
        return [UInt8](data)
    }
    
  • 钥匙串本身是系统级安全存储,二进制存储比字符串更高效、可靠。

3. 密文和IV存储到Firebase的格式选择

  • 优先使用Base64:Base64的空间利用率比Hex高约33%(相同二进制数据,Base64长度是Hex的2/3),能节省Firebase的存储资源和传输带宽。
  • Hex格式适用场景:仅当你需要人工查看密文内容时选择Hex,否则一律用Base64。
  • 代码中已调整为使用.toBase64(),注意处理可选值(因为二进制转Base64可能失败,需添加错误处理)。

内容的提问来源于stack exchange,提问作者Siddhant Mehta

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 13:55:38