158 lines
4.1 KiB
Python
158 lines
4.1 KiB
Python
|
|
"""crypto 模块测试"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
# 测试开始前设置密钥(必须在 import data.orders.crypto 之前)
|
|||
|
|
os.environ.setdefault("GAOKAO_ORDERS_FERNET_KEY", "test-secret-for-unit-tests")
|
|||
|
|
|
|||
|
|
from data.orders.crypto import (
|
|||
|
|
derive_key,
|
|||
|
|
encrypt,
|
|||
|
|
decrypt,
|
|||
|
|
hash_for_index,
|
|||
|
|
constant_time_equals,
|
|||
|
|
EncryptionError,
|
|||
|
|
MissingEncryptionKey,
|
|||
|
|
ENV_KEY_NAME,
|
|||
|
|
)
|
|||
|
|
from cryptography.fernet import Fernet
|
|||
|
|
|
|||
|
|
|
|||
|
|
def setup_function(_):
|
|||
|
|
"""每个用例前清掉缓存,避免环境变量副作用。"""
|
|||
|
|
from data.orders import crypto
|
|||
|
|
|
|||
|
|
crypto.get_fernet.cache_clear()
|
|||
|
|
|
|||
|
|
|
|||
|
|
# -------- derive_key --------
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_derive_key_is_deterministic():
|
|||
|
|
"""同 secret 派生同 key。"""
|
|||
|
|
k1 = derive_key("hello")
|
|||
|
|
k2 = derive_key("hello")
|
|||
|
|
assert k1 == k2
|
|||
|
|
assert len(k1) > 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_derive_key_distinct_for_different_secrets():
|
|||
|
|
"""不同 secret 派生不同 key。"""
|
|||
|
|
assert derive_key("a") != derive_key("b")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_derive_key_rejects_empty():
|
|||
|
|
with pytest.raises(EncryptionError):
|
|||
|
|
derive_key("")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_derive_key_accepts_fernet_key_directly():
|
|||
|
|
"""Fernet 直接生成的 key 应被接受(兼容)。"""
|
|||
|
|
direct = Fernet.generate_key()
|
|||
|
|
# derive_key 用 sha256 派生;这里验证 Fernet 能用直接 key 初始化
|
|||
|
|
Fernet(direct) # 不抛即可
|
|||
|
|
|
|||
|
|
|
|||
|
|
# -------- encrypt/decrypt round-trip --------
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_encrypt_decrypt_round_trip_chinese():
|
|||
|
|
"""中文明文 round-trip。"""
|
|||
|
|
plaintext = "张同学 13800001234"
|
|||
|
|
ct = encrypt(plaintext)
|
|||
|
|
assert ct != plaintext
|
|||
|
|
assert decrypt(ct) == plaintext
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_encrypt_decrypt_round_trip_empty_string():
|
|||
|
|
"""空字符串也能 round-trip。"""
|
|||
|
|
assert decrypt(encrypt("")) == ""
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_encrypt_output_is_ascii():
|
|||
|
|
"""密文为 base64-url 字符串,仅含 ASCII。"""
|
|||
|
|
ct = encrypt("hello world")
|
|||
|
|
ct.encode("ascii") # 不抛即通过
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_decrypt_with_wrong_key_fails():
|
|||
|
|
"""错误密钥解密应抛 EncryptionError。"""
|
|||
|
|
# 先用当前 key 加密
|
|||
|
|
ct = encrypt("secret-value")
|
|||
|
|
# 切换密钥
|
|||
|
|
os.environ[ENV_KEY_NAME] = "different-secret"
|
|||
|
|
from data.orders import crypto
|
|||
|
|
|
|||
|
|
crypto.get_fernet.cache_clear()
|
|||
|
|
with pytest.raises(EncryptionError):
|
|||
|
|
decrypt(ct)
|
|||
|
|
# 恢复
|
|||
|
|
os.environ[ENV_KEY_NAME] = "test-secret-for-unit-tests"
|
|||
|
|
crypto.get_fernet.cache_clear()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_encrypt_rejects_non_string():
|
|||
|
|
with pytest.raises(EncryptionError):
|
|||
|
|
encrypt(12345) # type: ignore[arg-type]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_decrypt_rejects_non_string():
|
|||
|
|
with pytest.raises(EncryptionError):
|
|||
|
|
decrypt(12345) # type: ignore[arg-type]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_missing_key_raises_missing_encryption_key(monkeypatch):
|
|||
|
|
"""未配置密钥时 get_fernet 抛 MissingEncryptionKey,不静默降级。"""
|
|||
|
|
monkeypatch.delenv(ENV_KEY_NAME, raising=False)
|
|||
|
|
from data.orders import crypto
|
|||
|
|
|
|||
|
|
crypto.get_fernet.cache_clear()
|
|||
|
|
with pytest.raises(MissingEncryptionKey):
|
|||
|
|
encrypt("test")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# -------- hash_for_index --------
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_hash_for_index_is_stable():
|
|||
|
|
"""同输入同输出。"""
|
|||
|
|
assert hash_for_index("13800001234") == hash_for_index("13800001234")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_hash_for_index_strips_whitespace():
|
|||
|
|
"""前后空白不影响 hash。"""
|
|||
|
|
assert hash_for_index(" 13800001234 ") == hash_for_index("13800001234")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_hash_for_index_is_hex_64_chars():
|
|||
|
|
"""SHA-256 hex 应为 64 字符。"""
|
|||
|
|
h = hash_for_index("13800001234")
|
|||
|
|
assert len(h) == 64
|
|||
|
|
int(h, 16) # 可被解析为 16 进制
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_hash_for_index_different_inputs():
|
|||
|
|
"""不同输入产生不同 hash。"""
|
|||
|
|
assert hash_for_index("13800001234") != hash_for_index("13800001235")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_hash_for_index_rejects_non_string():
|
|||
|
|
with pytest.raises(EncryptionError):
|
|||
|
|
hash_for_index(12345) # type: ignore[arg-type]
|
|||
|
|
|
|||
|
|
|
|||
|
|
# -------- constant_time_equals --------
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_constant_time_equals_equal():
|
|||
|
|
assert constant_time_equals("abc", "abc") is True
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_constant_time_equals_unequal():
|
|||
|
|
assert constant_time_equals("abc", "abd") is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_constant_time_equals_different_length():
|
|||
|
|
assert constant_time_equals("abc", "abcd") is False
|