如何在Swift中解密Python cryptography库生成的Fernet加密文本?
Fernet密文Swift解密失败解决方案
错误原因
当前实现存在几个核心逻辑错误导致解密失败:
- 直接对Base64编码后的密文字符串做偏移拆分,Fernet的字段拆分规则是针对解码后的二进制数据的,编码后字符串的偏移和二进制偏移完全不对应,拆分出来的IV和密文自然是错误的
- 没有正确拆分Fernet密钥:Fernet标准密钥是Base64URL编码的32字节二进制数据,前16字节为HMAC-SHA256签名校验密钥,后16字节才是AES解密密钥
- 未做签名校验就直接解密,Fernet密文末尾携带32字节的HMAC签名,必须先校验签名合法性再执行解密操作
- 未处理URL安全Base64的编码适配,Fernet用的是
-/_替代+//的URL安全Base64标准,普通Base64解码前需要先做字符替换、补全补位等号
正确解密步骤
- 先对密钥和密文字符串做URL安全Base64转标准Base64处理,再解码为二进制数组
- 拆分密钥:取解码后密钥的后16字节作为AES密钥,前16字节作为HMAC校验密钥
- 拆分解码后的密文二进制:
- 偏移0:1字节版本号
- 偏移1:8字节时间戳
- 偏移9:16字节IV
- 偏移25到末尾前32字节:AES密文
- 末尾32字节:HMAC签名
- 先校验HMAC签名:将版本号、时间戳、IV、密文拼接后用HMAC-SHA256和签名密钥计算签名,和密文携带的签名对比,一致再继续
- 用AES-CBC模式、PKCS7填充执行解密,得到明文
可运行Swift代码(基于CryptoSwift)
import CryptoSwift func fernetDecrypt(keyStr: String, encStr: String) throws -> String { // 1. 处理URL安全Base64转标准Base64辅助函数 func urlSafeBase64ToStandard(_ str: String) -> String { var res = str .replacingOccurrences(of: "-", with: "+") .replacingOccurrences(of: "_", with: "/") // 补全补位等号 let padding = res.count % 4 if padding > 0 { res += String(repeating: "=", count: 4 - padding) } return res } // 2. 解码并拆分密钥 let standardKeyStr = urlSafeBase64ToStandard(keyStr) guard let keyData = Data(base64Encoded: standardKeyStr), keyData.count == 32 else { throw NSError(domain: "FernetError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的Fernet密钥"]) } let aesKey = Array(keyData[16..<32]) // 后16字节为AES解密密钥 let hmacKey = Array(keyData[0..<16]) // 前16字节为HMAC校验密钥 // 3. 解码密文 let standardEncStr = urlSafeBase64ToStandard(encStr) guard let encData = Data(base64Encoded: standardEncStr), encData.count >= 1+8+16+32 else { throw NSError(domain: "FernetError", code: -2, userInfo: [NSLocalizedDescriptionKey: "无效的Fernet密文"]) } let encBytes = Array(encData) // 4. 拆分密文字段 guard encBytes[0] == 0x80 else { // Fernet固定版本号为0x80 throw NSError(domain: "FernetError", code: -3, userInfo: [NSLocalizedDescriptionKey: "不支持的Fernet版本"]) } let iv = Array(encBytes[9..<25]) // 偏移9开始的16字节为IV let cipherText = Array(encBytes[25..<encBytes.count-32]) // 偏移25到末尾前32字节为密文 let hmacSign = Array(encBytes[encBytes.count-32..<encBytes.count]) // 末尾32字节为HMAC签名 // 5. 校验HMAC签名 let signedContent = Array(encBytes[0..<encBytes.count-32]) let calculatedSign = try HMAC(key: hmacKey, variant: .sha256).authenticate(signedContent) guard calculatedSign == hmacSign else { throw NSError(domain: "FernetError", code: -4, userInfo: [NSLocalizedDescriptionKey: "签名校验失败,密文被篡改"]) } // 6. AES解密得到明文 let aes = try AES(key: aesKey, blockMode: CBC(iv: iv), padding: .pkcs7) let decryptedBytes = try aes.decrypt(cipherText) guard let plaintext = String(bytes: decryptedBytes, encoding: .utf8) else { throw NSError(domain: "FernetError", code: -5, userInfo: [NSLocalizedDescriptionKey: "明文编码错误"]) } return plaintext } // 测试调用 func testDecrypt() { let key = "3b-Nqg6ry-jrAuDyVjSwEe8wrdyEPQfPuOQNH1q5olE=" let enc = "gAAAAABhBRBGKSwa7AluNJYhwWaHrQGwAA8UpMH8Wtw3tEoTD2E_-nbeoAvxbtBpFiC0ZjbVne_ZetFinKSyMjxwWaPRnXVSVqz5QqpUXp6h-34_TL7BaDs=" do { let plain = try fernetDecrypt(keyStr: key, encStr: enc) print("解密成功,明文:", plain) // 输出:my deep dark secret } catch { print("解密失败:", error.localizedDescription) } }
替换原有解密逻辑后调用testDecrypt()即可得到正确明文。
内容的提问来源于stack exchange,提问作者Syed Faizan
相关产品推荐
相关产品推荐

