"""Crypto-layer tests for app.services.portfolio_sync. These exercise only the pure wrap/unwrap helpers — no DB, no network. The DB-touching helpers (`upsert`, `fetch`, `consume_fetch_budget`) need sqlite+aiosqlite and live in the API integration tests so we don't duplicate the test-app scaffolding. """ from __future__ import annotations import os import pytest from app.services import portfolio_sync as svc def test_wrap_unwrap_round_trip(): inner = b"the inner ciphertext doesn't matter to the outer wrap" ct, nonce = svc.wrap(user_id=42, inner_blob=inner) assert ct != inner # we actually encrypted out = svc.unwrap(42, ct, nonce) assert out == inner def test_wrap_is_nondeterministic(): """Same plaintext, same user, two writes — outer ciphertexts must differ because the nonce is random. A deterministic outer wrap would leak 'this user re-uploaded the same pie' across snapshots.""" inner = b"identical inner blob across writes" ct1, nonce1 = svc.wrap(7, inner) ct2, nonce2 = svc.wrap(7, inner) assert ct1 != ct2 assert nonce1 != nonce2 def test_unwrap_rejects_cross_user_blob(): """A blob wrapped for user A must not decrypt under user B's key — HKDF binds the user_id into the salt.""" inner = b"alice's pie" ct, nonce = svc.wrap(1, inner) with pytest.raises(svc.SyncCryptoError): svc.unwrap(2, ct, nonce) def test_unwrap_rejects_tampered_ciphertext(): inner = b"don't flip my bits" ct, nonce = svc.wrap(99, inner) bad = bytearray(ct) bad[0] ^= 0x01 with pytest.raises(svc.SyncCryptoError): svc.unwrap(99, bytes(bad), nonce) def test_unwrap_rejects_wrong_nonce(): inner = b"nonce is part of the auth envelope" ct, _nonce = svc.wrap(5, inner) with pytest.raises(svc.SyncCryptoError): svc.unwrap(5, ct, os.urandom(12)) def test_server_key_changes_with_pepper(monkeypatch): """Rotating the pepper must produce a different key for the same user — that's how we'd invalidate all sync rows in a credential breach.""" from app.config import get_settings get_settings.cache_clear() monkeypatch.setenv("PORTFOLIO_SYNC_PEPPER", "first-pepper-value") k1 = svc._server_key(11) get_settings.cache_clear() monkeypatch.setenv("PORTFOLIO_SYNC_PEPPER", "second-pepper-value") k2 = svc._server_key(11) assert k1 != k2 get_settings.cache_clear() # don't poison other tests