如何实现Python与Swift的Fernet双向加密消息互通
Python与Swift双向加密互通实现方案
你之前遇到的兼容问题核心原因有两个:
- Fernet 不是原生AES-GCM算法,它有自定义的封包格式和算法组合,你之前写的Swift AES-GCM逻辑没有对齐Fernet规范,自然无法互通
- 你找到的ChaCha20-Poly1305方案没有统一两边的封包顺序、参数长度,才会出现单向可用的问题
下面给两个可直接落地的方案,按需选择即可。
方案1:统一使用标准AES-GCM(推荐,无额外依赖)
这个方案两边都用内置加密库实现,不需要引入第三方依赖,只要对齐核心参数和封包规则即可,调试成本最低:
提前约定统一规则:
- 对称密钥长度固定32字节(对应AES-256),密钥提前通过安全通道同步,统一用base64编码存储
- Nonce长度固定12字节,每次加密随机生成,不需要保密
- 认证标签长度固定16字节
- 最终封包顺序固定为
12字节Nonce + 密文 + 16字节Tag,二进制内容统一用标准base64编码成字符串传输 - 注意:两边统一使用标准base64编码,不要混用base64url,避免转码字符差异导致解密失败
Python侧实现(不需要用Fernet封装)
import os import base64 from cryptography.hazmat.primitives.ciphers.aead import AESGCM def aesgcm_encrypt(key: bytes, plaintext: str) -> str: cipher = AESGCM(key) nonce = os.urandom(12) # AESGCM.encrypt返回值为 密文+16字节Tag 的拼接结果 ciphertext_with_tag = cipher.encrypt(nonce, plaintext.encode("utf-8"), associated_data=None) return base64.b64encode(nonce + ciphertext_with_tag).decode("utf-8") def aesgcm_decrypt(key: bytes, encrypted_b64: str) -> str: cipher = AESGCM(key) raw_data = base64.b64decode(encrypted_b64) nonce = raw_data[:12] ciphertext_with_tag = raw_data[12:] plaintext = cipher.decrypt(nonce, ciphertext_with_tag, associated_data=None) return plaintext.decode("utf-8") # 密钥生成示例,生成一次后两边共用即可 # key = AESGCM.generate_key(bit_length=256) # shared_key_b64 = base64.b64encode(key).decode("utf-8")
Swift侧实现(用系统自带CryptoKit)
import CryptoKit import Foundation func aesgcmEncrypt(keyB64: String, plaintext: String) throws -> String { guard let keyData = Data(base64Encoded: keyB64) else { throw NSError(domain: "encrypt", code: -1) } let symmetricKey = SymmetricKey(data: keyData) let nonce = AES.GCM.Nonce() // 自动生成符合标准的12字节Nonce let sealedBox = try AES.GCM.seal(plaintext.data(using: .utf8)!, using: symmetricKey, nonce: nonce) // sealedBox.combined 天然符合 Nonce+密文+Tag 的顺序,直接转码即可 guard let combined = sealedBox.combined else { throw NSError(domain: "encrypt", code: -2) } return combined.base64EncodedString() } func aesgcmDecrypt(keyB64: String, encryptedB64: String) throws -> String { guard let keyData = Data(base64Encoded: keyB64), let encryptedData = Data(base64Encoded: encryptedB64) else { throw NSError(domain: "decrypt", code: -1) } let symmetricKey = SymmetricKey(data: keyData) let sealedBox = try AES.GCM.SealedBox(combined: encryptedData) let plaintextData = try AES.GCM.open(sealedBox, using: symmetricKey) guard let plaintext = String(data: plaintextData, encoding: .utf8) else { throw NSError(domain: "decrypt", code: -2) } return plaintext }
提示:这个方案和Fernet格式不兼容,但是逻辑透明,没有额外封装,只要两边代码严格按上面的规则写,双向加解密不会有任何兼容问题。
方案2:对齐Fernet规范实现Swift侧逻辑(兼容现有Fernet密文)
如果你必须兼容Python侧已经在用的Fernet密文,就不能用AES-GCM实现。Fernet的标准规范是固定的,和你之前的认知有差异:
- 算法组合是
AES-128-CBC + PKCS7填充 + HMAC-SHA256签名,不是AES-GCM - 密文格式固定为:
1字节版本号(固定0x80) + 8字节大端时间戳 + 16字节IV + AES-CBC密文 + 32字节HMAC签名 - 传入的Fernet密钥base64url解码后得到32字节,前16字节是CBC加密密钥,后16字节是HMAC签名密钥
Swift侧只要严格按照上面的规范组包、拆包、验签、加解密,不需要引入第三方Fernet库,手写100行以内代码就能完全兼容Python侧的Fernet接口。
你之前试的ChaCha20-Poly1305方案单向不通的问题,本质也是没有对齐封包规则,只要参考方案1的思路,统一Nonce长度、Tag位置、封包顺序、编码规则,双向打通非常简单,不需要找专门的跨语言绑定库。
内容的提问来源于stack exchange,提问作者DevShark
相关产品推荐
相关产品推荐

