putty-key 1.0.1 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -1
  3. data/CHANGES.md +23 -0
  4. data/Gemfile +10 -5
  5. data/LICENSE +1 -1
  6. data/README.md +32 -6
  7. data/Rakefile +24 -0
  8. data/lib/putty/key/argon2_params.rb +101 -0
  9. data/lib/putty/key/error.rb +17 -0
  10. data/lib/putty/key/libargon2.rb +54 -0
  11. data/lib/putty/key/openssl.rb +279 -48
  12. data/lib/putty/key/ppk.rb +482 -104
  13. data/lib/putty/key/util.rb +10 -10
  14. data/lib/putty/key/version.rb +1 -1
  15. data/lib/putty/key.rb +6 -6
  16. data/putty-key.gemspec +11 -2
  17. data/test/argon2_params_test.rb +144 -0
  18. data/test/fixtures/{dss-1024-encrypted.ppk → dss-1024-encrypted-format-2.ppk} +17 -17
  19. data/test/fixtures/dss-1024-encrypted-format-3.ppk +22 -0
  20. data/test/fixtures/{dss-1024.ppk → dss-1024-format-2.ppk} +17 -17
  21. data/test/fixtures/dss-1024-format-3.ppk +17 -0
  22. data/test/fixtures/{ecdsa-sha2-nistp256-encrypted.ppk → ecdsa-sha2-nistp256-encrypted-format-2.ppk} +10 -10
  23. data/test/fixtures/ecdsa-sha2-nistp256-encrypted-format-3.ppk +15 -0
  24. data/test/fixtures/{ecdsa-sha2-nistp256.ppk → ecdsa-sha2-nistp256-format-2.ppk} +10 -10
  25. data/test/fixtures/ecdsa-sha2-nistp256-format-3.ppk +10 -0
  26. data/test/fixtures/{ecdsa-sha2-nistp384-encrypted.ppk → ecdsa-sha2-nistp384-encrypted-format-2.ppk} +11 -11
  27. data/test/fixtures/ecdsa-sha2-nistp384-encrypted-format-3.ppk +16 -0
  28. data/test/fixtures/{ecdsa-sha2-nistp384.ppk → ecdsa-sha2-nistp384-format-2.ppk} +11 -11
  29. data/test/fixtures/ecdsa-sha2-nistp384-format-3.ppk +11 -0
  30. data/test/fixtures/{ecdsa-sha2-nistp521-encrypted.ppk → ecdsa-sha2-nistp521-encrypted-format-2.ppk} +12 -12
  31. data/test/fixtures/ecdsa-sha2-nistp521-encrypted-format-3.ppk +17 -0
  32. data/test/fixtures/{ecdsa-sha2-nistp521.ppk → ecdsa-sha2-nistp521-format-2.ppk} +12 -12
  33. data/test/fixtures/ecdsa-sha2-nistp521-format-3.ppk +12 -0
  34. data/test/fixtures/{rsa-2048-encrypted.ppk → rsa-2048-encrypted-format-2.ppk} +26 -26
  35. data/test/fixtures/rsa-2048-encrypted-format-3.ppk +31 -0
  36. data/test/fixtures/{rsa-2048.ppk → rsa-2048-format-2.ppk} +26 -26
  37. data/test/fixtures/rsa-2048-format-3.ppk +26 -0
  38. data/test/fixtures/test-blank-comment.ppk +11 -11
  39. data/test/fixtures/test-empty-blobs-encrypted.ppk +6 -0
  40. data/test/fixtures/test-empty-blobs.ppk +6 -0
  41. data/test/fixtures/{test-encrypted.ppk → test-encrypted-format-2.ppk} +11 -11
  42. data/test/fixtures/test-encrypted-format-3.ppk +16 -0
  43. data/test/fixtures/test-encrypted-type-d-format-3.ppk +16 -0
  44. data/test/fixtures/test-encrypted-type-i-format-3.ppk +16 -0
  45. data/test/fixtures/{test-unix-line-endings.ppk → test-format-2.ppk} +0 -0
  46. data/test/fixtures/test-format-3.ppk +11 -0
  47. data/test/fixtures/test-invalid-argon2-memory-for-libargon2.ppk +16 -0
  48. data/test/fixtures/test-invalid-argon2-memory-maximum.ppk +16 -0
  49. data/test/fixtures/test-invalid-argon2-memory.ppk +16 -0
  50. data/test/fixtures/test-invalid-argon2-parallelism-maximum.ppk +16 -0
  51. data/test/fixtures/test-invalid-argon2-parallelism.ppk +16 -0
  52. data/test/fixtures/test-invalid-argon2-passes-maximum.ppk +16 -0
  53. data/test/fixtures/test-invalid-argon2-passes.ppk +16 -0
  54. data/test/fixtures/test-invalid-argon2-salt.ppk +16 -0
  55. data/test/fixtures/test-invalid-blob-lines.ppk +11 -11
  56. data/test/fixtures/test-invalid-encryption-type.ppk +11 -11
  57. data/test/fixtures/test-invalid-format-1.ppk +11 -11
  58. data/test/fixtures/{test-invalid-format-3.ppk → test-invalid-format-4.ppk} +11 -11
  59. data/test/fixtures/test-invalid-key-derivation.ppk +16 -0
  60. data/test/fixtures/test-invalid-private-mac.ppk +11 -11
  61. data/test/fixtures/test-legacy-mac-line-endings.ppk +1 -0
  62. data/test/fixtures/test-missing-final-line-ending.ppk +11 -0
  63. data/test/fixtures/test-truncated.ppk +10 -10
  64. data/test/fixtures/{test.ppk → test-windows-line-endings.ppk} +0 -0
  65. data/test/openssl_test.rb +243 -53
  66. data/test/ppk_test.rb +325 -44
  67. data/test/test_helper.rb +10 -3
  68. data.tar.gz.sig +0 -0
  69. metadata +73 -23
  70. metadata.gz.sig +0 -0
data/test/ppk_test.rb CHANGED
@@ -16,18 +16,39 @@ class PPKTest < Minitest::Test
16
16
  assert_nil(ppk.private_blob)
17
17
  end
18
18
 
19
- def test_initialize_invalid_format
20
- [1,3].each do |format|
21
- assert_raises(PuTTY::Key::FormatError) { PuTTY::Key::PPK.new(fixture_path("test-invalid-format-#{format}.ppk")) }
19
+ def test_initialize_invalid_format_too_old
20
+ format = PuTTY::Key::PPK::MINIMUM_FORMAT - 1
21
+ error = assert_raises(PuTTY::Key::FormatError) { PuTTY::Key::PPK.new(fixture_path("test-invalid-format-#{format}.ppk")) }
22
+ assert_equal("The ppk file is using an old unsupported format (#{format})", error.message)
23
+ end
24
+
25
+ def test_initialize_invalid_format_too_new
26
+ format = PuTTY::Key::PPK::MAXIMUM_FORMAT + 1
27
+ error = assert_raises(PuTTY::Key::FormatError) { PuTTY::Key::PPK.new(fixture_path("test-invalid-format-#{format}.ppk")) }
28
+ assert_equal("The ppk file is using a format that is too new (#{format})", error.message)
29
+ end
30
+
31
+ [:encryption_type, :key_derivation, :argon2_memory, :argon2_memory_maximum,
32
+ :argon2_passes, :argon2_passes_maximum, :argon2_parallelism,
33
+ :argon2_parallelism_maximum, :argon2_salt
34
+ ].each do |feature|
35
+ define_method("test_initialize_invalid_#{feature}") do
36
+ path = fixture_path("test-invalid-#{feature.to_s.gsub('_', '-')}.ppk")
37
+ assert_raises(PuTTY::Key::FormatError) { PuTTY::Key::PPK.new(path, 'Test Passphrase') }
22
38
  end
23
39
  end
24
40
 
25
- def test_initialize_invalid_encryption_type
26
- assert_raises(PuTTY::Key::FormatError) { PuTTY::Key::PPK.new(fixture_path('test-invalid-encryption-type.ppk')) }
41
+ [:private_mac, :blob_lines].each do |feature|
42
+ define_method("test_initialize_invalid_#{feature}") do
43
+ path = fixture_path("test-invalid-#{feature.to_s.gsub('_', '-')}.ppk")
44
+ assert_raises(PuTTY::Key::FormatError) { PuTTY::Key::PPK.new(path) }
45
+ end
27
46
  end
28
47
 
29
- def test_initialize_invalid_private_mac
30
- assert_raises(PuTTY::Key::FormatError) { PuTTY::Key::PPK.new(fixture_path('test-invalid-private-mac.ppk')) }
48
+ def test_initialize_invalid_argon2_memory_for_libargon2
49
+ # Allowed by Argon2Params, but not by libargon2.
50
+ path = fixture_path("test-invalid-argon2-memory-for-libargon2.ppk")
51
+ assert_raises(PuTTY::Key::Argon2Error) { PuTTY::Key::PPK.new(path, 'Test Passphrase') }
31
52
  end
32
53
 
33
54
  def test_initialize_file_not_exists
@@ -44,30 +65,51 @@ class PPKTest < Minitest::Test
44
65
  assert_raises(PuTTY::Key::FormatError) { PuTTY::Key::PPK.new(fixture_path('test-truncated.ppk')) }
45
66
  end
46
67
 
47
- def test_initialize_invalid_blob_lines
48
- assert_raises(PuTTY::Key::FormatError) { PuTTY::Key::PPK.new(fixture_path('test-invalid-blob-lines.ppk')) }
49
- end
50
-
51
- def assert_test_ppk_properties(ppk, comment: TEST_COMMENT, encrypted: false)
68
+ def assert_test_ppk_properties(ppk, comment: TEST_COMMENT, public_blob: TEST_PUBLIC_BLOB, private_blob: TEST_PRIVATE_BLOB, encrypted: false)
52
69
  assert_equal(Encoding::ASCII_8BIT, ppk.algorithm.encoding)
53
70
  assert_equal(Encoding::ASCII_8BIT, ppk.comment.encoding)
54
71
  assert_equal(Encoding::ASCII_8BIT, ppk.public_blob.encoding)
55
72
  assert_equal(Encoding::ASCII_8BIT, ppk.private_blob.encoding)
56
73
  assert_equal('test'.b, ppk.algorithm)
57
74
  assert_equal(comment, ppk.comment)
58
- assert_equal(TEST_PUBLIC_BLOB, ppk.public_blob)
75
+ assert_equal(public_blob, ppk.public_blob)
59
76
 
60
77
  if encrypted
61
78
  # When loading an encrypted ppk file, the padding added to the private blob cannot be removed.
62
- assert(ppk.private_blob.start_with?(TEST_PRIVATE_BLOB), "Private blob does not start with #{TEST_PRIVATE_BLOB}")
79
+ assert(ppk.private_blob.start_with?(private_blob), "Private blob does not start with #{TEST_PRIVATE_BLOB}")
63
80
  else
64
- assert_equal(TEST_PRIVATE_BLOB, ppk.private_blob)
81
+ assert_equal(private_blob, ppk.private_blob)
65
82
  end
66
83
  end
67
84
 
68
- def test_initialize_unencrypted
69
- ppk = PuTTY::Key::PPK.new(fixture_path('test.ppk'))
70
- assert_test_ppk_properties(ppk)
85
+ [2, 3].each do |format|
86
+ define_method("test_initialize_unencrypted_format_#{format}") do
87
+ ppk = PuTTY::Key::PPK.new(fixture_path("test-format-#{format}.ppk"))
88
+ assert_test_ppk_properties(ppk)
89
+ end
90
+
91
+ define_method("test_initialize_encrypted_format_#{format}") do
92
+ ppk = PuTTY::Key::PPK.new(fixture_path("test-encrypted-format-#{format}.ppk"), 'Test Passphrase')
93
+ assert_test_ppk_properties(ppk, encrypted: true)
94
+ end
95
+
96
+ define_method("test_initialize_encrypted_no_passphrase_#{format}") do
97
+ path = fixture_path("test-encrypted-format-#{format}.ppk")
98
+ assert_raises(ArgumentError) { PuTTY::Key::PPK.new(path) }
99
+ end
100
+
101
+ define_method("test_initialize_encrypted_incorrect_passphrase_#{format}") do
102
+ path = fixture_path("test-encrypted-format-#{format}.ppk")
103
+ assert_raises(ArgumentError) { PuTTY::Key::PPK.new(path, 'Not Test Passphrase') }
104
+ end
105
+ end
106
+
107
+ # type-d and type-i fixtures also use different Argon2 parameters.
108
+ [:d, :i].each do |type|
109
+ define_method("test_initialize_encrypted_format_3_type_#{type}") do
110
+ ppk = PuTTY::Key::PPK.new(fixture_path("test-encrypted-type-#{type}-format-3.ppk"), 'Test Passphrase')
111
+ assert_test_ppk_properties(ppk, encrypted: true)
112
+ end
71
113
  end
72
114
 
73
115
  def test_initialize_blank_comment
@@ -75,27 +117,56 @@ class PPKTest < Minitest::Test
75
117
  assert_test_ppk_properties(ppk, comment: ''.b)
76
118
  end
77
119
 
78
- def test_initialize_encrypted
79
- ppk = PuTTY::Key::PPK.new(fixture_path('test-encrypted.ppk'), 'Test Passphrase')
80
- assert_test_ppk_properties(ppk, encrypted: true)
120
+ def test_initialize_empty_blobs
121
+ ppk = PuTTY::Key::PPK.new(fixture_path('test-empty-blobs.ppk'))
122
+ assert_test_ppk_properties(ppk, public_blob: ''.b, private_blob: ''.b, encrypted: true)
81
123
  end
82
124
 
83
- def test_initialize_encrypted_no_passphrase
84
- assert_raises(ArgumentError) { PuTTY::Key::PPK.new(fixture_path('test-encrypted.ppk')) }
125
+ def test_initialize_empty_blobs_encrypted
126
+ ppk = PuTTY::Key::PPK.new(fixture_path('test-empty-blobs-encrypted.ppk'), 'Test Passphrase')
127
+ assert_test_ppk_properties(ppk, public_blob: ''.b, private_blob: ''.b, encrypted: true)
85
128
  end
86
129
 
87
- def test_initialize_encrypted_incorrect_passphrase
88
- assert_raises(ArgumentError) { PuTTY::Key::PPK.new(fixture_path('test-encrypted.ppk'), 'Not Test Passphrase') }
130
+ def test_initialize_missing_final_line_ending
131
+ ppk = PuTTY::Key::PPK.new(fixture_path('test-missing-final-line-ending.ppk'))
132
+ assert_test_ppk_properties(ppk)
89
133
  end
90
134
 
91
135
  def test_initialize_pathname
92
- ppk = PuTTY::Key::PPK.new(Pathname.new(fixture_path('test.ppk')))
136
+ ppk = PuTTY::Key::PPK.new(Pathname.new(fixture_path('test-format-2.ppk')))
93
137
  assert_test_ppk_properties(ppk)
94
138
  end
95
139
 
96
- def test_initialize_unix_line_endings
97
- ppk = PuTTY::Key::PPK.new(fixture_path('test-unix-line-endings.ppk'))
98
- assert_test_ppk_properties(ppk)
140
+ def test_initialize_from_io_with_getbyte
141
+ File.open(fixture_path('test-format-2.ppk'), 'rb') do |file|
142
+ reader = TestReaderWithGetbyte.new(file)
143
+ ppk = PuTTY::Key::PPK.new(reader)
144
+ assert_test_ppk_properties(ppk)
145
+ end
146
+ end
147
+
148
+ def test_initialize_from_io_with_read
149
+ File.open(fixture_path('test-format-2.ppk'), 'rb') do |file|
150
+ reader = TestReaderWithRead.new(file)
151
+ ppk = PuTTY::Key::PPK.new(reader)
152
+ assert_test_ppk_properties(ppk)
153
+ end
154
+ end
155
+
156
+ def test_initialize_from_io_with_binmode
157
+ File.open(fixture_path('test-format-2.ppk'), 'r') do |file|
158
+ reader = TestReaderWithBinmode.new(file)
159
+ ppk = PuTTY::Key::PPK.new(reader)
160
+ assert_equal(1, reader.binmode_calls)
161
+ assert_test_ppk_properties(ppk)
162
+ end
163
+ end
164
+
165
+ %w(legacy_mac windows).each do |type|
166
+ define_method("test_initialize_#{type}_line_endings") do
167
+ ppk = PuTTY::Key::PPK.new(fixture_path("test-#{type.gsub('_', '-')}-line-endings.ppk"))
168
+ assert_test_ppk_properties(ppk)
169
+ end
99
170
  end
100
171
 
101
172
  def create_test_ppk
@@ -140,10 +211,10 @@ class PPKTest < Minitest::Test
140
211
  end
141
212
  end
142
213
 
143
- def test_save_format_not_supported
144
- ppk = create_test_ppk
145
- temp_file_name do |file|
146
- [1,3].each do |format|
214
+ [PuTTY::Key::PPK::MINIMUM_FORMAT - 1, PuTTY::Key::PPK::MAXIMUM_FORMAT + 1].each do |format|
215
+ define_method("test_save_format_#{format}_not_supported") do
216
+ ppk = create_test_ppk
217
+ temp_file_name do |file|
147
218
  assert_raises(ArgumentError) { ppk.save(file, 'Test Passphrase', format: format) }
148
219
  end
149
220
  end
@@ -180,27 +251,93 @@ class PPKTest < Minitest::Test
180
251
  end
181
252
  end
182
253
 
183
- def test_save_unencrypted
254
+ [2, 3].each do |format|
255
+ define_method("test_save_unencrypted_format_#{format}") do
256
+ ppk = create_test_ppk
257
+ temp_file_name do |file|
258
+ ppk.save(file, format: format)
259
+ assert_identical_to_fixture("test-format-#{format}.ppk", file)
260
+ end
261
+ end
262
+
263
+ define_method("test_save_passphrase_empty_format_#{format}") do
264
+ ppk = create_test_ppk
265
+ temp_file_name do |file|
266
+ ppk.save(file, '', format: format)
267
+ assert_identical_to_fixture("test-format-#{format}.ppk", file)
268
+ end
269
+ end
270
+
271
+ define_method("test_save_encrypted_format_#{format}") do
272
+ ppk = create_test_ppk
273
+ temp_file_name do |file|
274
+ ppk.save(file, 'Test Passphrase', format: format, argon2_params: PuTTY::Key::Argon2Params.new(passes: 8, salt: "\x7d\x5d\x45\x57\xc5\x56\x3a\x5b\x50\x09\xe1\x45\x2c\x51\x8e\x04".b))
275
+ assert_identical_to_fixture("test-encrypted-format-#{format}.ppk", file)
276
+ end
277
+ end
278
+ end
279
+
280
+ # type_d and type_i tests cover other Argon2 parameters too.
281
+ def test_save_encrypted_type_d_format_3
184
282
  ppk = create_test_ppk
185
283
  temp_file_name do |file|
186
- ppk.save(file)
187
- assert_identical_to_fixture('test.ppk', file)
284
+ ppk.save(file, 'Test Passphrase', format: 3, argon2_params: PuTTY::Key::Argon2Params.new(type: :d, memory: 4096, passes: 9, salt: "\xbc\x44\x19\x1a\xa9\x26\x73\xa5\xc0\x54\x3f\x37\x36\x33\xdd\xf4".b ))
285
+ assert_identical_to_fixture("test-encrypted-type-d-format-3.ppk", file)
188
286
  end
189
287
  end
190
288
 
191
- def test_save_passphrase_empty
289
+ def test_save_encrypted_type_i_format_3
192
290
  ppk = create_test_ppk
193
291
  temp_file_name do |file|
194
- ppk.save(file, '')
195
- assert_identical_to_fixture('test.ppk', file)
292
+ ppk.save(file, 'Test Passphrase', format: 3, argon2_params: PuTTY::Key::Argon2Params.new(type: :i, memory: 2048, passes: 5, parallelism: 3, salt: "\xbd\x5e\x3d\x94\x03\xec\x37\x41\x8b\xa5\xae\x1d\x11\x6f\xa9\x75".b ))
293
+ assert_identical_to_fixture("test-encrypted-type-i-format-3.ppk", file)
196
294
  end
197
295
  end
198
296
 
199
- def test_save_encrypted
297
+ def get_field(file, name)
298
+ line = File.readlines(file, mode: 'rb').find {|l| l.start_with?("#{name}: ")}
299
+ line && line.byteslice(name.bytesize + 2, line.bytesize - name.bytesize - 2).chomp("\n")
300
+ end
301
+
302
+ def test_save_chooses_random_salt
200
303
  ppk = create_test_ppk
201
304
  temp_file_name do |file|
202
- ppk.save(file, 'Test Passphrase')
203
- assert_identical_to_fixture('test-encrypted.ppk', file)
305
+ ppk.save(file, 'Test Passphrase', format: 3)
306
+ salt1 = get_field(file, 'Argon2-Salt')
307
+ assert_match(/\A[0-9a-f]{32}\z/, salt1)
308
+
309
+ File.unlink(file)
310
+ ppk.save(file, 'Test Passphrase', format: 3)
311
+ salt2 = get_field(file, 'Argon2-Salt')
312
+ assert_match(/\A[0-9a-f]{32}\z/, salt2)
313
+
314
+ refute_equal(salt1, salt2)
315
+ end
316
+ end
317
+
318
+ def test_save_calculates_passes_required_for_time
319
+ ppk = create_test_ppk
320
+ temp_file_name do |file|
321
+ ppk.save(file, 'Test Passphrase', format: 3, argon2_params: PuTTY::Key::Argon2Params.new(desired_time: 0))
322
+ passes = get_field(file, 'Argon2-Passes').to_i
323
+ initial_passes = passes
324
+
325
+ 100.step(by: 100, to: 1000) do |desired_time|
326
+ File.unlink(file)
327
+ ppk.save(file, 'Test Passphrase', format: 3, argon2_params: PuTTY::Key::Argon2Params.new(desired_time: desired_time))
328
+ passes = get_field(file, 'Argon2-Passes').to_i
329
+ break if passes > initial_passes
330
+ end
331
+
332
+ assert(passes > initial_passes)
333
+ end
334
+ end
335
+
336
+ def test_save_raises_error_if_libargon2_rejects_parameters
337
+ ppk = create_test_ppk
338
+ temp_file_name do |file|
339
+ argon2_params = PuTTY::Key::Argon2Params.new(memory: 1)
340
+ assert_raises(PuTTY::Key::Argon2Error) { ppk.save(file, 'Test Passphrase', format: 3, argon2_params: argon2_params) }
204
341
  end
205
342
  end
206
343
 
@@ -222,11 +359,91 @@ class PPKTest < Minitest::Test
222
359
  end
223
360
  end
224
361
 
362
+ def test_save_empty_blobs
363
+ ppk = create_test_ppk
364
+ ppk.public_blob = ''.b
365
+ ppk.private_blob = ''.b
366
+ temp_file_name do |file|
367
+ ppk.save(file)
368
+ assert_identical_to_fixture('test-empty-blobs.ppk', file)
369
+ end
370
+ end
371
+
372
+ def test_save_empty_blobs_encrypted
373
+ ppk = create_test_ppk
374
+ ppk.public_blob = ''.b
375
+ ppk.private_blob = ''.b
376
+ temp_file_name do |file|
377
+ ppk.save(file, 'Test Passphrase')
378
+ assert_identical_to_fixture('test-empty-blobs-encrypted.ppk', file)
379
+ end
380
+ end
381
+
382
+ def get_blob(file, name)
383
+ lines = File.readlines(file, mode: 'rb')
384
+ index = lines.find_index {|l| l.start_with?("#{name}-Lines: ")}
385
+ return nil unless index
386
+ line = lines[index]
387
+ count = line.byteslice(name.bytesize + 8, line.bytesize - name.bytesize - 2).chomp("\n").to_i
388
+ blob_lines = lines[index + 1, count]
389
+ blob_lines.join("\n").unpack('m48').first
390
+ end
391
+
392
+ [[0, 0], [15, 1], [16, 0], [17, 15], [18, 14], [30, 2], [31, 1], [32, 0]].each do |length, needed|
393
+ define_method("test_save_encrypted_pads_private_blob_of_length_#{length}_to_multiple_of_block_size_with_sha1") do
394
+ private_blob = "\0".b * length
395
+ ppk = create_test_ppk
396
+ ppk.private_blob = private_blob
397
+
398
+ temp_file_name do |file|
399
+ ppk.save(file, 'Test Passphrase')
400
+ encrypted_padded_private_blob = get_blob(file, 'Private')
401
+ assert_equal(length + needed, encrypted_padded_private_blob.bytesize)
402
+ assert_equal(private_blob, ppk.private_blob)
403
+
404
+ loaded_ppk = PuTTY::Key::PPK.new(file, 'Test Passphrase')
405
+ assert_equal(length + needed, loaded_ppk.private_blob.bytesize)
406
+
407
+ if needed == 0
408
+ assert_equal(private_blob, loaded_ppk.private_blob)
409
+ else
410
+ assert_equal(private_blob, loaded_ppk.private_blob.byteslice(0, length))
411
+ padding = loaded_ppk.private_blob.byteslice(length, needed)
412
+ expected_padding = OpenSSL::Digest::SHA1.new(private_blob).digest.byteslice(0, needed)
413
+ assert_equal(expected_padding, padding)
414
+ end
415
+ end
416
+ end
417
+ end
418
+
225
419
  def test_save_pathname
226
420
  ppk = create_test_ppk
227
421
  temp_file_name do |file|
228
422
  ppk.save(Pathname.new(file))
229
- assert_identical_to_fixture('test.ppk', file)
423
+ assert_identical_to_fixture('test-format-2.ppk', file)
424
+ end
425
+ end
426
+
427
+ def test_save_to_io
428
+ ppk = create_test_ppk
429
+ temp_file_name do |file_name|
430
+ File.open(file_name, 'wb') do |file|
431
+ writer = TestWriter.new(file)
432
+ ppk.save(writer)
433
+ end
434
+ assert_identical_to_fixture('test-format-2.ppk', file_name)
435
+ end
436
+ end
437
+
438
+ def test_save_to_io_with_binmode
439
+ ppk = create_test_ppk
440
+ temp_file_name do |file_name|
441
+ File.open(file_name, 'w') do |file|
442
+ writer = TestWriterWithBinmode.new(file)
443
+ ppk.save(writer)
444
+ assert_equal(1, writer.binmode_calls)
445
+ end
446
+ assert_identical_to_fixture('test-format-2.ppk', file_name)
230
447
  end
231
448
  end
232
449
 
@@ -235,7 +452,7 @@ class PPKTest < Minitest::Test
235
452
  temp_file_name do |file|
236
453
  File.open(file, 'w') { |f| f.write('not test.ppk') }
237
454
  ppk.save(file)
238
- assert_identical_to_fixture('test.ppk', file)
455
+ assert_identical_to_fixture('test-format-2.ppk', file)
239
456
  end
240
457
  end
241
458
 
@@ -246,4 +463,68 @@ class PPKTest < Minitest::Test
246
463
  assert_equal(File.size(file), result)
247
464
  end
248
465
  end
466
+
467
+ module BinmodeCallsTest
468
+ def binmode
469
+ if instance_variable_defined?(:@binmode_calls)
470
+ @binmode_calls += 1
471
+ else
472
+ @binmode_calls = 1
473
+ end
474
+ @io.binmode
475
+ self
476
+ end
477
+
478
+ def binmode_calls
479
+ instance_variable_defined?(:@binmode_calls) ? @binmode_calls : 0
480
+ end
481
+ end
482
+
483
+ class TestReaderWithGetbyte
484
+ def initialize(io)
485
+ @io = io
486
+ end
487
+
488
+ def getbyte(*args)
489
+ @io.getbyte(*args)
490
+ end
491
+ end
492
+
493
+ class TestReaderWithRead
494
+ def initialize(io)
495
+ @io = io
496
+ end
497
+
498
+ def read(*args)
499
+ @io.read(*args)
500
+ end
501
+ end
502
+
503
+ class TestReaderWithBinmode < TestReaderWithGetbyte
504
+ include BinmodeCallsTest
505
+
506
+ def getbyte(*args)
507
+ raise 'binmode must be called before getbyte' unless binmode_calls > 0
508
+ super
509
+ end
510
+ end
511
+
512
+ class TestWriter
513
+ def initialize(io)
514
+ @io = io
515
+ end
516
+
517
+ def write(*args)
518
+ @io.write(*args)
519
+ end
520
+ end
521
+
522
+ class TestWriterWithBinmode < TestWriter
523
+ include BinmodeCallsTest
524
+
525
+ def write(*args)
526
+ raise 'binmode must be called before write' unless binmode_calls > 0
527
+ super
528
+ end
529
+ end
249
530
  end
data/test/test_helper.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'putty/key'
4
+
3
5
  TEST_TYPE = (ENV['TEST_TYPE'] || 'refinement').to_sym
4
6
  raise "Unrecognized TEST_TYPE: #{TEST_TYPE}" unless [:refinement, :global].include?(TEST_TYPE)
5
7
 
@@ -15,6 +17,13 @@ if TEST_COVERAGE
15
17
  "#{object.respond_to?(method) ? '' : 'no_'}#{Regexp.escape(object.class.name.downcase.gsub('::', '_'))}_#{Regexp.escape(method)}"
16
18
  end
17
19
 
20
+ feature_support = [
21
+ ['openssl3', PuTTY::Key::OpenSSL.const_get(:Version).openssl?(3)],
22
+ ['refinement_class', defined?(Refinement)]
23
+ ].map do |feature, available|
24
+ "#{available ? '' : 'no_'}#{feature}"
25
+ end
26
+
18
27
  SimpleCov.command_name TEST_TYPE.to_s
19
28
 
20
29
  SimpleCov.formatters = [
@@ -23,13 +32,11 @@ if TEST_COVERAGE
23
32
 
24
33
  SimpleCov.start do
25
34
  add_filter 'test'
26
- nocov_token "nocov_(#{method_support.join('|')})"
35
+ nocov_token "nocov_(#{(method_support + feature_support).join('|')})"
27
36
  project_name 'PuTTY::Key'
28
37
  end
29
38
  end
30
39
 
31
- require 'putty/key'
32
-
33
40
  require 'fileutils'
34
41
  require 'minitest/autorun'
35
42
  require 'tmpdir'
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: putty-key
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Ross
@@ -29,10 +29,24 @@ cert_chain:
29
29
  J3Zn/kSTjTekiaspyGbczC3PUaeJNxr+yCvR4sk71Xmk/GaKKGOHedJ1uj/LAXrA
30
30
  MR0mpl7b8zCg0PFC1J73uw==
31
31
  -----END CERTIFICATE-----
32
- date: 2019-12-26 00:00:00.000000000 Z
33
- dependencies: []
32
+ date: 2022-10-23 00:00:00.000000000 Z
33
+ dependencies:
34
+ - !ruby/object:Gem::Dependency
35
+ name: ffi
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ type: :runtime
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
34
48
  description: |
35
- PuTTY::Key is a pure-Ruby implementation of the PuTTY private key (ppk) format,
49
+ PuTTY::Key is a Ruby implementation of the PuTTY private key (ppk) format,
36
50
  handling reading and writing .ppk files. It includes a refinement to Ruby's
37
51
  OpenSSL library to add support for converting DSA, EC and RSA private keys to
38
52
  and from PuTTY private key files. This allows OpenSSH ecdsa, ssh-dss and ssh-rsa
@@ -52,38 +66,68 @@ files:
52
66
  - README.md
53
67
  - Rakefile
54
68
  - lib/putty/key.rb
69
+ - lib/putty/key/argon2_params.rb
55
70
  - lib/putty/key/error.rb
71
+ - lib/putty/key/libargon2.rb
56
72
  - lib/putty/key/openssl.rb
57
73
  - lib/putty/key/ppk.rb
58
74
  - lib/putty/key/util.rb
59
75
  - lib/putty/key/version.rb
60
76
  - putty-key.gemspec
61
- - test/fixtures/dss-1024-encrypted.ppk
77
+ - test/argon2_params_test.rb
78
+ - test/fixtures/dss-1024-encrypted-format-2.ppk
79
+ - test/fixtures/dss-1024-encrypted-format-3.ppk
80
+ - test/fixtures/dss-1024-format-2.ppk
81
+ - test/fixtures/dss-1024-format-3.ppk
62
82
  - test/fixtures/dss-1024.pem
63
- - test/fixtures/dss-1024.ppk
64
83
  - test/fixtures/ecdsa-secp256k1.pem
65
- - test/fixtures/ecdsa-sha2-nistp256-encrypted.ppk
84
+ - test/fixtures/ecdsa-sha2-nistp256-encrypted-format-2.ppk
85
+ - test/fixtures/ecdsa-sha2-nistp256-encrypted-format-3.ppk
86
+ - test/fixtures/ecdsa-sha2-nistp256-format-2.ppk
87
+ - test/fixtures/ecdsa-sha2-nistp256-format-3.ppk
66
88
  - test/fixtures/ecdsa-sha2-nistp256.pem
67
- - test/fixtures/ecdsa-sha2-nistp256.ppk
68
- - test/fixtures/ecdsa-sha2-nistp384-encrypted.ppk
89
+ - test/fixtures/ecdsa-sha2-nistp384-encrypted-format-2.ppk
90
+ - test/fixtures/ecdsa-sha2-nistp384-encrypted-format-3.ppk
91
+ - test/fixtures/ecdsa-sha2-nistp384-format-2.ppk
92
+ - test/fixtures/ecdsa-sha2-nistp384-format-3.ppk
69
93
  - test/fixtures/ecdsa-sha2-nistp384.pem
70
- - test/fixtures/ecdsa-sha2-nistp384.ppk
71
- - test/fixtures/ecdsa-sha2-nistp521-encrypted.ppk
94
+ - test/fixtures/ecdsa-sha2-nistp521-encrypted-format-2.ppk
95
+ - test/fixtures/ecdsa-sha2-nistp521-encrypted-format-3.ppk
96
+ - test/fixtures/ecdsa-sha2-nistp521-format-2.ppk
97
+ - test/fixtures/ecdsa-sha2-nistp521-format-3.ppk
72
98
  - test/fixtures/ecdsa-sha2-nistp521.pem
73
- - test/fixtures/ecdsa-sha2-nistp521.ppk
74
- - test/fixtures/rsa-2048-encrypted.ppk
99
+ - test/fixtures/rsa-2048-encrypted-format-2.ppk
100
+ - test/fixtures/rsa-2048-encrypted-format-3.ppk
101
+ - test/fixtures/rsa-2048-format-2.ppk
102
+ - test/fixtures/rsa-2048-format-3.ppk
75
103
  - test/fixtures/rsa-2048.pem
76
- - test/fixtures/rsa-2048.ppk
77
104
  - test/fixtures/test-blank-comment.ppk
78
- - test/fixtures/test-encrypted.ppk
105
+ - test/fixtures/test-empty-blobs-encrypted.ppk
106
+ - test/fixtures/test-empty-blobs.ppk
107
+ - test/fixtures/test-encrypted-format-2.ppk
108
+ - test/fixtures/test-encrypted-format-3.ppk
109
+ - test/fixtures/test-encrypted-type-d-format-3.ppk
110
+ - test/fixtures/test-encrypted-type-i-format-3.ppk
111
+ - test/fixtures/test-format-2.ppk
112
+ - test/fixtures/test-format-3.ppk
113
+ - test/fixtures/test-invalid-argon2-memory-for-libargon2.ppk
114
+ - test/fixtures/test-invalid-argon2-memory-maximum.ppk
115
+ - test/fixtures/test-invalid-argon2-memory.ppk
116
+ - test/fixtures/test-invalid-argon2-parallelism-maximum.ppk
117
+ - test/fixtures/test-invalid-argon2-parallelism.ppk
118
+ - test/fixtures/test-invalid-argon2-passes-maximum.ppk
119
+ - test/fixtures/test-invalid-argon2-passes.ppk
120
+ - test/fixtures/test-invalid-argon2-salt.ppk
79
121
  - test/fixtures/test-invalid-blob-lines.ppk
80
122
  - test/fixtures/test-invalid-encryption-type.ppk
81
123
  - test/fixtures/test-invalid-format-1.ppk
82
- - test/fixtures/test-invalid-format-3.ppk
124
+ - test/fixtures/test-invalid-format-4.ppk
125
+ - test/fixtures/test-invalid-key-derivation.ppk
83
126
  - test/fixtures/test-invalid-private-mac.ppk
127
+ - test/fixtures/test-legacy-mac-line-endings.ppk
128
+ - test/fixtures/test-missing-final-line-ending.ppk
84
129
  - test/fixtures/test-truncated.ppk
85
- - test/fixtures/test-unix-line-endings.ppk
86
- - test/fixtures/test.ppk
130
+ - test/fixtures/test-windows-line-endings.ppk
87
131
  - test/openssl_test.rb
88
132
  - test/ppk_test.rb
89
133
  - test/test_helper.rb
@@ -92,7 +136,12 @@ files:
92
136
  homepage: https://github.com/philr/putty-key
93
137
  licenses:
94
138
  - MIT
95
- metadata: {}
139
+ metadata:
140
+ bug_tracker_uri: https://github.com/philr/putty-key/issues
141
+ changelog_uri: https://github.com/philr/putty-key/blob/master/CHANGES.md
142
+ documentation_uri: https://rubydoc.info/gems/putty-key/1.1.1
143
+ homepage_uri: https://github.com/philr/putty-key
144
+ source_code_uri: https://github.com/philr/putty-key/tree/v1.1.1
96
145
  post_install_message:
97
146
  rdoc_options:
98
147
  - "--title"
@@ -113,10 +162,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
162
  - - ">="
114
163
  - !ruby/object:Gem::Version
115
164
  version: '0'
116
- requirements: []
117
- rubygems_version: 3.1.2
165
+ requirements:
166
+ - libargon2 to handle format 3 .ppk files
167
+ rubygems_version: 3.3.7
118
168
  signing_key:
119
169
  specification_version: 4
120
- summary: Reads and writes PuTTY private key (.ppk) files. Refines OpenSSL::PKey to
121
- allow key conversion.
170
+ summary: PuTTY private key (.ppk) library. Supports reading and writing with a refinement
171
+ to OpenSSL::PKey to allow key conversion.
122
172
  test_files: []
metadata.gz.sig CHANGED
Binary file