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

如何实现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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 15:39:26