05/18 Enhanced codes and functionalities
This commit is contained in:
+26
-8
@@ -19,6 +19,8 @@ from app.services.auth_service import (
|
||||
verify_recovery_proof,
|
||||
generate_backup_codes,
|
||||
verify_and_consume_backup_code,
|
||||
is_totp_code_used,
|
||||
mark_totp_code_used,
|
||||
)
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
@@ -280,7 +282,11 @@ def mfa_enable():
|
||||
if not pyotp.TOTP(secret).verify(totp_code, valid_window=1):
|
||||
return jsonify({'error': 'Invalid verification code'}), 400
|
||||
|
||||
totp_secret_enc, totp_iv = encrypt_totp_secret(secret)
|
||||
# Prevent replay: reject a code that was already consumed within the valid window.
|
||||
# user.id is not yet persisted (MFA not enabled), so use g.current_user_id directly.
|
||||
if is_totp_code_used(g.current_user_id, totp_code):
|
||||
return jsonify({'error': 'Verification code already used. Wait for the next code.'}), 400
|
||||
mark_totp_code_used(g.current_user_id, totp_code)
|
||||
user.totp_secret = totp_secret_enc
|
||||
user.totp_iv = totp_iv
|
||||
user.totp_enabled = True
|
||||
@@ -324,7 +330,11 @@ def mfa_disable():
|
||||
verified = False
|
||||
|
||||
if totp_code:
|
||||
if is_totp_code_used(user.id, totp_code):
|
||||
return jsonify({'error': 'Verification code already used. Wait for the next code.'}), 400
|
||||
verified = pyotp.TOTP(plaintext_secret).verify(totp_code, valid_window=1)
|
||||
if verified:
|
||||
mark_totp_code_used(user.id, totp_code)
|
||||
elif backup_code:
|
||||
stored = json.loads(user.mfa_backup_codes or '[]')
|
||||
matched, remaining = verify_and_consume_backup_code(stored, backup_code)
|
||||
@@ -380,7 +390,11 @@ def mfa_verify():
|
||||
verified = False
|
||||
|
||||
if totp_code:
|
||||
if is_totp_code_used(user.id, totp_code):
|
||||
return jsonify({'error': 'Verification code already used. Wait for the next code.'}), 400
|
||||
verified = pyotp.TOTP(plaintext_secret).verify(totp_code, valid_window=1)
|
||||
if verified:
|
||||
mark_totp_code_used(user.id, totp_code)
|
||||
|
||||
if not verified and backup_code:
|
||||
stored = json.loads(user.mfa_backup_codes or '[]')
|
||||
@@ -454,8 +468,11 @@ def mfa_backup_codes_regenerate():
|
||||
|
||||
import pyotp
|
||||
plaintext_secret = decrypt_totp_secret(user.totp_secret, user.totp_iv)
|
||||
if is_totp_code_used(user.id, totp_code):
|
||||
return jsonify({'error': 'Verification code already used. Wait for the next code.'}), 400
|
||||
if not pyotp.TOTP(plaintext_secret).verify(totp_code, valid_window=1):
|
||||
return jsonify({'error': 'Invalid verification code'}), 400
|
||||
mark_totp_code_used(user.id, totp_code)
|
||||
|
||||
plaintext_codes, hashed_codes = generate_backup_codes()
|
||||
user.mfa_backup_codes = json.dumps(hashed_codes)
|
||||
@@ -886,7 +903,6 @@ def recovery_data():
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'enc_key_salt': user.enc_key_salt,
|
||||
'recovery_enc_salt': user.recovery_enc_salt,
|
||||
'recovery_iv': user.recovery_iv,
|
||||
'nonce': nonce,
|
||||
@@ -902,11 +918,12 @@ def recovery_items():
|
||||
Requires X-Recovery-Proof header containing the HMAC-SHA256 proof:
|
||||
proof = HMAC-SHA256(key=enc_key_salt_bytes, msg=nonce_from_recovery_data)
|
||||
|
||||
The server validates the proof against the value stored in the DB
|
||||
during /recovery/data — enc_key_salt is never sent in plaintext.
|
||||
The challenge row is NOT consumed here; it is consumed by the final
|
||||
POST /recover call so the client can call this endpoint to preview
|
||||
items before committing the full re-encryption.
|
||||
The enc_key_salt used as the HMAC key is NOT returned by /recovery/data;
|
||||
the client must derive it by decrypting the recovery blob with the recovery
|
||||
code. This ensures only the holder of the recovery code can compute the proof.
|
||||
|
||||
The challenge row is NOT consumed here — it is consumed by the final
|
||||
POST /recover call so that endpoint can also validate the proof.
|
||||
Items are returned as encrypted ciphertext blobs only.
|
||||
"""
|
||||
from app.models.recovery_challenge import RecoveryChallenge
|
||||
@@ -921,7 +938,8 @@ def recovery_items():
|
||||
if not user or not user.recovery_enc_salt:
|
||||
return jsonify({'error': 'No recovery data found'}), 404
|
||||
|
||||
# Validate against the DB-stored challenge — safe across all workers.
|
||||
# Validate against the DB-stored challenge without consuming it —
|
||||
# POST /recover will consume it atomically on commit.
|
||||
challenge = RecoveryChallenge.query.filter_by(user_id=user.id).first()
|
||||
from datetime import datetime, timezone
|
||||
if not challenge or challenge.expires_at < datetime.now(timezone.utc).replace(tzinfo=None):
|
||||
|
||||
Reference in New Issue
Block a user