| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- """契约1.21 信封加密(KEK/DEK,AES-256-GCM)单元测试。
- 覆盖:加解密往返、DMSE 识别、明文兼容、promote 门控(original/ 前缀
- 与 KEK 配置双条件)、篡改检测、缺 KEK 拒绝读取、KEK 配置解析校验。
- """
- from __future__ import annotations
- import base64
- import os
- from pathlib import Path
- import pytest
- from flask import Flask
- from dms.common.errors import InternalError
- from dms.storage import envelope
- @pytest.fixture()
- def kek() -> bytes:
- return os.urandom(32)
- @pytest.fixture()
- def crypto_app(kek):
- app = Flask("dms-envelope-test")
- app.config["DMS_STORAGE_KEK_BYTES"] = kek
- return app
- @pytest.fixture()
- def plain_app():
- app = Flask("dms-envelope-plain-test")
- app.config["DMS_STORAGE_KEK_BYTES"] = None
- return app
- class TestLoadKek:
- def test_empty_returns_none(self):
- assert envelope.load_kek("") is None
- assert envelope.load_kek(" ") is None
- def test_valid_base64_32_bytes(self):
- raw = os.urandom(32)
- assert envelope.load_kek(base64.b64encode(raw).decode()) == raw
- def test_invalid_base64_rejected(self):
- with pytest.raises(ValueError, match="DMS_STORAGE_KEK"):
- envelope.load_kek("!!not-base64!!")
- def test_wrong_length_rejected(self):
- with pytest.raises(ValueError, match="DMS_STORAGE_KEK"):
- envelope.load_kek(base64.b64encode(os.urandom(16)).decode())
- class TestEncryptDecryptRoundTrip:
- def test_round_trip(self, crypto_app, kek, tmp_path: Path):
- plaintext = os.urandom(4096)
- source = tmp_path / "plain.bin"
- source.write_bytes(plaintext)
- target = tmp_path / "encrypted.bin"
- envelope.encrypt_file(kek, source, target)
- payload = target.read_bytes()
- assert payload[:4] == envelope.ENVELOPE_MAGIC
- assert payload[4] == envelope.KEK_VERSION
- assert envelope.decrypt_bytes(kek, payload) == plaintext
- def test_each_file_uses_distinct_dek(self, crypto_app, kek, tmp_path: Path):
- source = tmp_path / "plain.bin"
- source.write_bytes(b"same content")
- first = tmp_path / "a.bin"
- second = tmp_path / "b.bin"
- envelope.encrypt_file(kek, source, first)
- envelope.encrypt_file(kek, source, second)
- # DEK/nonce 随机,同明文两次加密产物必须不同
- assert first.read_bytes() != second.read_bytes()
- def test_wrong_kek_rejected(self, crypto_app, kek, tmp_path: Path):
- source = tmp_path / "plain.bin"
- source.write_bytes(b"secret")
- target = tmp_path / "encrypted.bin"
- envelope.encrypt_file(kek, source, target)
- with pytest.raises(InternalError, match="解密失败"):
- envelope.decrypt_bytes(os.urandom(32), target.read_bytes())
- def test_tampered_ciphertext_rejected(self, crypto_app, kek, tmp_path: Path):
- source = tmp_path / "plain.bin"
- source.write_bytes(b"integrity check")
- target = tmp_path / "encrypted.bin"
- envelope.encrypt_file(kek, source, target)
- payload = bytearray(target.read_bytes())
- payload[-1] ^= 0xFF
- with pytest.raises(InternalError, match="解密失败"):
- envelope.decrypt_bytes(kek, bytes(payload))
- def test_truncated_payload_rejected(self, crypto_app, kek):
- with pytest.raises(InternalError, match="解密失败"):
- envelope.decrypt_bytes(kek, b"DMSE")
- with pytest.raises(InternalError, match="解密失败"):
- envelope.decrypt_bytes(kek, b"NOPE" + b"\x00" * 64)
- class TestIsEnvelope:
- def test_recognizes_envelope(self, crypto_app, kek, tmp_path: Path):
- source = tmp_path / "plain.bin"
- source.write_bytes(b"data")
- target = tmp_path / "encrypted.bin"
- envelope.encrypt_file(kek, source, target)
- assert envelope.is_envelope(target) is True
- def test_plain_file_not_envelope(self, tmp_path: Path):
- path = tmp_path / "plain.pdf"
- path.write_bytes(b"%PDF-1.7 body")
- assert envelope.is_envelope(path) is False
- def test_missing_file_not_envelope(self, tmp_path: Path):
- assert envelope.is_envelope(tmp_path / "missing.bin") is False
- class TestReadPlaintextBytes:
- def test_plain_passthrough(self, plain_app, tmp_path: Path):
- path = tmp_path / "plain.pdf"
- content = b"%PDF-1.7 plain content"
- path.write_bytes(content)
- with plain_app.app_context():
- assert envelope.read_plaintext_bytes(path) == content
- def test_envelope_decrypted(self, crypto_app, kek, tmp_path: Path):
- plaintext = b"confidential bytes"
- source = tmp_path / "plain.bin"
- source.write_bytes(plaintext)
- target = tmp_path / "encrypted.bin"
- envelope.encrypt_file(kek, source, target)
- with crypto_app.app_context():
- assert envelope.read_plaintext_bytes(target) == plaintext
- def test_envelope_without_kek_rejected(self, plain_app, kek, tmp_path: Path):
- source = tmp_path / "plain.bin"
- source.write_bytes(b"encrypted at rest")
- target = tmp_path / "encrypted.bin"
- envelope.encrypt_file(kek, source, target)
- with plain_app.app_context():
- with pytest.raises(InternalError, match="未配置"):
- envelope.read_plaintext_bytes(target)
- class TestPlaintextFileFor:
- def test_plain_returns_same_path(self, plain_app, tmp_path: Path):
- path = tmp_path / "plain.pdf"
- path.write_bytes(b"%PDF-1.7 body")
- with plain_app.app_context():
- with envelope.plaintext_file_for(path) as yielded:
- assert yielded == path
- assert path.exists()
- def test_envelope_yields_temp_and_cleans_up(
- self, crypto_app, kek, tmp_path: Path
- ):
- plaintext = b"decrypt me"
- source = tmp_path / "plain.bin"
- source.write_bytes(plaintext)
- target = tmp_path / "encrypted.bin"
- envelope.encrypt_file(kek, source, target)
- with crypto_app.app_context():
- with envelope.plaintext_file_for(target) as yielded:
- assert yielded != target
- assert yielded.read_bytes() == plaintext
- assert not yielded.exists()
- # 密文本体不受临时明文清理影响
- assert target.exists()
- class TestPromoteEncrypt:
- def test_original_prefix_encrypted(self, crypto_app, tmp_path: Path):
- temporary = tmp_path / "staged.upload"
- temporary.write_bytes(b"new upload")
- final = tmp_path / "final.bin"
- with crypto_app.app_context():
- assert envelope.promote_encrypt(
- temporary, final, "original/final.bin"
- ) is True
- assert final.read_bytes()[:4] == envelope.ENVELOPE_MAGIC
- assert not temporary.exists()
- def test_non_original_prefix_skipped(self, crypto_app, tmp_path: Path):
- temporary = tmp_path / "staged.upload"
- temporary.write_bytes(b"voucher pdf bytes")
- final = tmp_path / "final.pdf"
- with crypto_app.app_context():
- assert envelope.promote_encrypt(
- temporary, final, "voucher/final.pdf"
- ) is False
- assert not final.exists()
- assert temporary.exists()
- def test_no_kek_skipped(self, plain_app, tmp_path: Path):
- temporary = tmp_path / "staged.upload"
- temporary.write_bytes(b"plain stays plain")
- final = tmp_path / "final.bin"
- with plain_app.app_context():
- assert envelope.promote_encrypt(
- temporary, final, "original/final.bin"
- ) is False
- assert not final.exists()
- assert temporary.exists()
|