kdbx 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/rumtid/kdbx.rb.svg?branch=master)](https://travis-ci.org/rumtid/kdbx.rb)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/rumtid/kdbx.rb/badges/gpa.svg)](https://codeclimate.com/github/rumtid/kdbx.rb)
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/kdbx.svg)](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
|