diff --git a/tests/security/test_encryption.py b/tests/security/test_encryption.py new file mode 100644 index 00000000..659924aa --- /dev/null +++ b/tests/security/test_encryption.py @@ -0,0 +1,95 @@ +import base64 + +import pytest +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + +from application.security import encryption + + +def test_derive_key_uses_secret_and_user(monkeypatch): + monkeypatch.setattr(encryption.settings, "ENCRYPTION_SECRET_KEY", "test-secret") + salt = bytes(range(16)) + + expected_kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + backend=default_backend(), + ) + expected_key = expected_kdf.derive(b"test-secret#user-123") + + derived = encryption._derive_key("user-123", salt) + + assert derived == expected_key + + +def _fake_os_urandom_factory(values): + values_iter = iter(values) + + def _fake(length): + value = next(values_iter) + assert len(value) == length + return value + + return _fake + + +def test_encrypt_and_decrypt_round_trip(monkeypatch): + monkeypatch.setattr(encryption.settings, "ENCRYPTION_SECRET_KEY", "test-secret") + salt = bytes(range(16)) + iv = bytes(range(16, 32)) + monkeypatch.setattr(encryption.os, "urandom", _fake_os_urandom_factory([salt, iv])) + + credentials = {"token": "abc123", "refresh": "xyz789"} + + encrypted = encryption.encrypt_credentials(credentials, "user-123") + + decoded = base64.b64decode(encrypted) + assert decoded[:16] == salt + assert decoded[16:32] == iv + + decrypted = encryption.decrypt_credentials(encrypted, "user-123") + + assert decrypted == credentials + + +def test_encrypt_credentials_returns_empty_for_empty_input(monkeypatch): + monkeypatch.setattr(encryption.settings, "ENCRYPTION_SECRET_KEY", "test-secret") + + assert encryption.encrypt_credentials({}, "user-123") == "" + assert encryption.encrypt_credentials(None, "user-123") == "" + + +def test_encrypt_credentials_returns_empty_on_serialization_error(monkeypatch): + monkeypatch.setattr(encryption.settings, "ENCRYPTION_SECRET_KEY", "test-secret") + monkeypatch.setattr(encryption.os, "urandom", lambda length: b"\x00" * length) + + class NonSerializable: # pragma: no cover - simple helper container + pass + + credentials = {"bad": NonSerializable()} + + assert encryption.encrypt_credentials(credentials, "user-123") == "" + + +def test_decrypt_credentials_returns_empty_for_invalid_input(monkeypatch): + monkeypatch.setattr(encryption.settings, "ENCRYPTION_SECRET_KEY", "test-secret") + + assert encryption.decrypt_credentials("", "user-123") == {} + assert encryption.decrypt_credentials("not-base64", "user-123") == {} + + invalid_payload = base64.b64encode(b"short").decode() + assert encryption.decrypt_credentials(invalid_payload, "user-123") == {} + + +def test_pad_and_unpad_are_inverse(): + original = b"secret-data" + + padded = encryption._pad_data(original) + + assert len(padded) % 16 == 0 + assert encryption._unpad_data(padded) == original +