import hashlib
import pytest
from cryptography.exceptions import InvalidTag
from gnexus_creds import crypto
from gnexus_creds.errors import AppError
def test_derive_without_salt_is_legacy_sha256():
assert crypto.derive_master_key("key") == hashlib.sha256(b"key").digest()
def test_derive_with_salt_is_stretched_and_differs():
legacy = crypto.derive_master_key("key")
salted = crypto.derive_master_key("key", "install-salt")
assert salted != legacy
assert len(salted) == 32
# same call again hits the cache and stays deterministic
assert crypto.derive_master_key("key", "install-salt") == salted
def test_text_envelope_roundtrip_with_aad():
key = crypto.new_raw_key()
payload = crypto.encrypt_text(key, "uk_abc", "hint", aad="user:secret:version")
assert crypto.decrypt_text(key, payload, aad="user:secret:version") == "hint"
def test_legacy_envelope_still_decrypts_with_aad_context():
# rows written before the aad binding carry no bound context — the
# tolerant decrypt keeps them open (migration rebinds them)
key = crypto.new_raw_key()
payload = crypto.encrypt_text(key, "uk_abc", "legacy")
assert crypto.decrypt_text(key, payload, aad="user:secret:version") == "legacy"
def test_transplanted_envelope_fails_authentication():
key = crypto.new_raw_key()
p1 = crypto.encrypt_text(key, "uk_abc", "one", aad="user:s1:v1")
p2 = crypto.encrypt_text(key, "uk_abc", "two", aad="user:s2:v2")
# transplant p2's ciphertext into p1's envelope (same user, same key)
p1["ciphertext"] = p2["ciphertext"]
p1["nonce"] = p2["nonce"]
with pytest.raises(InvalidTag):
crypto.decrypt_text(key, p1, aad="user:s1:v1")
def test_list_secrets_rejects_unknown_sort_by(db_session, actor):
from gnexus_creds.models import Secret
from gnexus_creds.services import list_secrets
with pytest.raises(AppError) as exc:
list_secrets(db_session, actor, sort_by="__dict__")
assert exc.value.status_code == 422
with pytest.raises(AppError) as exc:
list_secrets(db_session, actor, sort_by="versions") # a relationship
assert exc.value.status_code == 422
# whitelist values keep working
list_secrets(db_session, actor, sort_by="title")
list_secrets(db_session, actor, sort_by="updated_at")
assert list_secrets(db_session, actor, limit=200)[1] == 0