octokey 0.1.pre.2 → 0.1.pre.3
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/octokey/auth_request.rb +210 -0
- data/lib/octokey/buffer.rb +239 -74
- data/lib/octokey/challenge.rb +178 -0
- data/lib/octokey/config.rb +101 -0
- data/lib/octokey/public_key.rb +157 -0
- data/lib/octokey.rb +109 -350
- metadata +79 -23
data/lib/octokey.rb
CHANGED
@@ -1,390 +1,149 @@
|
|
1
|
-
require
|
2
|
-
require 'ipaddr'
|
1
|
+
require 'openssl'
|
3
2
|
require 'securerandom'
|
3
|
+
require 'ipaddr'
|
4
4
|
require 'uri'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
SIGNING_ALGORITHM = "ssh-rsa"
|
12
|
-
DIGEST_ALGORITHM = "SHA1"
|
13
|
-
SSH_RSA_MINIMUM_MODULUS_SIZE = 768
|
6
|
+
require File.expand_path('octokey/buffer', File.dirname(__FILE__))
|
7
|
+
require File.expand_path('octokey/config', File.dirname(__FILE__))
|
8
|
+
require File.expand_path('octokey/challenge', File.dirname(__FILE__))
|
9
|
+
require File.expand_path('octokey/public_key', File.dirname(__FILE__))
|
10
|
+
require File.expand_path('octokey/auth_request', File.dirname(__FILE__))
|
14
11
|
|
12
|
+
# Octokey is a private key based login mechanism for websites inspired
|
13
|
+
# heavily by, and borrowing algorithms from, OpenSSH.
|
14
|
+
class Octokey
|
15
|
+
# Raised when you try and access details of an invalid octokey request.
|
16
|
+
# If you always check .can_log_in? or .can_sign_up? first, you should not
|
17
|
+
# see this exception.
|
15
18
|
class InvalidRequest < StandardError; end
|
19
|
+
|
20
|
+
# Raised if an Octokey::Buffer is invalid. This is usually caught by Octokey
|
21
|
+
# so you will only need to catch it if you are parsing buffers yourself.
|
16
22
|
class InvalidBuffer < InvalidRequest; end
|
17
23
|
|
18
|
-
#
|
24
|
+
# Create a new challenge.
|
19
25
|
#
|
20
|
-
#
|
26
|
+
# Once created, the challenge should be sent to the client for it to sign.
|
21
27
|
#
|
22
|
-
#
|
23
|
-
#
|
28
|
+
# The client_ip address is included in the outgoing challenge to verify that
|
29
|
+
# incoming login requests came from the same client as requested the challenge.
|
24
30
|
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
# @
|
28
|
-
def self.
|
29
|
-
|
30
|
-
@hmac_secret_fingerprint = nil
|
31
|
+
# @param [Hash] opts
|
32
|
+
# @option opts [String,IPAddr] client_ip The IP address of the client.
|
33
|
+
# @return [String] The Base64 encoded challenge.
|
34
|
+
def self.new_challenge(opts)
|
35
|
+
Octokey::Challenge.generate(opts).to_s
|
31
36
|
end
|
32
37
|
|
33
|
-
#
|
34
|
-
#
|
35
|
-
# The client will include this challenge in their octokey auth request when
|
36
|
-
# they log in. It hopefully provides some security against replay attacks by
|
37
|
-
# ensuring that if a signed auth-request is stolen, it is only valid in a
|
38
|
-
# limited set of circumstances.
|
38
|
+
# Sign a challenge.
|
39
39
|
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
40
|
+
# If you're acting as an Octokey client, then you use this function to turn
|
41
|
+
# a challenge that you've been issued into an auth_request to send back to the
|
42
|
+
# server.
|
43
43
|
#
|
44
|
-
# @
|
45
|
-
# @
|
46
|
-
#
|
47
|
-
# @
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
client_ip = IPAddr.new(client_ip.to_s)
|
53
|
-
buffer = Octokey::Buffer.new
|
54
|
-
|
55
|
-
buffer.add_uint8 Octokey::CHALLENGE_VERSION
|
56
|
-
buffer.add_uint8 Octokey.hmac_secret_fingerprint
|
57
|
-
buffer.add_time time
|
58
|
-
buffer.add_ip client_ip
|
59
|
-
buffer.add_varbytes SecureRandom.random_bytes(32)
|
60
|
-
buffer.add_varbytes OpenSSL::HMAC.digest("sha1", Octokey.hmac_secret, buffer.raw)
|
61
|
-
|
62
|
-
buffer.to_s
|
44
|
+
# @param [String] challenge The base64-encoded challenge issued by the server.
|
45
|
+
# @param [Hash] opts
|
46
|
+
# @option opts [String] :username Which username would you like to log in as.
|
47
|
+
# @option opts [String] :request_url Which page would you like to log in to.
|
48
|
+
# @option opts [OpenSSL::PKey::RSA] :private_key The private key with which to sign the challenge.
|
49
|
+
# @return [String] The Base64 encoded auth_request
|
50
|
+
def self.sign_challenge(challenge, opts)
|
51
|
+
Octokey::AuthRequest.generate({:challenge => challenge}.merge(opts)).to_s
|
63
52
|
end
|
64
53
|
|
65
|
-
#
|
54
|
+
# Handle a new Octokey request.
|
66
55
|
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
# @option opts [Array<String>] :valid_hostnames The list of hostnames which clients may
|
70
|
-
# log in from.
|
71
|
-
# @option opts [Time] :time (Time.now)
|
56
|
+
# The options passed in to this method will be passed in as the second parameter to
|
57
|
+
# all the configuration blocks.
|
72
58
|
#
|
73
|
-
# @
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
def self.login(auth_request, opts = {}, &block)
|
83
|
-
raise ArgumentError, "No public key lookup block given to login" unless block_given?
|
84
|
-
|
85
|
-
username, public_key = signup(auth_request, opts)
|
86
|
-
valid_public_keys = block.call(username)
|
87
|
-
valid_public_keys.map!{ |public_key| format_public_key(unformat_public_key(public_key)) }
|
88
|
-
|
89
|
-
unless valid_public_keys.include? public_key
|
90
|
-
raise InvalidRequest, "Got unknown public key for #{username.inspect}: #{format_public_key(public_key).inspect}"
|
91
|
-
end
|
92
|
-
|
93
|
-
username
|
59
|
+
# @param [String] auth_request The Base64 encoded auth request from the client.
|
60
|
+
# @param [Hash] opts
|
61
|
+
# @option opts [String] :username The username that the user wishes to log in as.
|
62
|
+
# @option opts [String,IPAddr] :client_ip The ip address of the client.
|
63
|
+
def initialize(auth_request, opts)
|
64
|
+
raise ArgumentError, "no :username given" unless opts[:username]
|
65
|
+
raise ArgumentError, "no :client_ip given" unless opts[:client_ip]
|
66
|
+
@opts = opts.dup
|
67
|
+
@auth_request = Octokey::Config.get_auth_request(auth_request, opts)
|
94
68
|
end
|
95
69
|
|
96
|
-
#
|
97
|
-
#
|
98
|
-
# @param [String] auth_request The string sent by the Octokey client.
|
99
|
-
# @option opts [String] :client_ip The IP address of the client (see {.new_challenge)}
|
100
|
-
# @option opts [Array<String>] :valid_hostnames The list of hostnames which clients may
|
101
|
-
# log in from.
|
102
|
-
# @option opts [Time] :time (Time.now)
|
103
|
-
#
|
104
|
-
# @yield [String] username The block should (when given a username) return a list of
|
105
|
-
# public keys that are associated with that users account.
|
70
|
+
# Should the user be allowed to log in?
|
106
71
|
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
# this.
|
72
|
+
# If this method returns true then you can assume that the user with :username
|
73
|
+
# is actually the user who's trying to log in.
|
110
74
|
#
|
111
|
-
# @return [
|
112
|
-
|
113
|
-
|
114
|
-
def self.signup(auth_request, opts = {})
|
115
|
-
client_ip = opts[:client_ip] or raise ArgumentError, "No :client_ip given to login"
|
116
|
-
hostnames = opts[:valid_hostnames] or raise ArgumentError, "No :valid_hostnames given to login"
|
117
|
-
time = opts[:time] || Time.now
|
118
|
-
|
119
|
-
buffer = Octokey::Buffer.new(auth_request)
|
120
|
-
|
121
|
-
challenge = buffer.scan_string rescue ""
|
122
|
-
request_url = buffer.scan_string
|
123
|
-
username = buffer.scan_string
|
124
|
-
service_name = buffer.scan_string
|
125
|
-
auth_method = buffer.scan_string
|
126
|
-
signing_alg = buffer.scan_string
|
127
|
-
public_key_b = buffer.scan_buffer
|
128
|
-
signature_b = buffer.scan_buffer
|
129
|
-
|
130
|
-
public_key, errors = decode_public_key(public_key_b, "ssh-rsa")
|
131
|
-
signature, sig_errors = decode_signature(signature_b, signing_alg)
|
132
|
-
|
133
|
-
errors += sig_errors
|
134
|
-
errors += validate_challenge(challenge, client_ip, time)
|
135
|
-
|
136
|
-
hostname = URI.parse(request_url).host
|
137
|
-
|
138
|
-
to_verify = Octokey::Buffer.new
|
139
|
-
to_verify.add_string challenge
|
140
|
-
to_verify.add_string request_url
|
141
|
-
to_verify.add_string username
|
142
|
-
to_verify.add_string service_name
|
143
|
-
to_verify.add_string auth_method
|
144
|
-
to_verify.add_string signing_alg
|
145
|
-
to_verify.add_buffer public_key_b
|
146
|
-
|
147
|
-
expected_digest = OpenSSL::Digest::SHA1.digest(to_verify.raw)
|
148
|
-
raw_asn1 = public_key.public_decrypt(signature)
|
149
|
-
|
150
|
-
errors += validate_digest(raw_asn1, expected_digest)
|
151
|
-
|
152
|
-
unless buffer.empty?
|
153
|
-
errors << "Request contained trailing bytes"
|
154
|
-
end
|
155
|
-
|
156
|
-
unless hostnames.include?(hostname)
|
157
|
-
errors << "Request was for unknown hostname: #{hostname.inspect}"
|
158
|
-
end
|
159
|
-
|
160
|
-
unless service_name == SERVICE_NAME
|
161
|
-
errors << "Incorrect service name: Got #{service_name.inspect}, expected: #{SERVICE_NAME.inspect}"
|
162
|
-
end
|
163
|
-
|
164
|
-
unless auth_method == AUTH_METHOD
|
165
|
-
errors << "Incorrect auth type: Got #{auth_method.inspect}, expected: #{AUTH_TYPE.inspect}"
|
166
|
-
end
|
167
|
-
|
168
|
-
unless signing_alg == SIGNING_ALGORITHM
|
169
|
-
errors << "Incorrect signing algorithm: Got #{signing_alg.inspect}, expected: #{SIGNING_ALGORITHM.inspect}"
|
170
|
-
end
|
171
|
-
|
172
|
-
unless errors.empty?
|
173
|
-
raise InvalidRequest.new("Octokey request failed: #{errors.join(". ")}.")
|
174
|
-
end
|
175
|
-
|
176
|
-
[username, format_public_key(public_key)]
|
177
|
-
end
|
178
|
-
|
179
|
-
private
|
180
|
-
|
181
|
-
def self.hmac_secret
|
182
|
-
@hmac_secret or raise "No Octokey.hmac_secret set."
|
183
|
-
end
|
184
|
-
|
185
|
-
def self.hmac_secret_fingerprint
|
186
|
-
@hmac_secret_fingerprint ||= OpenSSL::Digest::SHA1.digest(hmac_secret).bytes.first
|
75
|
+
# @return [Boolean]
|
76
|
+
def can_log_in?
|
77
|
+
valid_auth_request? && valid_public_key?
|
187
78
|
end
|
188
79
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
fingerprint = buffer.scan_uint8
|
199
|
-
if fingerprint != hmac_secret_fingerprint
|
200
|
-
errors << "Challenge HMAC was signed with a different secret: Got #{fingerprint.inspect}, expected: #{hmac_secret_fingerprint.inspect}"
|
201
|
-
end
|
202
|
-
|
203
|
-
time = buffer.scan_time
|
204
|
-
puts time.inspect
|
205
|
-
if expected_time - time > 5 * 60
|
206
|
-
errors << "Challenge Timestamp is too dated: Got #{time.to_i}, expected > #{expected_time.to_i - 5 * 60}"
|
207
|
-
end
|
208
|
-
|
209
|
-
if time - expected_time > 5
|
210
|
-
errors << "Challenge Timestamp is too new: Got #{time.to_i}, expected < #{expected_time.to_i + 5}"
|
211
|
-
end
|
212
|
-
|
213
|
-
client_ip = buffer.scan_ip
|
214
|
-
if client_ip != expected_ip
|
215
|
-
errors << "Client IP address mismatch: Got #{client_ip}, expected: #{expected_ip}"
|
216
|
-
end
|
217
|
-
|
218
|
-
random = buffer.scan_varbytes
|
219
|
-
hmac = buffer.scan_varbytes
|
220
|
-
|
221
|
-
if !buffer.empty?
|
222
|
-
errors << "Challenge contained trailing bytes"
|
223
|
-
end
|
224
|
-
|
225
|
-
rehash = Octokey::Buffer.new
|
226
|
-
rehash.add_uint8 version
|
227
|
-
rehash.add_uint8 fingerprint
|
228
|
-
rehash.add_time time
|
229
|
-
rehash.add_ip client_ip
|
230
|
-
rehash.add_varbytes random
|
231
|
-
expected_hmac = OpenSSL::HMAC.digest("sha1", Octokey.hmac_secret, rehash.raw)
|
232
|
-
|
233
|
-
if hmac != expected_hmac
|
234
|
-
errors << "Challenge HMAC was incorrect: Got #{hmac.inspect}, expected: #{expected_hmac.inspect}"
|
235
|
-
end
|
236
|
-
|
237
|
-
errors
|
238
|
-
rescue InvalidBuffer => e
|
239
|
-
errors + [e.message]
|
240
|
-
end
|
241
|
-
|
242
|
-
def self.sign_challenge(challenge, username, request_url, private_key)
|
243
|
-
to_sign = Octokey::Buffer.new
|
244
|
-
to_sign.add_string challenge
|
245
|
-
to_sign.add_string request_url
|
246
|
-
to_sign.add_string username
|
247
|
-
to_sign.add_string SERVICE_NAME
|
248
|
-
to_sign.add_string AUTH_METHOD
|
249
|
-
to_sign.add_string SIGNING_ALGORITHM
|
250
|
-
to_sign.add_buffer encode_public_key(private_key)
|
251
|
-
|
252
|
-
digest = OpenSSL::Digest::SHA1.digest(to_sign.raw)
|
253
|
-
sigblob = private_key.private_encrypt(digest_into_asn1(digest))
|
254
|
-
|
255
|
-
sig_buf = Octokey::Buffer.new
|
256
|
-
sig_buf.add_string SIGNING_ALGORITHM
|
257
|
-
sig_buf.add_string sigblob
|
258
|
-
|
259
|
-
to_sign.add_buffer(sig_buf)
|
260
|
-
|
261
|
-
to_sign.to_s
|
262
|
-
end
|
263
|
-
|
264
|
-
# TODO: replace this by a call to OpenSSL
|
265
|
-
def self.digest_into_asn1(digest)
|
266
|
-
raise "not a valid digest" unless digest.size == 20
|
267
|
-
"0!0\t\x06\x05+\x0E\x03\x02\x1A\x05\x00\x04\x14#{digest}"
|
80
|
+
# Should the user be allowed to sign up?
|
81
|
+
#
|
82
|
+
# If this method returns true then you can store the public_key against the
|
83
|
+
# user's username. Future logins by that user will use that to verify that the user
|
84
|
+
# trying to log in has access to the private key that corresponds to this public key.
|
85
|
+
#
|
86
|
+
# @return [Boolean]
|
87
|
+
def can_sign_up?
|
88
|
+
valid_auth_request?
|
268
89
|
end
|
269
90
|
|
270
|
-
#
|
91
|
+
# Was the failure to log in or sign up transient?
|
271
92
|
#
|
272
|
-
#
|
93
|
+
# This will return true if the client may be able to log in simply by requesting
|
94
|
+
# a new challenge from the server and retrying the auth_request.
|
273
95
|
#
|
274
|
-
# [
|
275
|
-
|
276
|
-
|
277
|
-
# ]
|
278
|
-
def self.validate_digest(raw_asn1, expected_digest)
|
279
|
-
asn1 = OpenSSL::ASN1.decode(raw_asn1)
|
280
|
-
errors = []
|
281
|
-
|
282
|
-
unless asn1.tag == 16 && asn1.value.size == 2
|
283
|
-
return ["Invalid asn1 was signed"]
|
284
|
-
end
|
285
|
-
|
286
|
-
algorithm_asn1 = asn1.value[0]
|
287
|
-
digest_asn1 = asn1.value[1]
|
288
|
-
|
289
|
-
unless digest_asn1.tag == 4
|
290
|
-
return ["Invalid digest_asn1 was signed"]
|
291
|
-
end
|
292
|
-
|
293
|
-
unless algorithm_asn1.tag == 16 && algorithm_asn1.value.size <= 2
|
294
|
-
return ["Invalid algorithm_asn1 was signed"]
|
295
|
-
end
|
296
|
-
|
297
|
-
unless algorithm_asn1.value[0].value == DIGEST_ALGORITHM
|
298
|
-
errors << "Incorrect digest algorithm: Got #{algorithm_asn1.value[0].value}, expected: #{DIGEST_ALGORITHM.inspect}"
|
299
|
-
end
|
300
|
-
|
301
|
-
unless algorithm_asn1.value.size == 1 || algorithm_asn1.value[1].is_a?(OpenSSL::ASN1::Null)
|
302
|
-
errors << "Invalid parameters passed to SHA1 digest, expected NULL."
|
303
|
-
end
|
304
|
-
|
305
|
-
unless digest_asn1.value == expected_digest
|
306
|
-
errors << "Incorrect message digest"
|
307
|
-
end
|
308
|
-
|
309
|
-
return errors
|
96
|
+
# @return [Boolean]
|
97
|
+
def should_retry?
|
98
|
+
!valid_auth_request? && auth_request.valid_ignoring_challenge?(opts)
|
310
99
|
end
|
311
100
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
unless signing_alg == expected_alg
|
325
|
-
errors << "Signature algorithm mismatch: Got #{signing_alg.inspect}, expected: #{expected_alg.inspect}"
|
326
|
-
end
|
327
|
-
|
328
|
-
[signature, errors]
|
329
|
-
rescue InvalidBuffer => e
|
330
|
-
[nil, e.message]
|
101
|
+
# Get the username used for this request.
|
102
|
+
#
|
103
|
+
# You must validate that the username meets your requirements for a valid username,
|
104
|
+
# Octokey allows any username (for example the empty string, the string "\r\n"). You might
|
105
|
+
# want to enforce that the username is an email address, or contains only visible characters.
|
106
|
+
#
|
107
|
+
# @return [String] username
|
108
|
+
# @raise [InvalidRequest] if neither .can_sign_up? nor .can_log_in?
|
109
|
+
def username
|
110
|
+
raise InvalidRequest, "Tried to get username from invalid octokey" unless valid_auth_request?
|
111
|
+
auth_request.username
|
331
112
|
end
|
332
113
|
|
333
|
-
|
334
|
-
|
114
|
+
# Get the public_key used for this request.
|
115
|
+
#
|
116
|
+
# You will need this when handling a sign up request for the user in order to
|
117
|
+
# extract the public key needed to log in.
|
118
|
+
#
|
119
|
+
# The format of the returned public key is exactly the same as used by SSH in the
|
120
|
+
# ~/.authorized_keys file. That format can be parsed by {Octokey::PublicKey} if you
|
121
|
+
# need more information.
|
122
|
+
#
|
123
|
+
# @return [String]
|
124
|
+
# @raise [InvalidRequest] if neither .can_sign_up? nor .can_log_in?
|
125
|
+
def public_key
|
126
|
+
raise InvalidRequest, "Tried to get username from invalid octokey" unless valid_auth_request?
|
127
|
+
auth_request.public_key.to_s
|
335
128
|
end
|
336
129
|
|
337
|
-
|
338
|
-
|
339
|
-
key, errors = decode_public_key(Octokey::Buffer.new($2), $1)
|
340
|
-
raise "Invalid public key: #{errors.join(". ")}." unless errors.empty?
|
341
|
-
|
342
|
-
key
|
343
|
-
else
|
344
|
-
raise "Invalid public key: Got #{public_key.inspect}, expected \"ssh-rsa AAAAf...\""
|
345
|
-
end
|
346
|
-
end
|
130
|
+
private
|
131
|
+
attr_accessor :opts, :auth_request
|
347
132
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
buffer.add_mpint public_key.e
|
353
|
-
buffer.add_mpint public_key.n
|
354
|
-
buffer
|
133
|
+
# Is the auth_request valid?
|
134
|
+
# @return [Boolean]
|
135
|
+
def valid_auth_request?
|
136
|
+
@valid ||= auth_request.valid?(opts)
|
355
137
|
end
|
356
138
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
errors = []
|
365
|
-
|
366
|
-
unless buffer.empty?
|
367
|
-
errors << "Public key contained trailing bytes"
|
368
|
-
end
|
369
|
-
|
370
|
-
unless key_type == expected_type
|
371
|
-
errors << "Got unknown public key type: Got #{key_type.inspect}, expected: #{expected_type.inspect}"
|
372
|
-
end
|
373
|
-
|
374
|
-
unless n.num_bits > SSH_RSA_MINIMUM_MODULUS_SIZE
|
375
|
-
errors << "RSA modulus too small: #{n.num_bits.inspect} < #{SSH_RSA_MINIMUM_MODULUS_SIZE.inspect}"
|
139
|
+
# Is the public key used to sign the auth request one of those that belongs to the username?
|
140
|
+
# @return [Boolean]
|
141
|
+
def valid_public_key?
|
142
|
+
strings = Octokey::Config.get_public_keys(opts[:username], opts)
|
143
|
+
public_keys = strings.map{ |string| Octokey::PublicKey.from_string(string) }
|
144
|
+
unless public_keys.all?(&:valid?)
|
145
|
+
raise ArgumentError, "Invalid public key returned to Octokey for #{username}"
|
376
146
|
end
|
377
|
-
|
378
|
-
# TODO: verify size of modulus and exponent
|
379
|
-
|
380
|
-
if errors == []
|
381
|
-
key = OpenSSL::PKey::RSA.new
|
382
|
-
key.e = e
|
383
|
-
key.n = n
|
384
|
-
end
|
385
|
-
|
386
|
-
[key, errors]
|
387
|
-
rescue InvalidBuffer => e
|
388
|
-
[nil, e.message]
|
147
|
+
public_keys.include?(auth_request.public_key)
|
389
148
|
end
|
390
149
|
end
|
metadata
CHANGED
@@ -1,46 +1,102 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: octokey
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.pre.2
|
3
|
+
version: !ruby/object:Gem::Version
|
5
4
|
prerelease: 4
|
5
|
+
version: 0.1.pre.3
|
6
6
|
platform: ruby
|
7
|
-
authors:
|
8
|
-
- Conrad Irwin
|
7
|
+
authors:
|
8
|
+
- Conrad Irwin
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
|
13
|
+
date: 2012-07-30 00:00:00 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
prerelease: false
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
type: :development
|
25
|
+
version_requirements: *id001
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: active_support
|
28
|
+
prerelease: false
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: "0"
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id002
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: i18n
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id003
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: simplecov
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
type: :development
|
58
|
+
version_requirements: *id004
|
59
|
+
description: Allows you to use secure authentication mechanisms in place of passwords
|
15
60
|
email: conrad.irwin@gmail.com
|
16
61
|
executables: []
|
62
|
+
|
17
63
|
extensions: []
|
64
|
+
|
18
65
|
extra_rdoc_files: []
|
19
|
-
|
20
|
-
|
21
|
-
- lib/octokey
|
66
|
+
|
67
|
+
files:
|
68
|
+
- lib/octokey.rb
|
69
|
+
- lib/octokey/auth_request.rb
|
70
|
+
- lib/octokey/buffer.rb
|
71
|
+
- lib/octokey/challenge.rb
|
72
|
+
- lib/octokey/config.rb
|
73
|
+
- lib/octokey/public_key.rb
|
22
74
|
homepage: https://github.com/octokey/octokey-gem
|
23
75
|
licenses: []
|
76
|
+
|
24
77
|
post_install_message:
|
25
78
|
rdoc_options: []
|
26
|
-
|
27
|
-
|
28
|
-
|
79
|
+
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
83
|
none: false
|
30
|
-
requirements:
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: "0"
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
35
89
|
none: false
|
36
|
-
requirements:
|
37
|
-
|
38
|
-
|
39
|
-
|
90
|
+
requirements:
|
91
|
+
- - ">"
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 1.3.1
|
40
94
|
requirements: []
|
95
|
+
|
41
96
|
rubyforge_project:
|
42
97
|
rubygems_version: 1.8.24
|
43
98
|
signing_key:
|
44
99
|
specification_version: 3
|
45
100
|
summary: Public key authentication for the web!
|
46
101
|
test_files: []
|
102
|
+
|