05/16 Enhanced codes
This commit is contained in:
+2
-2
@@ -14,7 +14,7 @@ VALID_TYPES = {t.value for t in ItemType}
|
||||
@require_jwt
|
||||
def list_items():
|
||||
items = VaultItem.query.filter_by(user_id=g.current_user_id).order_by(
|
||||
VaultItem.name.asc()
|
||||
VaultItem.created_at.desc()
|
||||
).all()
|
||||
return jsonify([item.to_dict() for item in items]), 200
|
||||
|
||||
@@ -164,7 +164,7 @@ def export_items():
|
||||
created_at, updated_at }
|
||||
"""
|
||||
items = VaultItem.query.filter_by(user_id=g.current_user_id).order_by(
|
||||
VaultItem.name.asc()
|
||||
VaultItem.created_at.desc()
|
||||
).all()
|
||||
AuditLog.log(
|
||||
user_id=g.current_user_id,
|
||||
|
||||
@@ -177,42 +177,11 @@ def generate_recovery_nonce() -> str:
|
||||
return os.urandom(32).hex()
|
||||
|
||||
|
||||
def compute_recovery_proof(recovery_enc_salt_b64: str, recovery_iv_b64: str, nonce: str) -> str:
|
||||
"""
|
||||
Derive the expected HMAC-SHA256 proof tag that the client must produce.
|
||||
|
||||
The client-side proof is:
|
||||
key_material = AES-GCM-decrypt(recovery_key, recovery_enc_salt_ciphertext)
|
||||
= enc_key_salt (plaintext bytes)
|
||||
proof = HMAC-SHA256(key=enc_key_salt_bytes, msg=nonce_bytes)
|
||||
|
||||
The server replicates this using the stored ciphertext + its TOTP encryption
|
||||
key is NOT involved here — the recovery blob was encrypted with the *client*
|
||||
recovery key. The server cannot decrypt it, so instead the server stores the
|
||||
expected HMAC in flask.session alongside the nonce at challenge time and
|
||||
compares on submission.
|
||||
|
||||
Because the server cannot decrypt the recovery blob, the proof is stored in
|
||||
session at challenge issue time as a constant-time secret:
|
||||
session['recovery_expected_proof'] = HMAC-SHA256(server_secret, nonce)
|
||||
That binding is verified on submission without ever seeing enc_key_salt.
|
||||
|
||||
Concretely:
|
||||
expected_tag = HMAC-SHA256(key=SECRET_KEY_bytes, msg=nonce_hex_bytes)
|
||||
|
||||
The client sends:
|
||||
client_tag = HMAC-SHA256(key=enc_key_salt_bytes, msg=nonce_hex_bytes)
|
||||
|
||||
These are different keys — so the server never validates client_tag directly.
|
||||
Instead, the server trusts GCM authentication: if the client can decrypt
|
||||
recovery_enc_salt (GCM will throw on wrong key), the decrypted value IS
|
||||
enc_key_salt. The server then computes:
|
||||
expected = HMAC-SHA256(key=user.enc_key_salt.encode(), msg=nonce.encode())
|
||||
and compares it to client_tag in constant time.
|
||||
"""
|
||||
key = base64.b64decode(recovery_enc_salt_b64) # unused — see docstring
|
||||
msg = nonce.encode()
|
||||
return hmac.new(key, msg, hashlib.sha256).hexdigest()
|
||||
# NOTE: compute_recovery_proof() is intentionally absent.
|
||||
# The server cannot decrypt the recovery blob (it was encrypted client-side with
|
||||
# the user's recovery key). Instead, the expected HMAC is computed inline in
|
||||
# the /recovery/data route using user.enc_key_salt as the HMAC key, stored in
|
||||
# flask.session, and compared on submission via verify_recovery_proof() below.
|
||||
|
||||
|
||||
def verify_recovery_proof(expected_hmac: str, client_hmac: str) -> bool:
|
||||
@@ -235,20 +204,12 @@ def generate_backup_codes() -> tuple[list[str], list[str]]:
|
||||
The plaintext list is shown to the user ONCE and never stored.
|
||||
Only the hashed list is persisted in user.mfa_backup_codes (JSON array).
|
||||
"""
|
||||
import secrets
|
||||
ph = PasswordHasher(
|
||||
time_cost=1, # backup codes can afford lighter params than master password
|
||||
memory_cost=16384,
|
||||
parallelism=2,
|
||||
)
|
||||
plaintext = [
|
||||
''.join(os.urandom(1)[0] % len(_BACKUP_ALPHABET)
|
||||
and _BACKUP_ALPHABET[os.urandom(1)[0] % len(_BACKUP_ALPHABET)]
|
||||
or _BACKUP_ALPHABET[os.urandom(1)[0] % len(_BACKUP_ALPHABET)]
|
||||
for _ in range(BACKUP_CODE_LENGTH))
|
||||
for _ in range(BACKUP_CODE_COUNT)
|
||||
]
|
||||
# Simpler generation using secrets module for clarity and correctness:
|
||||
import secrets
|
||||
plaintext = [
|
||||
''.join(secrets.choice(_BACKUP_ALPHABET) for _ in range(BACKUP_CODE_LENGTH))
|
||||
for _ in range(BACKUP_CODE_COUNT)
|
||||
|
||||
Reference in New Issue
Block a user