mirror of
https://github.com/we-promise/sure.git
synced 2026-05-10 22:25:00 +00:00
* fix(auth): hash MFA backup codes * fix(auth): lock and filter backup code verification * test(auth): assert consumed backup code digest * fix(auth): strengthen backup code handling * fix(auth): require otp secret before mfa enable * test(auth): assert backup code digest consumption * fix(auth): rehash legacy MFA backup codes * fix(auth): narrow legacy backup code migration
52 lines
1.5 KiB
Ruby
52 lines
1.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class RehashPlaintextMfaBackupCodes < ActiveRecord::Migration[7.2]
|
|
class MigrationUser < ActiveRecord::Base
|
|
self.table_name = "users"
|
|
end
|
|
|
|
BCRYPT_PREFIXES = %w[$2a$ $2b$ $2y$].freeze
|
|
PLAINTEXT_BACKUP_CODE_PATTERN = /\A[0-9a-f]{8}\z/
|
|
|
|
def up
|
|
require "bcrypt"
|
|
|
|
say_with_time "Rehashing plaintext MFA backup codes" do
|
|
rehashed_users_count = 0
|
|
|
|
MigrationUser.where(otp_required: true).find_each do |user|
|
|
backup_codes = Array(user.otp_backup_codes)
|
|
next if backup_codes.blank?
|
|
next unless backup_codes.any? { |code| plaintext_backup_code?(code) }
|
|
|
|
rehashed_codes = backup_codes.map do |code|
|
|
plaintext_backup_code?(code) ? BCrypt::Password.create(normalize_backup_code(code), cost: bcrypt_cost).to_s : code
|
|
end
|
|
|
|
user.update_columns(otp_backup_codes: rehashed_codes, updated_at: Time.current)
|
|
rehashed_users_count += 1
|
|
end
|
|
|
|
rehashed_users_count
|
|
end
|
|
end
|
|
|
|
def down
|
|
raise ActiveRecord::IrreversibleMigration
|
|
end
|
|
|
|
private
|
|
def plaintext_backup_code?(code)
|
|
normalized_code = normalize_backup_code(code)
|
|
normalized_code.match?(PLAINTEXT_BACKUP_CODE_PATTERN) && BCRYPT_PREFIXES.none? { |prefix| normalized_code.start_with?(prefix) }
|
|
end
|
|
|
|
def normalize_backup_code(code)
|
|
code.to_s.strip.downcase
|
|
end
|
|
|
|
def bcrypt_cost
|
|
ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
|
|
end
|
|
end
|