181 lines
6.5 KiB
Python
181 lines
6.5 KiB
Python
"""
|
|
加密引擎模块 (Crypto Engine)
|
|
|
|
流式 AES-256-GCM 分块加密,基于 cryptography.cobblestone (C2SP chunked-encryption)。
|
|
每块 16KiB 独立 GCM 认证,流式处理不整块进内存,适合大文件。
|
|
|
|
流程对应模块图 (02-加密引擎):
|
|
B(取密钥) -> C(初始化流式上下文) -> D(逐块加密) -> F(输出参数 + 密文流)
|
|
|
|
格式说明:
|
|
cobblestone 输出流自带头部 + 分块结构 (magic/版本/随机 IV/密文+tag),
|
|
IV 由库内部管理,不需要也不应该手动维护 iv 列表。
|
|
解密时只需密钥 + context,finalize() 验证整体完整性,篡改即抛 InvalidTag。
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import IO, Any, Callable
|
|
|
|
from cryptography.cobblestone import Cobblestone256Decryptor, Cobblestone256Encryptor
|
|
from cryptography.exceptions import InvalidTag
|
|
|
|
# 应用绑定上下文: 防止密文被挪用到其他系统/版本
|
|
CONTEXT = b"7z-encrypt:v1"
|
|
|
|
# 默认密钥库位置 (配置外置原则, 权限 600)
|
|
DEFAULT_KEYRING = Path(__file__).resolve().parent / "config" / "keyring.json"
|
|
|
|
|
|
class CryptoError(RuntimeError):
|
|
"""加密/解密过程错误"""
|
|
|
|
|
|
class KeyNotFoundError(CryptoError):
|
|
"""密钥 ID 不存在"""
|
|
|
|
|
|
class IntegrityError(CryptoError):
|
|
"""解密验证失败 (密钥错误或数据被篡改)"""
|
|
|
|
|
|
class CryptoEngine:
|
|
"""流式加密引擎, 密钥持久化到 keyring 文件"""
|
|
|
|
def __init__(self, keyring_path: str | Path | None = None) -> None:
|
|
self.keyring_path = Path(keyring_path) if keyring_path else DEFAULT_KEYRING
|
|
self._keyring = self._load_keyring()
|
|
|
|
# ---------- 密钥库 ----------
|
|
|
|
def _load_keyring(self) -> dict[str, bytes]:
|
|
"""从 keyring 文件加载密钥 (hex -> bytes)"""
|
|
if not self.keyring_path.exists():
|
|
return {}
|
|
data = json.loads(self.keyring_path.read_text(encoding="utf-8"))
|
|
return {kid: bytes.fromhex(hex_str) for kid, hex_str in data.items()}
|
|
|
|
def _save_keyring(self) -> None:
|
|
"""持久化密钥库, 文件权限 600 (仅本人可读写)"""
|
|
self.keyring_path.parent.mkdir(parents=True, exist_ok=True)
|
|
data = {kid: key.hex() for kid, key in self._keyring.items()}
|
|
self.keyring_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
os.chmod(self.keyring_path, 0o600)
|
|
|
|
def get_key(self, key_id: str) -> bytes:
|
|
"""步骤 B: 取密钥。密钥不存在抛 KeyNotFoundError"""
|
|
try:
|
|
return self._keyring[key_id]
|
|
except KeyError:
|
|
raise KeyNotFoundError(f"密钥ID '{key_id}' 未找到 (keyring: {self.keyring_path})") from None
|
|
|
|
def generate_key(self, key_id: str) -> bytes:
|
|
"""生成新密钥 (os.urandom 32B) 并持久化。已存在则不覆盖, 抛错防误用"""
|
|
if key_id in self._keyring:
|
|
raise CryptoError(f"密钥ID '{key_id}' 已存在, 不覆盖 (需轮换请换新 key_id)")
|
|
key = os.urandom(32)
|
|
self._keyring[key_id] = key
|
|
self._save_keyring()
|
|
return key
|
|
|
|
# ---------- 加解密 ----------
|
|
|
|
def encrypt_stream(
|
|
self,
|
|
src: IO[bytes],
|
|
dst: IO[bytes],
|
|
key_id: str,
|
|
context: bytes = CONTEXT,
|
|
progress: Callable[[int], Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""步骤 A & F: 流式加密。src 读明文 -> dst 写密文, 返回加密参数
|
|
|
|
Args:
|
|
src: 可读二进制流 (文件/BytesIO)
|
|
dst: 可写二进制流
|
|
key_id: 密钥标识
|
|
context: 应用绑定上下文 (默认 7z-encrypt:v1)
|
|
progress: 进度回调, 每读块后调 progress(已读字节)
|
|
"""
|
|
key = self.get_key(key_id)
|
|
enc = Cobblestone256Encryptor(key, context)
|
|
done = 0
|
|
while True:
|
|
chunk = src.read(1 << 16) # 64KiB 读块
|
|
if not chunk:
|
|
break
|
|
dst.write(enc.update(chunk))
|
|
done += len(chunk)
|
|
if progress is not None:
|
|
progress(done)
|
|
dst.write(enc.finalize())
|
|
return {"alg": "cobblestone-aes256gcm", "key_id": key_id, "context": context.decode()}
|
|
|
|
def decrypt_stream(
|
|
self,
|
|
src: IO[bytes],
|
|
dst: IO[bytes],
|
|
key_id: str,
|
|
context: bytes = CONTEXT,
|
|
progress: Callable[[int], Any] | None = None,
|
|
) -> None:
|
|
"""解密流 (进度回调: 每读块后调 progress(已读字节))"""
|
|
key = self.get_key(key_id)
|
|
try:
|
|
dec = Cobblestone256Decryptor(key, context)
|
|
done = 0
|
|
while True:
|
|
chunk = src.read(1 << 16)
|
|
if not chunk:
|
|
break
|
|
dst.write(dec.update(chunk)) # 块级认证: 篡改在这里就抛 InvalidTag
|
|
done += len(chunk)
|
|
if progress is not None:
|
|
progress(done)
|
|
dst.write(dec.finalize()) # finalize 返回剩余明文
|
|
except InvalidTag as e:
|
|
raise IntegrityError("解密验证失败: 密钥不匹配或密文被篡改") from e
|
|
|
|
|
|
# ================= 使用示例 (Usage Example) =================
|
|
if __name__ == "__main__":
|
|
import io
|
|
|
|
engine = CryptoEngine()
|
|
|
|
# 1. 确保密钥存在 (首次运行生成并持久化)
|
|
try:
|
|
engine.get_key("default_key")
|
|
except KeyNotFoundError:
|
|
engine.generate_key("default_key")
|
|
print("[密钥库] 已生成 default_key ->", engine.keyring_path)
|
|
|
|
# 2. 模拟流式加密 (大文件场景: 分块读入)
|
|
plain = (b"This is a streaming encryption test. " * 10000) # ~330KB
|
|
src = io.BytesIO(plain)
|
|
dst = io.BytesIO()
|
|
params = engine.encrypt_stream(src, dst, key_id="default_key")
|
|
|
|
print("=== 加密输出 (JSON) ===")
|
|
print(json.dumps(params, indent=2))
|
|
print(f"明文 {len(plain)}B -> 密文 {len(dst.getvalue())}B (含头部+分块tag开销)")
|
|
|
|
# 3. 流式解密还原
|
|
out = io.BytesIO()
|
|
dst.seek(0)
|
|
engine.decrypt_stream(dst, out, key_id="default_key")
|
|
restored = out.getvalue()
|
|
assert restored == plain, "解密结果与明文不一致!"
|
|
print("解密还原: 一致 ✓")
|
|
|
|
# 4. 篡改检测: 改密文一个字节, finalize 必须抛 IntegrityError
|
|
tampered = bytearray(dst.getvalue())
|
|
tampered[len(tampered) // 2] ^= 0xFF
|
|
try:
|
|
out2 = io.BytesIO()
|
|
engine.decrypt_stream(io.BytesIO(bytes(tampered)), out2, key_id="default_key")
|
|
print("篡改检测: 未检出! (异常)")
|
|
except IntegrityError:
|
|
print("篡改检测: 捕获 IntegrityError ✓")
|