octokey 0.1.pre.2 → 0.1.pre.3
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.
- 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
|
+
|