putty-key 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/CHANGES.md +16 -0
  5. data/Gemfile +0 -6
  6. data/LICENSE +1 -1
  7. data/README.md +32 -6
  8. data/Rakefile +24 -0
  9. data/lib/putty/key.rb +6 -6
  10. data/lib/putty/key/argon2_params.rb +101 -0
  11. data/lib/putty/key/error.rb +17 -0
  12. data/lib/putty/key/libargon2.rb +54 -0
  13. data/lib/putty/key/ppk.rb +469 -103
  14. data/lib/putty/key/util.rb +10 -10
  15. data/lib/putty/key/version.rb +1 -1
  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. metadata +73 -23
  68. 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