feat(backend): pose les primitives de sécurité
Couche pure, sans FastAPI ni session : rôles ordonnés, `Principal`, encodage et décodage des jetons d'accès, empreinte des jetons de rafraîchissement, et hachage Argon2id poussé dans un fil borné. Aucun de ces modules ne lit `get_settings()`, mis en cache par `lru_cache` et donc contaminé entre tests : les paramètres arrivent par `TokenPolicy` et par `build_hasher()`. Argon2id est calibré à m=19456 KiB, t=2, p=1, soit 17 ms mesurés sur un poste de développement.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
from app.core.hashing import Argon2Hasher, build_hasher
|
||||
|
||||
MOT_DE_PASSE = "un-mot-de-passe-de-test-assez-long"
|
||||
|
||||
|
||||
def fabrique(time_cost: int = 1, max_concurrency: int = 2) -> Argon2Hasher:
|
||||
return build_hasher(
|
||||
time_cost=time_cost,
|
||||
memory_cost_kib=8192,
|
||||
parallelism=1,
|
||||
max_concurrency=max_concurrency,
|
||||
)
|
||||
|
||||
|
||||
async def test_hash_produces_a_distinct_digest_for_the_same_password() -> None:
|
||||
hacheur = fabrique()
|
||||
|
||||
premier = await hacheur.hash(MOT_DE_PASSE)
|
||||
second = await hacheur.hash(MOT_DE_PASSE)
|
||||
|
||||
assert premier != second
|
||||
assert premier.startswith("$argon2id$")
|
||||
|
||||
|
||||
async def test_verify_accepts_the_right_password_and_rejects_the_others() -> None:
|
||||
hacheur = fabrique()
|
||||
|
||||
empreinte = await hacheur.hash(MOT_DE_PASSE)
|
||||
|
||||
assert await hacheur.verify(empreinte, MOT_DE_PASSE) is True
|
||||
assert await hacheur.verify(empreinte, "un-autre-mot-de-passe") is False
|
||||
|
||||
|
||||
async def test_verify_returns_false_when_the_stored_digest_is_malformed() -> None:
|
||||
hacheur = fabrique()
|
||||
|
||||
accorde = await hacheur.verify("pas-une-empreinte-argon2", MOT_DE_PASSE)
|
||||
|
||||
assert accorde is False
|
||||
|
||||
|
||||
async def test_needs_rehash_is_true_when_the_parameters_changed() -> None:
|
||||
ancien = fabrique(time_cost=1)
|
||||
recent = fabrique(time_cost=3)
|
||||
|
||||
empreinte = await ancien.hash(MOT_DE_PASSE)
|
||||
|
||||
assert ancien.needs_rehash(empreinte) is False
|
||||
assert recent.needs_rehash(empreinte) is True
|
||||
|
||||
|
||||
def test_needs_rehash_is_true_when_the_stored_digest_is_malformed() -> None:
|
||||
hacheur = fabrique()
|
||||
|
||||
assert hacheur.needs_rehash("pas-une-empreinte-argon2") is True
|
||||
|
||||
|
||||
async def test_verify_dummy_completes_without_revealing_anything() -> None:
|
||||
hacheur = fabrique()
|
||||
|
||||
await hacheur.verify_dummy()
|
||||
Reference in New Issue
Block a user