加密技能
名称: 加密 版本: 1.0.0 领域: 安全/密码学 风险等级: 高 语言: [python, typescript, rust, go] 框架: [sqlcipher, cryptography, libsodium] 需要安全审查: 是 合规: [GDPR, HIPAA, PCI-DSS, SOC2] 最后更新: 2025-01-15
强制阅读协议: 在实施任何加密之前,阅读
references/advanced-patterns.md获取密钥派生信息,以及references/security-examples.md获取实现模式。
1. 概述
1.1 目的和范围
此技能为 JARVIS AI 助手提供了安全默认的实现加密模式,涵盖:
- SQLCipher: 使用 AES-256-GCM 的加密 SQLite 数据库
- Argon2id: 内存硬度密钥派生函数
- 密钥管理: 安全生成、存储、轮换和销毁
- 安全内存: 防止内存泄露攻击
1.2 风险评估
风险等级: 高
理由:
- 加密失败会暴露所有受保护数据
- 密钥泄露导致完全机密性丧失
- 实现错误是灾难性的且往往无法检测
- 违反法规(GDPR、HIPAA、PCI-DSS)会带来严重处罚
攻击面:
- 密钥派生弱点
- 不安全随机数生成
- 时序侧信道
- 内存泄露(冷启动、崩溃转储)
- 跨上下文密钥重用
2. 核心职责
2.1 主要功能
- 加密静态数据 使用 AES-256-GCM 进行认证加密
- 安全派生密钥 使用 Argon2id 和适当参数
- 管理密钥生命周期 包括轮换、托管和销毁
- 保护密钥材料 在内存中和操作期间
- 集成操作系统密钥链 用于主密钥存储
2.2 核心原则
- 测试驱动开发优先 - 先写测试;测试加密/解密往返、认证失败和边缘情况
- 性能意识 - 缓存派生密钥,对大数据使用流处理,利用硬件加速
- 安全默认 - 使用认证加密模式、内存硬度 KDF、安全随机源
- 深度防御 - 多层保护,安全失败,最小化密钥暴露
2.3 安全原则
- 绝不 实现自定义加密算法
- 绝不 使用 ECB 模式或未认证加密
- 始终 使用加密安全随机数生成器
- 始终 在解密前验证密文真实性
- 始终 使用恒定时间比较认证标签
3. 实现工作流(TDD)
步骤 1: 先写失败测试
import pytest
from cryptography.exceptions import InvalidTag
class TestEncryptionTDD:
"""TDD 测试加密实现。"""
def test_encrypt_decrypt_roundtrip(self):
"""测试加密后解密返回原始数据。"""
from jarvis.security.encryption import SecureEncryption
key = secrets.token_bytes(32)
encryptor = SecureEncryption(key)
plaintext = b"sensitive data for JARVIS"
ciphertext = encryptor.encrypt(plaintext)
decrypted = encryptor.decrypt(ciphertext)
assert decrypted == plaintext
assert ciphertext != plaintext # 必须加密
def test_tampered_ciphertext_raises_error(self):
"""测试篡改的密文被拒绝。"""
from jarvis.security.encryption import SecureEncryption
key = secrets.token_bytes(32)
encryptor = SecureEncryption(key)
ciphertext = encryptor.encrypt(b"secret")
tampered = ciphertext[:-1] + bytes([ciphertext[-1] ^ 0xFF])
with pytest.raises(InvalidTag):
encryptor.decrypt(tampered)
def test_key_derivation_consistency(self):
"""相同密码 + 盐 = 相同密钥;不同盐 = 不同密钥。"""
from jarvis.security.encryption import SecureKeyDerivation
password = "strong_password_123"
salt = secrets.token_bytes(16)
key1, _ = SecureKeyDerivation.derive_key(password, salt)
key2, _ = SecureKeyDerivation.derive_key(password, salt)
assert key1 == key2 and len(key1) == 32
key3, salt3 = SecureKeyDerivation.derive_key(password)
assert key1 != key3 # 不同盐 = 不同密钥
步骤 2: 实现最小通过
只实现通过测试所需的内容。从基本加密/解密开始,然后添加密钥派生。
步骤 3: 遵循模式重构
测试通过后,添加:内存保护、错误处理、AAD 支持、密钥缓存。
步骤 4: 运行完整验证
# 运行加密测试并覆盖
pytest tests/security/test_encryption.py -v --cov=jarvis.security.encryption --cov-fail-under=90
# 运行安全特定测试
pytest tests/security/ -k "encryption or crypto" -v
# 检查时序漏洞
pytest tests/security/test_timing.py -v
# 验证输出中无秘密
pytest --log-cli-level=DEBUG 2>&1 | grep -i "key\|secret\|password" && echo "WARNING: Secrets in logs!"
4. 技术栈
4.1 推荐库
| 语言 | 库 | 版本 | 备注 |
|---|---|---|---|
| Python | cryptography |
>=42.0.0 | 使用 OpenSSL 3.x 后端 |
| Python | argon2-cffi |
>=23.1.0 | 参考 Argon2 实现 |
| TypeScript | @noble/ciphers |
>=0.5.0 | 审计过的纯 JS 实现 |
| Rust | ring |
>=0.17.0 | BoringSSL 支持 |
| Go | crypto/cipher |
标准库 | 与 golang.org/x/crypto 使用 |
4.2 SQLCipher 配置
最小版本: SQLCipher 4.5.6+(包括 SQLite 3.44.2)
# SQLCipher 安全配置
SQLCIPHER_PRAGMAS = {
'key': None, # 通过安全密钥注入设置
'cipher': 'aes-256-gcm',
'kdf_iter': 256000, # PBKDF2 迭代
'cipher_page_size': 4096,
'cipher_kdf_algorithm': 'PBKDF2_HMAC_SHA512',
'cipher_hmac_algorithm': 'HMAC_SHA512',
'cipher_plaintext_header_size': 0,
}
5. 性能模式
5.1 密钥缓存
坏: 每次操作派生密钥(约 500ms 每次 Argon2id 调用)
好 - 缓存带 TTL:
class CachedKeyManager:
def __init__(self, cache_ttl: int = 300):
self._cache: dict[str, tuple[bytes, float]] = {}
self._ttl = cache_ttl
def get_key(self, password: str, salt: bytes) -> bytes:
cache_key = f"{hash(password)}:{salt.hex()}"
if cache_key in self._cache:
key, ts = self._cache[cache_key]
if time.time() - ts < self._ttl:
return key
key, _ = SecureKeyDerivation.derive_key(password, salt)
self._cache[cache_key] = (key, time.time())
return key
5.2 大数据的流加密
坏: data = f.read() 将整个文件加载到内存
好 - 流处理带分块(64KB 块):
nonce = secrets.token_bytes(12)
encryptor = Cipher(algorithms.AES(key), modes.GCM(nonce)).encryptor()
with open(input_path, 'rb') as fin, open(output_path, 'wb') as fout:
fout.write(nonce)
while chunk := fin.read(64 * 1024):
fout.write(encryptor.update(chunk))
fout.write(encryptor.finalize() + encryptor.tag)
5.3 硬件加速
坏: 无 OpenSSL 后端的 PyCryptodome(慢 10-100 倍)
好: 使用 cryptography 库 - 通过 OpenSSL 3.x 后端自动检测 AES-NI
5.4 批量操作
坏 - 带附加的单个循环:
results = []
for record in records:
results.append(encryptor.encrypt(record))
好 - 使用单个加密器的列表推导:
encryptor = SecureEncryption(key)
results = [encryptor.encrypt(record) for record in records]
# 对于大批量,使用 ProcessPoolExecutor 进行并行化
5.5 内存安全密钥处理
坏 - 密钥保留在内存中:
self.key = SecureKeyDerivation.derive_key(password) # 从未清除
好 - 使用后归零密钥的上下文管理器:
import ctypes
class SecureKeyHolder:
def __init__(self, password: str):
self._key, self.salt = SecureKeyDerivation.derive_key(password)
def __exit__(self, *args):
if self._key:
key_buffer = (ctypes.c_char * len(self._key)).from_buffer_copy(self._key)
ctypes.memset(key_buffer, 0, len(self._key))
self._key = None
# 用法: with SecureKeyHolder(password) as kh: encrypt(kh._key, data)
6. 实现模式
6.1 使用 Argon2id 派生密钥
from argon2 import PasswordHasher
from argon2.low_level import hash_secret_raw, Type
import secrets
class SecureKeyDerivation:
"""使用 Argon2id 从密码派生加密密钥。"""
# OWASP 推荐参数用于敏感数据
TIME_COST = 3 # 迭代次数
MEMORY_COST = 65536 # 64 MiB
PARALLELISM = 4 # 线程数
HASH_LEN = 32 # 256 位用于 AES-256
SALT_LEN = 16 # 128 位最小
@classmethod
def derive_key(cls, password: str, salt: bytes = None) -> tuple[bytes, bytes]:
"""
从密码派生 256 位密钥。
返回:
tuple: (派生密钥, 盐) 用于存储
"""
if salt is None:
salt = secrets.token_bytes(cls.SALT_LEN)
# 验证输入
if not password or len(password) < 12:
raise ValueError("密码必须至少 12 个字符")
key = hash_secret_raw(
secret=password.encode('utf-8'),
salt=salt,
time_cost=cls.TIME_COST,
memory_cost=cls.MEMORY_COST,
parallelism=cls.PARALLELISM,
hash_len=cls.HASH_LEN,
type=Type.ID # Argon2id
)
return key, salt
6.2 AES-256-GCM 加密
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import secrets
class SecureEncryption:
"""AES-256-GCM 认证加密。"""
NONCE_SIZE = 12 # 96 位推荐用于 GCM
KEY_SIZE = 32 # 256 位
def __init__(self, key: bytes):
if len(key) != self.KEY_SIZE:
raise ValueError(f"密钥必须为 {self.KEY_SIZE} 字节")
self._aesgcm = AESGCM(key)
def encrypt(self, plaintext: bytes, associated_data: bytes = None) -> bytes:
"""
使用随机 nonce 加密,前置到密文。
返回:
bytes: nonce || 密文 || 标签
"""
nonce = secrets.token_bytes(self.NONCE_SIZE)
ciphertext = self._aesgcm.encrypt(nonce, plaintext, associated_data)
return nonce + ciphertext
def decrypt(self, ciphertext: bytes, associated_data: bytes = None) -> bytes:
"""
解密并验证真实性。
抛出:
InvalidTag: 如果认证失败
"""
if len(ciphertext) < self.NONCE_SIZE + 16: # nonce + 标签最小
raise ValueError("密文太短")
nonce = ciphertext[:self.NONCE_SIZE]
actual_ciphertext = ciphertext[self.NONCE_SIZE:]
return self._aesgcm.decrypt(nonce, actual_ciphertext, associated_data)
6.3 SQLCipher 数据库集成
import sqlcipher3
from contextlib import contextmanager
class EncryptedDatabase:
"""使用 SQLCipher 的加密 SQLite 数据库。"""
def __init__(self, db_path: str, key: bytes):
self._db_path = db_path
self._key = key
self._conn = None
@contextmanager
def connect(self):
"""数据库连接的上下文管理器。"""
conn = sqlcipher3.connect(self._db_path)
try:
# 应用安全编译指示
conn.execute(f"PRAGMA key = \"x'{self._key.hex()}'\";")
conn.execute("PRAGMA cipher = 'aes-256-gcm';")
conn.execute("PRAGMA kdf_iter = 256000;")
conn.execute("PRAGMA cipher_page_size = 4096;")
# 验证加密激活
result = conn.execute("PRAGMA cipher_version;").fetchone()
if not result:
raise RuntimeError("SQLCipher 加密未激活")
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def rekey(self, new_key: bytes):
"""轮换数据库加密密钥。"""
with self.connect() as conn:
conn.execute(f"PRAGMA rekey = \"x'{new_key.hex()}'\";")
self._key = new_key
7. 安全标准
7.1 已知漏洞
| CVE | 严重性 | 组件 | 描述 | 缓解措施 |
|---|---|---|---|---|
| CVE-2020-27207 | 高 | SQLCipher <4.4.1 | 编解码编译指示中的使用后释放 | 升级到 4.5.6+ |
| CVE-2024-0232 | 中 | SQLite <3.44.0 | JSON 中的堆使用后释放 | 升级 SQLCipher 4.5.6+ |
| CVE-2023-42811 | 高 | aes-gcm (Rust) | 认证失败时的明文暴露 | 升级到 0.10.3+ |
| CVE-2024-4603 | 中 | OpenSSL | 密钥派生时序攻击 | 升级 OpenSSL 3.3+ |
| CVE-2023-48056 | 中 | 加密库 | IV 重用检测失败 | 使用随机 nonce |
7.2 OWASP 映射
| OWASP 2025 | 相关性 | 实现 |
|---|---|---|
| A02: 加密失败 | 关键 | AES-256-GCM、Argon2id、安全 RNG |
| A04: 不安全设计 | 高 | 威胁建模、密钥轮换 |
| A05: 安全配置错误 | 高 | 安全默认、验证 |
| A08: 软件完整性失败 | 中 | 认证加密 |
7.3 加密标准
批准算法:
- 对称: AES-256-GCM(主要)、ChaCha20-Poly1305(替代)
- KDF: Argon2id(主要)、PBKDF2-HMAC-SHA512(SQLCipher)
- 哈希: SHA-256、SHA-512、BLAKE2b
- RNG: 仅操作系统 CSPRNG(
secrets模块、/dev/urandom)
禁止:
- DES、3DES、RC4、Blowfish
- MD5、SHA-1 用于安全目的
- 任何密码的 ECB 模式
- 自定义随机数生成器
8. 测试要求
参见第 3 节(实现工作流 - TDD)获取综合测试示例,包括:
- 加密/解密往返
- 密文篡改检测
- 密钥派生一致性
- Nonce 唯一性验证
9. 常见错误
9.1 关键反模式
| 反模式 | 绝不这样做 | 始终这样做 |
|---|---|---|
| ECB 模式 | modes.ECB() |
AESGCM(key) |
| 硬编码密钥 | SECRET_KEY = b"..." |
os_keychain.get_key() |
| 可预测 Nonce | struct.pack(">Q", time()) |
secrets.token_bytes(12) |
| 无认证 | modes.CBC(iv) |
aesgcm.encrypt(nonce, pt, aad) |
| 弱 KDF | sha256(password) |
Argon2id.derive_key() |
10. 预实现清单
阶段 1: 编码前
- [ ] 阅读
references/threat-model.md中的威胁模型 - [ ] 识别数据分类(PII、PHI、凭证)
- [ ] 选择适当算法(AES-256-GCM 或 ChaCha20-Poly1305)
- [ ] 设计密钥派生策略(Argon2id 参数)
- [ ] 计划密钥存储(操作系统密钥链集成)
- [ ] 编写加密/解密往返的失败测试
- [ ] 编写认证标签验证测试
- [ ] 编写密钥派生一致性测试
阶段 2: 实现期间
- [ ] 使用
cryptography库(非自定义实现) - [ ] 使用
secrets.token_bytes(12)生成 nonce - [ ] 实现带 TTL 的密钥缓存以提升性能
- [ ] 对 >10MB 文件使用流处理
- [ ] 使用后归零密钥材料(SecureKeyHolder 模式)
- [ ] 添加关联数据(AAD)用于上下文绑定
- [ ] 处理 InvalidTag 异常而不泄露信息
- [ ] 每个函数实现后运行测试
阶段 3: 提交前
- [ ] 所有 TDD 测试通过,覆盖 90%+
- [ ] 在 10,000+ 操作上验证 nonce 唯一性
- [ ] 密钥派生时序差异 <10%
- [ ] 日志中无秘密(
grep -i "key\|secret\|password") - [ ] 依赖扫描干净(无 CVE)
- [ ] 性能基准达到目标:
- 密钥派生: <1s
- 加密: >100MB/s
- 批量操作: 线性扩展
- [ ] 对高风险代码请求安全审查
11. 总结
关键目标: AES-256-GCM 带随机 nonce、Argon2id KDF、操作系统密钥链集成、认证加密、密钥轮换支持。
安全提醒: 无自定义加密,使用审计过的库,测试认证标签,按计划轮换密钥。
参考: references/advanced-patterns.md、references/security-examples.md、references/threat-model.md
错误的加密比没有加密更糟——它提供虚假信心。