欢迎光临
我们一直在努力

Python---Fernet 对称加密

Fernet 是 Python cryptography 库中封装的安全对称加密方案,专为解决新手使用对称加密时易踩的「密钥管理、IV 随机化、数据防篡改、重放攻击」等问题而设计。它基于 AES-128-CBC 对称加密算法,整合了 HMAC-SHA256 签名、时间戳验证、URL 安全编码等特性,是工业级的对称加密实现,无需开发者手动处理加密流程的底层细节。

一、Fernet 核心基础:对称加密与设计目标
1. 对称加密前置知识

对称加密的核心是加密和解密使用同一密钥,与非对称加密(公钥加密、私钥解密)相比,对称加密速度快、资源消耗低,适合高频次、小数据量的加密场景(如 API 密钥、用户令牌、配置文件加密)。但原生对称加密(如单纯的 AES)存在三大痛点:

  • 需要手动生成随机 IV(初始化向量),否则易被破解;
  • 无法验证数据是否被篡改;
  • 无时间戳机制,易遭受重放攻击(攻击者重复提交加密后的有效数据)。

Fernet 的核心设计目标就是封装这些痛点,让开发者只需关注「密钥管理」和「数据加解密」,无需处理底层加密细节。

2. Fernet 核心依赖与环境准备

Fernet 是 cryptography 库的子模块,需先安装依赖:

pip install cryptography

该库是由密码学专家维护的工业级库,避免了「手写加密算法」的安全风险。

二、Fernet 核心原理:令牌结构与加解密流程

Fernet 加密后生成的不是原始密文,而是一个结构化的令牌(Token),所有安全机制都封装在令牌中。理解令牌结构是掌握 Fernet 的核心。

1. Fernet 令牌的完整结构(字节级)

Fernet 令牌是 URL 安全的 Base64 编码字符串,解码后包含 5 个固定部分(总长度固定,无冗余):

字段长度(字节)作用
版本号 1 固定为 0x80(即十进制 128),标识 Fernet 版本,兼容未来迭代
时间戳 8 加密时的 Unix 时间戳(秒级),用于验证令牌有效期,防止重放攻击
IV(初始化向量) 16 随机生成的 AES-128-CBC 初始化向量,每次加密都不同,避免相同明文加密成相同密文
密文 可变 原始数据经 AES-128-CBC 加密后的结果(明文长度不足 16 字节时自动补位)
HMAC 签名 32 对「版本号+时间戳+IV+密文」的 HMAC-SHA256 签名,验证数据是否被篡改

关键特性:URL 安全 Base64 编码(替换 + 为 -、/ 为 _,去除 = 补位),确保令牌可安全传输(如 URL 参数、HTTP 头部)。

2. Fernet 加密流程

Fernet 加密并非简单的「明文→AES 加密→密文」,而是一套完整的安全流程:

  • 密钥验证:检查 Fernet 密钥是否为合法的 URL 安全 Base64 编码(44 字符);
  • 数据序列化:将明文(字符串/二进制/JSON 等)转换为字节流(bytes 类型);
  • 生成随机 IV:每次加密生成 16 字节随机 IV(AES-128-CBC 要求 IV 长度等于块大小 16 字节);
  • AES 加密:用 128 位 AES 密钥对「序列化明文+PKCS7 补位」执行 CBC 模式加密,生成密文;
  • 生成时间戳:记录当前 Unix 时间戳(8 字节);
  • HMAC 签名:用 256 位 HMAC 密钥对「版本号+时间戳+IV+密文」生成 32 字节签名;
  • 令牌拼接:将上述所有字段拼接后,做 URL 安全 Base64 编码,生成最终 Fernet 令牌。
  • 3. Fernet 解密流程

    解密是加密的逆向过程,且增加了多层安全验证:

  • 令牌解码:将 URL 安全 Base64 令牌解码为字节流,拆分出各字段;
  • 签名验证:用 HMAC 密钥重新计算「版本号+时间戳+IV+密文」的签名,与令牌中的签名对比,不一致则抛出 InvalidToken 异常(防篡改);
  • 时间戳验证:若设置了 TTL(有效期),检查当前时间与令牌时间戳的差值,超过 TTL 则抛出 TimeoutError(防重放);
  • AES 解密:用 AES 密钥和 IV 对密文执行 CBC 模式解密,去除 PKCS7 补位;
  • 数据反序列化:将解密后的字节流还原为原始数据(字符串/JSON 等)。
  • 4. Fernet 密钥的组成

    Fernet 密钥不是随机字符串,而是固定结构的加密密钥:

    • 原始密钥:48 字节(384 位),分为两部分:
      • 前 32 字节:AES-128-CBC 加密密钥(实际仅用前 16 字节,AES-128 要求 16 字节密钥);
      • 后 16 字节:HMAC-SHA256 签名密钥;
    • 编码后:48 字节原始密钥经 URL 安全 Base64 编码后,得到 44 字符的 Fernet 密钥(如 gAAAAABkG…)。

    生成合法密钥的唯一方式:使用 Fernet.generate_key() 方法,禁止手动拼接/修改密钥。

    三、Fernet 实战:核心用法与代码示例
    1. 基础用法:生成密钥+加解密

    from cryptography.fernet import Fernet

    # 1. 生成合法的 Fernet 密钥(仅需生成一次,需安全存储)
    key = Fernet.generate_key()
    print("Fernet 密钥:", key.decode()) # 输出示例:gAAAAABkG98…

    # 2. 初始化 Fernet 实例(核心对象,所有加解密操作通过实例完成)
    fernet = Fernet(key)

    # 3. 加密数据(仅支持 bytes 类型,字符串需 encode)
    plaintext = "我的敏感数据:123456"
    cipher_token = fernet.encrypt(plaintext.encode("utf-8"))
    print("加密令牌:", cipher_token.decode()) # 输出 URL 安全的 Base64 字符串

    # 4. 解密数据(自动验证签名和时间戳)
    decrypted_bytes = fernet.decrypt(cipher_token)
    decrypted_text = decrypted_bytes.decode("utf-8")
    print("解密结果:", decrypted_text) # 输出:我的敏感数据:123456

    2. 进阶用法1:带 TTL 的解密(防重放攻击)

    通过 decrypt(token, ttl=秒数) 设置令牌有效期,超过有效期则解密失败:

    import time

    # 加密令牌
    token = fernet.encrypt(b"临时数据:5分钟有效")

    # 正常解密(5分钟内)
    fernet.decrypt(token, ttl=300) # ttl=300 秒=5分钟

    # 模拟超过有效期(睡眠 301 秒)
    time.sleep(301)
    try:
    fernet.decrypt(token, ttl=300)
    except fernet.TimeoutError:
    print("令牌已过期,解密失败") # 触发该提示

    3. 进阶用法2:密钥轮换(生产环境必备)

    生产环境中密钥需定期轮换,Fernet 支持「多密钥解密」(新密钥加密,旧密钥仍可解密):

    # 生成新旧两个密钥
    old_key = Fernet.generate_key()
    new_key = Fernet.generate_key()

    # 用旧密钥加密
    old_fernet = Fernet(old_key)
    token = old_fernet.encrypt(b"需要轮换密钥的数据")

    # 密钥轮换:创建多密钥实例(优先用新密钥加密,旧密钥解密)
    multi_fernet = Fernet.MultiFernet([Fernet(new_key), Fernet(old_key)])

    # 用多密钥实例解密(自动匹配可用密钥)
    decrypted = multi_fernet.decrypt(token)
    print(decrypted.decode()) # 正常解密

    # 用新密钥重新加密(完成密钥轮换)
    new_token = multi_fernet.encrypt(decrypted)

    4. 进阶用法3:异常处理

    Fernet 解密时可能抛出多种异常,需针对性捕获:

    from cryptography.fernet import InvalidToken

    try:
    # 篡改令牌(模拟数据被修改)
    tampered_token = cipher_token[:1] + b"x"
    fernet.decrypt(tampered_token)
    except InvalidToken:
    print("令牌被篡改,解密失败") # 触发签名验证失败
    except TimeoutError:
    print("令牌已过期")
    except Exception as e:
    print(f"其他加密错误:{e}")

    四、Fernet 安全特性(核心优势)
  • 不可伪造性:HMAC-SHA256 签名确保令牌一旦被篡改,解密时直接抛出 InvalidToken,无法绕过;
  • 防重放攻击:时间戳+TTL 机制限制令牌有效期,即使令牌泄露,攻击者也无法长期使用;
  • 数据保密性:AES-128-CBC 是 NIST 认证的安全算法,随机 IV 避免「相同明文→相同密文」的字典攻击;
  • 易用性:封装所有底层细节(IV 生成、补位、签名、编码),新手无需了解密码学细节即可安全使用;
  • 兼容性:URL 安全 Base64 编码的令牌可在 URL、HTTP 头、JSON 中安全传输,无编码冲突。
  • 五、Fernet 局限性
  • 对称加密固有缺陷:密钥分发难,一旦密钥泄露,所有加密数据都可被解密;需通过密钥管理服务(KMS)、环境变量、加密存储等方式管理密钥(绝对禁止硬编码到代码中);
  • 算法固定:仅支持 AES-128-CBC,无法自定义加密算法(如需 AES-256,需手动封装);
  • 性能限制:不支持分块加密大文件(单令牌加密数据建议 < 1MB,大文件需手动分块+多令牌);
  • 无身份认证:仅验证数据完整性,无法验证加密者身份(如需身份认证,需结合非对称加密)。
  • 六、工程项目注意
  • 密钥存储:使用环境变量(os.getenv("FERNET_KEY"))、云厂商 KMS(AWS KMS/阿里云 KMS)、本地加密文件存储,禁止硬编码;
  • 密钥轮换:每 3-6 个月轮换一次密钥,通过 MultiFernet 平滑过渡;
  • 异常监控:捕获 InvalidToken/TimeoutError 并告警,排查是否有篡改/重放攻击;
  • 数据类型:加密 JSON 数据时,先 json.dumps() 序列化,再加密;解密后 json.loads() 反序列化;
  • 令牌传输:通过 HTTPS 传输令牌,避免明文传输。

  • Yolo模型加密示例

    以下给出一个实际加密yolo训练的模型的示例代码

    import os
    from pathlib import Path
    from cryptography.fernet import Fernet

    class LegacyEncryptor:
    def __init__(self, exp_root):
    self.exp_root = Path(exp_root)
    if not self.exp_root.exists():
    raise ValueError(f"实验目录不存在: {self.exp_root}")

    def encrypt_experiment(self, exp_name):
    """加密单个实验目录"""
    exp_dir = self.exp_root / exp_name
    if not exp_dir.exists():
    print(f"跳过不存在的实验目录: {exp_name}")
    return

    weights_dir = exp_dir / "weights"
    if not weights_dir.exists():
    print(f"{exp_name} 无weights目录,跳过")
    return

    # 生成密钥
    key = Fernet.generate_key()
    cipher = Fernet(key)

    # 加密所有pt文件
    encrypted_files = []
    for pt_file in weights_dir.glob("*.pt"):
    # 加密
    with open(pt_file, "rb") as f:
    data = f.read()
    encrypted_data = cipher.encrypt(data)

    # 保存加密文件
    enc_file = pt_file.with_suffix(".enc")
    with open(enc_file, "wb") as f:
    f.write(encrypted_data)

    # 删除原始文件
    # os.remove(pt_file)
    # encrypted_files.append(enc_file.name)

    # 保存密钥到实验目录
    key_file = exp_dir / "model.key"
    with open(key_file, "wb") as f:
    f.write(key)

    print(f"加密完成: {exp_name}")
    print(f"生成加密文件: {', '.join(encrypted_files)}")
    print(f"密钥位置: {key_file}\\n")

    def batch_encrypt(self, exp_prefix="exp"):
    """批量加密所有匹配的实验目录"""
    exp_dirs = sorted([
    d.name for d in self.exp_root.iterdir()
    if d.is_dir() and d.name.startswith(exp_prefix)
    ], key=lambda x: int(x[len(exp_prefix):]) if x[len(exp_prefix):].isdigit() else 0)

    for exp_name in exp_dirs:
    self.encrypt_experiment(exp_name)

    if __name__ == "__main__":
    # 使用示例 —————————————————-
    # EXP_ROOT = "/home/ysj/ncf/yolov5/runs/train" # 修改为实际路径
    EXP_ROOT = r"D:\\code\\yolov5\\runs\\train7" # 修改为实际路径

    encryptor = LegacyEncryptor(EXP_ROOT)

    # 加密单个实验
    encryptor.encrypt_experiment("exp")

    # 加密所有exp开头的实验目录
    # encryptor.batch_encrypt()


    睡眠等同于希望。每次醒来都是一个新的开始,一个新的希望。 —E·M·齐奥朗

    赞(0)
    未经允许不得转载:171主机测评 » Python---Fernet 对称加密
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址