MCPcopy Create free account
hub / github.com/PierreGode/Ragnar / recover

Method recover

auth_manager.py:579–671  ·  view source on GitHub ↗

Use a recovery code to reset the password and decrypt the database.

(self, username: str, recovery_code: str, new_password: str)

Source from the content-addressed store, hash-verified

577 # =========================================================================
578
579 def recover(self, username: str, recovery_code: str, new_password: str) -> dict:
580 """Use a recovery code to reset the password and decrypt the database."""
581 if not self.is_configured():
582 return {'success': False, 'error': 'Authentication not configured'}
583
584 if len(new_password) < 8:
585 return {'success': False, 'error': 'New password must be at least 8 characters'}
586
587 try:
588 with self._get_auth_conn() as conn:
589 cursor = conn.cursor()
590
591 # Verify username
592 cursor.execute("SELECT * FROM auth WHERE username = ?", (username,))
593 auth_row = cursor.fetchone()
594 if not auth_row:
595 return {'success': False, 'error': 'Invalid username or recovery code'}
596
597 hw_fingerprint = auth_row['hardware_fingerprint']
598
599 # Check hardware
600 current_hw = self.get_hardware_fingerprint()
601 if not secrets.compare_digest(hw_fingerprint, current_hw):
602 return {'success': False, 'error': 'Hardware mismatch', 'hw_mismatch': True}
603
604 # Normalize recovery code (strip spaces, uppercase, ensure dash format)
605 recovery_code = recovery_code.replace(' ', '').strip().upper()
606 if '-' not in recovery_code and len(recovery_code) == 8:
607 recovery_code = recovery_code[:4] + '-' + recovery_code[4:]
608
609 # Find matching recovery code
610 cursor.execute("SELECT * FROM recovery_codes WHERE used = 0")
611 rows = cursor.fetchall()
612
613 matched_row = None
614 for row in rows:
615 if self._verify_password(recovery_code, row['code_hash'], row['code_salt']):
616 matched_row = row
617 break
618
619 if not matched_row:
620 return {'success': False, 'error': 'Invalid username or recovery code'}
621
622 # Unwrap Fernet key using recovery code
623 code_wrapping = self._derive_wrapping_key(recovery_code, hw_fingerprint)
624 try:
625 fernet_key = self._unwrap_fernet_key(matched_row['encrypted_fernet_key'], code_wrapping)
626 except InvalidToken:
627 return {'success': False, 'error': 'Recovery code decryption failed'}
628
629 # Mark recovery code as used
630 cursor.execute("""
631 UPDATE recovery_codes SET used = 1, used_at = CURRENT_TIMESTAMP
632 WHERE id = ?
633 """, (matched_row['id'],))
634
635 # Re-wrap Fernet key with new password
636 new_pw_hash, new_pw_salt = self._hash_password(new_password)

Callers 1

auth_recoverFunction · 0.80

Calls 15

is_configuredMethod · 0.95
_get_auth_connMethod · 0.95
_verify_passwordMethod · 0.95
_derive_wrapping_keyMethod · 0.95
_unwrap_fernet_keyMethod · 0.95
_hash_passwordMethod · 0.95
_wrap_fernet_keyMethod · 0.95
cursorMethod · 0.80
commitMethod · 0.80
infoMethod · 0.80
errorMethod · 0.80

Tested by

no test coverage detected