一、什么是 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 == decrypted7.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=="十、与其他加密方案对比
| 方案 | 算法 | 密钥类型 | 特点 | 适用场景 |
|---|---|---|---|---|
| Fernet | AES-128-CBC | 对称(单一密钥) | 简单易用,防篡改 | 中小规模应用凭证存储 |
| cryptography (raw) | AES-256-GCM | 对称 | 更灵活,需要手动处理 IV | 高安全需求 |
| RSA | RSA-2048 | 非对称(公钥+私钥) | 适合密钥交换 | 跨系统通信 |
| KMS | 云服务 | 托管 | 密钥轮换、审计 | 生产环境大规模部署 |
Fernet 是中小型项目凭证加密的理想选择,兼顾了安全性和易用性。