From e70a63e4f96ae92f1634f730084f6225e1227aa9 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Mon, 22 Sep 2025 14:54:29 +0200 Subject: [PATCH] add security py --- backend/dev-requirements.txt | 6 +--- backend/requirements.in | 2 +- backend/requirements.txt | 6 +--- backend/settings.py | 3 ++ backend/tests/test_security.py | 22 ++++++++++++- backend/trading_journal/security.py | 48 ++++++++++++++++++++++++++--- 6 files changed, 71 insertions(+), 16 deletions(-) diff --git a/backend/dev-requirements.txt b/backend/dev-requirements.txt index aebd637..dd46058 100644 --- a/backend/dev-requirements.txt +++ b/backend/dev-requirements.txt @@ -17,7 +17,7 @@ anyio==4.10.0 \ argon2-cffi==25.1.0 \ --hash=sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1 \ --hash=sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741 - # via passlib + # via -r requirements.in argon2-cffi-bindings==25.1.0 \ --hash=sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99 \ --hash=sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6 \ @@ -230,10 +230,6 @@ packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f # via pytest -passlib[argon2]==1.7.4 \ - --hash=sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1 \ - --hash=sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04 - # via -r requirements.in pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 diff --git a/backend/requirements.in b/backend/requirements.in index dc105e4..0eafd8e 100644 --- a/backend/requirements.in +++ b/backend/requirements.in @@ -4,4 +4,4 @@ httpx pyyaml pydantic-settings sqlmodel -passlib[argon2] \ No newline at end of file +argon2-cffi \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 6131b3f..c06f8dc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -17,7 +17,7 @@ anyio==4.10.0 \ argon2-cffi==25.1.0 \ --hash=sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1 \ --hash=sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741 - # via passlib + # via -r requirements.in argon2-cffi-bindings==25.1.0 \ --hash=sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99 \ --hash=sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6 \ @@ -222,10 +222,6 @@ idna==3.10 \ # via # anyio # httpx -passlib[argon2]==1.7.4 \ - --hash=sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1 \ - --hash=sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04 - # via -r requirements.in pycparser==2.23 \ --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 diff --git a/backend/settings.py b/backend/settings.py index 25ad0dc..62305be 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from pathlib import Path from typing import Any @@ -13,6 +15,7 @@ class Settings(BaseSettings): workers: int = 1 log_level: str = "info" database_url: str = "sqlite:///:memory:" + hmac_key: str | None = None model_config = ConfigDict(env_file=".env", env_file_encoding="utf-8") diff --git a/backend/tests/test_security.py b/backend/tests/test_security.py index cab62d7..bfd5e63 100644 --- a/backend/tests/test_security.py +++ b/backend/tests/test_security.py @@ -1,4 +1,24 @@ from trading_journal import security -def test_hash_password() -> None: + +def test_hash_and_verify_password() -> None: plain = "password" + hashed = security.hash_password(plain) + assert hashed != plain + assert security.verify_password(plain, hashed) + + +def test_generate_session_token() -> None: + token1 = security.generate_session_token() + token2 = security.generate_session_token() + assert token1 != token2 + assert len(token1) > 0 + assert len(token2) > 0 + + +def test_hash_and_verify_session_token_sha256() -> None: + token = security.generate_session_token() + token_hash = security.hash_session_token_sha256(token) + assert token_hash != token + assert security.verify_token_sha256(token, token_hash) + assert not security.verify_token_sha256(token + "x", token_hash) diff --git a/backend/trading_journal/security.py b/backend/trading_journal/security.py index edb32ce..23bf03a 100644 --- a/backend/trading_journal/security.py +++ b/backend/trading_journal/security.py @@ -1,11 +1,51 @@ -from passlib.context import CryptContext +import hashlib +import hmac +import secrets -pwd_ctx = CryptContext(schemes=["argon2"], deprecated="auto") +from argon2 import PasswordHasher +from argon2.exceptions import VerifyMismatchError + +import settings + +ph = PasswordHasher() + +# Utility functions for password hashing and verification def hash_password(plain: str) -> str: - return pwd_ctx.hash(plain) + return ph.hash(plain) + def verify_password(plain: str, hashed: str) -> bool: - return pwd_ctx.verify(plain, hashed) + try: + return ph.verify(hashed, plain) + except VerifyMismatchError: + return False + +# Session token hash + + +def generate_session_token(nbytes: int = 32) -> str: + return secrets.token_urlsafe(nbytes) + + +def hash_session_token_sha256(token: str) -> str: + return hashlib.sha256(token.encode("utf-8")).hexdigest() + + +def sign_token_hmac(token: str) -> str: + if not settings.settings.hmac_key: + return token + return hmac.new(settings.settings.hmac_key.encode("utf-8"), token.encode("utf-8"), hashlib.sha256).hexdigest() + + +def verify_token_sha256(token: str, expected_hash: str) -> bool: + return hmac.compare_digest(hash_session_token_sha256(token), expected_hash) + + +def verify_token_hmac(token: str, expected_hmac: str) -> bool: + if not settings.settings.hmac_key: + return verify_token_sha256(token, expected_hmac) + sig = hmac.new(settings.settings.hmac_key.encode("utf-8"), token.encode("utf-8"), hashlib.sha256).hexdigest() + return hmac.compare_digest(sig, expected_hmac)