OTP security hardened
This commit is contained in:
@@ -7,12 +7,43 @@ import hmac
|
||||
import secrets
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
from uuid import uuid4
|
||||
|
||||
from backend.app.database import get_connection
|
||||
|
||||
|
||||
def create_secret() -> str:
|
||||
return base64.b32encode(secrets.token_bytes(20)).decode('ascii').rstrip('=')
|
||||
|
||||
|
||||
def create_recovery_codes(user_id: str, count: int = 10) -> list[str]:
|
||||
codes = [secrets.token_urlsafe(9) for _ in range(count)]
|
||||
with get_connection() as conn:
|
||||
conn.execute('DELETE FROM otp_recovery_codes WHERE user_id = ?', (user_id,))
|
||||
conn.executemany(
|
||||
'INSERT INTO otp_recovery_codes (id, user_id, code_hash) VALUES (?, ?, ?)',
|
||||
[(str(uuid4()), user_id, hash_recovery_code(code)) for code in codes],
|
||||
)
|
||||
conn.commit()
|
||||
return codes
|
||||
|
||||
|
||||
def hash_recovery_code(code: str) -> str:
|
||||
return hashlib.sha256(code.strip().encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def consume_recovery_code(user_id: str, code: str) -> bool:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.execute(
|
||||
'''UPDATE otp_recovery_codes
|
||||
SET used = 1, used_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = ? AND code_hash = ? AND used = 0''',
|
||||
(user_id, hash_recovery_code(code)),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount == 1
|
||||
|
||||
|
||||
def provisioning_uri(secret: str, username: str, issuer: str = 'LinkLog') -> str:
|
||||
return f'otpauth://totp/{quote(issuer)}:{quote(username)}?secret={secret}&issuer={quote(issuer)}'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user