two_factor_auth 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +634 -0
  3. data/Rakefile +34 -0
  4. data/app/assets/javascripts/two_factor_auth/application.js +13 -0
  5. data/app/assets/stylesheets/two_factor_auth/application.css +15 -0
  6. data/app/controllers/two_factor_auth/authentications_controller.rb +32 -0
  7. data/app/controllers/two_factor_auth/registrations_controller.rb +29 -0
  8. data/app/controllers/two_factor_auth/trusted_facets_controller.rb +10 -0
  9. data/app/controllers/two_factor_auth/two_factor_auth_controller.rb +19 -0
  10. data/app/helpers/two_factor_auth/application_helper.rb +21 -0
  11. data/app/helpers/two_factor_auth/authentications_helper.rb +17 -0
  12. data/app/helpers/two_factor_auth/registrations_helper.rb +17 -0
  13. data/app/models/two_factor_auth/authentication_client_data.rb +11 -0
  14. data/app/models/two_factor_auth/authentication_request.rb +30 -0
  15. data/app/models/two_factor_auth/authentication_response.rb +49 -0
  16. data/app/models/two_factor_auth/authentication_verifier.rb +68 -0
  17. data/app/models/two_factor_auth/client_data.rb +57 -0
  18. data/app/models/two_factor_auth/registration.rb +18 -0
  19. data/app/models/two_factor_auth/registration_request.rb +33 -0
  20. data/app/models/two_factor_auth/registration_response.rb +91 -0
  21. data/app/models/two_factor_auth/registration_verifier.rb +91 -0
  22. data/app/views/layouts/two_factor_auth/application.html.erb +16 -0
  23. data/app/views/two_factor_auth/authentications/new.html.erb +30 -0
  24. data/app/views/two_factor_auth/registrations/new.html.erb +26 -0
  25. data/config/routes.rb +3 -0
  26. data/lib/generators/templates/README +6 -0
  27. data/lib/generators/templates/initializer.rb +38 -0
  28. data/lib/generators/templates/migration.rb +15 -0
  29. data/lib/generators/two_factor_auth/install_generator.rb +32 -0
  30. data/lib/tasks/two_factor_auth_tasks.rake +13 -0
  31. data/lib/two_factor_auth/authentication_hook.rb +18 -0
  32. data/lib/two_factor_auth/engine.rb +5 -0
  33. data/lib/two_factor_auth/registration_hook.rb +17 -0
  34. data/lib/two_factor_auth/version.rb +3 -0
  35. data/lib/two_factor_auth.rb +155 -0
  36. data/test/controllers/two_factor_auth/authentications_controller_test.rb +70 -0
  37. data/test/controllers/two_factor_auth/registrations_controller_test.rb +57 -0
  38. data/test/controllers/two_factor_auth/trusted_facets_controller_test.rb +17 -0
  39. data/test/dummy/README.rdoc +28 -0
  40. data/test/dummy/Rakefile +6 -0
  41. data/test/dummy/app/assets/javascripts/application.js +13 -0
  42. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  43. data/test/dummy/app/controllers/application_controller.rb +5 -0
  44. data/test/dummy/app/controllers/secrets_controller.rb +3 -0
  45. data/test/dummy/app/helpers/application_helper.rb +2 -0
  46. data/test/dummy/app/models/user.rb +8 -0
  47. data/test/dummy/app/views/layouts/application.html.erb +16 -0
  48. data/test/dummy/app/views/secrets/index.html.erb +10 -0
  49. data/test/dummy/bin/bundle +3 -0
  50. data/test/dummy/bin/rails +4 -0
  51. data/test/dummy/bin/rake +4 -0
  52. data/test/dummy/config/application.rb +24 -0
  53. data/test/dummy/config/boot.rb +5 -0
  54. data/test/dummy/config/database.yml +25 -0
  55. data/test/dummy/config/environment.rb +5 -0
  56. data/test/dummy/config/environments/development.rb +37 -0
  57. data/test/dummy/config/environments/production.rb +78 -0
  58. data/test/dummy/config/environments/test.rb +39 -0
  59. data/test/dummy/config/initializers/assets.rb +8 -0
  60. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  61. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  62. data/test/dummy/config/initializers/devise.rb +259 -0
  63. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  64. data/test/dummy/config/initializers/inflections.rb +16 -0
  65. data/test/dummy/config/initializers/mime_types.rb +4 -0
  66. data/test/dummy/config/initializers/session_store.rb +3 -0
  67. data/test/dummy/config/initializers/two_factor_auth.rb +38 -0
  68. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  69. data/test/dummy/config/locales/devise.en.yml +60 -0
  70. data/test/dummy/config/locales/en.yml +23 -0
  71. data/test/dummy/config/routes.rb +5 -0
  72. data/test/dummy/config/secrets.yml +22 -0
  73. data/test/dummy/config.ru +4 -0
  74. data/test/dummy/db/development.sqlite3 +0 -0
  75. data/test/dummy/db/migrate/20141026231953_devise_create_users.rb +42 -0
  76. data/test/dummy/db/migrate/20141224135949_create_two_factor_auth_registrations.rb +15 -0
  77. data/test/dummy/db/schema.rb +50 -0
  78. data/test/dummy/db/test.sqlite3 +0 -0
  79. data/test/dummy/log/development.log +198 -0
  80. data/test/dummy/log/test.log +3490 -0
  81. data/test/dummy/public/404.html +67 -0
  82. data/test/dummy/public/422.html +67 -0
  83. data/test/dummy/public/500.html +66 -0
  84. data/test/dummy/public/favicon.ico +0 -0
  85. data/test/dummy/tmp/cache/assets/test/sprockets/13fe41fee1fe35b49d145bcc06610705 +0 -0
  86. data/test/dummy/tmp/cache/assets/test/sprockets/2f5173deea6c795b8fdde723bb4b63af +0 -0
  87. data/test/dummy/tmp/cache/assets/test/sprockets/357970feca3ac29060c1e3861e2c0953 +0 -0
  88. data/test/dummy/tmp/cache/assets/test/sprockets/cffd775d018f68ce5dba1ee0d951a994 +0 -0
  89. data/test/dummy/tmp/cache/assets/test/sprockets/d771ace226fc8215a3572e0aa35bb0d6 +0 -0
  90. data/test/dummy/tmp/cache/assets/test/sprockets/f7cbd26ba1d28d48de824f0e94586655 +0 -0
  91. data/test/helpers/two_factor_auth/authentication_helper_test.rb +54 -0
  92. data/test/helpers/two_factor_auth/registrations_helper_test.rb +34 -0
  93. data/test/integration/navigation_test.rb +10 -0
  94. data/test/lib/two_factor_auth_test.rb +169 -0
  95. data/test/models/two_factor_auth/authentication_request_test.rb +35 -0
  96. data/test/models/two_factor_auth/authentication_response_test.rb +44 -0
  97. data/test/models/two_factor_auth/authentication_verifier_test.rb +83 -0
  98. data/test/models/two_factor_auth/client_data_test.rb +79 -0
  99. data/test/models/two_factor_auth/registration_request_test.rb +29 -0
  100. data/test/models/two_factor_auth/registration_response_test.rb +87 -0
  101. data/test/models/two_factor_auth/registration_verifier_test.rb +96 -0
  102. data/test/test_helper.rb +43 -0
  103. metadata +351 -0
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The page you were looking for doesn't exist (404)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/404.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>The page you were looking for doesn't exist.</h1>
62
+ <p>You may have mistyped the address or the page may have moved.</p>
63
+ </div>
64
+ <p>If you are the application owner check the logs for more information.</p>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/422.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>The change you wanted was rejected.</h1>
62
+ <p>Maybe you tried to change something you didn't have access to.</p>
63
+ </div>
64
+ <p>If you are the application owner check the logs for more information.</p>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/500.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>We're sorry, but something went wrong.</h1>
62
+ </div>
63
+ <p>If you are the application owner check the logs for more information.</p>
64
+ </div>
65
+ </body>
66
+ </html>
File without changes
@@ -0,0 +1,54 @@
1
+ require 'test_helper'
2
+
3
+ module TwoFactorAuth
4
+ describe AuthenticationsHelper do
5
+ def current_user
6
+ @current_user ||= User.create! email: 'user@example.com', password: 'password'
7
+ end
8
+
9
+ before do
10
+ Registration.create!({
11
+ login: current_user,
12
+ key_handle: "key handle",
13
+ public_key: "public key",
14
+ certificate: "certificate",
15
+ counter: 0,
16
+ last_authenticated_at: Time.now,
17
+ })
18
+ end
19
+
20
+ after do
21
+ User.delete_all
22
+ end
23
+
24
+ describe "#authentication_request" do
25
+ def user_session
26
+ {}
27
+ end
28
+
29
+ it "creates AuthenticationRequests" do
30
+ authentication_request.must_be_instance_of AuthenticationRequest
31
+ end
32
+
33
+ it "persists the challenge in the request" do
34
+ ch1 = authentication_request.challenge
35
+ ch2 = authentication_request.challenge
36
+ ch1.must_equal ch2
37
+ end
38
+ end
39
+
40
+ describe "#authentication_request with pending challenge" do
41
+ let(:encoded_challenge) { TwoFactorAuth.websafe_base64_encode('0' * 32) }
42
+ def user_session
43
+ {
44
+ 'pending_authentication_request_challenge' => encoded_challenge
45
+ }
46
+ end
47
+
48
+ it "uses pending challenge" do
49
+ authentication_request.challenge.must_equal encoded_challenge
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,34 @@
1
+ require 'test_helper'
2
+
3
+ module TwoFactorAuth
4
+ describe RegistrationsHelper do
5
+ describe "#registration_request" do
6
+ def user_session
7
+ {}
8
+ end
9
+
10
+ it "creates RegistrationRequests" do
11
+ registration_request.must_be_instance_of RegistrationRequest
12
+ end
13
+
14
+ it "persists the challenge in the request" do
15
+ ch1 = registration_request.challenge
16
+ ch2 = registration_request.challenge
17
+ ch1.must_equal ch2
18
+ end
19
+ end
20
+
21
+ describe "#registration_request with pending challenge" do
22
+ let(:encoded_challenge) { TwoFactorAuth.websafe_base64_encode('0' * 32) }
23
+ def user_session
24
+ {
25
+ 'pending_registration_request_challenge' => encoded_challenge
26
+ }
27
+ end
28
+ it "uses pending challenge" do
29
+ registration_request.challenge.must_equal encoded_challenge
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ require 'test_helper'
2
+
3
+ class NavigationTest < ActionDispatch::IntegrationTest
4
+ fixtures :all
5
+
6
+ # test "the truth" do
7
+ # assert true
8
+ # end
9
+ end
10
+
@@ -0,0 +1,169 @@
1
+ require 'test_helper'
2
+
3
+ describe TwoFactorAuth do
4
+ let(:pubkey) { "\x049\xC6GC\xE6\xD3un;a\xD2\x04\e\x9B,vk\xEB\xDC\xB51\b\x14\x16\a\x92\x8D\xA1\xA92\xD7Z\x17\xB3D\xDF}P\xDA\x9Cg\xEB\xFD7h)N\xC4\xF2\xF1\x10\e\x8A\xC7\x88\x9AM\x17V\xEA\xBEX\x14\xAB" }
5
+
6
+ describe "U2F_VERSION" do
7
+ it "is the only version" do
8
+ TwoFactorAuth::U2F_VERSION.must_equal 'U2F_V2'
9
+ end
10
+ end
11
+
12
+ describe "setting facet_domain" do
13
+ it "remembers what is set" do
14
+ TwoFactorAuth.facet_domain = "https://www.example.net"
15
+ TwoFactorAuth.facet_domain.must_equal "https://www.example.net"
16
+ end
17
+
18
+ it "normalizes to remove trailing /" do
19
+ TwoFactorAuth.facet_domain = "https://www.example.net/"
20
+ TwoFactorAuth.facet_domain.must_equal "https://www.example.net"
21
+ end
22
+
23
+ # Yes, after 5 hours of debugging to solve the vague error caused by this,
24
+ # it deserves a redundant test.
25
+ it "doesn't let http domains end in a /" do
26
+ TwoFactorAuth.facet_domain = "http://www.example.net/"
27
+ TwoFactorAuth.facet_domain.must_equal "http://www.example.net"
28
+ end
29
+
30
+ it "raises if you try to use localhost" do
31
+ Proc.new {
32
+ TwoFactorAuth.facet_domain = "http://localhost:3000"
33
+ }.must_raise(TwoFactorAuth::InvalidFacetDomain)
34
+ Proc.new {
35
+ TwoFactorAuth.facet_domain = "http://localhost:3000/"
36
+ }.must_raise(TwoFactorAuth::InvalidFacetDomain)
37
+ Proc.new {
38
+ TwoFactorAuth.facet_domain = "http://localhost/"
39
+ }.must_raise(TwoFactorAuth::InvalidFacetDomain)
40
+ end
41
+
42
+ it "raises if you try to use something.dev" do
43
+ Proc.new {
44
+ TwoFactorAuth.facet_domain = "http://local.dev:3000"
45
+ }.must_raise(TwoFactorAuth::InvalidFacetDomain)
46
+ Proc.new {
47
+ TwoFactorAuth.facet_domain = "http://local.dev:3000/"
48
+ }.must_raise(TwoFactorAuth::InvalidFacetDomain)
49
+ Proc.new {
50
+ TwoFactorAuth.facet_domain = "http://local.dev/"
51
+ }.must_raise(TwoFactorAuth::InvalidFacetDomain)
52
+ end
53
+
54
+ it "raises if you use the default from the initializer" do
55
+ Proc.new {
56
+ TwoFactorAuth.facet_domain = "https://www.example.com"
57
+ }.must_raise(TwoFactorAuth::InvalidFacetDomain)
58
+ end
59
+ end
60
+
61
+ describe "setting trusted_facet_list_url" do
62
+ it "can be set explicitly" do
63
+ TwoFactorAuth.trusted_facet_list_url = "https://www.example.net/facets"
64
+ TwoFactorAuth.trusted_facet_list_url.must_equal "https://www.example.net/facets"
65
+ end
66
+
67
+ it "is based on facet_domain otherwise" do
68
+ # this value is persisted across tests...
69
+ TwoFactorAuth.trusted_facet_list_url = nil
70
+ TwoFactorAuth.facet_domain = "https://www.example.net"
71
+ TwoFactorAuth.trusted_facet_list_url.must_equal "https://www.example.net/two_factor_auth/trusted_facets"
72
+ end
73
+ end
74
+
75
+ describe "setting facets" do
76
+ it "can be set explicitly" do
77
+ TwoFactorAuth.facets = ["https://www.example.net", "https://staging.example.net"]
78
+ TwoFactorAuth.facets.must_equal ["https://www.example.net", "https://staging.example.net"]
79
+ end
80
+
81
+ it "is just the facet_domain by default" do
82
+ # this value is persisted across tests...
83
+ TwoFactorAuth.facets = nil
84
+ TwoFactorAuth.facet_domain = "https://www.example.net"
85
+ TwoFactorAuth.facets.must_equal ["https://www.example.net"]
86
+ end
87
+
88
+ it "is just the facet_domain if empty list" do
89
+ # this value is persisted across tests...
90
+ TwoFactorAuth.facets = []
91
+ TwoFactorAuth.facet_domain = "https://www.example.net"
92
+ TwoFactorAuth.facets.must_equal ["https://www.example.net"]
93
+ end
94
+ end
95
+
96
+ describe ".websafe_base64u_encode" do
97
+ it "encodes data" do
98
+ TwoFactorAuth.websafe_base64_encode("foo").must_equal "Zm9v"
99
+ end
100
+
101
+ it "does not include trailing =s" do
102
+ Base64.urlsafe_encode64("ab").must_equal "YWI="
103
+ TwoFactorAuth.websafe_base64_encode("ab").must_equal "YWI"
104
+ end
105
+ end
106
+
107
+ describe ".websafe_base64u_decode" do
108
+ it "decodes data" do
109
+ TwoFactorAuth.websafe_base64_decode("Zm9v").must_equal "foo"
110
+ end
111
+
112
+ it "does not mind missing trailing =s" do
113
+ Base64.urlsafe_decode64("YWI=").must_equal "ab"
114
+ TwoFactorAuth.websafe_base64_decode("YWI").must_equal "ab"
115
+ end
116
+ end
117
+
118
+ describe "#random_encoded_challenge" do
119
+ it "is 32 bytes, encoded" do
120
+ challenge = TwoFactorAuth.random_encoded_challenge
121
+ decoded = TwoFactorAuth.websafe_base64_decode(challenge)
122
+ decoded.length.must_equal 32
123
+ end
124
+
125
+ it "is different every time" do
126
+ c1 = TwoFactorAuth.random_encoded_challenge
127
+ c2 = TwoFactorAuth.random_encoded_challenge
128
+ c1.wont_equal c2
129
+ end
130
+
131
+ it "raises if out of entropy" do
132
+ OpenSSL::Random.stub(:pseudo_bytes, Proc.new { raise OpenSSL::Random::RandomError }) do
133
+ Proc.new {
134
+ TwoFactorAuth.random_encoded_challenge
135
+ }.must_raise TwoFactorAuth::CantGenerateRandomNumbers
136
+ end
137
+ end
138
+ end
139
+
140
+ describe "#decode_pubkey" do
141
+ it "returns the Point" do
142
+ point = TwoFactorAuth.decode_pubkey pubkey
143
+ point.to_bn.to_i.must_equal 56657129115817956563260749049282610971311272788676140555509072492610442759820703941373942638593733546160471073247664666784884688116961061627926493680637099
144
+ end
145
+
146
+ it "raises if invalid" do
147
+ Proc.new {
148
+ TwoFactorAuth.decode_pubkey "invalid key"
149
+ }.must_raise TwoFactorAuth::InvalidPublicKey
150
+ end
151
+ end
152
+
153
+ describe "#pubkey_valid?" do
154
+ it "is if key is valid" do
155
+ b = TwoFactorAuth.pubkey_valid? pubkey
156
+ b.must_equal true
157
+ end
158
+
159
+ it "is not if key is not a valid key" do
160
+ b = TwoFactorAuth.pubkey_valid? "invalid key"
161
+ b.must_equal false
162
+ end
163
+
164
+ # I'd like a second test for a well-structured key that is not on the
165
+ # curve, but I can't see how to construct one; OpenSSL::PKey::EC::Point
166
+ # raises "OpenSSL::PKey::EC::Point::Error: point is not on curve" if I try
167
+ # to construct one.
168
+ end
169
+ end
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ module TwoFactorAuth
4
+ describe AuthenticationRequest do
5
+ let(:app_id) { 'http://twofactorauth.example.com' }
6
+ let(:key_handle) { TwoFactorAuth.websafe_base64_decode "W9G8D38l_29TSKdejU57l4YZsTVeRUUHSuBenSHZ-J_hwL7YI1R_MN20OQETXmWy_tdVDBAnxAct8ys_R-h7cw" }
7
+
8
+ it "holds app id" do
9
+ ar = AuthenticationRequest.new('http://id.example.com', key_handle)
10
+ ar.app_id.must_equal 'http://id.example.com'
11
+ end
12
+
13
+ it 'holds key handle' do
14
+ ar = AuthenticationRequest.new('http://id.example.com', 'key handle')
15
+ ar.key_handle.must_equal 'key handle'
16
+ end
17
+
18
+ it "creates random challenges" do
19
+ ar1 = AuthenticationRequest.new(app_id, key_handle)
20
+ ar2 = AuthenticationRequest.new(app_id, key_handle)
21
+ ar1.challenge.wont_equal ar2.challenge
22
+ end
23
+
24
+ it "uses the challenge given" do
25
+ encoded_challenge = TwoFactorAuth.websafe_base64_encode('0' * 32)
26
+ ar = AuthenticationRequest.new(app_id, key_handle, encoded_challenge)
27
+ ar.challenge.must_equal encoded_challenge
28
+ end
29
+
30
+ it 'encodes the key handle when serialized' do
31
+ ar = AuthenticationRequest.new('http://id.example.com', 'key handle')
32
+ ar.serialized.must_include '"a2V5IGhhbmRsZQ"'
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,44 @@
1
+ require 'test_helper'
2
+
3
+ module TwoFactorAuth
4
+ describe AuthenticationResponse do
5
+ #parallelize_me!
6
+
7
+ let(:app_id) { "http://local.twofactorauth.io:3000" }
8
+ let(:signatureData) { "AQAAAAUwRQIgaeZw29qOaQ50Wb4a7LjxV32_JR-Bru_0bPm0lIdrK1kCIQCl5_-Lssd4pzZ3tyaLIWIZEKhwZzwhceV2mHN2qzH3mw" }
9
+ let(:response) { AuthenticationResponse.new(encoded: signatureData) }
10
+
11
+ describe "decomposing fields" do
12
+ it "extracts bitfield byte" do
13
+ response.bitfield.must_equal 1
14
+ end
15
+
16
+ it "extracts counter" do
17
+ response.counter.must_equal 5
18
+ end
19
+
20
+ it "extracts signature" do
21
+ response.signature.must_equal "0E\x02 i\xE6p\xDB\xDA\x8Ei\x0EtY\xBE\x1A\xEC\xB8\xF1W}\xBF%\x1F\x81\xAE\xEF\xF4l\xF9\xB4\x94\x87k+Y\x02!\x00\xA5\xE7\xFF\x8B\xB2\xC7x\xA76w\xB7&\x8B!b\x19\x10\xA8pg<!q\xE5v\x98sv\xAB1\xF7\x9B".force_encoding('ASCII-8BIT')
22
+ response.signature.length.must_equal 71
23
+ end
24
+
25
+ it "starts with signatureData a known size" do
26
+ raw = TwoFactorAuth.websafe_base64_decode signatureData
27
+ raw.length.must_equal 76
28
+ end
29
+ end
30
+
31
+ describe "validation" do
32
+ it "is when correct" do
33
+ response.valid?.must_equal true
34
+ end
35
+
36
+ it "is not when bitfield is wrong" do
37
+ response.bitfield = 8
38
+ response.valid?.must_equal false
39
+ response.errors[:bitfield].wont_be_empty
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,83 @@
1
+ require 'test_helper'
2
+
3
+ module TwoFactorAuth
4
+ describe AuthenticationVerifier do
5
+ #parallelize_me!
6
+
7
+ let(:app_id) { "http://local.fidologin.com:3000" }
8
+ let(:key_handle) { TwoFactorAuth.websafe_base64_decode "fNKqlc0cHr7CcAScmiwJF3qL5WP5YY9vSZR5i474rPWmg8qjTHIckZA_v2Xioj6RB6BNJqzxUVUwG6wfksKXtA" }
9
+ let(:challenge) { "i6M5PrWJbrwwn_25MqHJzbWVdILVCBfg1nLIiVJ_zOs" }
10
+ let(:request) { AuthenticationRequest.new(app_id, key_handle, challenge) }
11
+ let(:registration) { Registration.new({
12
+ key_handle: key_handle,
13
+ public_key: "\x049\xC6GC\xE6\xD3un;a\xD2\x04\e\x9B,vk\xEB\xDC\xB51\b\x14\x16\a\x92\x8D\xA1\xA92\xD7Z\x17\xB3D\xDF}P\xDA\x9Cg\xEB\xFD7h)N\xC4\xF2\xF1\x10\e\x8A\xC7\x88\x9AM\x17V\xEA\xBEX\x14\xAB".force_encoding('ASCII-8BIT'),
14
+ certificate: "0\x82\x02\x1C0\x82\x01\x06\xA0\x03\x02\x01\x02\x02\x04$\xDB\xAB@0\v\x06\t*\x86H\x86\xF7\r\x01\x01\v0.1,0*\x06\x03U\x04\x03\x13#Yubico U2F Root CA Serial 4572006310 \x17\r140801000000Z\x18\x0F20500904000000Z0+1)0'\x06\x03U\x04\x03\f Yubico U2F EE Serial 135032778880Y0\x13\x06\a*\x86H\xCE=\x02\x01\x06\b*\x86H\xCE=\x03\x01\a\x03B\x00\x04\x02\xB0\x94\xBE4}GyA\xC4w\x8E\xBE\xC5\xCAM\xED*G\x9F\xAA\x1Eo\xEC9\xAF\xEB\xDE\f p\xCB[\xD4\xBDi\xC9jx\xE3\xBF\x87Q\xFE\xB5y\e\x8D\xFA\xCA\xC2\x94\x01u\x1C\xB1W\xB9|\t\xE49\x1A6\xA3\x120\x100\x0E\x06\n+\x06\x01\x04\x01\x82\xC4\n\x01\x01\x04\x000\v\x06\t*\x86H\x86\xF7\r\x01\x01\v\x03\x82\x01\x01\x00\xA3c\xAE\x0E\x98:\xF3\v\xBA\xF1,\x8B-\xF3ZY\xBF\x1C\xBBJ\e\x0F\xCBh\xC4\x84U\x84\x90\xF6\x874Xe\xB8\xDB\x02i\xC3F\xE5S\x88L,V\a\xAF\x0E\xA2{\x90\xAC\x8C\xF1\xEFC\x1Fr\xAC\x18\x9D\xB2\x1C\x82I\x14\xBF\x17\x88\xA5Q\x1A3\xD0{L\x8E4d|\xE9\xF6\x1E\x15\x16\xA9\xA9\xB3n\x90\n@ a\xF6\x9A\xA4n\x12\xC52\xB9\x93\xF9B>\xFA\xAAL\xF9\xA3\xB6T\xB4\xDD\xDE\xF2\x92JT\x8F\xD5\x99\x95Q\r\xD4\xF7\xF4\xD9\xA4\xD5!\x93\x87<q\xC9\xB8~\x86\x85>\x9E-\xA7^\x8F\fm(0St\xD4\xEF\xDD^\x14\x96\xF8\xC39\x06\x10{\xD6\x8B\xD65\r\xAA\xD2\xC3x\x11\xEC\xA3\xCAC\xBC\x93\vs@\x97\xDE\xF6\x9Dh\x8D\x94U\fL\xFB\x18\xA9\xE2K\x86\xA2\xE5\xD8\x8FI\x98\x99\xA0\x9B\xCE[\x81\fSl\xAF9\r\xC8\xBD\xDE\x96\r\xF30\xCA\xCA\xBC\x05!\xA1\x83#\x95\x7F\xFE\xBC\xA5\x9C\xA9\v \xB1\r\t\xB5#\x1CX\xC2~\xBAg\x83".force_encoding('ASCII-8BIT'),
15
+ counter: 0,
16
+ }) }
17
+ let(:client_data) { ClientData.new encoded: "eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwiY2hhbGxlbmdlIjoiaTZNNVByV0picnd3bl8yNU1xSEp6YldWZElMVkNCZmcxbkxJaVZKX3pPcyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbC5maWRvbG9naW4uY29tOjMwMDAiLCJjaWRfcHVia2V5IjoiIn0", correct_typ: 'navigator.id.getAssertion' }
18
+ let(:signatureData) { "AQAAAAcwRQIhAKCimZ21ZEsqVRLkDW2MRlDVFLFFAdCsvCPRTRCzD_brAiBDrE3KuTNanagb1UtW2YM7tUqwIS-D_MK_EWvVsA-8xQ" }
19
+ let(:response) { AuthenticationResponse.new(encoded: signatureData) }
20
+ let(:verifier) { AuthenticationVerifier.new({
21
+ registration: registration,
22
+ request: request,
23
+ client_data: client_data,
24
+ response: response,
25
+ }) }
26
+
27
+ describe '#application_parameter' do
28
+ it "is the sha256 hash of the app id" do
29
+ verifier.application_parameter.must_equal "Nh\xA3j\x7F\xB2\xEE\xD6D\x1F\x18\t\xF1\xAEL\t\x7F\fjo\n\x80\xDE\xAD{\x9E\x13\x04\xAE\xFD.\xD6".force_encoding('ASCII-8BIT')
30
+ end
31
+ end
32
+
33
+ describe '#challenge_parameter' do
34
+ it "is the sha256 hash of the base64 decoded json string" do
35
+ verifier.challenge_parameter.must_equal "\x92F\xAAS\t\x88Cxe\xD04E\xD7R\xCA)\x05\x1E\xAA\xCA\x18w\xC3\xBFfl\x17V1M\xC0\x95".force_encoding('ASCII-8BIT')
36
+ end
37
+ end
38
+
39
+ describe '#digest' do
40
+ it "combines fields to spec" do
41
+ verifier.digest.must_equal "\fH\xF7\x81n41uT6kr\x7F$(\x06A\xF0\xE4\x96\xD6\x12\xB1U\x90\xA2\xB7\a\xBE]\xDF\xFA".force_encoding('ASCII-8BIT')
42
+ end
43
+ end
44
+
45
+ # describe '#signature_verified?' do
46
+ # it "is if the user's public key signed the digest to produce the signature" do
47
+ # verifier.signature_verified?(registration).must_equal true
48
+ # end
49
+ #
50
+ # it "is not otherwise" do
51
+ # request = AuthenticationRequest.new "http://different.app.id", key_handle, challenge
52
+ # verifier = AuthenticationVerifier.new(request, clientData, response)
53
+ # verifier.signature_verified?(registration).must_equal false
54
+ # end
55
+ # end
56
+
57
+ describe "validations" do
58
+ it "is valid if everything is right" do
59
+ verifier.valid?.must_equal true
60
+ end
61
+
62
+ it "is not if challenge doesn't match" do
63
+ verifier.client_data.challenge = "different challenge"
64
+ verifier.valid?.must_equal false
65
+ verifier.errors.full_messages.join(' ').must_include 'challenge'
66
+ end
67
+
68
+ it "is not if origin doesn't match" do
69
+ verifier.client_data.origin = "http://different.origin"
70
+ verifier.valid?.must_equal false
71
+ verifier.errors.full_messages.join(' ').must_include 'origin'
72
+ end
73
+
74
+ it "is not if counter doesn't advance" do
75
+ verifier.registration.counter = 1
76
+ verifier.response.counter = 1
77
+ verifier.valid?.must_equal false
78
+ verifier.errors.full_messages.join(' ').must_include 'counter'
79
+ end
80
+ end
81
+
82
+ end
83
+ end