require 'spec_helper' TEST_TIME = Time.utc 2016, 9, 23, 9 # 2016-09-23 09:00:00 UTC TEST_TOKEN = '082630'.freeze RSpec.describe ROTP::TOTP do let(:now) { TEST_TIME } let(:token) { TEST_TOKEN } let(:totp) { ROTP::TOTP.new 'JBSWY3DPEHPK3PXP' } describe '#at' do let(:token) { totp.at now } it 'is a string number' do expect(token).to eq TEST_TOKEN end context 'RFC compatibility' do let(:totp) { ROTP::TOTP.new('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') } it 'matches the RFC documentation examples' do expect(totp.at(1_111_111_111)).to eq '050471' expect(totp.at(1_234_567_890)).to eq '005924' expect(totp.at(2_000_000_000)).to eq '279037' end end end describe '#verify' do let(:verification) { totp.verify token, at: now } context 'numeric token' do let(:token) { 82_630 } it 'raises an error with an integer' do expect { verification }.to raise_error(ArgumentError) end end context 'unpadded string token' do let(:token) { '82630' } it 'fails to verify' do expect(verification).to be_falsey end end context 'correctly padded string token' do it 'verifies' do expect(verification).to be_truthy end end context 'RFC compatibility' do let(:totp) { ROTP::TOTP.new 'wrn3pqx5uqxqvnqr' } before do Timecop.freeze now end context 'correct time based OTP' do let(:token) { '102705' } let(:now) { Time.at 1_297_553_958 } it 'verifies' do expect(totp.verify('102705')).to be_truthy end end context 'wrong time based OTP' do it 'fails to verify' do expect(totp.verify('102705')).to be_falsey end end end context 'invalidating reused tokens' do let(:verification) do totp.verify token, after: after, at: now end let(:after) { nil } context 'passing in the `after` timestamp' do let(:after) do totp.verify TEST_TOKEN, after: nil, at: now end it 'returns a timecode' do expect(after).to be_kind_of(Integer) expect(after).to be_within(30).of(now.to_i) end context 'reusing same token' do it 'is false' do expect(verification).to be_falsy end end end end end def get_timecodes(at, b, a) # Test the private method totp.send('get_timecodes', at, b, a) end describe 'drifting timecodes' do it 'should get timecodes behind' do expect(get_timecodes(TEST_TIME + 15, 15, 0)).to eq([49_154_040]) expect(get_timecodes(TEST_TIME, 15, 0)).to eq([49_154_039, 49_154_040]) expect(get_timecodes(TEST_TIME, 40, 0)).to eq([49_154_038, 49_154_039, 49_154_040]) expect(get_timecodes(TEST_TIME, 90, 0)).to eq([49_154_037, 49_154_038, 49_154_039, 49_154_040]) end it 'should get timecodes ahead' do expect(get_timecodes(TEST_TIME, 0, 15)).to eq([49_154_040]) expect(get_timecodes(TEST_TIME + 15, 0, 15)).to eq([49_154_040, 49_154_041]) expect(get_timecodes(TEST_TIME, 0, 30)).to eq([49_154_040, 49_154_041]) expect(get_timecodes(TEST_TIME, 0, 70)).to eq([49_154_040, 49_154_041, 49_154_042]) expect(get_timecodes(TEST_TIME, 0, 90)).to eq([49_154_040, 49_154_041, 49_154_042, 49_154_043]) end it 'should get timecodes behind and ahead' do expect(get_timecodes(TEST_TIME, 30, 30)).to eq([49_154_039, 49_154_040, 49_154_041]) expect(get_timecodes(TEST_TIME, 60, 60)).to eq([49_154_038, 49_154_039, 49_154_040, 49_154_041, 49_154_042]) end end describe '#verify with drift' do let(:verification) { totp.verify token, drift_ahead: drift_ahead, drift_behind: drift_behind, at: now } let(:drift_ahead) { 0 } let(:drift_behind) { 0 } context 'with an old OTP' do let(:token) { totp.at TEST_TIME - 30 } # Previous token at 2016-09-23 08:59:30 UTC let(:drift_behind) { 15 } # Tested at 2016-09-23 09:00:00 UTC, and with drift back to 2016-09-23 08:59:45 UTC # This would therefore include 2 intervals it 'inside of drift range' do expect(verification).to be_truthy end # Tested at 2016-09-23 09:00:20 UTC, and with drift back to 2016-09-23 09:00:05 UTC # This only includes 1 interval, therefore only the current token is valid context 'outside of drift range' do let(:now) { TEST_TIME + 20 } it 'is nil' do expect(verification).to be_nil end end end context 'with a future OTP' do let(:token) { totp.at TEST_TIME + 30 } # The next valid token - 2016-09-23 09:00:30 UTC let(:drift_ahead) { 15 } # Tested at 2016-09-23 09:00:00 UTC, and ahead to 2016-09-23 09:00:15 UTC # This only includes 1 interval, therefore only the current token is valid it 'outside of drift range' do expect(verification).to be_falsey end # Tested at 2016-09-23 09:00:20 UTC, and with drift ahead to 2016-09-23 09:00:35 UTC # This would therefore include 2 intervals context 'inside of drift range' do let(:now) { TEST_TIME + 20 } it 'is true' do expect(verification).to be_truthy end end end end describe '#verify with drift and prevent token reuse' do let(:verification) { totp.verify token, drift_ahead: drift_ahead, drift_behind: drift_behind, after: after, at: now } let(:drift_ahead) { 0 } let(:drift_behind) { 0 } let(:after) { nil } context 'with the `after` timestamp set' do context 'older token' do let(:token) { totp.at TEST_TIME - 30 } let(:drift_behind) { 15 } it 'is true' do expect(verification).to be_truthy expect(verification).to eq((TEST_TIME - 30).to_i) end context 'after it has been used' do let(:after) do totp.verify token, after: nil, at: now, drift_behind: drift_behind end it 'is false' do expect(verification).to be_falsey end end end context 'newer token' do let(:token) { totp.at TEST_TIME + 30 } let(:drift_ahead) { 15 } let(:now) { TEST_TIME + 15 } it 'is true' do expect(verification).to be_truthy expect(verification).to eq((TEST_TIME + 30).to_i) end context 'after it has been used' do let(:after) do totp.verify token, after: nil, at: now, drift_ahead: drift_ahead end it 'is false' do expect(verification).to be_falsey end end end end end describe '#provisioning_uri' do it 'accepts the account name' do expect(totp.provisioning_uri('mark@percival')) .to eq 'otpauth://totp/mark%40percival?secret=JBSWY3DPEHPK3PXP' end end describe '#now' do before do Timecop.freeze now end context 'Google Authenticator' do let(:totp) { ROTP::TOTP.new 'wrn3pqx5uqxqvnqr' } let(:now) { Time.at 1_297_553_958 } it 'matches the known output' do expect(totp.now).to eq '102705' end end context 'Dropbox 26 char secret output' do let(:totp) { ROTP::TOTP.new 'tjtpqea6a42l56g5eym73go2oa' } let(:now) { Time.at 1_378_762_454 } it 'matches the known output' do expect(totp.now).to eq '747864' end end end end