putty-key 1.0.0
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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +3 -0
- data.tar.gz.sig +1 -0
- data/.yardopts +7 -0
- data/CHANGES.md +4 -0
- data/Gemfile +14 -0
- data/LICENSE +19 -0
- data/README.md +137 -0
- data/Rakefile +110 -0
- data/lib/putty/key.rb +25 -0
- data/lib/putty/key/error.rb +26 -0
- data/lib/putty/key/openssl.rb +182 -0
- data/lib/putty/key/ppk.rb +374 -0
- data/lib/putty/key/util.rb +128 -0
- data/lib/putty/key/version.rb +6 -0
- data/putty-key.gemspec +29 -0
- data/test/fixtures/dss-1024-encrypted.ppk +17 -0
- data/test/fixtures/dss-1024.pem +12 -0
- data/test/fixtures/dss-1024.ppk +17 -0
- data/test/fixtures/ecdsa-secp256k1.pem +5 -0
- data/test/fixtures/ecdsa-sha2-nistp256-encrypted.ppk +10 -0
- data/test/fixtures/ecdsa-sha2-nistp256.pem +5 -0
- data/test/fixtures/ecdsa-sha2-nistp256.ppk +10 -0
- data/test/fixtures/ecdsa-sha2-nistp384-encrypted.ppk +11 -0
- data/test/fixtures/ecdsa-sha2-nistp384.pem +6 -0
- data/test/fixtures/ecdsa-sha2-nistp384.ppk +11 -0
- data/test/fixtures/ecdsa-sha2-nistp521-encrypted.ppk +12 -0
- data/test/fixtures/ecdsa-sha2-nistp521.pem +7 -0
- data/test/fixtures/ecdsa-sha2-nistp521.ppk +12 -0
- data/test/fixtures/rsa-2048-encrypted.ppk +26 -0
- data/test/fixtures/rsa-2048.pem +27 -0
- data/test/fixtures/rsa-2048.ppk +26 -0
- data/test/fixtures/test-blank-comment.ppk +11 -0
- data/test/fixtures/test-encrypted.ppk +11 -0
- data/test/fixtures/test-invalid-blob-lines.ppk +11 -0
- data/test/fixtures/test-invalid-encryption-type.ppk +11 -0
- data/test/fixtures/test-invalid-format-1.ppk +11 -0
- data/test/fixtures/test-invalid-format-3.ppk +11 -0
- data/test/fixtures/test-invalid-private-mac.ppk +11 -0
- data/test/fixtures/test-truncated.ppk +10 -0
- data/test/fixtures/test-unix-line-endings.ppk +11 -0
- data/test/fixtures/test.ppk +11 -0
- data/test/openssl_test.rb +252 -0
- data/test/ppk_test.rb +247 -0
- data/test/test_helper.rb +81 -0
- data/test/util_test.rb +180 -0
- data/test/version_test.rb +7 -0
- metadata +124 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,374 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module PuTTY
|
4
|
+
module Key
|
5
|
+
# Represents a PuTTY private key (.ppk) file.
|
6
|
+
#
|
7
|
+
# The {PPK} {#initialize constructor} can be used to either create an
|
8
|
+
# uninitialized key or to load a .ppk file.
|
9
|
+
#
|
10
|
+
# The {#save} method can be used to save a {PPK} instance to a .ppk file.
|
11
|
+
#
|
12
|
+
# The {#algorithm}, {#comment}, {#public_blob} and {#private_blob}
|
13
|
+
# attributes provide access to the high level fields of the PuTTY private
|
14
|
+
# key as binary `String` instances. The structure of the two blobs will vary
|
15
|
+
# based on the algorithm.
|
16
|
+
#
|
17
|
+
# Encrypted .ppk files can be read and written by specifying a passphrase
|
18
|
+
# when loading or saving. As supported by PuTTY, files are always encrypted
|
19
|
+
# using AES in CBC mode with a 256-bit key derived from the passphrase using
|
20
|
+
# SHA-1.
|
21
|
+
#
|
22
|
+
# The {PPK} class supports files corresponding to PuTTY's format 2. Format 1
|
23
|
+
# was only used briefly early on in the development of the .ppk format.
|
24
|
+
class PPK
|
25
|
+
# String used in the computation of the private MAC.
|
26
|
+
MAC_KEY = 'putty-private-key-file-mac-key'#.b#.freeze
|
27
|
+
private_constant :MAC_KEY
|
28
|
+
|
29
|
+
# The default (and only supported) encryption algorithm.
|
30
|
+
DEFAULT_ENCRYPTION_TYPE = 'aes256-cbc'.freeze
|
31
|
+
|
32
|
+
# The default (and only supported) PuTTY private key file format.
|
33
|
+
DEFAULT_FORMAT = 2
|
34
|
+
|
35
|
+
# The key's algorithm, for example, 'ssh-rsa' or 'ssh-dss'.
|
36
|
+
#
|
37
|
+
# @return [String] The key's algorithm, for example, 'ssh-rsa' or
|
38
|
+
# 'ssh-dss'.
|
39
|
+
attr_accessor :algorithm
|
40
|
+
|
41
|
+
# A comment to describe the PuTTY private key.
|
42
|
+
#
|
43
|
+
# @return [String] A comment to describe the PuTTY private key.
|
44
|
+
attr_accessor :comment
|
45
|
+
|
46
|
+
# Get or set the public component of the key.
|
47
|
+
#
|
48
|
+
# @return [String] The public component of the key.
|
49
|
+
attr_accessor :public_blob
|
50
|
+
|
51
|
+
# The private component of the key (after decryption when loading and
|
52
|
+
# before encryption when saving).
|
53
|
+
#
|
54
|
+
# Note that when loading an encrypted .ppk file, this may include
|
55
|
+
# additional 'random' suffix used as padding.
|
56
|
+
#
|
57
|
+
# @return [String] The private component of the key
|
58
|
+
attr_accessor :private_blob
|
59
|
+
|
60
|
+
# Constructs a new {PPK} instance either uninitialized, or by loading a
|
61
|
+
# .ppk file.
|
62
|
+
#
|
63
|
+
# @param path [Object] Set to the path of a .ppk file to load the file.
|
64
|
+
# Leave as `nil` to leave the new {PPK} instance uninitialized.
|
65
|
+
# @param passphrase [String] The passphrase to use when loading an
|
66
|
+
# encrypted .ppk file.
|
67
|
+
#
|
68
|
+
# @raise [Errno::ENOENT] If the file specified by `path` does not exist.
|
69
|
+
# @raise [ArgumentError] If the .ppk file was encrypted, but either no
|
70
|
+
# passphrase or an incorrect passphrase was supplied.
|
71
|
+
# @raise [FormatError] If the .ppk file is malformed.
|
72
|
+
def initialize(path = nil, passphrase = nil)
|
73
|
+
passphrase = nil if passphrase && passphrase.to_s.empty?
|
74
|
+
|
75
|
+
if path
|
76
|
+
encryption_type, encrypted_private_blob, private_mac = Reader.open(path) do |reader|
|
77
|
+
@algorithm = reader.field('PuTTY-User-Key-File-2')
|
78
|
+
encryption_type = reader.field('Encryption')
|
79
|
+
@comment = reader.field('Comment')
|
80
|
+
@public_blob = reader.blob('Public')
|
81
|
+
encrypted_private_blob = reader.blob('Private')
|
82
|
+
private_mac = reader.field('Private-MAC')
|
83
|
+
[encryption_type, encrypted_private_blob, private_mac]
|
84
|
+
end
|
85
|
+
|
86
|
+
if encryption_type == 'none'
|
87
|
+
passphrase = nil
|
88
|
+
@private_blob = encrypted_private_blob
|
89
|
+
else
|
90
|
+
raise FormatError, "The ppk file is encrypted with #{encryption_type}, which is not supported" unless encryption_type == DEFAULT_ENCRYPTION_TYPE
|
91
|
+
raise ArgumentError, 'The ppk file is encrypted, a passphrase must be supplied' unless passphrase
|
92
|
+
|
93
|
+
# PuTTY uses a zero IV.
|
94
|
+
cipher = ::OpenSSL::Cipher::AES.new(256, :CBC)
|
95
|
+
cipher.decrypt
|
96
|
+
cipher.key = generate_encryption_key(passphrase, cipher.key_len)
|
97
|
+
cipher.padding = 0
|
98
|
+
@private_blob = cipher.update(encrypted_private_blob) + cipher.final
|
99
|
+
end
|
100
|
+
|
101
|
+
expected_private_mac = compute_private_mac(passphrase, encryption_type, @private_blob)
|
102
|
+
|
103
|
+
unless private_mac == expected_private_mac
|
104
|
+
raise ArgumentError, 'Incorrect passphrase supplied' if passphrase
|
105
|
+
raise FormatError, "Invalid Private MAC (expected #{expected_private_mac}, but found #{private_mac})"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Saves this PuTTY private key instance to a .ppk file.
|
111
|
+
#
|
112
|
+
# The {#algorithm}, {#private_blob} and {#public_blob} attributes must
|
113
|
+
# have been set before calling {#save}.
|
114
|
+
#
|
115
|
+
# @param path [Object] The path to write to. If a file already exists, it
|
116
|
+
# will be overwritten.
|
117
|
+
# @param passphrase [String] Set `passphrase` to encrypt the .ppk file
|
118
|
+
# using the specified passphrase. Leave as `nil` to create an
|
119
|
+
# unencrypted .ppk file.
|
120
|
+
# @param encryption_type [String] The encryption algorithm to use.
|
121
|
+
# Defaults to and currently only supports `'aes256-cbc'`.
|
122
|
+
# @param format [Integer] The format of .ppk file to create. Defaults to
|
123
|
+
# and currently only supports `2`.
|
124
|
+
#
|
125
|
+
# @return [Integer] The number of bytes written to the file.
|
126
|
+
#
|
127
|
+
# @raise [InvalidStateError] If either of the {#algorithm},
|
128
|
+
# {#private_blob} or {#public_blob} attributes have not been set.
|
129
|
+
# @raise [ArgumentError] If `path` is nil.
|
130
|
+
# @raise [ArgumentError] If a passphrase has been specified and
|
131
|
+
# `encryption_type` is not `'aes256-cbc'`.
|
132
|
+
# @raise [ArgumentError] If `format` is not `2`.
|
133
|
+
# @raise [Errno::ENOENT] If a directory specified by `path` does not
|
134
|
+
# exist.
|
135
|
+
def save(path, passphrase = nil, encryption_type: DEFAULT_ENCRYPTION_TYPE, format: DEFAULT_FORMAT)
|
136
|
+
raise InvalidStateError, 'algorithm must be set before calling save' unless @algorithm
|
137
|
+
raise InvalidStateError, 'public_blob must be set before calling save' unless @public_blob
|
138
|
+
raise InvalidStateError, 'private_blob must be set before calling save' unless @private_blob
|
139
|
+
|
140
|
+
passphrase = nil if passphrase && passphrase.to_s.empty?
|
141
|
+
encryption_type = 'none' unless passphrase
|
142
|
+
|
143
|
+
raise ArgumentError, 'An output path must be specified' unless path
|
144
|
+
|
145
|
+
if passphrase
|
146
|
+
raise ArgumentError, 'An encryption_type must be specified if a passphrase is specified' unless encryption_type
|
147
|
+
raise ArgumentError, "Unsupported encryption_type: #{encryption_type}" unless encryption_type == DEFAULT_ENCRYPTION_TYPE
|
148
|
+
end
|
149
|
+
|
150
|
+
raise ArgumentError, 'A format must be specified' unless format
|
151
|
+
raise ArgumentError, "Unsupported format: #{format}" unless format == DEFAULT_FORMAT
|
152
|
+
|
153
|
+
padded_private_blob = @private_blob
|
154
|
+
|
155
|
+
if passphrase
|
156
|
+
# Pad using an SHA-1 hash of the unpadded private blob in order to
|
157
|
+
# prevent an easily known plaintext attack on the last block.
|
158
|
+
cipher = ::OpenSSL::Cipher::AES.new(256, :CBC)
|
159
|
+
cipher.encrypt
|
160
|
+
padding_length = cipher.block_size - (padded_private_blob.bytesize % cipher.block_size)
|
161
|
+
padded_private_blob += ::OpenSSL::Digest::SHA1.new(padded_private_blob).digest[0, padding_length]
|
162
|
+
|
163
|
+
# PuTTY uses a zero IV.
|
164
|
+
cipher.key = generate_encryption_key(passphrase, cipher.key_len)
|
165
|
+
cipher.padding = 0
|
166
|
+
encrypted_private_blob = cipher.update(padded_private_blob) + cipher.final
|
167
|
+
else
|
168
|
+
encrypted_private_blob = private_blob
|
169
|
+
end
|
170
|
+
|
171
|
+
private_mac = compute_private_mac(passphrase, encryption_type, padded_private_blob)
|
172
|
+
|
173
|
+
Writer.open(path) do |writer|
|
174
|
+
writer.field('PuTTY-User-Key-File-2', @algorithm)
|
175
|
+
writer.field('Encryption', encryption_type)
|
176
|
+
writer.field('Comment', @comment)
|
177
|
+
writer.blob('Public', @public_blob)
|
178
|
+
writer.blob('Private', encrypted_private_blob)
|
179
|
+
writer.field('Private-MAC', private_mac)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
# Generates an encryption key of the specified length from a passphrase.
|
186
|
+
#
|
187
|
+
# @param passphrase [String] The passphrase to use.
|
188
|
+
# @param key_length [Integer] The length of the desired key in bytes.
|
189
|
+
#
|
190
|
+
# @return [String] The generated key.
|
191
|
+
def generate_encryption_key(passphrase, key_length)
|
192
|
+
key = String.new
|
193
|
+
key_digest = ::OpenSSL::Digest::SHA1.new
|
194
|
+
iteration = 0
|
195
|
+
|
196
|
+
while true
|
197
|
+
key_digest.update([iteration].pack('N'))
|
198
|
+
key_digest.update(passphrase.bytes.pack('c*'))
|
199
|
+
key += key_digest.digest
|
200
|
+
|
201
|
+
break if key.bytesize > key_length
|
202
|
+
|
203
|
+
key_digest.reset
|
204
|
+
iteration += 1
|
205
|
+
end
|
206
|
+
|
207
|
+
key[0, key_length]
|
208
|
+
end
|
209
|
+
|
210
|
+
# Computes the value of the Private-MAC field given the passphrase,
|
211
|
+
# encryption type and padded private blob (the value of the private blob
|
212
|
+
# after padding bytes have been appended prior to encryption).
|
213
|
+
#
|
214
|
+
# @param passphrase [String] The encryption passphrase.
|
215
|
+
# @param encryption_type [String] The value of the Encryption field.
|
216
|
+
# @param padded_private_blob [String] The private blob after padding bytes
|
217
|
+
# have been appended prior to encryption.
|
218
|
+
#
|
219
|
+
# @return [String] The computed private MAC.
|
220
|
+
def compute_private_mac(passphrase, encryption_type, padded_private_blob)
|
221
|
+
key = ::OpenSSL::Digest::SHA1.new
|
222
|
+
key.update(MAC_KEY)
|
223
|
+
key.update(passphrase) if passphrase
|
224
|
+
data = Util.ssh_pack(@algorithm, encryption_type, @comment || '', @public_blob, padded_private_blob)
|
225
|
+
::OpenSSL::HMAC.hexdigest(::OpenSSL::Digest::SHA1.new, key.digest, data)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Handles reading .ppk files.
|
229
|
+
#
|
230
|
+
# @private
|
231
|
+
class Reader
|
232
|
+
# Opens a .ppk file for reading, creates a new instance of `Reader` and
|
233
|
+
# yields it to the caller.
|
234
|
+
#
|
235
|
+
# @param path [Object] The path of the .ppk file to be read.
|
236
|
+
#
|
237
|
+
# @return [Object] The result of yielding to the caller.
|
238
|
+
#
|
239
|
+
# raise [Errno::ENOENT] If the file specified by `path` does not exist.
|
240
|
+
def self.open(path)
|
241
|
+
File.open(path.to_s, 'rb') do |file|
|
242
|
+
yield Reader.new(file)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Initializes a new {Reader} with an {IO} to read from.
|
247
|
+
#
|
248
|
+
# @param file [IO] The file to read from.
|
249
|
+
def initialize(file)
|
250
|
+
@file = file
|
251
|
+
end
|
252
|
+
|
253
|
+
# Reads the next field from the file.
|
254
|
+
#
|
255
|
+
# @param name [String] The expected field name.
|
256
|
+
#
|
257
|
+
# @return [String] The value of the field.
|
258
|
+
#
|
259
|
+
# @raise [FormatError] If the current position in the file was not the
|
260
|
+
# start of a field with the expected name.
|
261
|
+
def field(name)
|
262
|
+
line = read_line
|
263
|
+
raise FormatError, "Expected field #{name}, but found #{line}" unless line.start_with?("#{name}: ")
|
264
|
+
line.byteslice(name.bytesize + 2, line.bytesize - name.bytesize - 2)
|
265
|
+
end
|
266
|
+
|
267
|
+
# Reads a blob from the file consisting of a Lines field whose value
|
268
|
+
# gives the number of Base64 encoded lines in the blob.
|
269
|
+
#
|
270
|
+
# @return [String] The Base64-decoded value of the blob.
|
271
|
+
#
|
272
|
+
# @raise [FormatError] If there is not a blob starting at the current
|
273
|
+
# file position.
|
274
|
+
# @raise [FormatError] If the value of the Lines field is not a
|
275
|
+
# positive integer.
|
276
|
+
def blob(name)
|
277
|
+
lines = field("#{name}-Lines")
|
278
|
+
raise FormatError, "Invalid value for #{name}-Lines" unless lines =~ /\A\d+\z/
|
279
|
+
lines.to_i.times.map { read_line }.join("\n").unpack('m48').first
|
280
|
+
end
|
281
|
+
|
282
|
+
private
|
283
|
+
|
284
|
+
# Reads a single new-line (\n or \r\n) terminated line from the file,
|
285
|
+
# removing the new-line character.
|
286
|
+
#
|
287
|
+
# @return [String] The line.
|
288
|
+
#
|
289
|
+
# @raise [FormatError] If the end of file was detected before reading a
|
290
|
+
# line.
|
291
|
+
def read_line
|
292
|
+
@file.readline("\n").chomp("\n")
|
293
|
+
rescue EOFError
|
294
|
+
raise FormatError, 'Truncated ppk file detected'
|
295
|
+
end
|
296
|
+
end
|
297
|
+
private_constant :Reader
|
298
|
+
|
299
|
+
# Handles writing .ppk files.
|
300
|
+
#
|
301
|
+
# @private
|
302
|
+
class Writer
|
303
|
+
# The number of bytes that have been written.
|
304
|
+
#
|
305
|
+
# @return [Integer] The number of bytes that have been written.
|
306
|
+
attr_reader :bytes_written
|
307
|
+
|
308
|
+
# Opens a .ppk file for writing, creates a new instance of `Writer` and
|
309
|
+
# yields it to the caller.
|
310
|
+
#
|
311
|
+
# @param path [Object] The path of the .ppk file to be written.
|
312
|
+
#
|
313
|
+
# @return [Object] The result of yielding to the caller.
|
314
|
+
#
|
315
|
+
# @raise [Errno::ENOENT] If a directory specified by `path` does not
|
316
|
+
# exist.
|
317
|
+
def self.open(path)
|
318
|
+
File.open(path.to_s, 'wb') do |file|
|
319
|
+
yield Writer.new(file)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Initializes a new {Writer} with an {IO} to write to.
|
324
|
+
#
|
325
|
+
# @param file [IO] The file to write to.
|
326
|
+
def initialize(file)
|
327
|
+
@file = file
|
328
|
+
@bytes_written = 0
|
329
|
+
end
|
330
|
+
|
331
|
+
# Writes a field to the file.
|
332
|
+
#
|
333
|
+
# @param name [String] The field name.
|
334
|
+
# @param value [String] The field value.
|
335
|
+
def field(name, value)
|
336
|
+
write(name)
|
337
|
+
write(': ')
|
338
|
+
write(value.to_s)
|
339
|
+
write_line
|
340
|
+
end
|
341
|
+
|
342
|
+
# Writes a blob to the file (Lines field and Base64 encoded value).
|
343
|
+
#
|
344
|
+
# @param name [String] The name of the blob (used as a prefix for the
|
345
|
+
# lines field).
|
346
|
+
# @param blob [String] The value of the blob. This is Base64 encoded
|
347
|
+
# before being written to the file.
|
348
|
+
def blob(name, blob)
|
349
|
+
lines = [blob].pack('m48').split("\n")
|
350
|
+
field("#{name}-Lines", lines.length)
|
351
|
+
lines.each do |line|
|
352
|
+
write(line)
|
353
|
+
write_line
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
private
|
358
|
+
|
359
|
+
# Writes a line separator to the file (\r\n on all platforms).
|
360
|
+
def write_line
|
361
|
+
write("\r\n")
|
362
|
+
end
|
363
|
+
|
364
|
+
# Writes a string to the file.
|
365
|
+
#
|
366
|
+
# @param string [String] The string to be written.
|
367
|
+
def write(string)
|
368
|
+
@bytes_written += @file.write(string)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
private_constant :Writer
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
module PuTTY
|
5
|
+
module Key
|
6
|
+
# Utility methods used internally by PuTTY::Key.
|
7
|
+
#
|
8
|
+
# @private
|
9
|
+
module Util
|
10
|
+
# Encodes a list of values (String and OpenSSL::BN instances) according
|
11
|
+
# to RFC 4251 section 5 (as strings and mpints).
|
12
|
+
#
|
13
|
+
# No encoding conversion is performed on Strings.
|
14
|
+
#
|
15
|
+
# @param [Array] values An Array of String and OpenSSL::BN instances to
|
16
|
+
# be encoded.
|
17
|
+
#
|
18
|
+
# @return [String] A binary String containing the encoded values.
|
19
|
+
#
|
20
|
+
# @raise NilValueError If a value is `nil`.
|
21
|
+
def self.ssh_pack(*values)
|
22
|
+
return ''.b if values.empty?
|
23
|
+
|
24
|
+
values.map do |value|
|
25
|
+
raise NilValueError, 'values must not contain nil elements' unless value
|
26
|
+
|
27
|
+
if value.kind_of?(::OpenSSL::BN)
|
28
|
+
value = value.to_i
|
29
|
+
if value == 0
|
30
|
+
value = ''
|
31
|
+
else
|
32
|
+
bytes = []
|
33
|
+
|
34
|
+
if value > 0
|
35
|
+
begin
|
36
|
+
bytes << (value & 0xff)
|
37
|
+
value = value >> 8
|
38
|
+
end until value == 0
|
39
|
+
|
40
|
+
# 0 pad if necessary to resolve ambiguity with negative numbers
|
41
|
+
# in two's complement representation.
|
42
|
+
bytes << 0 if bytes.last & 0x80 != 0
|
43
|
+
else
|
44
|
+
begin
|
45
|
+
bytes << (value & 0xff)
|
46
|
+
value = value >> 8
|
47
|
+
end until value == -1 && bytes.last & 0x80 != 0
|
48
|
+
end
|
49
|
+
|
50
|
+
value = bytes.reverse!.pack('C*')
|
51
|
+
end
|
52
|
+
else
|
53
|
+
value = value.to_s.b
|
54
|
+
end
|
55
|
+
|
56
|
+
[value.bytesize].pack('N') + value
|
57
|
+
end.join
|
58
|
+
end
|
59
|
+
|
60
|
+
# Decodes a string containing RFC 4251 section 5 encoded string and
|
61
|
+
# mpint values.
|
62
|
+
#
|
63
|
+
# @param [String] encoded A binary {String} containing the encoded values.
|
64
|
+
# @param [Array<Symbol>] spec An array consisting of :string or :mpint
|
65
|
+
# elements describing the contents of encoded.
|
66
|
+
#
|
67
|
+
# @return [Array] An array of decoded (binary) {String} and {OpenSSL::BN}
|
68
|
+
# instances.
|
69
|
+
#
|
70
|
+
# @raise [ArgumentError] If `encoded` is `nil`.
|
71
|
+
# @raise [ArgumentError] If `encoded` does not use the `ASCII_8BIT`
|
72
|
+
# (binary) encoding.
|
73
|
+
# @raise [ArgumentError] If `spec` contains elements other than `:mpint`
|
74
|
+
# and `:string`.
|
75
|
+
# @raise [FormatError] If the encoded structure is malformed.
|
76
|
+
# @raise [FormatError] If `spec` contains more elements than are present
|
77
|
+
# within `encoded`.
|
78
|
+
def self.ssh_unpack(encoded, *spec)
|
79
|
+
raise ArgumentError, 'encoded must not be nil' unless encoded
|
80
|
+
encoded = encoded.to_s
|
81
|
+
raise ArgumentError, 'encoded must be a binary String' unless encoded.encoding == Encoding::ASCII_8BIT
|
82
|
+
|
83
|
+
io = StringIO.new(encoded)
|
84
|
+
|
85
|
+
spec.map do |type|
|
86
|
+
length_bytes = io.read(4)
|
87
|
+
raise FormatError, 'spec contains more elements than are contained within the encoded String' unless length_bytes
|
88
|
+
raise FormatError, 'Truncated length encountered' unless length_bytes.bytesize == 4
|
89
|
+
|
90
|
+
length = length_bytes.unpack('N').first
|
91
|
+
|
92
|
+
if length > 0
|
93
|
+
encoded_value = io.read(length)
|
94
|
+
raise FormatError, 'Missing value encountered' unless encoded_value
|
95
|
+
raise FormatError, 'Truncated value encountered' unless encoded_value.bytesize == length
|
96
|
+
else
|
97
|
+
encoded_value = nil
|
98
|
+
end
|
99
|
+
|
100
|
+
case type
|
101
|
+
when :string
|
102
|
+
encoded_value || String.new
|
103
|
+
when :mpint
|
104
|
+
value = 0
|
105
|
+
|
106
|
+
if encoded_value
|
107
|
+
bytes = encoded_value.unpack('C*')
|
108
|
+
bytes.each {|b| value = (value << 8) | b }
|
109
|
+
|
110
|
+
if bytes.first & 0x80 != 0
|
111
|
+
# A negative value. Reinterpret the bytes as the two's
|
112
|
+
# complement representation of the negative integer.
|
113
|
+
mask = 0xff
|
114
|
+
(bytes.length - 1).times { mask = mask << 8 | 0xff }
|
115
|
+
value = -(-value & mask)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
::OpenSSL::BN.new(value)
|
120
|
+
else
|
121
|
+
raise ArgumentError, 'spec must contain only :string and :mpint elements'
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
private_constant :Util
|
127
|
+
end
|
128
|
+
end
|