kdbx 0.1.0 → 0.2.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/README.md +30 -6
- data/lib/kdbx.rb +34 -26
- data/lib/kdbx/attributes.rb +73 -68
- data/lib/kdbx/crypto.rb +130 -0
- data/lib/kdbx/exception.rb +6 -0
- data/lib/kdbx/header.rb +58 -41
- data/lib/kdbx/version.rb +1 -1
- metadata +19 -5
- data/lib/kdbx/encryption.rb +0 -62
- data/lib/kdbx/wrapper.rb +0 -65
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a7bc07ea8275bb2ada03a4cc9da04d76d7cbee79
|
4
|
+
data.tar.gz: 7d086b8150446b8bdfdfb41a746c9086fc5ce873
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 947542790231a62649f280adb0d4f8e990ec3940c9c6415e123abe99cff67d0b6976c0984987620fbb557bfd9253a253c25761b307b7badcd39d2c5403b55b49
|
7
|
+
data.tar.gz: e7c3dc8d5eb254530406bece0cc5573e6c6f03927bd8bd72052f295a1b35d4e3b9d036646d597ca0ebe79e061df3170a47900ec2d42af8743439059bbcd1682b
|
data/README.md
CHANGED
@@ -1,18 +1,42 @@
|
|
1
|
-
#
|
1
|
+
# Kdbx.rb
|
2
2
|
|
3
|
-
|
3
|
+
[](https://travis-ci.org/rumtid/kdbx.rb)
|
4
|
+
[](https://codeclimate.com/github/rumtid/kdbx.rb)
|
5
|
+
[](https://badge.fury.io/rb/kdbx)
|
6
|
+
|
7
|
+
A library for accessing [KeePass](http://keepass.info/) database (v2+), aka kdbx format file.
|
8
|
+
|
9
|
+
## Capability
|
10
|
+
|
11
|
+
- [x] Read/Write kdbx (v2) file.
|
12
|
+
- [x] Change keys and headers.
|
13
|
+
- [ ] Support kdbx (v4) file.
|
4
14
|
|
5
15
|
## Installation
|
6
16
|
|
7
|
-
|
8
|
-
|
17
|
+
$ gem install kdbx
|
18
|
+
|
19
|
+
## Examples
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
# Open existing kdbx file
|
23
|
+
kdbx = Kdbx.open("demo.kdbx", password: "password", keyfile: "demo.key")
|
24
|
+
|
25
|
+
# Read contents
|
26
|
+
puts kdbx.content
|
27
|
+
|
28
|
+
# Change password
|
29
|
+
kdbx.password = "foobar"
|
30
|
+
|
31
|
+
# Save
|
32
|
+
kdbx.save
|
9
33
|
```
|
10
34
|
|
11
35
|
## Development
|
12
36
|
|
13
|
-
|
37
|
+
First, install dependencies: `bundle install`
|
14
38
|
|
15
|
-
|
39
|
+
Then run tests: `rspec`
|
16
40
|
|
17
41
|
## License
|
18
42
|
|
data/lib/kdbx.rb
CHANGED
@@ -1,39 +1,47 @@
|
|
1
|
-
require "kdbx/version"
|
2
1
|
require "kdbx/attributes"
|
3
|
-
require "kdbx/encryption"
|
4
|
-
require "kdbx/wrapper"
|
5
2
|
require "kdbx/header"
|
3
|
+
require "kdbx/crypto"
|
4
|
+
require "kdbx/version"
|
6
5
|
|
7
6
|
class Kdbx
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
@header = Header.new
|
15
|
-
@content = String.new
|
16
|
-
else
|
17
|
-
self.filename = filename
|
18
|
-
self.password = opts[:password] if opts.has_key? :password
|
19
|
-
self.keyfile = opts[:keyfile] if opts.has_key? :keyfile
|
20
|
-
load
|
7
|
+
def self.open(filename, **options)
|
8
|
+
new(**options).tap do |kdbx|
|
9
|
+
File.open filename, "rb" do |file|
|
10
|
+
kdbx.header = Header.load file
|
11
|
+
kdbx.decrypt_content file.read
|
12
|
+
end
|
21
13
|
end
|
22
14
|
end
|
23
15
|
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
|
16
|
+
def initialize(**options)
|
17
|
+
@password = @keyfile = nil
|
18
|
+
@header, @content = Header.new, String.new
|
19
|
+
self.password = options[:password] if options.has_key? :password
|
20
|
+
self.keyfile = options[:keyfile] if options.has_key? :keyfile
|
21
|
+
end
|
22
|
+
|
23
|
+
def save(filename)
|
24
|
+
secure_write filename do |file|
|
25
|
+
file.write header.dump
|
26
|
+
file.write encrypt_content
|
28
27
|
end
|
29
|
-
|
28
|
+
true
|
30
29
|
end
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
31
|
+
private
|
32
|
+
|
33
|
+
def secure_write(name)
|
34
|
+
name = File.absolute_path name
|
35
|
+
index = -1 - File.extname(name).length
|
36
|
+
temp = 1.step do |i|
|
37
|
+
t = name.dup.insert index, ".#{i}"
|
38
|
+
break t unless File.exist? t
|
39
|
+
end
|
40
|
+
begin
|
41
|
+
File.open(temp, "wb") { |file| yield file }
|
42
|
+
File.rename temp, name
|
43
|
+
ensure
|
44
|
+
File.delete temp if File.exist? temp
|
36
45
|
end
|
37
|
-
self
|
38
46
|
end
|
39
47
|
end
|
data/lib/kdbx/attributes.rb
CHANGED
@@ -1,90 +1,95 @@
|
|
1
1
|
require "base64"
|
2
|
+
require "rexml/document"
|
2
3
|
|
3
4
|
class Kdbx
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
end
|
5
|
+
attr_reader :password
|
6
|
+
def password=(str)
|
7
|
+
@password = str == nil ? "" : sha256(str)
|
8
|
+
end
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
attr_reader :keyfile
|
11
|
+
def keyfile=(str)
|
12
|
+
@keyfile = File.absolute_path str
|
13
|
+
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
def credential
|
16
|
+
cred = password || String.new
|
17
|
+
return cred if keyfile == nil
|
18
|
+
data = IO.read keyfile
|
19
|
+
if !data.valid_encoding?
|
20
|
+
return cred + sha256(data)
|
18
21
|
end
|
19
|
-
|
20
|
-
|
21
|
-
secrets = String.new
|
22
|
-
secrets << password if password
|
23
|
-
if keyfile
|
24
|
-
data = File.read keyfile
|
25
|
-
if [32, 64].include? data.bytesize
|
26
|
-
secrets << data
|
27
|
-
else
|
28
|
-
begin
|
29
|
-
doc = REXML::Document.new data
|
30
|
-
ele = doc.elements["/KeyFile/Key/Data"]
|
31
|
-
secrets << Base64.decode64(ele.text)
|
32
|
-
rescue REXML::ParseException
|
33
|
-
secrets << sha256(data)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
secrets
|
22
|
+
if data.bytesize == 32
|
23
|
+
return cred + data
|
38
24
|
end
|
39
|
-
|
40
|
-
|
41
|
-
|
25
|
+
if data =~ /\A\h{64}\z/
|
26
|
+
data = [data].pack("H*")
|
27
|
+
return cred + data
|
42
28
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
29
|
+
begin
|
30
|
+
xpath = "/KeyFile/Key/Data"
|
31
|
+
tnd = REXML::Document.new(data).get_text(xpath)
|
32
|
+
cred + Base64.decode64(tnd.to_s)
|
33
|
+
rescue REXML::ParseException
|
34
|
+
cred + sha256(data)
|
46
35
|
end
|
36
|
+
end
|
47
37
|
|
48
|
-
|
49
|
-
compressionflags == 1
|
50
|
-
end
|
38
|
+
attr_accessor :header
|
51
39
|
|
52
|
-
|
53
|
-
|
54
|
-
|
40
|
+
def compressionflags
|
41
|
+
@header[3].unpack("L").first
|
42
|
+
end
|
55
43
|
|
56
|
-
|
57
|
-
|
58
|
-
|
44
|
+
def compressionflags=(flag)
|
45
|
+
@header[3] = [flag].pack("L")
|
46
|
+
end
|
59
47
|
|
60
|
-
|
61
|
-
|
62
|
-
|
48
|
+
def masterseed
|
49
|
+
@header[4]
|
50
|
+
end
|
63
51
|
|
64
|
-
|
65
|
-
|
66
|
-
|
52
|
+
def transformseed
|
53
|
+
@header[5]
|
54
|
+
end
|
67
55
|
|
68
|
-
|
69
|
-
|
70
|
-
|
56
|
+
def transformrounds
|
57
|
+
@header[6].unpack("Q").first
|
58
|
+
end
|
71
59
|
|
72
|
-
|
73
|
-
|
74
|
-
|
60
|
+
def transformrounds=(num)
|
61
|
+
@header[6] = [num].pack("Q")
|
62
|
+
end
|
75
63
|
|
76
|
-
|
77
|
-
|
78
|
-
|
64
|
+
def encryptioniv
|
65
|
+
@header[7]
|
66
|
+
end
|
79
67
|
|
80
|
-
|
81
|
-
|
82
|
-
|
68
|
+
def protectedstreamkey
|
69
|
+
@header[8]
|
70
|
+
end
|
83
71
|
|
84
|
-
|
85
|
-
|
86
|
-
|
72
|
+
def streamstartbytes
|
73
|
+
@header[9]
|
74
|
+
end
|
75
|
+
|
76
|
+
def innerrandomstreamid
|
77
|
+
@header[10].unpack("L").first
|
78
|
+
end
|
79
|
+
|
80
|
+
def innerrandomstreamid=(id)
|
81
|
+
@header[10] = [id].pack("L")
|
82
|
+
end
|
83
|
+
|
84
|
+
attr_accessor :content
|
85
|
+
|
86
|
+
def inspect
|
87
|
+
super
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
87
91
|
|
88
|
-
|
92
|
+
def nonce
|
93
|
+
"\xE8\x30\x09\x4B\x97\x20\x5D\x2A".b
|
89
94
|
end
|
90
95
|
end
|
data/lib/kdbx/crypto.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require "zlib"
|
2
|
+
require "base64"
|
3
|
+
require "openssl"
|
4
|
+
require "salsa20"
|
5
|
+
require "rexml/document"
|
6
|
+
|
7
|
+
class Kdbx
|
8
|
+
def encrypt_content
|
9
|
+
data = @content.to_s
|
10
|
+
data = obfuscate data if innerrandomstreamid == 2
|
11
|
+
data = gzip data if compressionflags == 1
|
12
|
+
encrypt streamstartbytes + encode(data)
|
13
|
+
end
|
14
|
+
|
15
|
+
def decrypt_content(data)
|
16
|
+
data = decrypt data.to_s
|
17
|
+
if data.start_with? streamstartbytes
|
18
|
+
size = streamstartbytes.bytesize
|
19
|
+
data = data.byteslice size..-1
|
20
|
+
else
|
21
|
+
fail KeyError, "wrong password or keyfile"
|
22
|
+
end
|
23
|
+
data = decode data
|
24
|
+
data = gunzip data if compressionflags == 1
|
25
|
+
data = reverse data if innerrandomstreamid == 2
|
26
|
+
@content = data
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def sha256(data)
|
32
|
+
OpenSSL::Digest::SHA256.digest data
|
33
|
+
end
|
34
|
+
|
35
|
+
def masterkey
|
36
|
+
cipher = OpenSSL::Cipher.new("AES-256-ECB").encrypt
|
37
|
+
cipher.key, key = transformseed, sha256(credential)
|
38
|
+
transformrounds.times { key = cipher.update key }
|
39
|
+
sha256(masterseed + sha256(key))
|
40
|
+
end
|
41
|
+
|
42
|
+
def encrypt(data)
|
43
|
+
cipher = OpenSSL::Cipher.new("AES-256-CBC").encrypt
|
44
|
+
cipher.iv, cipher.key = encryptioniv, masterkey
|
45
|
+
cipher.update(data) + cipher.final
|
46
|
+
end
|
47
|
+
|
48
|
+
def decrypt(data)
|
49
|
+
cipher = OpenSSL::Cipher.new("AES-256-CBC").decrypt
|
50
|
+
cipher.iv, cipher.key = encryptioniv, masterkey
|
51
|
+
cipher.update(data) + cipher.final
|
52
|
+
rescue OpenSSL::Cipher::CipherError
|
53
|
+
fail KeyError, "wrong password or keyfile"
|
54
|
+
end
|
55
|
+
|
56
|
+
def encode(data)
|
57
|
+
StringIO.new.binmode.tap do |io|
|
58
|
+
io.write "\x00" * 4 + sha256(data)
|
59
|
+
io.write [data.bytesize].pack("L<")
|
60
|
+
io.write data + "\x01" + "\x00" * 39
|
61
|
+
end.string
|
62
|
+
end
|
63
|
+
|
64
|
+
def decode(data)
|
65
|
+
io = StringIO.new.binmode
|
66
|
+
dt = StringIO.new data
|
67
|
+
loop do
|
68
|
+
t = dt.readpartial 40
|
69
|
+
(hash, size) = t.unpack("x4a32L<")
|
70
|
+
break io.string if size == 0
|
71
|
+
block = dt.readpartial size
|
72
|
+
if sha256(block) != hash
|
73
|
+
fail "broken file"
|
74
|
+
else
|
75
|
+
io.write block
|
76
|
+
end
|
77
|
+
end
|
78
|
+
rescue TypeError, EOFError
|
79
|
+
fail ParseError, "truncated payload"
|
80
|
+
end
|
81
|
+
|
82
|
+
def gzip(data)
|
83
|
+
StringIO.open do |io|
|
84
|
+
gz = Zlib::GzipWriter.new io.binmode
|
85
|
+
gz.write data; gz.close; io.string
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def gunzip(data)
|
90
|
+
StringIO.open data do |io|
|
91
|
+
gz = Zlib::GzipReader.new io
|
92
|
+
[gz.read, gz.close].first
|
93
|
+
end
|
94
|
+
rescue Zlib::GzipFile::Error => e
|
95
|
+
fail ParseError, e.message
|
96
|
+
end
|
97
|
+
|
98
|
+
def sequence
|
99
|
+
cipher = Salsa20.new sha256(protectedstreamkey), nonce
|
100
|
+
Enumerator.new do |e|
|
101
|
+
loop { cipher.encrypt("\x00" * 64).each_byte { |b| e << b } }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def obfuscate(data)
|
106
|
+
xpath = "//Value[@Protected='True']"
|
107
|
+
doc, seq = REXML::Document.new(data), sequence
|
108
|
+
doc.each_element xpath do |ele|
|
109
|
+
t = ele.texts.join.bytes
|
110
|
+
t.map! { |b| b ^ seq.next }
|
111
|
+
t = Base64.encode64 t.pack("C*")
|
112
|
+
ele.text = t.strip
|
113
|
+
end
|
114
|
+
doc.to_s
|
115
|
+
rescue REXML::ParseException => e
|
116
|
+
fail FormatError, e.message
|
117
|
+
end
|
118
|
+
|
119
|
+
def reverse(data)
|
120
|
+
xpath = "//Value[@Protected='True']"
|
121
|
+
doc, seq = REXML::Document.new(data), sequence
|
122
|
+
doc.each_element xpath do |ele|
|
123
|
+
t = Base64.decode64(ele.texts.join).bytes
|
124
|
+
ele.text = t.map! { |b| b ^ seq.next }.pack("C*")
|
125
|
+
end
|
126
|
+
doc.to_s
|
127
|
+
rescue REXML::ParseException => e
|
128
|
+
fail ParseError, e.message
|
129
|
+
end
|
130
|
+
end
|
data/lib/kdbx/header.rb
CHANGED
@@ -2,63 +2,79 @@ require "openssl"
|
|
2
2
|
require "forwardable"
|
3
3
|
|
4
4
|
class Kdbx::Header
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
5
|
+
FILEMAGIC = "\x03\xD9\xA2\x9A\x67\xFB\x4B\xB5\x01\x00\x03\x00".b
|
6
|
+
|
7
|
+
def self.load(file)
|
8
|
+
if file.readpartial(12) != FILEMAGIC
|
9
|
+
fail ParseError, "bad magic number"
|
10
|
+
end
|
11
|
+
fields = {}
|
11
12
|
loop do
|
12
|
-
id =
|
13
|
-
|
14
|
-
sz = sz.unpack("S").first
|
15
|
-
fields[id] = stream.readpartial sz
|
13
|
+
(id, sz) = file.readpartial(3).unpack("CS<")
|
14
|
+
fields[id] = file.readpartial sz
|
16
15
|
break if id == 0
|
17
16
|
end
|
18
|
-
new
|
17
|
+
new fields
|
18
|
+
rescue TypeError, EOFError
|
19
|
+
fail ParseError, "truncated header"
|
19
20
|
end
|
20
21
|
|
21
22
|
extend Forwardable
|
22
|
-
def_delegators :@fields, :[], :[]
|
23
|
-
|
24
|
-
def initialize
|
25
|
-
@fields = {}
|
26
|
-
end
|
27
|
-
|
28
|
-
def initialize_copy(other)
|
29
|
-
super
|
30
|
-
@fields = other.instance_variable_get(:@fields).clone
|
31
|
-
end
|
23
|
+
def_delegators :@fields, :[], :[]=
|
32
24
|
|
33
|
-
def
|
34
|
-
|
25
|
+
def initialize(fields = {})
|
26
|
+
@fields = fields
|
27
|
+
validate
|
35
28
|
end
|
36
29
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
30
|
+
def dump
|
31
|
+
merge_defaults and validate
|
32
|
+
StringIO.new.binmode.tap do |io|
|
33
|
+
io.write FILEMAGIC
|
34
|
+
@fields.each do |k, v|
|
35
|
+
io.write [k, v.bytesize].pack("CS<") + v if k != 0
|
36
|
+
end
|
37
|
+
io.write [0, @fields[0].bytesize].pack("CS<") + @fields[0]
|
38
|
+
end.string
|
40
39
|
end
|
41
40
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
41
|
+
def validate
|
42
|
+
@fields.each do |k, v|
|
43
|
+
fail FormatError, "header #{k.inspect}: #{v}" unless k.is_a? Integer
|
44
|
+
fail FormatError, "header #{k}: #{v.inspect}" unless v.is_a? String
|
45
|
+
@fields[k] = v = v.b unless v.encoding == Encoding::ASCII_8BIT
|
46
|
+
case k
|
47
|
+
when 2
|
48
|
+
if v != "\x31\xC1\xF2\xE6\xBF\x71\x43\x50\xBE\x58\x05\x21\x6A\xFC\x5A\xFF".b
|
49
|
+
fail FormatError, "header #{k}: #{v.inspect}"
|
50
|
+
end
|
51
|
+
when 3
|
52
|
+
if v.bytesize != 4 || !(0..1).include?(v.unpack("L").first)
|
53
|
+
fail FormatError, "header #{k}: #{v.inspect}"
|
54
|
+
end
|
55
|
+
when 4, 5
|
56
|
+
fail FormatError, "header #{k}: #{v.inspect}" if v.bytesize != 32
|
57
|
+
when 6
|
58
|
+
fail FormatError, "header #{k}: #{v.inspect}" if v.bytesize != 8
|
59
|
+
when 7
|
60
|
+
fail FormatError, "header #{k}: #{v.inspect}" if v.bytesize != 16
|
61
|
+
when 8
|
62
|
+
if @fields[10] == "\x02\x00\x00\x00".b && v.bytesize != 32
|
63
|
+
fail FormatError, "header #{k}: #{v.inspect}"
|
64
|
+
end
|
65
|
+
when 10
|
66
|
+
fail FormatError, "header #{k}: #{v.inspect}" if v.bytesize != 4
|
67
|
+
if (n = v.unpack("L<").first) != 0 && n != 2
|
68
|
+
fail FormatError, "header #{k}: #{v.inspect}"
|
69
|
+
end
|
70
|
+
end
|
50
71
|
end
|
51
|
-
val = @fields.fetch 0
|
52
|
-
stream.write [0, val.bytesize].pack("CS") + val
|
53
72
|
end
|
54
73
|
|
55
74
|
private
|
56
75
|
|
57
|
-
def
|
76
|
+
def merge_defaults
|
58
77
|
@fields.merge!({
|
59
|
-
:pid => "\x03\xD9\xA2\x9A",
|
60
|
-
:sid => "\x67\xFB\x4B\xB5",
|
61
|
-
:ver => "\x01\x00\x03\x00",
|
62
78
|
0 => "\x00\xD0\xAD\x0A",
|
63
79
|
2 => "\x31\xC1\xF2\xE6\xBF\x71\x43\x50\xBE\x58\x05\x21\x6A\xFC\x5A\xFF",
|
64
80
|
3 => "\x01\x00\x00\x00",
|
@@ -69,6 +85,7 @@ class Kdbx::Header
|
|
69
85
|
8 => OpenSSL::Random.random_bytes(32),
|
70
86
|
9 => OpenSSL::Random.random_bytes(32),
|
71
87
|
10 => "\x02\x00\x00\x00"
|
72
|
-
}) { |
|
88
|
+
}) { |_k, v1, _v2| v1 }
|
89
|
+
true
|
73
90
|
end
|
74
91
|
end
|
data/lib/kdbx/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kdbx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- rumtid
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-07-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: salsa20
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '3.5'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.10'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.10'
|
41
55
|
description:
|
42
56
|
email:
|
43
57
|
executables: []
|
@@ -48,10 +62,10 @@ files:
|
|
48
62
|
- README.md
|
49
63
|
- lib/kdbx.rb
|
50
64
|
- lib/kdbx/attributes.rb
|
51
|
-
- lib/kdbx/
|
65
|
+
- lib/kdbx/crypto.rb
|
66
|
+
- lib/kdbx/exception.rb
|
52
67
|
- lib/kdbx/header.rb
|
53
68
|
- lib/kdbx/version.rb
|
54
|
-
- lib/kdbx/wrapper.rb
|
55
69
|
homepage: https://github.com/rumtid/kdbx.rb
|
56
70
|
licenses:
|
57
71
|
- MIT
|
@@ -72,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
86
|
version: '0'
|
73
87
|
requirements: []
|
74
88
|
rubyforge_project:
|
75
|
-
rubygems_version: 2.6.
|
89
|
+
rubygems_version: 2.6.12
|
76
90
|
signing_key:
|
77
91
|
specification_version: 4
|
78
92
|
summary: A kdbx library to access kdbx file format
|
data/lib/kdbx/encryption.rb
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
require "zlib"
|
2
|
-
require "openssl"
|
3
|
-
require "salsa20"
|
4
|
-
|
5
|
-
class Kdbx
|
6
|
-
module Encryption
|
7
|
-
private
|
8
|
-
|
9
|
-
def sha256(data)
|
10
|
-
OpenSSL::Digest::SHA256.digest data
|
11
|
-
end
|
12
|
-
|
13
|
-
def salsa20
|
14
|
-
key = sha256 protectedstreamkey
|
15
|
-
Salsa20.new key, "\xE8\x30\x09\x4B\x97\x20\x5D\x2A"
|
16
|
-
end
|
17
|
-
|
18
|
-
def masterkey
|
19
|
-
cipher = OpenSSL::Cipher.new("AES-256-ECB").encrypt
|
20
|
-
cipher.key, data = transformseed, sha256(credential)
|
21
|
-
transformrounds.times { data = cipher.update data }
|
22
|
-
sha256(masterseed + sha256(data))
|
23
|
-
end
|
24
|
-
|
25
|
-
def encrypt(data)
|
26
|
-
cipher = OpenSSL::Cipher.new("AES-256-CBC").encrypt
|
27
|
-
cipher.iv, cipher.key = encryptioniv, masterkey
|
28
|
-
cipher.update(streamstartbytes + data) + cipher.final
|
29
|
-
end
|
30
|
-
|
31
|
-
def decrypt(data)
|
32
|
-
cipher = OpenSSL::Cipher.new("AES-256-CBC").decrypt
|
33
|
-
cipher.iv, cipher.key = encryptioniv, masterkey
|
34
|
-
data = cipher.update(data) + cipher.final
|
35
|
-
unless data.start_with? streamstartbytes
|
36
|
-
fail "InvalidKey"
|
37
|
-
end
|
38
|
-
size = streamstartbytes.bytesize
|
39
|
-
return data.byteslice size..-1
|
40
|
-
end
|
41
|
-
|
42
|
-
def encode_content(file)
|
43
|
-
data = Wrapper.protect @content do |block|
|
44
|
-
salsa20.encrypt block
|
45
|
-
end if innerrandomstreamid == 2
|
46
|
-
data = Zlib.gzip data if zipped?
|
47
|
-
data = Wrapper.wrap data
|
48
|
-
data = encrypt data
|
49
|
-
file.write data
|
50
|
-
end
|
51
|
-
|
52
|
-
def decode_content(data)
|
53
|
-
data = decrypt data
|
54
|
-
data = Wrapper.unwrap data
|
55
|
-
data = Zlib.gunzip data if zipped?
|
56
|
-
data = Wrapper.expose data do |block|
|
57
|
-
salsa20.decrypt block
|
58
|
-
end if innerrandomstreamid == 2
|
59
|
-
@content = data
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
data/lib/kdbx/wrapper.rb
DELETED
@@ -1,65 +0,0 @@
|
|
1
|
-
require "base64"
|
2
|
-
require "openssl"
|
3
|
-
require "rexml/document"
|
4
|
-
|
5
|
-
module Kdbx::Wrapper
|
6
|
-
XPATH = "//Value[@Protected='True']"
|
7
|
-
|
8
|
-
module_function
|
9
|
-
|
10
|
-
def unwrap(data)
|
11
|
-
stream = StringIO.new data
|
12
|
-
payload = String.new
|
13
|
-
loop do
|
14
|
-
head = stream.readpartial 40
|
15
|
-
id, hash, size = head.unpack "La32L"
|
16
|
-
break if size == 0
|
17
|
-
part = stream.readpartial size
|
18
|
-
payload << part
|
19
|
-
end
|
20
|
-
payload
|
21
|
-
end
|
22
|
-
|
23
|
-
def wrap(payload)
|
24
|
-
data = String.new
|
25
|
-
data << "\x00\x00\x00\x00"
|
26
|
-
data << OpenSSL::Digest::SHA256.digest(payload)
|
27
|
-
data << [payload.bytesize].pack("L") << payload
|
28
|
-
data << "\x01\x00\x00\x00"
|
29
|
-
data << "\x00" * 36
|
30
|
-
data
|
31
|
-
end
|
32
|
-
|
33
|
-
def pieces(doc)
|
34
|
-
doc.elements.to_a(XPATH).map(&:text).compact
|
35
|
-
end
|
36
|
-
|
37
|
-
def expose(data)
|
38
|
-
doc = REXML::Document.new data
|
39
|
-
ciphertext = pieces(doc).map do |t|
|
40
|
-
Base64.decode64 t
|
41
|
-
end
|
42
|
-
plaintext = yield ciphertext.join
|
43
|
-
doc.elements.each XPATH do |e|
|
44
|
-
next if e.text == nil
|
45
|
-
size = ciphertext.shift.bytesize
|
46
|
-
e.text = plaintext.byteslice 0, size
|
47
|
-
plaintext = plaintext.byteslice size..-1
|
48
|
-
end
|
49
|
-
doc.to_s
|
50
|
-
end
|
51
|
-
|
52
|
-
def protect(data)
|
53
|
-
doc = REXML::Document.new data
|
54
|
-
plaintext = pieces doc
|
55
|
-
ciphertext = yield plaintext.join
|
56
|
-
doc.elements.each XPATH do |e|
|
57
|
-
next if e.text == nil
|
58
|
-
size = plaintext.shift.bytesize
|
59
|
-
text = ciphertext.byteslice 0, size
|
60
|
-
ciphertext = ciphertext.byteslice size..-1
|
61
|
-
e.text = Base64.encode64 text
|
62
|
-
end
|
63
|
-
doc.to_s
|
64
|
-
end
|
65
|
-
end
|