""" Unit tests for KeyEncryptionService (Fernet-based encryption of BYO API keys). Tests: - encrypt/decrypt roundtrip - different ciphertexts produced from same plaintext (Fernet random IV) - invalid ciphertext raises InvalidToken - MultiFernet rotation produces new ciphertext decryptable by current key """ from __future__ import annotations import pytest from cryptography.fernet import Fernet, InvalidToken from shared.crypto import KeyEncryptionService @pytest.fixture() def primary_key() -> str: """Generate a fresh Fernet key for each test.""" return Fernet.generate_key().decode() @pytest.fixture() def secondary_key() -> str: """Second Fernet key for rotation tests.""" return Fernet.generate_key().decode() def test_encrypt_decrypt_roundtrip(primary_key: str) -> None: """Encrypt then decrypt returns the original plaintext.""" svc = KeyEncryptionService(primary_key=primary_key) plaintext = "sk-my-secret-api-key-12345" ciphertext = svc.encrypt(plaintext) result = svc.decrypt(ciphertext) assert result == plaintext def test_encrypt_produces_different_ciphertext(primary_key: str) -> None: """Same plaintext encrypted twice produces different ciphertexts (Fernet random IV).""" svc = KeyEncryptionService(primary_key=primary_key) plaintext = "sk-same-plaintext" ct1 = svc.encrypt(plaintext) ct2 = svc.encrypt(plaintext) assert ct1 != ct2 # Both must still decrypt to the same value assert svc.decrypt(ct1) == plaintext assert svc.decrypt(ct2) == plaintext def test_decrypt_invalid_raises(primary_key: str) -> None: """Decrypting garbage raises InvalidToken (or ValueError wrapping it).""" svc = KeyEncryptionService(primary_key=primary_key) with pytest.raises((InvalidToken, ValueError)): svc.decrypt("this-is-not-valid-fernet-ciphertext") def test_multifernet_rotation(primary_key: str, secondary_key: str) -> None: """ Rotation scenario: 1. Encrypt with 'old' key (secondary_key as primary, no previous) 2. Create new service with primary_key=new and previous=old 3. rotate(old_ciphertext) produces a new ciphertext decryptable by primary_key """ # Step 1: encrypt with the old (secondary) key old_svc = KeyEncryptionService(primary_key=secondary_key) plaintext = "sk-rotate-me" old_ciphertext = old_svc.encrypt(plaintext) # Step 2: new service knows both keys — primary=new, previous=old new_svc = KeyEncryptionService(primary_key=primary_key, previous_key=secondary_key) # Verify old ciphertext is still decryptable via the new service (previous key fallback) assert new_svc.decrypt(old_ciphertext) == plaintext # Step 3: rotate — re-encrypt with the primary key rotated_ciphertext = new_svc.rotate(old_ciphertext) assert rotated_ciphertext != old_ciphertext # Rotated ciphertext must be decryptable by a service with only the new primary key only_new_svc = KeyEncryptionService(primary_key=primary_key) assert only_new_svc.decrypt(rotated_ciphertext) == plaintext