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
@@ -0,0 +1,210 @@
|
|
1
|
+
class Octokey
|
2
|
+
# An AuthRequest is sent by the client when it wants to log in or sign up.
|
3
|
+
#
|
4
|
+
# It includes an {Octokey::Challenge} so that we can verify its recency, and
|
5
|
+
# also the username the user wishes to log in as, the url that they wish to
|
6
|
+
# log in to, and the public key corresponding to their private key.
|
7
|
+
#
|
8
|
+
# You can create an Octokey::AuthRequest from any string, and later determine
|
9
|
+
# whether or not it was valid by calling {#valid?}
|
10
|
+
class AuthRequest
|
11
|
+
# The service name is used to check that the client knows which protocol it is speaking.
|
12
|
+
SERVICE_NAME = "octokey-auth"
|
13
|
+
# The auth method indicates that the client wants to use publickey authentication.
|
14
|
+
AUTH_METHOD = "publickey"
|
15
|
+
# The signing algorithm is copied straight from SSH.
|
16
|
+
SIGNING_ALGORITHM = "ssh-rsa"
|
17
|
+
|
18
|
+
attr_accessor :challenge_buffer, :request_url, :username, :service_name,
|
19
|
+
:auth_method, :signing_algorithm, :public_key, :signature_buffer,
|
20
|
+
:invalid_buffer
|
21
|
+
|
22
|
+
# Given a challenge and a private key, generate an auth request.
|
23
|
+
#
|
24
|
+
# @param [Hash] opts
|
25
|
+
# @option opts [String] :request_url
|
26
|
+
# @option opts [String] :username
|
27
|
+
# @option opts [String] :challenge The base64-encoded challenge
|
28
|
+
# @option opts [OpenSSL::PKey::RSA] :private_key
|
29
|
+
# @return [Octokey::AuthRequest]
|
30
|
+
def self.generate(opts)
|
31
|
+
private_key = opts[:private_key] or raise ArgumentError, "No private_key given"
|
32
|
+
challenge = opts[:challenge] or raise ArgumentError, "No challenge given"
|
33
|
+
|
34
|
+
new.instance_eval do
|
35
|
+
self.challenge_buffer = Octokey::Buffer.new(challenge)
|
36
|
+
self.request_url = opts[:request_url] or raise ArgumentError, "No request_url given"
|
37
|
+
self.username = opts[:username] or raise ArgumentError, "No username given"
|
38
|
+
self.service_name = SERVICE_NAME
|
39
|
+
self.auth_method = AUTH_METHOD
|
40
|
+
self.signing_algorithm = SIGNING_ALGORITHM
|
41
|
+
self.public_key = Octokey::PublicKey.from_key(private_key.public_key)
|
42
|
+
self.signature_buffer = signature_buffer_with(private_key)
|
43
|
+
|
44
|
+
self
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Parse an auth request sent from the client.
|
49
|
+
#
|
50
|
+
# @param[String] The base64-encoded auth request from the client.
|
51
|
+
# @return [Octokey::AuthRequest]
|
52
|
+
def self.from_string(string)
|
53
|
+
buffer = Octokey::Buffer.new(string)
|
54
|
+
new.instance_eval do
|
55
|
+
begin
|
56
|
+
self.challenge_buffer, self.request_url, self.username,
|
57
|
+
self.service_name, self.auth_method, self.signing_algorithm,
|
58
|
+
self.public_key, self.signature_buffer =
|
59
|
+
buffer.scan_all(
|
60
|
+
:buffer, :string, :string,
|
61
|
+
:string, :string, :string,
|
62
|
+
:public_key, :buffer)
|
63
|
+
rescue Octokey::InvalidBuffer => e
|
64
|
+
self.invalid_buffer = e.message
|
65
|
+
end
|
66
|
+
|
67
|
+
self
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Get any errors ignoring those caused by the challenge.
|
72
|
+
#
|
73
|
+
# @param [Hash] opts
|
74
|
+
# @return [Array<String>]
|
75
|
+
def errors_ignoring_challenge(opts)
|
76
|
+
return [invalid_buffer] if invalid_buffer
|
77
|
+
errors = []
|
78
|
+
|
79
|
+
errors += request_url_errors(opts)
|
80
|
+
errors << "Auth request username mismatch" unless username == opts[:username]
|
81
|
+
errors << "Auth request service name mismatch" unless service_name == SERVICE_NAME
|
82
|
+
errors << "Auth request auth method unsupported" unless auth_method == AUTH_METHOD
|
83
|
+
errors << "Auth request signing algorithm unsupported" unless signing_algorithm == SIGNING_ALGORITHM
|
84
|
+
|
85
|
+
if public_key.valid?
|
86
|
+
errors += signature_errors(public_key.public_key, signature_buffer.dup)
|
87
|
+
else
|
88
|
+
errors += public_key.errors
|
89
|
+
end
|
90
|
+
|
91
|
+
errors
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get any errors caused by the challenge.
|
95
|
+
#
|
96
|
+
# @param [Hash] opts
|
97
|
+
# @return [Array<String>]
|
98
|
+
def challenge_errors(opts)
|
99
|
+
return [] if invalid_buffer
|
100
|
+
Octokey::Config.get_challenge(challenge_buffer.to_s, opts).errors(opts)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get all the error for this auth request.
|
104
|
+
#
|
105
|
+
# @param [Hash] opts
|
106
|
+
# @return [Array<String>]
|
107
|
+
def errors(opts)
|
108
|
+
errors_ignoring_challenge(opts) + challenge_errors(opts)
|
109
|
+
end
|
110
|
+
|
111
|
+
# If the challenge was valid, would this auth request be valid?
|
112
|
+
#
|
113
|
+
# This can be used to check whether the auth request should be retried.
|
114
|
+
#
|
115
|
+
# @param [Hash] opts
|
116
|
+
# @return [Boolean]
|
117
|
+
def valid_ignoring_challenge?(opts)
|
118
|
+
errors_ignoring_challenge(opts) == []
|
119
|
+
end
|
120
|
+
|
121
|
+
# Is this auth request valid?
|
122
|
+
#
|
123
|
+
# @param [Hash] opts
|
124
|
+
# @return [Boolean]
|
125
|
+
def valid?(opts)
|
126
|
+
errors(opts) == []
|
127
|
+
end
|
128
|
+
|
129
|
+
# Get the Base64-encoded version of this auth request.
|
130
|
+
#
|
131
|
+
# @return [String]
|
132
|
+
def to_s
|
133
|
+
unsigned_buffer.add_buffer(signature_buffer).to_s
|
134
|
+
end
|
135
|
+
|
136
|
+
# Get a string that identifies this auth request while debugging
|
137
|
+
#
|
138
|
+
# @return [String]
|
139
|
+
def inspect
|
140
|
+
"#<Octokey::AuthRequest #{to_s.inspect}>"
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
# What are the problems with the signature?
|
146
|
+
#
|
147
|
+
# @param [OpenSSL::PKey::RSA] key the public key
|
148
|
+
# @param [Octokey::Buffer] signature_buffer the signature buffer
|
149
|
+
# @return [Array<String>]
|
150
|
+
def signature_errors(key, signature_buffer)
|
151
|
+
algorithm_used, signature = signature_buffer.scan_all(:string, :varbytes)
|
152
|
+
|
153
|
+
errors = []
|
154
|
+
errors << "Signature type mismatch" unless algorithm_used == signing_algorithm
|
155
|
+
errors << "Signature mismatch" unless key.verify(OpenSSL::Digest::SHA1.new, signature, unsigned_buffer.raw)
|
156
|
+
errors
|
157
|
+
|
158
|
+
rescue Octokey::InvalidBuffer => e
|
159
|
+
["Signature #{e.message}"]
|
160
|
+
end
|
161
|
+
|
162
|
+
# What are the problems with the request url?
|
163
|
+
#
|
164
|
+
# @param [Hash] opts
|
165
|
+
# @return [Array<String>]
|
166
|
+
def request_url_errors(opts)
|
167
|
+
url = URI.parse(request_url)
|
168
|
+
|
169
|
+
valid_hostname = Octokey::Config.valid_hostnames.any? do |hostname|
|
170
|
+
if hostname[/\A\*\.(.*)\z/]
|
171
|
+
url.host.end_with?($1)
|
172
|
+
else
|
173
|
+
url.host == hostname
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
errors = []
|
178
|
+
errors << "Request url insecure" unless url.scheme == "https"
|
179
|
+
errors << "Request url mismatch" unless valid_hostname
|
180
|
+
errors
|
181
|
+
|
182
|
+
rescue URI::InvalidURIError
|
183
|
+
["Request url invalid"]
|
184
|
+
end
|
185
|
+
|
186
|
+
# Get the buffer containing everything other than the signature.
|
187
|
+
#
|
188
|
+
# @return [Octokey::Buffer]
|
189
|
+
def unsigned_buffer
|
190
|
+
Octokey::Buffer.new.
|
191
|
+
add_buffer(challenge_buffer).
|
192
|
+
add_string(request_url).
|
193
|
+
add_string(username).
|
194
|
+
add_string(service_name).
|
195
|
+
add_string(auth_method).
|
196
|
+
add_string(signing_algorithm).
|
197
|
+
add_public_key(public_key)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Get the signature buffer using the given key.
|
201
|
+
#
|
202
|
+
# @param [OpenSSL::PKey::RSA] private_key
|
203
|
+
# @return [Octokey::Buffer]
|
204
|
+
def signature_buffer_with(private_key)
|
205
|
+
Octokey::Buffer.new.
|
206
|
+
add_string(SIGNING_ALGORITHM).
|
207
|
+
add_varbytes(private_key.sign(OpenSSL::Digest::SHA1.new, unsigned_buffer.raw))
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
data/lib/octokey/buffer.rb
CHANGED
@@ -1,12 +1,24 @@
|
|
1
1
|
require 'base64'
|
2
2
|
class Octokey
|
3
|
+
# Buffers are used throughout Octokey to provide a bijective serialization format.
|
4
|
+
# For any valid buffer, there's exactly one valid object, and vice-versa.
|
5
|
+
#
|
6
|
+
# Mostly we used Base64-encoded buffers to avoid problems with potentially 8-bit
|
7
|
+
# unsafe channels. You should take care not to perform any operations on the Base64
|
8
|
+
# encoded form as there are many accepted formats for Base64-encoding a given string.
|
9
|
+
#
|
10
|
+
# In the current implementation, reading out of a buffer is a destructive operation,
|
11
|
+
# you should first .dup any buffer that you want to read more than once.
|
3
12
|
class Buffer
|
4
|
-
attr_accessor :buffer, :
|
13
|
+
attr_accessor :buffer, :invalid_buffer
|
5
14
|
|
6
15
|
# to avoid DOS caused by duplicating enourmous buffers,
|
7
16
|
# we limit the maximum size of any string stored to 100k
|
8
17
|
MAX_STRING_SIZE = 100 * 1024
|
9
18
|
|
19
|
+
# Create a new buffer from raw bits.
|
20
|
+
#
|
21
|
+
# @param [String] raw
|
10
22
|
def self.from_raw(raw = "")
|
11
23
|
ret = new
|
12
24
|
ret.buffer = raw.dup
|
@@ -14,95 +26,107 @@ class Octokey
|
|
14
26
|
ret
|
15
27
|
end
|
16
28
|
|
29
|
+
# Create a new buffer from a Base64-encoded string.
|
30
|
+
# @param [String] string
|
17
31
|
def initialize(string = "")
|
18
32
|
self.buffer = Base64.decode64(string || "")
|
19
|
-
|
20
|
-
|
33
|
+
buffer.force_encoding('BINARY') if buffer.respond_to?(:force_encoding)
|
34
|
+
self.invalid_buffer = "Badly formatted Base64" unless to_s == string
|
21
35
|
end
|
22
36
|
|
37
|
+
# Get the underlying bits contained in this buffer.
|
38
|
+
# @return [String]
|
23
39
|
def raw
|
24
40
|
buffer
|
25
41
|
end
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
|
42
|
+
|
43
|
+
# Get the canonical Base64 representation of this buffer.
|
44
|
+
# @return [String]
|
31
45
|
def to_s
|
32
46
|
Base64.encode64(buffer).gsub("\n", "")
|
33
47
|
end
|
34
48
|
|
35
|
-
|
36
|
-
|
49
|
+
# Get a string that describes this buffer suitably for debugging.
|
50
|
+
# @return [String]
|
51
|
+
def inspect
|
52
|
+
"#<Octokey::Buffer @buffer=#{to_s.inspect}>"
|
37
53
|
end
|
38
54
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
end
|
44
|
-
self.buffer = buf
|
45
|
-
ret
|
55
|
+
# Is this buffer empty?
|
56
|
+
# @return [Boolean]
|
57
|
+
def empty?
|
58
|
+
buffer.empty?
|
46
59
|
end
|
47
60
|
|
61
|
+
# Add an unsigned 8-bit number to this buffer
|
62
|
+
# @param [Fixnum] x
|
63
|
+
# @return [Octokey::Buffer] self
|
64
|
+
# @raise [Octokey::InvalidBuffer] if x is not a uint8
|
48
65
|
def add_uint8(x)
|
49
66
|
raise InvalidBuffer, "Invalid uint8: #{x}" if x < 0 || x >= 2 ** 8
|
50
67
|
buffer << [x].pack("C")
|
68
|
+
self
|
51
69
|
end
|
52
70
|
|
71
|
+
# Destructively read an unsigned 8-bit number from this buffer
|
72
|
+
# @return [Fixnum]
|
73
|
+
# @raise [Octokey::InvalidBuffer]
|
53
74
|
def scan_uint8
|
54
75
|
scan(1).unpack("C").first
|
55
76
|
end
|
56
77
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
raise InvalidBuffer, "Invalid uint64: #{x}" if x < 0 || x >= 2 ** 64
|
68
|
-
add_uint32(x >> 32 & 0xffff_ffff)
|
69
|
-
add_uint32(x & 0xffff_ffff)
|
70
|
-
end
|
71
|
-
|
72
|
-
def scan_uint64
|
73
|
-
(scan_uint32 << 32) + scan_uint32
|
74
|
-
end
|
75
|
-
|
76
|
-
def add_uint128(x)
|
77
|
-
raise InvalidBuffer, "Invalid uint128: #{x}" if x < 0 || x >= 2 ** 128
|
78
|
-
add_uint64(x >> 64 & 0xffff_ffff_ffff_ffff)
|
79
|
-
add_uint64(x & 0xffff_ffff_ffff_ffff)
|
80
|
-
end
|
81
|
-
|
82
|
-
def scan_uint128
|
83
|
-
(scan_uint64 << 64) + scan_uint64
|
84
|
-
end
|
85
|
-
|
78
|
+
# Add a timestamp to this buffer
|
79
|
+
#
|
80
|
+
# Times are stored to millisecond precision, and are limited to
|
81
|
+
# 2 **48 to give plenty of margin for implementations using doubles
|
82
|
+
# as the backing for their date time, which nicely gives us a range
|
83
|
+
# ending just after the year 10000.
|
84
|
+
#
|
85
|
+
# @param [Time] time
|
86
|
+
# @return [Octokey::Buffer] self
|
87
|
+
# @raise [Octokey::InvalidBuffer] if the time is too far into the future
|
86
88
|
def add_time(time)
|
87
|
-
|
89
|
+
seconds, millis = [time.to_i, (time.usec / 1000.0).round]
|
90
|
+
raw = seconds * 1000 + millis
|
91
|
+
raise Octokey::InvalidBuffer, "Invalid time" if raw >= 2 ** 48
|
92
|
+
add_uint64(raw)
|
93
|
+
self
|
88
94
|
end
|
89
95
|
|
96
|
+
# Destructively read a timestamp from this buffer
|
97
|
+
#
|
98
|
+
# Times are stored to millisecond precision
|
99
|
+
#
|
100
|
+
# @return [Time]
|
101
|
+
# @raise [Octokey::InvalidBuffer]
|
90
102
|
def scan_time
|
91
|
-
|
103
|
+
raw = scan_uint64
|
104
|
+
raise Octokey::InvalidBuffer, "Invalid time" if raw >= 2 ** 48
|
105
|
+
seconds, millis = [raw / 1000, raw % 1000]
|
106
|
+
Time.at(seconds).utc + (millis / 1000.0)
|
92
107
|
end
|
93
108
|
|
109
|
+
# Add an IPv4 or IPv6 address to this buffer
|
110
|
+
#
|
111
|
+
# @param [IPAddr] ipaddr
|
112
|
+
# @return [Octokey::Buffer] self
|
113
|
+
# @raise [Octokey::InvalidBuffer] not a valid IP address
|
94
114
|
def add_ip(ipaddr)
|
95
115
|
if ipaddr.ipv4?
|
96
116
|
add_uint8(4)
|
97
|
-
|
117
|
+
buffer << ipaddr.hton
|
98
118
|
elsif ipaddr.ipv6?
|
99
119
|
add_uint8(6)
|
100
|
-
|
120
|
+
buffer << ipaddr.hton
|
101
121
|
else
|
102
122
|
raise InvalidBuffer, "Unsupported IP address: #{ipaddr.to_s}"
|
103
123
|
end
|
124
|
+
self
|
104
125
|
end
|
105
126
|
|
127
|
+
# Destructively read an IPv4 or IPv6 address from this buffer.
|
128
|
+
# @return [IPAddr]
|
129
|
+
# @raise [Octokey::InvalidBuffer]
|
106
130
|
def scan_ip
|
107
131
|
type = scan_uint8
|
108
132
|
case type
|
@@ -111,69 +135,210 @@ class Octokey
|
|
111
135
|
when 6
|
112
136
|
IPAddr.new_ntoh scan(16)
|
113
137
|
else
|
114
|
-
raise InvalidBuffer, "
|
138
|
+
raise InvalidBuffer, "Unknown IP family: #{type.inspect}"
|
115
139
|
end
|
116
140
|
end
|
117
141
|
|
142
|
+
# Add a length-prefixed number of bytes to this buffer
|
143
|
+
# @param [String] bytes
|
144
|
+
# @return [Octokey::Buffer] self
|
145
|
+
# @raise [Octokey::InvalidBuffer] if there are too any bytes
|
118
146
|
def add_varbytes(bytes)
|
147
|
+
bytes.force_encoding('BINARY') if bytes.respond_to?(:force_encoding)
|
119
148
|
size = bytes.size
|
120
|
-
raise InvalidBuffer, "
|
149
|
+
raise InvalidBuffer, "Too much length: #{size}" if size > MAX_STRING_SIZE
|
121
150
|
add_uint32 size
|
122
|
-
|
151
|
+
buffer << bytes
|
152
|
+
self
|
123
153
|
end
|
124
154
|
|
155
|
+
# Destructively read a length-prefixed number of bytes from this buffer
|
156
|
+
# @return [String] bytes
|
157
|
+
# @raise [Octokey::InvalidBuffer]
|
125
158
|
def scan_varbytes
|
126
159
|
size = scan_uint32
|
127
|
-
raise InvalidBuffer, "
|
160
|
+
raise InvalidBuffer, "Too much length: #{size}" if size > MAX_STRING_SIZE
|
128
161
|
scan(size)
|
129
162
|
end
|
130
163
|
|
164
|
+
# Add a length-prefixed number of bytes of UTF-8 string to this buffer
|
165
|
+
# @param [String] string
|
166
|
+
# @return [Octokey::Buffer] self
|
167
|
+
# @raise [Octokey::InvalidBuffer] if the string is not utf-8
|
131
168
|
def add_string(string)
|
132
|
-
|
133
|
-
add_varbytes string.encode('BINARY')
|
134
|
-
else
|
135
|
-
add_varbytes string
|
136
|
-
end
|
169
|
+
add_varbytes(validate_utf8(string))
|
137
170
|
end
|
138
171
|
|
172
|
+
# Destructively read a length-prefixed number of bytes of UTF-8 string
|
173
|
+
# @return [String] with encoding == 'utf-8' on ruby-1.9
|
174
|
+
# @raise [Octokey::InvalidBuffer]
|
139
175
|
def scan_string
|
140
|
-
|
141
|
-
if string.respond_to?(:encode)
|
142
|
-
string.encode('UTF-8')
|
143
|
-
else
|
144
|
-
string
|
145
|
-
end
|
146
|
-
rescue EncodingError => e
|
147
|
-
raise InvalidBuffer, e
|
176
|
+
validate_utf8(scan_varbytes)
|
148
177
|
end
|
149
178
|
|
179
|
+
# Add the length-prefixed contents of another buffer to this one.
|
180
|
+
# @param [Octokey::Buffer] buffer
|
181
|
+
# @return [Octokey::Buffer] self
|
182
|
+
# @raise [Octokey::InvalidBuffer]
|
150
183
|
def add_buffer(buffer)
|
151
184
|
add_varbytes buffer.raw
|
185
|
+
self
|
152
186
|
end
|
153
187
|
|
188
|
+
# Destrictively read a length-prefixed buffer out of this one.
|
189
|
+
# @return [Octokey::Buffer]
|
190
|
+
# @raise [Octokey::InvalidBuffer]
|
154
191
|
def scan_buffer
|
155
192
|
Octokey::Buffer.from_raw scan_varbytes
|
156
193
|
end
|
157
194
|
|
195
|
+
# Add an unsigned multi-precision integer to this buffer
|
196
|
+
# @param [OpenSSL::BN,Fixnum] x
|
197
|
+
# @return [Octokey::Buffer] self
|
198
|
+
# @raise [Octokey::InvalidBuffer] if x is negative or enourmous
|
158
199
|
def add_mpint(x)
|
159
|
-
raise InvalidBuffer, "
|
200
|
+
raise InvalidBuffer, "Invalid mpint: #{mpint.inspect}" if x < 0
|
160
201
|
bytes = OpenSSL::BN.new(x.to_s, 10).to_s(2)
|
161
202
|
bytes = "\x00" + bytes if bytes.bytes.first >= 0x80
|
162
203
|
add_varbytes(bytes)
|
204
|
+
self
|
163
205
|
end
|
164
206
|
|
207
|
+
# Destructively read an unsigned multi-precision integer from this buffer
|
208
|
+
# @return [OpenSSL::BN]
|
209
|
+
# @raise [Octokey::InvalidBuffer]
|
165
210
|
def scan_mpint
|
166
|
-
|
211
|
+
raw = scan_varbytes
|
167
212
|
|
168
|
-
|
169
|
-
|
213
|
+
first, second = raw.bytes.first(2)
|
214
|
+
|
215
|
+
# ensure only positive numbers with no superflous leading 0s
|
216
|
+
if first >= 0x80 || first == 0x00 && second < 0x80
|
217
|
+
raise InvalidBuffer, "Badly formatted mpint"
|
170
218
|
end
|
171
219
|
|
172
|
-
OpenSSL::BN.new(
|
220
|
+
OpenSSL::BN.new(raw, 2)
|
173
221
|
end
|
174
222
|
|
175
|
-
|
176
|
-
|
223
|
+
# Destructively read a public key from this buffer
|
224
|
+
#
|
225
|
+
# NOTE: the returned public key may not be valid, you must call
|
226
|
+
# .valid? on it before trying to use it.
|
227
|
+
#
|
228
|
+
# @return [Octokey::PublicKey]
|
229
|
+
# @raise [Octokey::InvalidBuffer]
|
230
|
+
def scan_public_key
|
231
|
+
Octokey::PublicKey.from_buffer(scan_buffer)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Add a public key to this buffer
|
235
|
+
# @param [Octokey::PublicKey] public_key
|
236
|
+
# @return [Octokey::Buffer] self
|
237
|
+
# @raise [Octokey::InvalidBuffer]
|
238
|
+
def add_public_key(public_key)
|
239
|
+
add_buffer public_key.to_buffer
|
240
|
+
end
|
241
|
+
|
242
|
+
# Destructively read the entire buffer.
|
243
|
+
#
|
244
|
+
# It's strongly recommended that you use this method to parse buffers, as it
|
245
|
+
# remembers to verify that the buffer doesn't contain any trailing bytes; and
|
246
|
+
# will return nothing if the buffer is invalid, so your code doesn't have to
|
247
|
+
# deal with half-parsed buffers.
|
248
|
+
#
|
249
|
+
# The tokens should correspond to the scan_X methods defined here. For example:
|
250
|
+
# type, e, n = buffer.scan_all(:string, :mpint, :mpint)
|
251
|
+
# is equivalent to:
|
252
|
+
# type, e, n, _ = [buffer.scan_string, buffer.scan_mpint, buffer.scan_mpint,
|
253
|
+
# buffer.scan_end]
|
254
|
+
#
|
255
|
+
# @param [Array<Symbol>] tokens
|
256
|
+
# @return [Array<Object>]
|
257
|
+
# @raise [Octokey::InvalidBuffer]
|
258
|
+
def scan_all(*tokens)
|
259
|
+
ret = tokens.map do |token|
|
260
|
+
raise "invalid token type: #{token.inspect}" unless respond_to?("scan_#{token}")
|
261
|
+
send("scan_#{token}")
|
262
|
+
end
|
263
|
+
|
264
|
+
scan_end
|
265
|
+
ret
|
266
|
+
end
|
267
|
+
|
268
|
+
# Verify that the buffer has been completely scanned.
|
269
|
+
# @raise [Octokey::InvalidBuffer] if there is still buffer to read.
|
270
|
+
def scan_end
|
271
|
+
raise InvalidBuffer, "Buffer too long" unless empty?
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
|
276
|
+
# Destructively read bytes from the front of this buffer.
|
277
|
+
# @param [Fixnum] n
|
278
|
+
# @return [String]
|
279
|
+
# @raise [Octokey::InvalidBuffer]
|
280
|
+
def scan(n)
|
281
|
+
raise InvalidBuffer, invalid_buffer if invalid_buffer
|
282
|
+
ret, buf = [buffer[0...n], buffer[n..-1]]
|
283
|
+
if ret.size < n || !buf
|
284
|
+
raise InvalidBuffer, "Buffer too short"
|
285
|
+
end
|
286
|
+
self.buffer = buf
|
287
|
+
ret
|
288
|
+
end
|
289
|
+
|
290
|
+
# Add an unsigned 32-bit number to this buffer
|
291
|
+
# @param [Fixnum] x
|
292
|
+
# @return [Octokey::Buffer] self
|
293
|
+
# @raise [Octokey::InvalidBuffer] if x is not a uint32
|
294
|
+
def add_uint32(x)
|
295
|
+
raise InvalidBuffer, "Invalid uint32: #{x}" if x < 0 || x >= 2 ** 32
|
296
|
+
buffer << [x].pack("N")
|
297
|
+
self
|
298
|
+
end
|
299
|
+
|
300
|
+
# Destructively read an unsigned 32-bit number from this buffer
|
301
|
+
# @return [Fixnum]
|
302
|
+
# @raise [Octokey::InvalidBuffer]
|
303
|
+
def scan_uint32
|
304
|
+
scan(4).unpack("N").first
|
305
|
+
end
|
306
|
+
|
307
|
+
# Add an unsigned 64-bit number to this buffer
|
308
|
+
# @param [Fixnum] x
|
309
|
+
# @return [Octokey::Buffer] self
|
310
|
+
# @raise [Octokey::InvalidBuffer] if x is not a uint64
|
311
|
+
def add_uint64(x)
|
312
|
+
raise InvalidBuffer, "Invalid uint64: #{x}" if x < 0 || x >= 2 ** 64
|
313
|
+
add_uint32(x >> 32 & 0xffff_ffff)
|
314
|
+
add_uint32(x & 0xffff_ffff)
|
315
|
+
self
|
316
|
+
end
|
317
|
+
|
318
|
+
# Destructively read an unsigned 64-bit number from this buffer
|
319
|
+
# @return [Fixnum]
|
320
|
+
# @raise [Octokey::InvalidBuffer]
|
321
|
+
def scan_uint64
|
322
|
+
(scan_uint32 << 32) + scan_uint32
|
323
|
+
end
|
324
|
+
|
325
|
+
# Check whether a string is valid utf-8
|
326
|
+
# @param [String] string
|
327
|
+
# @return [String] string
|
328
|
+
# @raise [Octokey::InvalidBuffer] invalid utf-8
|
329
|
+
def validate_utf8(string)
|
330
|
+
if string.respond_to?(:force_encoding)
|
331
|
+
string.force_encoding('UTF-8')
|
332
|
+
raise InvalidBuffer, "String not UTF-8" unless string.valid_encoding?
|
333
|
+
string
|
334
|
+
else
|
335
|
+
require 'iconv'
|
336
|
+
begin
|
337
|
+
Iconv.conv('utf-8', 'utf-8', string)
|
338
|
+
rescue Iconv::Failure
|
339
|
+
raise InvalidBuffer, "String not UTF-8"
|
340
|
+
end
|
341
|
+
end
|
177
342
|
end
|
178
343
|
end
|
179
344
|
end
|