CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled
48 lines
1.5 KiB
Python
48 lines
1.5 KiB
Python
"""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')
|