rotp 2.1.1 → 6.2.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rotp might be problematic. Click here for more details.

Files changed (53) hide show
  1. checksums.yaml +5 -5
  2. data/.devcontainer/Dockerfile +19 -0
  3. data/.devcontainer/devcontainer.json +37 -0
  4. data/.dockerignore +1 -0
  5. data/.github/workflows/test.yaml +27 -0
  6. data/.gitignore +2 -0
  7. data/CHANGELOG.md +95 -0
  8. data/Dockerfile-2.3 +10 -0
  9. data/Dockerfile-2.7 +11 -0
  10. data/Dockerfile-3.0-rc +12 -0
  11. data/Guardfile +1 -1
  12. data/README.md +125 -31
  13. data/bin/rotp +1 -1
  14. data/docker-compose.yml +37 -0
  15. data/lib/rotp/arguments.rb +6 -5
  16. data/lib/rotp/base32.rb +56 -29
  17. data/lib/rotp/cli.rb +6 -10
  18. data/lib/rotp/hotp.rb +11 -26
  19. data/lib/rotp/otp/uri.rb +79 -0
  20. data/lib/rotp/otp.rb +20 -31
  21. data/lib/rotp/totp.rb +43 -29
  22. data/lib/rotp/version.rb +1 -1
  23. data/lib/rotp.rb +2 -3
  24. data/rotp.gemspec +15 -18
  25. data/spec/lib/rotp/arguments_spec.rb +18 -5
  26. data/spec/lib/rotp/base32_spec.rb +51 -19
  27. data/spec/lib/rotp/cli_spec.rb +42 -3
  28. data/spec/lib/rotp/hotp_spec.rb +39 -60
  29. data/spec/lib/rotp/otp/uri_spec.rb +99 -0
  30. data/spec/lib/rotp/totp_spec.rb +138 -120
  31. data/spec/spec_helper.rb +7 -0
  32. metadata +27 -45
  33. data/.travis.yml +0 -7
  34. data/Gemfile.lock +0 -75
  35. data/Rakefile +0 -9
  36. data/doc/ROTP/HOTP.html +0 -308
  37. data/doc/ROTP/OTP.html +0 -593
  38. data/doc/ROTP/TOTP.html +0 -493
  39. data/doc/Rotp.html +0 -179
  40. data/doc/_index.html +0 -144
  41. data/doc/class_list.html +0 -36
  42. data/doc/css/common.css +0 -1
  43. data/doc/css/full_list.css +0 -53
  44. data/doc/css/style.css +0 -310
  45. data/doc/file.README.html +0 -89
  46. data/doc/file_list.html +0 -38
  47. data/doc/frames.html +0 -13
  48. data/doc/index.html +0 -89
  49. data/doc/js/app.js +0 -203
  50. data/doc/js/full_list.js +0 -149
  51. data/doc/js/jquery.js +0 -154
  52. data/doc/method_list.html +0 -155
  53. data/doc/top-level-namespace.html +0 -88
@@ -1,25 +1,33 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe ROTP::Base32 do
4
-
5
- describe '.random_base32' do
4
+ describe '.random' do
6
5
  context 'without arguments' do
7
- let(:base32) { ROTP::Base32.random_base32 }
6
+ let(:base32) { ROTP::Base32.random }
8
7
 
9
- it 'is 16 characters long' do
10
- expect(base32.length).to eq 16
8
+ it 'is 20 bytes (160 bits) long (resulting in a 32 character base32 code)' do
9
+ expect(ROTP::Base32.decode(base32).length).to eq 20
10
+ expect(base32.length).to eq 32
11
11
  end
12
12
 
13
- it 'is hexadecimal' do
14
- expect(base32).to match %r{\A[a-z2-7]+\z}
13
+ it 'is base32 charset' do
14
+ expect(base32).to match(/\A[A-Z2-7]+\z/)
15
15
  end
16
16
  end
17
17
 
18
18
  context 'with arguments' do
19
- let(:base32) { ROTP::Base32.random_base32 32 }
19
+ let(:base32) { ROTP::Base32.random 48 }
20
20
 
21
- it 'allows a specific length' do
22
- expect(base32.length).to eq 32
21
+ it 'returns the appropriate byte length code' do
22
+ expect(ROTP::Base32.decode(base32).length).to eq 48
23
+ end
24
+ end
25
+
26
+ context 'alias to older random_base32' do
27
+ let(:base32) { ROTP::Base32.random_base32(36) }
28
+ it 'is base32 charset' do
29
+ expect(base32.length).to eq 36
30
+ expect(ROTP::Base32.decode(base32).length).to eq 22
23
31
  end
24
32
  end
25
33
  end
@@ -34,16 +42,40 @@ RSpec.describe ROTP::Base32 do
34
42
 
35
43
  context 'valid input data' do
36
44
  it 'correctly decodes a string' do
37
- expect(ROTP::Base32.decode('F').unpack('H*').first).to eq '28'
38
- expect(ROTP::Base32.decode('23').unpack('H*').first).to eq 'd6'
39
- expect(ROTP::Base32.decode('234').unpack('H*').first).to eq 'd6f8'
40
- expect(ROTP::Base32.decode('234A').unpack('H*').first).to eq 'd6f800'
41
- expect(ROTP::Base32.decode('234B').unpack('H*').first).to eq 'd6f810'
42
- expect(ROTP::Base32.decode('234BCD').unpack('H*').first).to eq 'd6f8110c'
43
- expect(ROTP::Base32.decode('234BCDE').unpack('H*').first).to eq 'd6f8110c80'
44
- expect(ROTP::Base32.decode('234BCDEFG').unpack('H*').first).to eq 'd6f8110c8530'
45
- expect(ROTP::Base32.decode('234BCDEFG234BCDEFG').unpack('H*').first).to eq 'd6f8110c8536b7c0886429'
45
+ expect(ROTP::Base32.decode('2EB7C66WC5TSO').unpack('H*').first).to eq 'd103f17bd6176727'
46
+ expect(ROTP::Base32.decode('Y6Y5ZCAC7NABCHSJ').unpack('H*').first).to eq 'c7b1dc8802fb40111e49'
47
+ end
48
+
49
+ it 'correctly decode strings with trailing bits (not a multiple of 8)' do
50
+ # Dropbox style 26 characters (26*5==130 bits or 16.25 bytes, but chopped to 128)
51
+ # Matches the behavior of Google Authenticator, drops extra 2 empty bits
52
+ expect(ROTP::Base32.decode('YVT6Z2XF4BQJNBMTD7M6QBQCEM').unpack('H*').first).to eq 'c567eceae5e0609685931fd9e8060223'
53
+
54
+ # For completeness, test all the possibilities allowed by Google Authenticator
55
+ # Drop the incomplete empty extra 4 bits (28*5==140bits or 17.5 bytes, chopped to 136 bits)
56
+ expect(ROTP::Base32.decode('5GGZQB3WN6LD7V3L5HPDYTQUANEQ').unpack('H*').first).to eq 'e98d9807766f963fd76be9de3c4e140349'
57
+
58
+ end
59
+
60
+ context 'with padding' do
61
+ it 'correctly decodes a string' do
62
+ expect(ROTP::Base32.decode('234A===').unpack('H*').first).to eq 'd6f8'
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+
69
+ describe '.encode' do
70
+ context 'encode input data' do
71
+ it 'correctly encodes data' do
72
+ expect(ROTP::Base32.encode(hex_to_bin('3c204da94294ff82103ee34e96f74b48'))).to eq 'HQQE3KKCST7YEEB64NHJN52LJA'
46
73
  end
47
74
  end
48
75
  end
76
+
77
+ end
78
+
79
+ def hex_to_bin(s)
80
+ s.scan(/../).map { |x| x.hex }.pack('c*')
49
81
  end
@@ -4,26 +4,65 @@ require 'rotp/cli'
4
4
  RSpec.describe ROTP::CLI do
5
5
  let(:cli) { described_class.new('executable', argv) }
6
6
  let(:output) { cli.output }
7
- let(:now) { Time.utc 2012,1,1 }
7
+ let(:now) { Time.utc 2012, 1, 1 }
8
8
 
9
9
  before do
10
10
  Timecop.freeze now
11
11
  end
12
12
 
13
13
  context 'generating a TOTP' do
14
- let(:argv) { %w(--secret JBSWY3DPEHPK3PXP) }
14
+ let(:argv) { %w[--secret JBSWY3DPEHPK3PXP] }
15
15
 
16
16
  it 'prints the corresponding token' do
17
17
  expect(output).to eq '068212'
18
18
  end
19
19
  end
20
20
 
21
+ context 'generating a TOTP with sha256 digest' do
22
+ let(:argv) { %w[--secret JBSWY3DPEHPK3PXP --digest sha256] }
23
+
24
+ it 'prints the corresponding token' do
25
+ expect(output).to eq '544902'
26
+ end
27
+ end
28
+
29
+ context 'generating a TOTP with no secret' do
30
+ let(:argv) { %w[--time --secret] }
31
+
32
+ it 'prints the corresponding token' do
33
+ expect(output).to match 'You must also specify a --secret'
34
+ end
35
+ end
36
+
37
+ context 'generating a TOTP with bad base32 secret' do
38
+ let(:argv) { %W[--time --secret #{'1' * 32}] }
39
+
40
+ it 'prints the corresponding token' do
41
+ expect(output).to match 'Secret must be in RFC4648 Base32 format'
42
+ end
43
+ end
44
+
45
+ context 'trying to generate an unsupport type' do
46
+ let(:argv) { %W[--notreal --secret #{'a' * 32}] }
47
+
48
+ it 'prints the corresponding token' do
49
+ expect(output).to match 'invalid option: --notreal'
50
+ end
51
+ end
52
+
21
53
  context 'generating a HOTP' do
22
- let(:argv) { %W(--hmac --secret #{'a' * 32} --counter 1234) }
54
+ let(:argv) { %W[--hmac --secret #{'a' * 32} --counter 1234] }
23
55
 
24
56
  it 'prints the corresponding token' do
25
57
  expect(output).to eq '161024'
26
58
  end
27
59
  end
28
60
 
61
+ context 'generating a HOTP' do
62
+ let(:argv) { %W[--hmac --secret #{'a' * 32} --counter 1234 --digest sha256] }
63
+
64
+ it 'prints the corresponding token' do
65
+ expect(output).to eq '325941'
66
+ end
67
+ end
29
68
  end
@@ -14,11 +14,9 @@ RSpec.describe ROTP::HOTP do
14
14
  end
15
15
  end
16
16
 
17
- context 'without padding' do
18
- let(:token) { hotp.at counter, false }
19
-
20
- it 'generates an integer OTP' do
21
- expect(token).to eq 161024
17
+ context 'invalid counter' do
18
+ it 'raises an error' do
19
+ expect { hotp.at(-123_456) }.to raise_error(ArgumentError)
22
20
  end
23
21
  end
24
22
 
@@ -39,7 +37,6 @@ RSpec.describe ROTP::HOTP do
39
37
  expect(hotp.at(8)).to eq '399871'
40
38
  expect(hotp.at(9)).to eq '520489'
41
39
  end
42
-
43
40
  end
44
41
  end
45
42
 
@@ -47,10 +44,10 @@ RSpec.describe ROTP::HOTP do
47
44
  let(:verification) { hotp.verify token, counter }
48
45
 
49
46
  context 'numeric token' do
50
- let(:token) { 161024 }
47
+ let(:token) { 161_024 }
51
48
 
52
49
  it 'raises an error' do
53
- expect { verification }.to raise_error
50
+ expect { verification }.to raise_error(ArgumentError)
54
51
  end
55
52
  end
56
53
 
@@ -69,74 +66,56 @@ RSpec.describe ROTP::HOTP do
69
66
  expect(hotp.verify(token, 10)).to be_falsey
70
67
  end
71
68
  end
72
- end
73
-
74
- describe '#provisioning_uri' do
75
- let(:uri) { hotp.provisioning_uri('mark@percival') }
76
- let(:params) { CGI::parse URI::parse(uri).query }
77
-
78
- it 'has the correct format' do
79
- expect(uri).to match %r{\Aotpauth:\/\/hotp.+}
80
- end
81
-
82
- it 'includes the secret as parameter' do
83
- expect(params['secret'].first).to eq 'a' * 32
84
- end
85
- end
86
-
87
- describe '#verify_with_retries' do
88
- let(:verification) { hotp.verify_with_retries token, counter, retries }
69
+ describe 'with retries' do
70
+ let(:verification) { hotp.verify token, counter, retries: retries }
89
71
 
90
- context 'negative retries' do
91
- let(:retries) { -1 }
72
+ context 'counter outside than retries' do
73
+ let(:counter) { 1223 }
74
+ let(:retries) { 10 }
92
75
 
93
- it 'is false' do
94
- expect(verification).to be_falsey
76
+ it 'is false' do
77
+ expect(verification).to be_falsey
78
+ end
95
79
  end
96
- end
97
80
 
98
- context 'zero retries' do
99
- let(:retries) { 0 }
81
+ context 'counter exactly in retry range' do
82
+ let(:counter) { 1224 }
83
+ let(:retries) { 10 }
100
84
 
101
- it 'is false' do
102
- expect(verification).to be_falsey
85
+ it 'is true' do
86
+ expect(verification).to eq 1234
87
+ end
103
88
  end
104
- end
105
89
 
106
- context 'counter lower than retries' do
107
- let(:counter) { 1223 }
108
- let(:retries) { 10 }
90
+ context 'counter in retry range' do
91
+ let(:counter) { 1224 }
92
+ let(:retries) { 11 }
109
93
 
110
- it 'is false' do
111
- expect(verification).to be_falsey
94
+ it 'is true' do
95
+ expect(verification).to eq 1234
96
+ end
112
97
  end
113
- end
114
98
 
115
- context 'counter exactly in retry range' do
116
- let(:counter) { 1224 }
117
- let(:retries) { 10 }
99
+ context 'counter ahead of token' do
100
+ let(:counter) { 1235 }
101
+ let(:retries) { 3 }
118
102
 
119
- it 'is true' do
120
- expect(verification).to eq 1234
103
+ it 'is false' do
104
+ expect(verification).to be_falsey
105
+ end
121
106
  end
122
107
  end
108
+ end
123
109
 
124
- context 'counter in retry range' do
125
- let(:counter) { 1224 }
126
- let(:retries) { 11 }
127
-
128
- it 'is true' do
129
- expect(verification).to eq 1234
130
- end
110
+ describe '#provisioning_uri' do
111
+ it 'accepts the account name' do
112
+ expect(hotp.provisioning_uri('mark@percival'))
113
+ .to eq 'otpauth://hotp/mark%40percival?secret=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&counter=0'
131
114
  end
132
115
 
133
- context 'counter too high' do
134
- let(:counter) { 1235 }
135
- let(:retries) { 3 }
136
-
137
- it 'is false' do
138
- expect(verification).to be_falsey
139
- end
116
+ it 'also accepts a custom counter value' do
117
+ expect(hotp.provisioning_uri('mark@percival', 17))
118
+ .to eq 'otpauth://hotp/mark%40percival?secret=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&counter=17'
140
119
  end
141
120
  end
142
121
  end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ROTP::OTP::URI do
4
+ it 'meets basic functionality' do
5
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP')
6
+ uri = described_class.new(otp, account_name: 'alice@google.com')
7
+ expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP'
8
+ end
9
+
10
+ it 'includes issuer' do
11
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: 'Example')
12
+ uri = described_class.new(otp, account_name: 'alice@google.com')
13
+ expect(uri.to_s).to eq 'otpauth://totp/Example:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example'
14
+ end
15
+
16
+ it 'encodes the account name' do
17
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: 'Provider1')
18
+ uri = described_class.new(otp, account_name: 'Alice Smith')
19
+ expect(uri.to_s).to eq 'otpauth://totp/Provider1:Alice%20Smith?secret=JBSWY3DPEHPK3PXP&issuer=Provider1'
20
+ end
21
+
22
+ it 'encodes the issuer' do
23
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: 'Big Corporation')
24
+ uri = described_class.new(otp, account_name: ' alice@bigco.com')
25
+ expect(uri.to_s).to eq 'otpauth://totp/Big%20Corporation:%20alice%40bigco.com?secret=JBSWY3DPEHPK3PXP&issuer=Big%20Corporation'
26
+ end
27
+
28
+ it 'includes non-default SHA256 algorithm' do
29
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digest: 'sha256')
30
+ uri = described_class.new(otp, account_name: 'alice@google.com')
31
+ expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256'
32
+ end
33
+
34
+ it 'includes non-default SHA512 algorithm' do
35
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digest: 'sha512')
36
+ uri = described_class.new(otp, account_name: 'alice@google.com')
37
+ expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512'
38
+ end
39
+
40
+ it 'includes non-default 8 digits' do
41
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digits: 8)
42
+ uri = described_class.new(otp, account_name: 'alice@google.com')
43
+ expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&digits=8'
44
+ end
45
+
46
+ it 'includes non-default period for TOTP' do
47
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', interval: 35)
48
+ uri = described_class.new(otp, account_name: 'alice@google.com')
49
+ expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&period=35'
50
+ end
51
+
52
+ it 'includes non-default counter for HOTP' do
53
+ otp = ROTP::HOTP.new('JBSWY3DPEHPK3PXP')
54
+ uri = described_class.new(otp, account_name: 'alice@google.com', counter: 17)
55
+ expect(uri.to_s).to eq 'otpauth://hotp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&counter=17'
56
+ end
57
+
58
+ it 'can include all parameters' do
59
+ otp = ROTP::TOTP.new(
60
+ 'HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ',
61
+ digest: 'sha512',
62
+ digits: 8,
63
+ interval: 60,
64
+ issuer: 'ACME Co',
65
+ )
66
+ uri = described_class.new(otp, account_name: 'john.doe@email.com')
67
+ expect(uri.to_s).to eq'otpauth://totp/ACME%20Co:john.doe%40email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA512&digits=8&period=60'
68
+ end
69
+
70
+ it 'strips leading and trailing whitespace from the issuer' do
71
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: ' Big Corporation ')
72
+ uri = described_class.new(otp, account_name: ' alice@bigco.com')
73
+ expect(uri.to_s).to eq 'otpauth://totp/Big%20Corporation:%20alice%40bigco.com?secret=JBSWY3DPEHPK3PXP&issuer=Big%20Corporation'
74
+ end
75
+
76
+ it 'strips trailing whitespace from the account name' do
77
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP')
78
+ uri = described_class.new(otp, account_name: ' alice@google.com ')
79
+ expect(uri.to_s).to eq 'otpauth://totp/%20%20alice%40google.com?secret=JBSWY3DPEHPK3PXP'
80
+ end
81
+
82
+ it 'replaces colons in the issuer with underscores' do
83
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: 'Big:Corporation')
84
+ uri = described_class.new(otp, account_name: 'alice@bigco.com')
85
+ expect(uri.to_s).to eq 'otpauth://totp/Big_Corporation:alice%40bigco.com?secret=JBSWY3DPEHPK3PXP&issuer=Big_Corporation'
86
+ end
87
+
88
+ it 'replaces colons in the account name with underscores' do
89
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP')
90
+ uri = described_class.new(otp, account_name: 'Alice:Smith')
91
+ expect(uri.to_s).to eq 'otpauth://totp/Alice_Smith?secret=JBSWY3DPEHPK3PXP'
92
+ end
93
+
94
+ it 'handles email account names with sub-addressing' do
95
+ otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP')
96
+ uri = described_class.new(otp, account_name: 'alice+1234@google.com')
97
+ expect(uri.to_s).to eq 'otpauth://totp/alice%2B1234%40google.com?secret=JBSWY3DPEHPK3PXP'
98
+ end
99
+ end