minhvtt commited on
Commit
1f7fc03
·
verified ·
1 Parent(s): cc3aaf7

Update app/services/key_service.py

Browse files
Files changed (1) hide show
  1. app/services/key_service.py +100 -92
app/services/key_service.py CHANGED
@@ -1,92 +1,100 @@
1
- import secrets
2
- from hashlib import sha256
3
- from datetime import datetime, timedelta, timezone
4
-
5
- from app.services.repos import key_state_repo
6
-
7
-
8
- def _hash_key(raw: str) -> str:
9
- return sha256(raw.encode("utf-8")).hexdigest()
10
-
11
-
12
- class KeyService:
13
- def __init__(self, *, ttl_seconds: int = 60, lock_minutes: int = 5) -> None:
14
- self.ttl_seconds = ttl_seconds
15
- self.lock_minutes = lock_minutes
16
-
17
- async def generate(self, device_id: str) -> tuple[str, datetime]:
18
- key = secrets.token_urlsafe(8)
19
- expires_at = datetime.now(timezone.utc) + timedelta(seconds=self.ttl_seconds)
20
- await key_state_repo.upsert(
21
- device_id,
22
- {
23
- "key_hash": _hash_key(key),
24
- "expires_at": expires_at,
25
- "wrong_attempts": 0,
26
- "lock_until": None,
27
- },
28
- )
29
- return key, expires_at
30
-
31
- async def validate(self, device_id: str, candidate: str) -> tuple[bool, datetime | None, int]:
32
- state = await key_state_repo.get(device_id)
33
- now = datetime.now(timezone.utc)
34
-
35
- if state is None:
36
- lock_until = now + timedelta(minutes=self.lock_minutes)
37
- await key_state_repo.upsert(
38
- device_id,
39
- {
40
- "wrong_attempts": 1,
41
- "lock_until": lock_until,
42
- "expires_at": now,
43
- "key_hash": "",
44
- },
45
- )
46
- return False, lock_until, 1
47
-
48
- lock_until_value = state.get("lock_until")
49
- if isinstance(lock_until_value, datetime) and now < lock_until_value:
50
- return False, lock_until_value, int(state.get("wrong_attempts", 0))
51
-
52
- expires_at = state.get("expires_at")
53
- if not isinstance(expires_at, datetime) or now > expires_at:
54
- lock_until = now + timedelta(minutes=self.lock_minutes)
55
- attempts = int(state.get("wrong_attempts", 0)) + 1
56
- await key_state_repo.upsert(
57
- device_id,
58
- {
59
- "wrong_attempts": attempts,
60
- "lock_until": lock_until,
61
- "expires_at": now,
62
- },
63
- )
64
- return False, lock_until, attempts
65
-
66
- candidate_hash = _hash_key(candidate)
67
- expected_hash = str(state.get("key_hash", ""))
68
- if not secrets.compare_digest(candidate_hash, expected_hash):
69
- attempts = int(state.get("wrong_attempts", 0)) + 1
70
- lock_until = now + timedelta(minutes=self.lock_minutes)
71
- await key_state_repo.upsert(
72
- device_id,
73
- {
74
- "wrong_attempts": attempts,
75
- "lock_until": lock_until,
76
- },
77
- )
78
- return False, lock_until, attempts
79
-
80
- await key_state_repo.upsert(
81
- device_id,
82
- {
83
- "wrong_attempts": 0,
84
- "lock_until": None,
85
- "expires_at": now,
86
- "key_hash": "",
87
- },
88
- )
89
- return True, None, 0
90
-
91
-
92
- key_service = KeyService()
 
 
 
 
 
 
 
 
 
1
+ import secrets
2
+ from hashlib import sha256
3
+ from datetime import datetime, timedelta, timezone
4
+
5
+ from app.services.repos import key_state_repo
6
+
7
+
8
+ def _hash_key(raw: str) -> str:
9
+ return sha256(raw.encode("utf-8")).hexdigest()
10
+
11
+
12
+ def _to_utc_aware(value: datetime | None) -> datetime | None:
13
+ if not isinstance(value, datetime):
14
+ return None
15
+ if value.tzinfo is None:
16
+ return value.replace(tzinfo=timezone.utc)
17
+ return value.astimezone(timezone.utc)
18
+
19
+
20
+ class KeyService:
21
+ def __init__(self, *, ttl_seconds: int = 60, lock_minutes: int = 5) -> None:
22
+ self.ttl_seconds = ttl_seconds
23
+ self.lock_minutes = lock_minutes
24
+
25
+ async def generate(self, device_id: str) -> tuple[str, datetime]:
26
+ key = secrets.token_urlsafe(8)
27
+ expires_at = datetime.now(timezone.utc) + timedelta(seconds=self.ttl_seconds)
28
+ await key_state_repo.upsert(
29
+ device_id,
30
+ {
31
+ "key_hash": _hash_key(key),
32
+ "expires_at": expires_at,
33
+ "wrong_attempts": 0,
34
+ "lock_until": None,
35
+ },
36
+ )
37
+ return key, expires_at
38
+
39
+ async def validate(self, device_id: str, candidate: str) -> tuple[bool, datetime | None, int]:
40
+ state = await key_state_repo.get(device_id)
41
+ now = datetime.now(timezone.utc)
42
+
43
+ if state is None:
44
+ lock_until = now + timedelta(minutes=self.lock_minutes)
45
+ await key_state_repo.upsert(
46
+ device_id,
47
+ {
48
+ "wrong_attempts": 1,
49
+ "lock_until": lock_until,
50
+ "expires_at": now,
51
+ "key_hash": "",
52
+ },
53
+ )
54
+ return False, lock_until, 1
55
+
56
+ lock_until_value = _to_utc_aware(state.get("lock_until"))
57
+ if lock_until_value is not None and now < lock_until_value:
58
+ return False, lock_until_value, int(state.get("wrong_attempts", 0))
59
+
60
+ expires_at = _to_utc_aware(state.get("expires_at"))
61
+ if expires_at is None or now > expires_at:
62
+ lock_until = now + timedelta(minutes=self.lock_minutes)
63
+ attempts = int(state.get("wrong_attempts", 0)) + 1
64
+ await key_state_repo.upsert(
65
+ device_id,
66
+ {
67
+ "wrong_attempts": attempts,
68
+ "lock_until": lock_until,
69
+ "expires_at": now,
70
+ },
71
+ )
72
+ return False, lock_until, attempts
73
+
74
+ candidate_hash = _hash_key(candidate)
75
+ expected_hash = str(state.get("key_hash", ""))
76
+ if not secrets.compare_digest(candidate_hash, expected_hash):
77
+ attempts = int(state.get("wrong_attempts", 0)) + 1
78
+ lock_until = now + timedelta(minutes=self.lock_minutes)
79
+ await key_state_repo.upsert(
80
+ device_id,
81
+ {
82
+ "wrong_attempts": attempts,
83
+ "lock_until": lock_until,
84
+ },
85
+ )
86
+ return False, lock_until, attempts
87
+
88
+ await key_state_repo.upsert(
89
+ device_id,
90
+ {
91
+ "wrong_attempts": 0,
92
+ "lock_until": None,
93
+ "expires_at": now,
94
+ "key_hash": "",
95
+ },
96
+ )
97
+ return True, None, 0
98
+
99
+
100
+ key_service = KeyService()