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 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