Upgrade password hashing from MD5 to bcrypt #152

Open
opened 2026-03-15 23:08:31 +00:00 by freemo · 0 comments
Owner

Metadata

  • Commit Message: fix(security): upgrade password hashing from MD5 to bcrypt
  • Branch: feature/m1-bcrypt-passwords

Background and Context

Player passwords are stored as MD5 hex digests in GDBM (storage/passwords) and ES password_hash fields. MD5 is cryptographically broken — rainbow tables make reversal trivial. The fix uses bcrypt which includes a per-password salt and configurable work factor. A transparent upgrade mechanism detects MD5 hashes (32-char hex strings) and re-hashes them with bcrypt on successful login.

Current Behavior

StorageMachine#save_player stores Digest::MD5.new.update(password).to_s. StorageMachine#check_password compares MD5 hashes. ES events store MD5 hashes in password_hash fields.

Expected Behavior

New passwords are stored as bcrypt hashes. On login, if the stored hash is a 32-character hex string (MD5 format), verify with MD5 first, then re-hash with bcrypt and update the store. ES events emit bcrypt hashes.

Acceptance Criteria

  • StorageMachine#save_player stores bcrypt hashes
  • StorageMachine#set_password stores bcrypt hashes
  • StorageMachine#check_password verifies against bcrypt; transparent MD5 upgrade on match
  • ES commands emit bcrypt hashes
  • bin/aethyr_setup password change uses bcrypt
  • bcrypt gem added to aethyr.gemspec runtime dependencies
  • All authentication tests pass

Subtasks

  • Add bcrypt gem to aethyr.gemspec runtime dependencies
  • Create PasswordService utility module with hash_password(plain) and verify_password(plain, stored) methods
  • Implement format detection in verify_password: 32-char hex = MD5 legacy, $2a$/$2b$ prefix = bcrypt
  • Implement transparent upgrade: on successful MD5 verify, re-hash with bcrypt, update GDBM store
  • Refactor StorageMachine#save_player to use PasswordService
  • Refactor StorageMachine#set_password to use PasswordService
  • Refactor StorageMachine#check_password to use PasswordService with transparent upgrade
  • Refactor bin/aethyr_setup password change to use PasswordService
  • Update ES commands to emit bcrypt hashes
  • Tests (Cucumber): Scenario for new password creation with bcrypt
  • Tests (Cucumber): Scenario for legacy MD5 password verification and transparent upgrade to bcrypt
  • Tests (Cucumber): Scenario for password change via set_password
  • Tests (Cucumber): Scenario for aethyr_setup password change
  • Run bundle exec rake unit, fix any failures
  • Docs: Update YARD comments on affected classes and methods. Update relevant Docusaurus documentation pages if applicable.
  • Tests (Cucumber Integration): Add integration feature in tests/integration/ verifying player login with legacy MD5 password transparently upgrades to bcrypt.
  • Tests (Profiling): Run bundle exec rake unit_profile and verify no performance regressions.
  • Quality: Verify coverage >=97% via bundle exec rake unit. If coverage is <97% then review the current unit test coverage report at build/tests/unit/coverage/ and use it to write new Cucumber based unit tests to improve code coverage. Specifically, write Cucumber/Gherkin style unit tests that are descriptively named and specifically improve coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun bundle exec rake unit to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%.
  • Quality: Run bundle exec rake (default task: unit tests with coverage) and bundle exec rake integration, fix any errors if needed ensuring both pass across entire code base, do not ignore any failure even if it seems unrelated to this commit, fix it.

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details about the implementation.
  • The commit is pushed to the remote on the branch matching the Branch in Metadata exactly.
  • The commit is submitted as a pull request to master, reviewed, and merged before this issue is marked done.
## Metadata - **Commit Message:** `fix(security): upgrade password hashing from MD5 to bcrypt` - **Branch:** `feature/m1-bcrypt-passwords` ## Background and Context Player passwords are stored as MD5 hex digests in GDBM (`storage/passwords`) and ES `password_hash` fields. MD5 is cryptographically broken — rainbow tables make reversal trivial. The fix uses bcrypt which includes a per-password salt and configurable work factor. A transparent upgrade mechanism detects MD5 hashes (32-char hex strings) and re-hashes them with bcrypt on successful login. ## Current Behavior `StorageMachine#save_player` stores `Digest::MD5.new.update(password).to_s`. `StorageMachine#check_password` compares MD5 hashes. ES events store MD5 hashes in `password_hash` fields. ## Expected Behavior New passwords are stored as bcrypt hashes. On login, if the stored hash is a 32-character hex string (MD5 format), verify with MD5 first, then re-hash with bcrypt and update the store. ES events emit bcrypt hashes. ## Acceptance Criteria - `StorageMachine#save_player` stores bcrypt hashes - `StorageMachine#set_password` stores bcrypt hashes - `StorageMachine#check_password` verifies against bcrypt; transparent MD5 upgrade on match - ES commands emit bcrypt hashes - `bin/aethyr_setup` password change uses bcrypt - `bcrypt` gem added to `aethyr.gemspec` runtime dependencies - All authentication tests pass ## Subtasks - [ ] Add `bcrypt` gem to `aethyr.gemspec` runtime dependencies - [ ] Create `PasswordService` utility module with `hash_password(plain)` and `verify_password(plain, stored)` methods - [ ] Implement format detection in `verify_password`: 32-char hex = MD5 legacy, `$2a$`/`$2b$` prefix = bcrypt - [ ] Implement transparent upgrade: on successful MD5 verify, re-hash with bcrypt, update GDBM store - [ ] Refactor `StorageMachine#save_player` to use `PasswordService` - [ ] Refactor `StorageMachine#set_password` to use `PasswordService` - [ ] Refactor `StorageMachine#check_password` to use `PasswordService` with transparent upgrade - [ ] Refactor `bin/aethyr_setup` password change to use `PasswordService` - [ ] Update ES commands to emit bcrypt hashes - [ ] Tests (Cucumber): Scenario for new password creation with bcrypt - [ ] Tests (Cucumber): Scenario for legacy MD5 password verification and transparent upgrade to bcrypt - [ ] Tests (Cucumber): Scenario for password change via `set_password` - [ ] Tests (Cucumber): Scenario for `aethyr_setup` password change - [ ] Run `bundle exec rake unit`, fix any failures - [ ] Docs: Update YARD comments on affected classes and methods. Update relevant Docusaurus documentation pages if applicable. - [ ] Tests (Cucumber Integration): Add integration feature in `tests/integration/` verifying player login with legacy MD5 password transparently upgrades to bcrypt. - [ ] Tests (Profiling): Run `bundle exec rake unit_profile` and verify no performance regressions. - [ ] Quality: Verify coverage >=97% via `bundle exec rake unit`. If coverage is <97% then review the current unit test coverage report at `build/tests/unit/coverage/` and use it to write new Cucumber based unit tests to improve code coverage. Specifically, write Cucumber/Gherkin style unit tests that are descriptively named and specifically improve coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `bundle exec rake unit` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - [ ] Quality: Run `bundle exec rake` (default task: unit tests with coverage) and `bundle exec rake integration`, fix any errors if needed ensuring both pass across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off. - A Git commit is created where the **first line** of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details about the implementation. - The commit is pushed to the remote on the branch matching the **Branch** in Metadata exactly. - The commit is submitted as a **pull request** to `master`, reviewed, and **merged** before this issue is marked done.
freemo added this to the v1.0.0 milestone 2026-03-15 23:19:40 +00:00
freemo self-assigned this 2026-03-16 01:27:03 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference: aethyr/Aethyr#152
No description provided.