Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate numerical backup codes #132

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 9 additions & 3 deletions lib/devise_two_factor/models/two_factor_backupable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ def self.required_fields(klass)
# 2) Generates otp_number_of_backup_codes backup codes
# 3) Stores the hashed backup codes in the database
# 4) Returns a plaintext array of the generated backup codes
def generate_otp_backup_codes!
def generate_otp_backup_codes!(opts={})
codes = []
number_of_codes = self.class.otp_number_of_backup_codes
code_length = self.class.otp_backup_code_length

number_of_codes.times do
codes << SecureRandom.hex(code_length / 2) # Hexstring has length 2*n
if opts[:numerical] == true
number_of_codes.times do
codes << code_length.times.map { SecureRandom.random_number(9) }.join
end
else
number_of_codes.times do
codes << SecureRandom.hex(code_length / 2) # Hexstring has length 2*n
end
end

hashed_codes = codes.map { |code| Devise::Encryptor.digest(self.class, code) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,39 @@
expect((subject.otp_backup_codes & old_codes_hashed)).to match []
end
end

context 'with numerical recovery codes' do
before do
subject.class.otp_backup_code_length = 6
@plaintext_codes = subject.generate_otp_backup_codes!(numerical: true)
end

it 'generates the correct number of new recovery codes' do
expect(subject.otp_backup_codes.length).to eq(subject.class.otp_number_of_backup_codes)
end

it 'generates recovery codes of the correct length' do
@plaintext_codes.each do |code|
expect(code.to_s.length).to eq(subject.class.otp_backup_code_length)
end
end

it 'generates distinct recovery codes' do
expect(@plaintext_codes.uniq).to contain_exactly(*@plaintext_codes)
end

it 'stores the codes as BCrypt hashes' do
subject.otp_backup_codes.each do |code|
expect(code).to match(/\A\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}\z/)
end
end

it 'does not generate alphabetical characters' do
@plaintext_codes.each do |code|
expect(code).to match(/\A\d{#{subject.class.otp_backup_code_length}}\z/)
end
end
end
end

describe '#invalidate_otp_backup_code!' do
Expand Down