Change the user's password. Re-wraps the Fernet key with the new password.
(self, current_password: str, new_password: str)
| 521 | # ========================================================================= |
| 522 | |
| 523 | def change_password(self, current_password: str, new_password: str) -> dict: |
| 524 | """Change the user's password. Re-wraps the Fernet key with the new password.""" |
| 525 | if not self.is_configured(): |
| 526 | return {'success': False, 'error': 'Authentication not configured'} |
| 527 | |
| 528 | if len(new_password) < 8: |
| 529 | return {'success': False, 'error': 'New password must be at least 8 characters'} |
| 530 | |
| 531 | try: |
| 532 | with self._get_auth_conn() as conn: |
| 533 | cursor = conn.cursor() |
| 534 | cursor.execute("SELECT * FROM auth LIMIT 1") |
| 535 | row = cursor.fetchone() |
| 536 | |
| 537 | if not row: |
| 538 | return {'success': False, 'error': 'No auth record found'} |
| 539 | |
| 540 | # Verify current password |
| 541 | if not self._verify_password(current_password, row['password_hash'], row['password_salt']): |
| 542 | return {'success': False, 'error': 'Current password is incorrect'} |
| 543 | |
| 544 | hw_fingerprint = row['hardware_fingerprint'] |
| 545 | |
| 546 | # Unwrap Fernet key with current password |
| 547 | old_wrapping = self._derive_wrapping_key(current_password, hw_fingerprint) |
| 548 | try: |
| 549 | fernet_key = self._unwrap_fernet_key(row['encrypted_fernet_key'], old_wrapping) |
| 550 | except InvalidToken: |
| 551 | return {'success': False, 'error': 'Failed to decrypt encryption key'} |
| 552 | |
| 553 | # Re-wrap with new password |
| 554 | new_pw_hash, new_pw_salt = self._hash_password(new_password) |
| 555 | new_wrapping = self._derive_wrapping_key(new_password, hw_fingerprint) |
| 556 | new_wrapped_key = self._wrap_fernet_key(fernet_key, new_wrapping) |
| 557 | |
| 558 | # Update auth record |
| 559 | cursor.execute(""" |
| 560 | UPDATE auth SET password_hash = ?, password_salt = ?, |
| 561 | encrypted_fernet_key = ?, |
| 562 | updated_at = CURRENT_TIMESTAMP |
| 563 | WHERE id = ? |
| 564 | """, (new_pw_hash, new_pw_salt, new_wrapped_key, row['id'])) |
| 565 | conn.commit() |
| 566 | |
| 567 | self._fernet_key = fernet_key |
| 568 | logger.info("Password changed successfully") |
| 569 | return {'success': True, 'message': 'Password changed successfully'} |
| 570 | |
| 571 | except Exception as e: |
| 572 | logger.error(f"Password change failed: {e}") |
| 573 | return {'success': False, 'error': f'Password change failed: {str(e)}'} |
| 574 | |
| 575 | # ========================================================================= |
| 576 | # PUBLIC API - RECOVERY |
no test coverage detected