Aug 26 - Enhance security
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

This commit is contained in:
2026-08-26 10:37:00 -04:00
parent 0295fac3fa
commit 0304095e53
9 changed files with 502 additions and 112 deletions
@@ -0,0 +1,47 @@
"""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')