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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4f1066ac2625d013aa37082c0e66ba15c59a35ad
4
- data.tar.gz: 3143044e49a5ba3299b56a3741acaaeba990a5b0
3
+ metadata.gz: a7bc07ea8275bb2ada03a4cc9da04d76d7cbee79
4
+ data.tar.gz: 7d086b8150446b8bdfdfb41a746c9086fc5ce873
5
5
  SHA512:
6
- metadata.gz: bcc4f6b347839ec701f234fefe47fde2311f3f79b71be7c28525f1a711b4f8e354d05724c77a8054f90b61146718dd8dea246edc7316d3ca7e49e133b59f1444
7
- data.tar.gz: 679881649d8cd3a479f3774b111a26ee24aa9279fecfb840d0d3ec0877e506d5ab35336f3a4cc24248d554f13fa2219273706539ebfd618529382de7495c5a75
6
+ metadata.gz: 947542790231a62649f280adb0d4f8e990ec3940c9c6415e123abe99cff67d0b6976c0984987620fbb557bfd9253a253c25761b307b7badcd39d2c5403b55b49
7
+ data.tar.gz: e7c3dc8d5eb254530406bece0cc5573e6c6f03927bd8bd72052f295a1b35d4e3b9d036646d597ca0ebe79e061df3170a47900ec2d42af8743439059bbcd1682b
data/README.md CHANGED
@@ -1,18 +1,42 @@
1
- # kdbx.rb
1
+ # Kdbx.rb
2
2
 
3
- Yet another library for kdbx file.
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
- $ gem install kdbx
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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
37
+ First, install dependencies: `bundle install`
14
38
 
15
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
39
+ Then run tests: `rspec`
16
40
 
17
41
  ## License
18
42
 
@@ -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
- include Attributes
9
- include Encryption
10
-
11
- def initialize(filename = nil, **opts)
12
- super()
13
- if filename == nil
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 load
25
- File.open filename, "rb" do |file|
26
- @header = Header.load file
27
- decode_content file.read
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
- self
28
+ true
30
29
  end
31
30
 
32
- def save
33
- File.open filename, "wb" do |file|
34
- @header.save file
35
- encode_content file
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
@@ -1,90 +1,95 @@
1
1
  require "base64"
2
+ require "rexml/document"
2
3
 
3
4
  class Kdbx
4
- module Attributes
5
- attr_reader :filename
6
- def filename=(name)
7
- @filename = File.absolute_path name
8
- end
5
+ attr_reader :password
6
+ def password=(str)
7
+ @password = str == nil ? "" : sha256(str)
8
+ end
9
9
 
10
- attr_reader :password
11
- def password=(str)
12
- @password = str == nil ? "" : sha256(str)
13
- end
10
+ attr_reader :keyfile
11
+ def keyfile=(str)
12
+ @keyfile = File.absolute_path str
13
+ end
14
14
 
15
- attr_reader :keyfile
16
- def keyfile=(str)
17
- @keyfile = File.absolute_path str
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
- def credential
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
- def compressionflags
41
- @header[3].unpack("L").first
25
+ if data =~ /\A\h{64}\z/
26
+ data = [data].pack("H*")
27
+ return cred + data
42
28
  end
43
-
44
- def compressionflags=(flag)
45
- @header[3] = [flag].pack("L")
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
- def zipped?
49
- compressionflags == 1
50
- end
38
+ attr_accessor :header
51
39
 
52
- def masterseed
53
- @header[4]
54
- end
40
+ def compressionflags
41
+ @header[3].unpack("L").first
42
+ end
55
43
 
56
- def transformseed
57
- @header[5]
58
- end
44
+ def compressionflags=(flag)
45
+ @header[3] = [flag].pack("L")
46
+ end
59
47
 
60
- def transformrounds
61
- @header[6].unpack("Q").first
62
- end
48
+ def masterseed
49
+ @header[4]
50
+ end
63
51
 
64
- def transformrounds=(num)
65
- @header[6] = [num].pack("Q")
66
- end
52
+ def transformseed
53
+ @header[5]
54
+ end
67
55
 
68
- def encryptioniv
69
- @header[7]
70
- end
56
+ def transformrounds
57
+ @header[6].unpack("Q").first
58
+ end
71
59
 
72
- def protectedstreamkey
73
- @header[8]
74
- end
60
+ def transformrounds=(num)
61
+ @header[6] = [num].pack("Q")
62
+ end
75
63
 
76
- def streamstartbytes
77
- @header[9]
78
- end
64
+ def encryptioniv
65
+ @header[7]
66
+ end
79
67
 
80
- def innerrandomstreamid
81
- @header[10].unpack("L").first
82
- end
68
+ def protectedstreamkey
69
+ @header[8]
70
+ end
83
71
 
84
- def innerrandomstreamid=(id)
85
- @header[10] = [id].pack("L")
86
- end
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
- attr_accessor :content
92
+ def nonce
93
+ "\xE8\x30\x09\x4B\x97\x20\x5D\x2A".b
89
94
  end
90
95
  end
@@ -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
@@ -0,0 +1,6 @@
1
+ class Kdbx
2
+ class KdbxError < StandardError; end
3
+ class FormatError < KdbxError; end
4
+ class ParseError < KdbxError; end
5
+ class KeyError < KdbxError; end
6
+ end
@@ -2,63 +2,79 @@ require "openssl"
2
2
  require "forwardable"
3
3
 
4
4
  class Kdbx::Header
5
- def self.load(stream)
6
- fields = {
7
- :pid => stream.readpartial(4),
8
- :sid => stream.readpartial(4),
9
- :ver => stream.readpartial(4)
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 = stream.readbyte
13
- sz = stream.readpartial 2
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.merge! fields
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, :[], :[]=, :has_key?
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 merge(hash)
34
- clone.merge! hash
25
+ def initialize(fields = {})
26
+ @fields = fields
27
+ validate
35
28
  end
36
29
 
37
- def merge!(hash)
38
- @fields.merge! hash
39
- self
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 save(stream)
43
- set_defaults
44
- stream.write @fields.fetch :pid
45
- stream.write @fields.fetch :sid
46
- stream.write @fields.fetch :ver
47
- @fields.each do |key, val|
48
- next if !(key.is_a? Integer) || key == 0
49
- stream.write [key, val.bytesize].pack("CS") + val
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 set_defaults
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
- }) { |k, v1, v2| v1 }
88
+ }) { |_k, v1, _v2| v1 }
89
+ true
73
90
  end
74
91
  end
@@ -1,3 +1,3 @@
1
1
  class Kdbx
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.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-03-24 00:00:00.000000000 Z
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/encryption.rb
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.8
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
@@ -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
@@ -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