rubeepass 3.2.0 → 3.3.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 +4 -4
- data/lib/rubeepass.rb +224 -184
- data/lib/rubeepass/cipher.rb +57 -0
- data/lib/rubeepass/entry.rb +4 -2
- data/lib/rubeepass/error/invalid_header.rb +6 -2
- data/lib/rubeepass/error/not_supported.rb +6 -2
- metadata +25 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ed30495bfd8a7608b35323c3869c6125ab65d823322a2d8c353260a251430ff
|
4
|
+
data.tar.gz: a278f095195446d1d8e892b25e674945f4ed377caf3b56eaa0a3b818e13521b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9386f560553334d3ecacd4d06fa183e1e68ddee4d1353011b928ebc109f8eaad54ffe635c66b713afb9cd5b3493ced91b48f1e6873135848e3c91c85373bf749
|
7
|
+
data.tar.gz: 0fc3c5fdce2ab15331a927e11e59585d2bf3122b6b796c23f345d7e983adfda564acf15b90fcf7d1933a016ee56bf7a638a2db492c66cc10341641870725a16b
|
data/lib/rubeepass.rb
CHANGED
@@ -11,34 +11,48 @@ require "zlib"
|
|
11
11
|
|
12
12
|
class RubeePass
|
13
13
|
# Header fields
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
14
|
+
module Header
|
15
|
+
END_OF_HEADER = 0
|
16
|
+
COMMENT = 1
|
17
|
+
CIPHER_ID = 2
|
18
|
+
COMPRESSION = 3
|
19
|
+
MASTER_SEED = 4
|
20
|
+
TRANSFORM_SEED = 5
|
21
|
+
TRANSFORM_ROUNDS = 6
|
22
|
+
ENCRYPTION_IV = 7
|
23
|
+
PROTECTED_STREAM_KEY = 8
|
24
|
+
STREAM_START_BYTES = 9
|
25
|
+
INNER_RANDOM_STREAM_ID = 10
|
26
|
+
KDF_PARAMETERS = 11
|
27
|
+
PUBLIC_CUSTOM_DATA = 12
|
28
|
+
end
|
29
|
+
|
30
|
+
# Inner header fields
|
31
|
+
module InnerHeader
|
32
|
+
END_OF_HEADER = 0
|
33
|
+
RANDOM_STREAM_ID = 1
|
34
|
+
RANDOM_STREAM_KEY = 2
|
35
|
+
BINARY = 3
|
36
|
+
end
|
25
37
|
|
26
38
|
# Magic values
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
39
|
+
module Magic
|
40
|
+
SIG1 = 0x9aa2d903
|
41
|
+
SIG2 = 0xb54bfb67
|
42
|
+
VERSION3 = 0x00030000
|
43
|
+
VERSION31 = 0x00030001
|
44
|
+
VERSION4 = 0x00040000
|
45
|
+
end
|
46
|
+
|
47
|
+
# Stream algorithm
|
48
|
+
module StreamAlgorithm
|
49
|
+
ARC_FOUR_VARIANT = 1
|
50
|
+
SALSA20 = 2
|
51
|
+
CHACHA20 = 3
|
52
|
+
end
|
38
53
|
|
39
54
|
attr_reader :attachment_decoder
|
40
55
|
attr_reader :db
|
41
|
-
attr_reader :gzip
|
42
56
|
attr_reader :protected_decryptor
|
43
57
|
attr_reader :xml
|
44
58
|
|
@@ -145,43 +159,136 @@ class RubeePass
|
|
145
159
|
end
|
146
160
|
end
|
147
161
|
|
148
|
-
def
|
162
|
+
def decompress(compressed)
|
163
|
+
if (!@header[Header::COMPRESSION])
|
164
|
+
# This feels like a hack
|
165
|
+
m = compressed.read.match(
|
166
|
+
/\<KeePassFile\>.+\<\/KeePassFile\>/m
|
167
|
+
)
|
168
|
+
return m[0] if (m.length > 0)
|
169
|
+
return nil
|
170
|
+
end
|
171
|
+
|
172
|
+
gzip = ""
|
173
|
+
block_id = 0
|
174
|
+
|
175
|
+
loop do
|
176
|
+
# Read block ID
|
177
|
+
data = compressed.read(4)
|
178
|
+
raise Error::InvalidGzip.new if (data.nil?)
|
179
|
+
id = data.unpack("L*")[0]
|
180
|
+
raise Error::InvalidGzip.new if (block_id != id)
|
181
|
+
|
182
|
+
block_id += 1
|
183
|
+
|
184
|
+
# Read expected hash
|
185
|
+
data = compressed.read(32)
|
186
|
+
raise Error::InvalidGzip.new if (data.nil?)
|
187
|
+
expected_hash = data
|
188
|
+
|
189
|
+
# Read size
|
190
|
+
data = compressed.read(4)
|
191
|
+
raise Error::InvalidGzip.new if (data.nil?)
|
192
|
+
size = data.unpack("L*")[0]
|
193
|
+
|
194
|
+
# Break if size is 0 and expected hash is all 0's
|
195
|
+
if (size == 0)
|
196
|
+
expected_hash.each_byte do |byte|
|
197
|
+
raise Error::InvalidGzip.new if (byte != 0)
|
198
|
+
end
|
199
|
+
break
|
200
|
+
end
|
201
|
+
|
202
|
+
# Read data and get actual hash
|
203
|
+
data = compressed.read(size)
|
204
|
+
actual_hash = Digest::SHA256.digest(data)
|
205
|
+
|
206
|
+
# Check that actual hash is same as expected hash
|
207
|
+
if (actual_hash != expected_hash)
|
208
|
+
raise Error::InvalidGzip.new
|
209
|
+
end
|
210
|
+
|
211
|
+
# Append data
|
212
|
+
gzip += data
|
213
|
+
end
|
214
|
+
|
215
|
+
# Unzip gzip data
|
216
|
+
return Zlib::GzipReader.new(StringIO.new(gzip)).read
|
217
|
+
end
|
218
|
+
private :decompress
|
219
|
+
|
220
|
+
def derive_kdf3_key
|
221
|
+
case @version
|
222
|
+
when Magic::VERSION4
|
223
|
+
raise Error::InvalidHeader.new("KDF3 with version 4")
|
224
|
+
end
|
225
|
+
|
149
226
|
irsi = "\x02\x00\x00\x00"
|
150
227
|
if (
|
151
|
-
(header[
|
152
|
-
(header[
|
228
|
+
(@header[Header::MASTER_SEED].length != 32) ||
|
229
|
+
(@header[Header::TRANSFORM_SEED].length != 32)
|
153
230
|
)
|
154
|
-
raise Error::InvalidHeader.new
|
155
|
-
elsif (header[
|
231
|
+
raise Error::InvalidHeader.new("Invalid seed size")
|
232
|
+
elsif (@header[Header::INNER_RANDOM_STREAM_ID] != irsi)
|
156
233
|
raise Error::NotSalsa.new
|
157
234
|
end
|
158
235
|
|
159
236
|
cipher = OpenSSL::Cipher::AES.new(256, :ECB)
|
160
237
|
cipher.encrypt
|
161
|
-
cipher.key = @header[
|
238
|
+
cipher.key = @header[Header::TRANSFORM_SEED]
|
162
239
|
cipher.padding = 0
|
163
240
|
|
164
241
|
key = @initial_key
|
165
|
-
@header[
|
242
|
+
@header[Header::TRANSFORM_ROUNDS].times do
|
166
243
|
key = cipher.update(key) + cipher.final
|
167
244
|
end
|
168
245
|
|
169
246
|
transform_key = Digest::SHA256::digest(key)
|
170
|
-
combined_key = @header[
|
247
|
+
combined_key = @header[Header::MASTER_SEED] + transform_key
|
171
248
|
|
172
|
-
@cipher =
|
173
|
-
|
174
|
-
|
249
|
+
@cipher = Cipher.new(
|
250
|
+
@header[Header::CIPHER_ID],
|
251
|
+
@header[Header::ENCRYPTION_IV],
|
252
|
+
Digest::SHA256::digest(combined_key)
|
253
|
+
)
|
175
254
|
end
|
176
|
-
private :
|
255
|
+
private :derive_kdf3_key
|
256
|
+
|
257
|
+
def derive_kdf4_key(file)
|
258
|
+
case @version
|
259
|
+
when Magic::VERSION3, Magic::VERSION31
|
260
|
+
raise Error::InvalidHeader.new("KDF4 with version 3")
|
261
|
+
end
|
177
262
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
263
|
+
sha = file.read(32)
|
264
|
+
hmac = file.read(32)
|
265
|
+
|
266
|
+
if (sha.nil? || (sha.length != 32))
|
267
|
+
raise Error::InvalidHeader.new("Invalid SHA size")
|
268
|
+
end
|
269
|
+
if (hmac.nil? || (hmac.length != 32))
|
270
|
+
raise Error::InvalidHeader.new("Invalid HMAC size")
|
271
|
+
end
|
272
|
+
|
273
|
+
# TODO check SHA and HMAC (eh, later)
|
274
|
+
|
275
|
+
# TODO implement kdf4 key derivation
|
276
|
+
|
277
|
+
raise Error::NotSupported.new("AES with new KDF")
|
183
278
|
end
|
184
|
-
private :
|
279
|
+
private :derive_kdf4_key
|
280
|
+
|
281
|
+
def derive_key(file)
|
282
|
+
if (
|
283
|
+
@header[Header::TRANSFORM_ROUNDS].nil? ||
|
284
|
+
@header[Header::TRANSFORM_SEED].nil?
|
285
|
+
)
|
286
|
+
derive_kdf4_key(file)
|
287
|
+
else
|
288
|
+
derive_kdf3_key
|
289
|
+
end
|
290
|
+
end
|
291
|
+
private :derive_key
|
185
292
|
|
186
293
|
def export(export_file, format)
|
187
294
|
start_opening
|
@@ -189,18 +296,15 @@ class RubeePass
|
|
189
296
|
File.open(export_file, "w") do |f|
|
190
297
|
case format
|
191
298
|
when "gzip"
|
192
|
-
|
299
|
+
gz = Zlib::GzipWriter.new(f)
|
300
|
+
gz.write(@xml)
|
301
|
+
gz.close
|
193
302
|
when "xml"
|
194
303
|
f.write(@xml)
|
195
304
|
end
|
196
305
|
end
|
197
306
|
end
|
198
307
|
|
199
|
-
def extract_xml
|
200
|
-
@xml = Zlib::GzipReader.new(StringIO.new(@gzip)).read
|
201
|
-
end
|
202
|
-
private :extract_xml
|
203
|
-
|
204
308
|
def find_group(path)
|
205
309
|
return @db.find_group(path)
|
206
310
|
end
|
@@ -221,7 +325,7 @@ class RubeePass
|
|
221
325
|
@password = password
|
222
326
|
|
223
327
|
if (@kdbx.nil?)
|
224
|
-
|
328
|
+
raise RubeePass::Error::FileNotFound.new("null")
|
225
329
|
elsif (!@kdbx.exist?)
|
226
330
|
raise RubeePass::Error::FileNotFound.new(@kdbx)
|
227
331
|
elsif (!@kdbx.readable?)
|
@@ -247,26 +351,11 @@ class RubeePass
|
|
247
351
|
contents = contents.unpack("H*").pack("H*")
|
248
352
|
end
|
249
353
|
if (contents[0..4] == "<?xml")
|
250
|
-
# XML
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
contents.each_line do |line|
|
256
|
-
line.strip!
|
257
|
-
case line
|
258
|
-
when "<KeyFile>"
|
259
|
-
keyfile_line = true
|
260
|
-
when "<Key>"
|
261
|
-
key_line = true
|
262
|
-
when %r{<Data>.*</Data>}
|
263
|
-
data = line.gsub(%r{^<Data>|</Data>$}, "")
|
264
|
-
data = data.unpack("m*")[0]
|
265
|
-
break if (!keyfile_line || !key_line)
|
266
|
-
break if (data.length != 32)
|
267
|
-
filehash = data
|
268
|
-
end
|
269
|
-
end
|
354
|
+
# Parse XML for data
|
355
|
+
doc = REXML::Document.new(contents)
|
356
|
+
data = doc.elements["KeyFile/Key/Data"]
|
357
|
+
raise Error::InvalidXML.new if (data.nil?)
|
358
|
+
filehash = data.text.unpack("m*")[0]
|
270
359
|
elsif (contents.length == 32)
|
271
360
|
# Not XML but a 32 byte Key file
|
272
361
|
filehash = contents
|
@@ -308,7 +397,7 @@ class RubeePass
|
|
308
397
|
|
309
398
|
@protected_decryptor = ProtectedDecryptor.new(
|
310
399
|
Digest::SHA256.digest(
|
311
|
-
@header[
|
400
|
+
@header[Header::PROTECTED_STREAM_KEY]
|
312
401
|
),
|
313
402
|
["E830094B97205D2A"].pack("H*")
|
314
403
|
)
|
@@ -318,79 +407,6 @@ class RubeePass
|
|
318
407
|
return self
|
319
408
|
end
|
320
409
|
|
321
|
-
def parse_gzip(file)
|
322
|
-
gzip = ""
|
323
|
-
block_id = 0
|
324
|
-
|
325
|
-
loop do
|
326
|
-
# Read block ID
|
327
|
-
data = file.read(4)
|
328
|
-
raise Error::InvalidGzip.new if (data.nil?)
|
329
|
-
id = data.unpack("L*")[0]
|
330
|
-
raise Error::InvalidGzip.new if (block_id != id)
|
331
|
-
|
332
|
-
block_id += 1
|
333
|
-
|
334
|
-
# Read expected hash
|
335
|
-
data = file.read(32)
|
336
|
-
raise Error::InvalidGzip.new if (data.nil?)
|
337
|
-
expected_hash = data
|
338
|
-
|
339
|
-
# Read size
|
340
|
-
data = file.read(4)
|
341
|
-
raise Error::InvalidGzip.new if (data.nil?)
|
342
|
-
size = data.unpack("L*")[0]
|
343
|
-
|
344
|
-
# Break is size is 0 and expected hash is all 0's
|
345
|
-
if (size == 0)
|
346
|
-
expected_hash.each_byte do |byte|
|
347
|
-
raise Error::InvalidGzip.new if (byte != 0)
|
348
|
-
end
|
349
|
-
break
|
350
|
-
end
|
351
|
-
|
352
|
-
# Read data and get actual hash
|
353
|
-
data = file.read(size)
|
354
|
-
actual_hash = Digest::SHA256.digest(data)
|
355
|
-
|
356
|
-
# Check that actual hash is same as expected hash
|
357
|
-
if (actual_hash != expected_hash)
|
358
|
-
raise Error::InvalidGzip.new
|
359
|
-
end
|
360
|
-
|
361
|
-
# Append data
|
362
|
-
gzip += data
|
363
|
-
end
|
364
|
-
|
365
|
-
return gzip
|
366
|
-
end
|
367
|
-
private :parse_gzip
|
368
|
-
|
369
|
-
def parse_header(header)
|
370
|
-
case header[@@CIPHER_ID].unpack("H*")[0]
|
371
|
-
when @@AES_AESKDF3
|
372
|
-
derive_aeskdf3_key(header)
|
373
|
-
when @@AES_AESKDF4_OR_ARGON2
|
374
|
-
derive_aeskdf4_or_argon2_key(header)
|
375
|
-
when @@CHACHA20_AESKDF3
|
376
|
-
# puts "ChaCha20 with AES-KDF3"
|
377
|
-
raise Error::NotSupported.new # TODO
|
378
|
-
when @@CHACHA20_AESKDF4_OR_ARGON2
|
379
|
-
# puts "ChaCha20 with AES-KDF4 or Argon2"
|
380
|
-
raise Error::NotSupported.new # TODO
|
381
|
-
when @@TWOFISH_AESKDF3
|
382
|
-
# puts "Twofish with AES-KDF3"
|
383
|
-
raise Error::NotSupported.new # TODO
|
384
|
-
when @@TWOFISH_AESKDF4_OR_ARGON2
|
385
|
-
# puts "Twofish with AES-KDF4 or Argon2"
|
386
|
-
raise Error::NotSupported.new # TODO
|
387
|
-
else
|
388
|
-
# puts header[@@CIPHER_ID].unpack("H*")[0]
|
389
|
-
raise Error::NotSupported.new
|
390
|
-
end
|
391
|
-
end
|
392
|
-
private :parse_header
|
393
|
-
|
394
410
|
def parse_xml
|
395
411
|
doc = REXML::Document.new(@xml)
|
396
412
|
if (doc.elements["KeePassFile/Root"].nil?)
|
@@ -406,30 +422,6 @@ class RubeePass
|
|
406
422
|
end
|
407
423
|
private :parse_xml
|
408
424
|
|
409
|
-
def read_gzip(file)
|
410
|
-
cipher = @cipher.clone
|
411
|
-
cipher.decrypt
|
412
|
-
cipher.key = @key
|
413
|
-
cipher.iv = @iv
|
414
|
-
|
415
|
-
encrypted = file.read
|
416
|
-
|
417
|
-
begin
|
418
|
-
data = StringIO.new(
|
419
|
-
cipher.update(encrypted) + cipher.final
|
420
|
-
)
|
421
|
-
rescue OpenSSL::Cipher::CipherError
|
422
|
-
raise Error::InvalidPassword.new
|
423
|
-
end
|
424
|
-
|
425
|
-
if (data.read(32) != @header[@@STREAM_START_BYTES])
|
426
|
-
raise Error::InvalidPassword.new
|
427
|
-
end
|
428
|
-
|
429
|
-
@gzip = parse_gzip(data)
|
430
|
-
end
|
431
|
-
private :read_gzip
|
432
|
-
|
433
425
|
def read_header(file)
|
434
426
|
header = Hash.new
|
435
427
|
loop do
|
@@ -437,7 +429,14 @@ class RubeePass
|
|
437
429
|
break if (data.nil?)
|
438
430
|
id = data.unpack("C*")[0]
|
439
431
|
|
440
|
-
|
432
|
+
case @version
|
433
|
+
when Magic::VERSION3, Magic::VERSION31
|
434
|
+
data = file.read(2)
|
435
|
+
when Magic::VERSION4
|
436
|
+
data = file.read(4)
|
437
|
+
else
|
438
|
+
raise Error::InvalidHeader.new
|
439
|
+
end
|
441
440
|
raise Error::InvalidHeader.new if (data.nil?)
|
442
441
|
size = data.unpack("S*")[0]
|
443
442
|
|
@@ -447,11 +446,40 @@ class RubeePass
|
|
447
446
|
end
|
448
447
|
|
449
448
|
case id
|
450
|
-
when
|
449
|
+
when Header::CIPHER_ID
|
450
|
+
header[id] = data.unpack("H*")[0]
|
451
|
+
when Header::COMPRESSION
|
452
|
+
header[id] = (data.unpack("L*")[0] > 0)
|
453
|
+
when Header::END_OF_HEADER
|
451
454
|
break
|
452
|
-
when
|
455
|
+
when Header::KDF_PARAMETERS
|
456
|
+
case @version
|
457
|
+
when Magic::VERSION3, Magic::VERSION31
|
458
|
+
raise Error::InvalidHeader.new
|
459
|
+
end
|
460
|
+
# raise Error::NotSupported.new("Custom KDF params")
|
461
|
+
when Header::PUBLIC_CUSTOM_DATA
|
462
|
+
case @version
|
463
|
+
when Magic::VERSION3, Magic::VERSION31
|
464
|
+
raise Error::InvalidHeader.new
|
465
|
+
end
|
466
|
+
raise Error::NotSupported.new("Public custom data")
|
467
|
+
when Header::TRANSFORM_ROUNDS
|
453
468
|
header[id] = data.unpack("Q*")[0]
|
454
469
|
else
|
470
|
+
case @version
|
471
|
+
when Magic::VERSION4
|
472
|
+
case id
|
473
|
+
when Header::INNER_RANDOM_STREAM_ID,
|
474
|
+
Header::PROTECTED_STREAM_KEY,
|
475
|
+
Header::STREAM_START_BYTES,
|
476
|
+
Header::TRANSFORM_ROUNDS,
|
477
|
+
Header::TRANSFORM_SEED
|
478
|
+
raise Error::InvalidHeader.new(
|
479
|
+
"Legacy header ID"
|
480
|
+
)
|
481
|
+
end
|
482
|
+
end
|
455
483
|
header[id] = data
|
456
484
|
end
|
457
485
|
end
|
@@ -463,43 +491,54 @@ class RubeePass
|
|
463
491
|
def read_magic_and_version(file)
|
464
492
|
data = file.read(4)
|
465
493
|
raise Error::InvalidMagic.new if (data.nil?)
|
466
|
-
sig1 = data.unpack("L*")[0]
|
467
|
-
raise Error::InvalidMagic.new if (sig1 !=
|
494
|
+
@sig1 = data.unpack("L*")[0]
|
495
|
+
# raise Error::InvalidMagic.new if (@sig1 != Magic::SIG1)
|
468
496
|
|
469
497
|
data = file.read(4)
|
470
498
|
raise Error::InvalidMagic.new if (data.nil?)
|
471
|
-
sig2 = data.unpack("L*")[0]
|
472
|
-
raise Error::InvalidMagic.new if (sig2 !=
|
499
|
+
@sig2 = data.unpack("L*")[0]
|
500
|
+
# raise Error::InvalidMagic.new if (@sig2 != Magic::SIG2)
|
473
501
|
|
474
502
|
data = file.read(4)
|
475
503
|
raise Error::InvalidVersion.new if (data.nil?)
|
476
|
-
|
477
|
-
|
478
|
-
|
504
|
+
@version = data.unpack("L*")[0]
|
505
|
+
case @version
|
506
|
+
when Magic::VERSION3, Magic::VERSION31, Magic::VERSION4
|
507
|
+
else
|
508
|
+
raise Error::InvalidVersion.new
|
479
509
|
end
|
480
510
|
end
|
481
511
|
private :read_magic_and_version
|
482
512
|
|
483
513
|
def start_opening
|
514
|
+
@cipher = nil
|
484
515
|
@db = nil
|
485
|
-
@gzip = nil
|
486
516
|
@header = nil
|
487
517
|
@initial_key = nil
|
488
|
-
@
|
489
|
-
@
|
518
|
+
@sig1 = nil
|
519
|
+
@sig2 = nil
|
520
|
+
@version = nil
|
490
521
|
@xml = nil
|
491
522
|
|
492
523
|
file = File.open(@kdbx)
|
493
524
|
|
525
|
+
# Read metadata and derive key
|
494
526
|
read_magic_and_version(file)
|
495
|
-
|
527
|
+
read_header(file)
|
496
528
|
join_key_and_keyfile
|
497
|
-
|
498
|
-
read_gzip(file)
|
529
|
+
derive_key(file)
|
499
530
|
|
500
|
-
file
|
531
|
+
# Decrypt file
|
532
|
+
encrypted = file.read
|
533
|
+
decrypted = @cipher.decrypt(encrypted)
|
534
|
+
if (decrypted.read(32) != @header[Header::STREAM_START_BYTES])
|
535
|
+
raise Error::InvalidPassword.new
|
536
|
+
end
|
501
537
|
|
502
|
-
|
538
|
+
# Decompress (if necessary)
|
539
|
+
@xml = decompress(decrypted)
|
540
|
+
|
541
|
+
file.close
|
503
542
|
end
|
504
543
|
private :start_opening
|
505
544
|
|
@@ -518,6 +557,7 @@ class RubeePass
|
|
518
557
|
end
|
519
558
|
|
520
559
|
require "rubeepass/attachment_decoder"
|
560
|
+
require "rubeepass/cipher"
|
521
561
|
require "rubeepass/entry"
|
522
562
|
require "rubeepass/error"
|
523
563
|
require "rubeepass/group"
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "openssl"
|
2
|
+
require "twofish"
|
3
|
+
|
4
|
+
class RubeePass::Cipher
|
5
|
+
# Encryption schemes
|
6
|
+
module ID
|
7
|
+
AES = "31c1f2e6bf714350be5805216afc5aff"
|
8
|
+
CHACHA20 = "d6038a2b8b6f4cb5a524339a31dbb59a"
|
9
|
+
TWOFISH = "ad68f29f576f4bb9a36ad47af965346c"
|
10
|
+
end
|
11
|
+
|
12
|
+
def decrypt(enc)
|
13
|
+
# Setup
|
14
|
+
case @id
|
15
|
+
when ID::AES
|
16
|
+
cipher = OpenSSL::Cipher::AES.new(256, :CBC)
|
17
|
+
when ID::CHACHA20
|
18
|
+
cipher = OpenSSL::Cipher.new("chacha20")
|
19
|
+
when ID::TWOFISH
|
20
|
+
cipher = Twofish.new(
|
21
|
+
@key,
|
22
|
+
{
|
23
|
+
:iv => @iv,
|
24
|
+
:mode => :cbc,
|
25
|
+
:padding => :none
|
26
|
+
}
|
27
|
+
)
|
28
|
+
else
|
29
|
+
raise RubeePass::Error::NotSupported.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# Decrypt
|
33
|
+
case @id
|
34
|
+
when ID::AES, ID::CHACHA20
|
35
|
+
begin
|
36
|
+
cipher.decrypt
|
37
|
+
cipher.key = @key
|
38
|
+
cipher.iv = @iv
|
39
|
+
return StringIO.new(cipher.update(enc) + cipher.final)
|
40
|
+
rescue OpenSSL::Cipher::CipherError
|
41
|
+
raise RubeePass::Error::InvalidPassword.new
|
42
|
+
end
|
43
|
+
when ID::TWOFISH
|
44
|
+
begin
|
45
|
+
return StringIO.new(cipher.decrypt(enc))
|
46
|
+
rescue ArgumentError
|
47
|
+
raise RubeePass::Error::InvalidPassword.new
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(id, iv, key)
|
53
|
+
@id = id
|
54
|
+
@iv = iv
|
55
|
+
@key = key
|
56
|
+
end
|
57
|
+
end
|
data/lib/rubeepass/entry.rb
CHANGED
@@ -154,9 +154,11 @@ class RubeePass::Entry
|
|
154
154
|
data = data.unpack("H*").pack("H*")
|
155
155
|
end
|
156
156
|
rescue ArgumentError
|
157
|
-
raise Error::InvalidProtectedData.new
|
157
|
+
raise RubeePass::Error::InvalidProtectedData.new
|
158
|
+
end
|
159
|
+
if (data.nil?)
|
160
|
+
raise RubeePass::Error::InvalidProtectedData.new
|
158
161
|
end
|
159
|
-
raise Error::InvalidProtectedData.new if (data.nil?)
|
160
162
|
|
161
163
|
return keepass.protected_decryptor.add_to_stream(data)
|
162
164
|
end
|
@@ -1,5 +1,9 @@
|
|
1
1
|
class RubeePass::Error::NotSupported < RubeePass::Error
|
2
|
-
def initialize
|
3
|
-
|
2
|
+
def initialize(msg = nil)
|
3
|
+
if (msg.nil?)
|
4
|
+
super("Encryption scheme not currently supported")
|
5
|
+
else
|
6
|
+
super("Encryption scheme not currently supported: #{msg}")
|
7
|
+
end
|
4
8
|
end
|
5
9
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubeepass
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miles Whittaker
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-09-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -139,7 +139,7 @@ dependencies:
|
|
139
139
|
version: '0.1'
|
140
140
|
- - ">="
|
141
141
|
- !ruby/object:Gem::Version
|
142
|
-
version: 0.1.
|
142
|
+
version: 0.1.3
|
143
143
|
type: :runtime
|
144
144
|
prerelease: false
|
145
145
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -149,7 +149,7 @@ dependencies:
|
|
149
149
|
version: '0.1'
|
150
150
|
- - ">="
|
151
151
|
- !ruby/object:Gem::Version
|
152
|
-
version: 0.1.
|
152
|
+
version: 0.1.3
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
154
|
name: scoobydoo
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -170,6 +170,26 @@ dependencies:
|
|
170
170
|
- - ">="
|
171
171
|
- !ruby/object:Gem::Version
|
172
172
|
version: 0.1.6
|
173
|
+
- !ruby/object:Gem::Dependency
|
174
|
+
name: twofish
|
175
|
+
requirement: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - "~>"
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '1.0'
|
180
|
+
- - ">="
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: 1.0.8
|
183
|
+
type: :runtime
|
184
|
+
prerelease: false
|
185
|
+
version_requirements: !ruby/object:Gem::Requirement
|
186
|
+
requirements:
|
187
|
+
- - "~>"
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '1.0'
|
190
|
+
- - ">="
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
version: 1.0.8
|
173
193
|
description: Ruby KeePass 2.x client. Currently it is read-only.
|
174
194
|
email: mjwhitta@gmail.com
|
175
195
|
executables:
|
@@ -180,6 +200,7 @@ files:
|
|
180
200
|
- bin/rpass
|
181
201
|
- lib/rubeepass.rb
|
182
202
|
- lib/rubeepass/attachment_decoder.rb
|
203
|
+
- lib/rubeepass/cipher.rb
|
183
204
|
- lib/rubeepass/entry.rb
|
184
205
|
- lib/rubeepass/error.rb
|
185
206
|
- lib/rubeepass/error/file_not_found.rb
|