Skip to content

一、什么是 Fernet ​

Fernet 是 cryptography 库提供的一种对称加密实现,基于 AES-128-CBC 算法,使用 Fernet 密钥进行加解密。

┌─────────────────────────────────────────────────────────────────┐
│                       Fernet 加密体系                           │
├─────────────────────────────────────────────────────────────────┤
│  算法: AES-128-CBC(对称加密)                                  │
│  密钥: 32 字节 URL-safe Base64 编码(44 字符)                  │
│  特点: 加密 + 签名,防篡改                                       │
└─────────────────────────────────────────────────────────────────┘

二、Fernet 密钥格式 ​

python
# Fernet 密钥结构(共 44 字符 URL-safe Base64)
# 版本(1字节) + 时间戳(8字节) + 盐(16字节) + 密文(16字节倍数) + 签名(32字节)

密钥格式:
┌────────┬────────────┬──────┬──────────────┬──────────┐
│ Version│ Timestamp  │ Salt │  Ciphertext  │  Sign    │
│ 1 byte │  8 bytes   │16byte│  16N bytes   │ 32 bytes │
└────────┴────────────┴──────┴──────────────┴──────────┘
   │         │          │          │              │
   ▼         ▼          ▼          ▼              ▼
  版本号    时间戳      随机盐     AES加密内容    HMAC签名

生成密钥示例:

python
from cryptography.fernet import Fernet

# 生成一个新密钥
key = Fernet.generate_key()
print(key)
# 输出: b'AbCdEfGhIjKlMnOpQrStUvWxYZ1234567890ABCD==' (44字符)

三、Fernet 在项目中的使用 ​

3.1 项目配置 (app/config.py) ​

python
# 文件: app/config.py, 行 24
secret_encryption_key: str = "CHANGE_ME_GENERATE_A_FERNET_KEY"

⚠️ 生产环境必须使用 Fernet.generate_key() 生成真正的密钥

3.2 加密实现 (app/db/crypto.py) ​

python
# 文件: app/db/crypto.py, 完整代码
"""
凭证字段加密。
所有密钥/密码/私钥落库前用 Fernet 对称加密,绝不明文存储。
主密钥来自环境变量 SECRET_ENCRYPTION_KEY。
登录会话令牌也用同一把 Fernet 密钥签名(Fernet 自带时间戳,可按 TTL 过期)。
"""
import json

from cryptography.fernet import Fernet, InvalidToken

from app.config import get_settings


def _fernet() -> Fernet:
    """获取 Fernet 实例"""
    key = get_settings().secret_encryption_key.encode()  # 行 15: 读取密钥
    return Fernet(key)                                    # 行 16: 创建 Fernet 实例


def encrypt(plaintext: str | None) -> str | None:
    """加密函数"""
    if plaintext is None or plaintext == "":
        return plaintext
    return _fernet().encrypt(plaintext.encode()).decode()  # 行 22: 加密


def decrypt(ciphertext: str | None) -> str | None:
    """解密函数"""
    if ciphertext is None or ciphertext == "":
        return ciphertext
    return _fernet().decrypt(ciphertext.encode()).decode()  # 行 28: 解密


def sign_token(payload: dict) -> str:
    """签名会话令牌"""
    return _fernet().encrypt(json.dumps(payload).encode()).decode()  # 行 33


def verify_token(token: str | None, max_age: int | None = None) -> dict | None:
    """验证会话令牌(带 TTL 过期)"""
    if not token:
        return None
    try:
        raw = _fernet().decrypt(token.encode(), ttl=max_age)  # 行 40: 解密+TTL验证
        return json.loads(raw)
    except (InvalidToken, ValueError):
        return None

四、Fernet 加密原理图解 ​

4.1 加密过程 ​

┌─────────────────────────────────────────────────────────────────┐
│                       加密过程 (encrypt)                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   明文: "my_secret_password"                                    │
│         │                                                       │
│         ▼                                                       │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │              Fernet 密钥 (32字节)                        │  │
│   │   "AbCdEfGhIjKlMnOpQrStUvWxYZ1234567890ABCD=="          │  │
│   └─────────────────────────────────────────────────────────┘  │
│         │                                                       │
│         ▼                                                       │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │  1. 生成随机 Salt (16字节)                               │  │
│   │  2. 用 PBKDF2 从密钥派生子密钥                           │  │
│   │  3. 生成随机 IV (16字节)                                 │  │
│   │  4. AES-128-CBC 加密明文                                │  │
│   │  5. 计算 HMAC-SHA256 签名                               │  │
│   │  6. 拼接: Version + Timestamp + Salt + IV + Cipher + Sig │  │
│   └─────────────────────────────────────────────────────────┘  │
│         │                                                       │
│         ▼                                                       │
│   密文: "gAAAAABk...(URL-safe Base64 编码)"                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4.2 解密过程 ​

┌─────────────────────────────────────────────────────────────────┐
│                       解密过程 (decrypt)                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   密文: "gAAAAABk...(URL-safe Base64 编码)"                  │
│         │                                                       │
│         ▼                                                       │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │  1. Base64 解码                                          │  │
│   │  2. 提取 Version, Timestamp, Salt, IV, Cipher, Sign      │  │
│   │  3. 验证 HMAC 签名(防篡改)                             │  │
│   │  4. 用 PBKDF2 派生密钥                                   │  │
│   │  5. AES-128-CBC 解密                                     │  │
│   │  6. 返回明文                                             │  │
│   └─────────────────────────────────────────────────────────┘  │
│         │                                                       │
│         ▼                                                       │
│   明文: "my_secret_password"                                    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

五、项目中加密的字段 ​

┌─────────────────────────────────────────────────────────────────┐
│                    所有加密字段一览                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  表: model_providers                                            │
│  ├── api_key_enc          ← LLM API Key                        │
│                                                                 │
│  表: ssh_keys                                                 │
│  ├── private_key_enc      ← SSH 私钥                           │
│  └── passphrase_enc       ← 私钥密码短语                        │
│                                                                 │
│  表: servers                                                  │
│  ├── password_enc         ← SSH 密码                           │
│  ├── private_key_enc      ← SSH 私钥                           │
│  └── passphrase_enc       ← 私钥密码短语                        │
│                                                                 │
│  表: cloud_accounts                                           │
│  └── secrets_enc          ← 云账号密钥(字典格式)              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

六、会话令牌签名与验证 ​

6.1 签名过程 (sign_token) ​

python
# 文件: app/db/crypto.py, 行 32-33
def sign_token(payload: dict) -> str:
    return _fernet().encrypt(json.dumps(payload).encode()).decode()
┌─────────────────────────────────────────────────────────────────┐
│                     会话令牌签名流程                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   用户信息: {"u": "admin", "t": 1719567800}                     │
│         │                                                       │
│         ▼                                                       │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │  JSON 序列化: '{"u": "admin", "t": 1719567800}'          │  │
│   │                    ↓                                     │  │
│   │  UTF-8 编码: b'{"u": "admin", "t": 1719567800}'          │  │
│   │                    ↓                                     │  │
│   │  Fernet 加密(自动添加时间戳+签名)                       │  │
│   │                    ↓                                     │  │
│   │  Base64 编码 → URL-safe 字符串                           │  │
│   └─────────────────────────────────────────────────────────┘  │
│         │                                                       │
│         ▼                                                       │
│   令牌: "gAAAAABk...(44字符 Base64)"                           │
│         │                                                       │
│         ▼                                                       │
│   Set-Cookie: oa_session=gAAAAABk...; HttpOnly; SameSite=Lax   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

6.2 验证过程 (verify_token) ​

python
# 文件: app/db/crypto.py, 行 36-43
def verify_token(token: str | None, max_age: int | None = None) -> dict | None:
    if not token:
        return None
    try:
        raw = _fernet().decrypt(token.encode(), ttl=max_age)
        return json.loads(raw)
    except (InvalidToken, ValueError):
        return None
┌─────────────────────────────────────────────────────────────────┐
│                     会话令牌验证流程                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   请求 Cookie: oa_session=gAAAAABk...                          │
│         │                                                       │
│         ▼                                                       │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │  Base64 解码 → 二进制数据                                │  │
│   │                    ↓                                     │  │
│   │  提取时间戳 + 验证 HMAC 签名                             │  │
│   │       │                                                  │  │
│   │       ├── 签名无效 → InvalidToken → 返回 None            │  │
│   │       │                                                  │  │
│   │       └── 时间戳过期(> max_age)→ InvalidToken → 返回 None │
│   │                    ↓                                     │  │
│   │  AES 解密 → UTF-8 解码 → JSON 解析                       │  │
│   └─────────────────────────────────────────────────────────┘  │
│         │                                                       │
│         ▼                                                       │
│   用户信息: {"u": "admin", "t": 1719567800}                     │
│         │                                                       │
│         ▼                                                       │
│   验证 u == settings.auth_username                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

七、使用示例 ​

7.1 基础加解密 ​

python
from cryptography.fernet import Fernet

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

# 加密
plaintext = "my_ssh_password_123"
ciphertext = fernet.encrypt(plaintext.encode())
print(f"加密: {ciphertext}")
# 输出: b'gAAAAABk...' (每次加密结果不同,因为有随机 Salt 和 IV)

# 解密
decrypted = fernet.decrypt(ciphertext).decode()
print(f"解密: {decrypted}")
# 输出: my_ssh_password_123

# 验证
assert plaintext == decrypted

7.2 带 TTL 的令牌验证 ​

python
from cryptography.fernet import Fernet
import time

key = Fernet.generate_key()
fernet = Fernet(key)

# 创建令牌(有效期 60 秒)
token = fernet.encrypt(b"session_data")
print(f"令牌: {token}")

# 30 秒后验证 - 成功
print(fernet.decrypt(token, ttl=60))  # b'session_data'

# 90 秒后验证 - 失败(过期)
try:
    fernet.decrypt(token, ttl=60)  # 抛出 InvalidToken
except Exception as e:
    print(f"过期: {e}")

7.3 防篡改测试 ​

python
from cryptography.fernet import Fernet

key = Fernet.generate_key()
fernet = Fernet(key)

# 加密
ciphertext = fernet.encrypt(b"important_data")
print(f"原始密文: {ciphertext}")

# 尝试篡改(修改最后一个字符)
tampered = ciphertext[:-1] + b'A'  # 篡改最后一位
print(f"篡改后: {tampered}")

# 解密验证
try:
    ferne.decrypt(tampered)
    print("解密成功(不应该发生)")
except Exception as e:
    print(f"解密失败(预期): {type(e).__name__}")  # InvalidToken

八、安全特性总结 ​

特性说明
对称加密加密和解密使用同一密钥
AES-128-CBC业界标准加密算法
HMAC 签名防篡改,检测密文修改
PBKDF2 密钥派生从主密钥派生子密钥,增强安全性
随机 Salt防止彩虹表攻击
随机 IV相同明文每次加密结果不同
时间戳验证支持 TTL 过期
URL-safe Base64适合 HTTP 传输

九、注意事项 ​

python
# ⚠️ 项目中的一些安全注意事项

# 1. 密钥必须妥善保管
# 丢失密钥 = 无法解密已存储的凭证
# 泄露密钥 = 所有凭证可被解密

# 2. 生产环境必须生成真正的密钥
# 不要使用默认占位符
key = Fernet.generate_key()  # 生成方式

# 3. 推荐通过环境变量配置密钥
# .env 文件
SECRET_ENCRYPTION_KEY="AbCdEfGhIjKlMnOpQrStUvWxYZ1234567890ABCD=="

十、与其他加密方案对比 ​

方案算法密钥类型特点适用场景
FernetAES-128-CBC对称(单一密钥)简单易用,防篡改中小规模应用凭证存储
cryptography (raw)AES-256-GCM对称更灵活,需要手动处理 IV高安全需求
RSARSA-2048非对称(公钥+私钥)适合密钥交换跨系统通信
KMS云服务托管密钥轮换、审计生产环境大规模部署

Fernet 是中小型项目凭证加密的理想选择,兼顾了安全性和易用性。

Released under the MIT License.