"""add recovery_verifier to users Revision ID: j0k1l2m3n4o5 Revises: i9j0k1l2m3n4 Create Date: 2026-08-26 00:00:00.000000 Decouples the account-recovery challenge-response proof from enc_key_salt. Before this change the recovery proof was HMAC-SHA256(key=enc_key_salt, msg=nonce). Because enc_key_salt is also the PBKDF2 salt for the vault key and is handed to the client at login, anyone who learned enc_key_salt could forge a recovery proof and pull the entire encrypted vault from the unauthenticated /recovery/items endpoint — bypassing MFA entirely. recovery_verifier is an independent 256-bit value derived client-side from the recovery code alone: verifier = PBKDF2(recovery_code, "passkeeper-recovery-verifier:" + email, 200_000 iter, SHA-256) → 64 hex chars It is stored server-side purely as the HMAC key for the recovery challenge, and is never used for any encryption. Knowing enc_key_salt no longer grants the ability to forge a proof. NULL = legacy recovery code (created before this migration). Those accounts fall back to the old enc_key_salt-keyed proof so existing recovery codes keep working; the settings UI prompts the user to regenerate. """ from alembic import op import sqlalchemy as sa revision = 'j0k1l2m3n4o5' down_revision = 'i9j0k1l2m3n4' branch_labels = None depends_on = None def upgrade(): op.add_column( 'users', sa.Column('recovery_verifier', sa.String(64), nullable=True), ) def downgrade(): op.drop_column('users', 'recovery_verifier')