Skip to content

Migrate password hashing from SHA-512 to bcrypt#440

Open
superconductor-for-github[bot] wants to merge 1 commit intomainfrom
cycomachead/6-audit-app-security-passwords/1
Open

Migrate password hashing from SHA-512 to bcrypt#440
superconductor-for-github[bot] wants to merge 1 commit intomainfrom
cycomachead/6-audit-app-security-passwords/1

Conversation

@superconductor-for-github
Copy link
Copy Markdown

Summary

  • Replaces SHA-512 password hashing with bcrypt (cost 12), addressing the most critical finding from the security audit. SHA-512 is a general-purpose hash vulnerable to GPU brute-force; bcrypt is purpose-built for passwords with a tunable work factor.
  • Adds a password_version column to the users table to support a three-phase just-in-time migration: v0 (legacy SHA-512) → v1 (bcrypt-wrapped SHA-512) → v2 (native bcrypt).
  • Includes a bulk migration script (bin/bcrypt-wrap-passwords.lua) that wraps all existing SHA-512 hashes with bcrypt, immediately protecting the database even if leaked.
  • Fixes the learner account double-hash bug where create_learners used hash_password(hash_password(pw, ''), salt) — an inconsistent derivation path vs. normal signup. Learner passwords are now SHA-512 pre-hashed (matching client login behavior) then bcrypt'd.

How the migration works

Phase 1: Bulk wrap (run once after deploy)

cd snapCloud
bin/lapis-migrate           # adds password_version column
lapis exec - < bin/bcrypt-wrap-passwords.lua  # wraps all v0 → v1

All users are immediately protected by bcrypt. No user action required.

Phase 2: JIT upgrade (automatic)

On each successful login, upgrade_password_to_bcrypt() re-hashes the client's SHA-512 prehash directly with bcrypt, upgrading the user from v1 → v2 and discarding the legacy salt dependency.

Phase 3: New accounts

All new signups, password resets, and password changes go straight to v2 (native bcrypt).

Files changed

File What changed
passwords.lua Added bcrypt require, verify_password() (handles all 3 versions), bcrypt_hash(), upgrade_password_to_bcrypt(). Legacy hash_password() and secure_salt() kept for backward compat.
controllers/user.lua Login uses verify_password() + JIT upgrade. Create, create_learners, change_password, change_email, delete, and reset_password all updated to use bcrypt.
migrations.lua New migration 2026-04-14:0 adds password_version integer column (default 0).
models/users.lua Updated schema comment to include password_version.
snapcloud-dev-0.rockspec Added bcrypt dependency.
bin/bcrypt-wrap-passwords.lua New script for bulk-wrapping existing hashes. Processes in batches of 100, safe to re-run, prints progress.

Password version reference

Version Format When used
0 sha512(prehash || salt) Legacy (before this PR)
1 bcrypt(sha512(prehash || salt)) After bulk migration script
2 bcrypt(prehash) New accounts, password changes, JIT upgrades

Test plan

  • Run bin/lapis-migrate — verify password_version column added with default 0
  • Run lapis exec - < bin/bcrypt-wrap-passwords.lua — verify all users upgraded to v1
  • Log in as an existing user — verify login succeeds and password_version becomes 2
  • Create a new account — verify password_version is 2 and salt is empty
  • Create learner accounts via teacher — verify learners can log in, password_version is 2
  • Change password — verify new hash is bcrypt (starts with $2b$)
  • Reset password via email link — verify new hash is bcrypt
  • Verify change_email and delete account flows still require correct password

🤖 Generated with Claude Code

SHA-512 is a general-purpose hash that can be brute-forced at billions of
attempts per second on modern GPUs. This replaces it with bcrypt (cost 12),
which is memory-hard and tunable.

The migration uses a three-version scheme:
  v0 (legacy):  sha512(prehash || salt) — existing passwords
  v1 (wrapped): bcrypt(v0_hash) — bulk migration output
  v2 (native):  bcrypt(prehash) — new accounts + JIT upgrades at login

New accounts and password resets go straight to v2. Existing users are
bulk-wrapped to v1 via bin/bcrypt-wrap-passwords.lua, then silently
upgraded to v2 on next login.

Learner accounts now SHA-512 pre-hash the plaintext password (matching
client behavior) before bcrypt, fixing the old double-hash inconsistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@superconductor-for-github
Copy link
Copy Markdown
Author

🔗 This pull request is linked to Superconductor implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant