keepassx 0.1.0 → 1.0.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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +30 -0
  3. data/.gitignore +9 -0
  4. data/.rubocop.yml +64 -0
  5. data/.travis.yml +12 -3
  6. data/Gemfile +4 -2
  7. data/Guardfile +16 -0
  8. data/LICENSE +19 -0
  9. data/README.md +33 -0
  10. data/Rakefile +3 -2
  11. data/keepassx.gemspec +20 -10
  12. data/lib/keepassx.rb +42 -3
  13. data/lib/keepassx/aes_crypt.rb +16 -6
  14. data/lib/keepassx/database.rb +218 -27
  15. data/lib/keepassx/database/dumper.rb +87 -0
  16. data/lib/keepassx/database/finder.rb +102 -0
  17. data/lib/keepassx/database/loader.rb +217 -0
  18. data/lib/keepassx/entry.rb +70 -38
  19. data/lib/keepassx/field/base.rb +191 -0
  20. data/lib/keepassx/field/entry.rb +32 -0
  21. data/lib/keepassx/field/group.rb +27 -0
  22. data/lib/keepassx/fieldable.rb +161 -0
  23. data/lib/keepassx/group.rb +93 -20
  24. data/lib/keepassx/hashable_payload.rb +6 -0
  25. data/lib/keepassx/header.rb +102 -27
  26. data/lib/keepassx/version.rb +5 -0
  27. data/spec/factories.rb +23 -0
  28. data/spec/fixtures/database_empty.kdb +0 -0
  29. data/spec/fixtures/database_test.kdb +0 -0
  30. data/spec/fixtures/database_test_dumped.yml +76 -0
  31. data/spec/fixtures/database_with_key.kdb +0 -0
  32. data/spec/fixtures/database_with_key.key +1 -0
  33. data/spec/fixtures/database_with_key2.key +1 -0
  34. data/spec/fixtures/test_data_array.yml +113 -0
  35. data/spec/fixtures/test_data_array_dumped.yml +124 -0
  36. data/spec/keepassx/database_spec.rb +491 -29
  37. data/spec/keepassx/entry_spec.rb +95 -0
  38. data/spec/keepassx/group_spec.rb +92 -0
  39. data/spec/keepassx_spec.rb +17 -0
  40. data/spec/spec_helper.rb +59 -3
  41. metadata +143 -69
  42. data/.rvmrc +0 -1
  43. data/Gemfile.lock +0 -28
  44. data/lib/keepassx/entry_field.rb +0 -49
  45. data/lib/keepassx/group_field.rb +0 -44
  46. data/spec/test_database.kdb +0 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4db73a09546a08ab55cb858af6633140b1330b29512f67b2fc535dfc7faa497d
4
+ data.tar.gz: 07ce161d5e70797499e7fc91c16ad6e4155e39ec38088ea615ca4e1adddf35e7
5
+ SHA512:
6
+ metadata.gz: 8b02a38627723697feda5d948e8a678840a4fbfefe035da2b53ce745bc4041ee3b18d933cefa313943a5710b30d5162b1a4037c13eb00ce6db9fb6127c361d71
7
+ data.tar.gz: b5724c17a5f53249ce6a54e48fbe99f9d4282ed017ce15b12ea4174afa5d7e4abef05096280bd7020afb948b881e3135b929b6e85ed4483a5167ceff0d4ac706
@@ -0,0 +1,30 @@
1
+ ---
2
+ engines:
3
+ duplication:
4
+ enabled: true
5
+ exclude_fingerprints:
6
+ # dumper.rb
7
+ - dec843d4724d26c64427d1037a2ea4f1
8
+ # aes_crypt.rb
9
+ - 33893778c027f4ce2aaef293c8eb00e0
10
+ config:
11
+ languages:
12
+ - ruby
13
+ - javascript
14
+ - python
15
+ - php
16
+ fixme:
17
+ enabled: true
18
+ rubocop:
19
+ enabled: true
20
+ ratings:
21
+ paths:
22
+ - "**.inc"
23
+ - "**.js"
24
+ - "**.jsx"
25
+ - "**.module"
26
+ - "**.php"
27
+ - "**.py"
28
+ - "**.rb"
29
+ exclude_paths:
30
+ - spec/
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ *.lock
3
+ *.log
4
+ .bundle
5
+ /pkg
6
+ /rdoc
7
+ /coverage
8
+ /junit
9
+ /tmp
@@ -0,0 +1,64 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+ Exclude:
4
+ - spec/**/*
5
+
6
+ Documentation:
7
+ Enabled: false
8
+
9
+ Layout/AlignHash:
10
+ Enabled: false
11
+
12
+ Layout/EmptyLines:
13
+ Enabled: false
14
+
15
+ Layout/EmptyLinesAroundClassBody:
16
+ Enabled: false
17
+
18
+ Layout/EmptyLinesAroundBlockBody:
19
+ Enabled: false
20
+
21
+ Layout/EmptyLinesAroundModuleBody:
22
+ Enabled: false
23
+
24
+ Layout/EmptyLineBetweenDefs:
25
+ Enabled: false
26
+
27
+ Layout/IndentationConsistency:
28
+ EnforcedStyle: rails
29
+
30
+ Metrics/AbcSize:
31
+ Enabled: false
32
+
33
+ Metrics/LineLength:
34
+ Enabled: false
35
+
36
+ Metrics/MethodLength:
37
+ Max: 12
38
+
39
+ Metrics/ModuleLength:
40
+ Max: 160
41
+
42
+ Metrics/ClassLength:
43
+ Max: 160
44
+
45
+ Metrics/CyclomaticComplexity:
46
+ Enabled: false
47
+
48
+ Metrics/PerceivedComplexity:
49
+ Enabled: false
50
+
51
+ Naming/AccessorMethodName:
52
+ Enabled: false
53
+
54
+ Style/TrailingCommaInArrayLiteral:
55
+ EnforcedStyleForMultiline: comma
56
+
57
+ Style/TrailingCommaInHashLiteral:
58
+ EnforcedStyleForMultiline: comma
59
+
60
+ Style/NumericPredicate:
61
+ EnforcedStyle: comparison
62
+
63
+ Style/UnpackFirst:
64
+ Enabled: false
@@ -1,5 +1,14 @@
1
1
  language: ruby
2
+ cache: bundler
3
+ sudo: false
2
4
  rvm:
3
- - 1.8.7
4
- - 1.9.1
5
- - 1.9.3
5
+ - 2.6.5
6
+ - 2.5.7
7
+ - 2.4.9
8
+ - 2.3.8
9
+ - jruby-9.2.8.0
10
+ before_install:
11
+ - gem update --system
12
+ - gem install bundler --no-document
13
+ after_success:
14
+ - bundle exec codeclimate-test-reporter
data/Gemfile CHANGED
@@ -1,5 +1,7 @@
1
- source "http://rubygems.org"
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
2
4
 
3
5
  gemspec
4
6
 
5
- gem 'rake', '0.8.7'
7
+ gem 'codeclimate-test-reporter', group: :test, require: false
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard :rspec, cmd: 'bundle exec rspec' do
4
+ require 'guard/rspec/dsl'
5
+ dsl = Guard::RSpec::Dsl.new(self)
6
+
7
+ # RSpec files
8
+ rspec = dsl.rspec
9
+ watch(rspec.spec_helper) { rspec.spec_dir }
10
+ watch(rspec.spec_support) { rspec.spec_dir }
11
+ watch(rspec.spec_files)
12
+
13
+ # Ruby files
14
+ ruby = dsl.ruby
15
+ dsl.watch_spec_files_for(ruby.lib_files)
16
+ end
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ The MIT License (MIT)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md CHANGED
@@ -0,0 +1,33 @@
1
+ ## Keepassx
2
+
3
+ [![GitHub license](https://img.shields.io/github/license/pitluga/keepassx.svg)](https://github.com/pitluga/keepassx/blob/master/LICENSE)
4
+ [![GitHub release](https://img.shields.io/github/release/pitluga/keepassx.svg)](https://github.com/pitluga/keepassx/releases/latest)
5
+ [![Build Status](https://travis-ci.org/pitluga/keepassx.svg?branch=master)](https://travis-ci.org/pitluga/keepassx)
6
+
7
+ ### A Ruby library to read and write [KeePassX](http://www.keepassx.org/) databases.
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ gem install keepassx
13
+ ```
14
+
15
+ or if you use bundler
16
+
17
+ ```ruby
18
+ gem 'keepassx'
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ require 'keepassx'
25
+
26
+ database = Keepassx::Database.open("/path/to/database.kdb")
27
+ database.unlock("the master password")
28
+ puts database.entry("entry's title").password
29
+ ```
30
+
31
+ ## Security Warning
32
+
33
+ No attempt is made to protect the memory used by this library; there may be something we can do with libgcrypt's secure-malloc functions, but right now your master password is unencrypted in ram that could possibly be paged to disk.
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
- require 'rspec/core/rake_task'
1
+ # frozen_string_literal: true
2
2
 
3
- task :default => :spec
3
+ require 'rspec/core/rake_task'
4
4
 
5
5
  RSpec::Core::RakeTask.new(:spec)
6
+ task default: :spec
@@ -1,14 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/keepassx/version'
4
+
1
5
  Gem::Specification.new do |s|
2
- s.name = "keepassx"
3
- s.summary = "Ruby API access for KeePassX databases"
4
- s.description = "See http://github.com/pitluga/keepassx"
5
- s.version = "0.1.0"
6
- s.authors = ["Tony Pitluga", "Paul Hinze"]
7
- s.email = ["tony.pitluga@gmail.com", "paul.t.hinze@gmail.com"]
8
- s.homepage = "http://github.com/pitluga/keepassx"
9
- s.files = `git ls-files`.split("\n")
6
+ s.name = 'keepassx'
7
+ s.version = Keepassx::VERSION
8
+ s.authors = ['Tony Pitluga', 'Paul Hinze']
9
+ s.email = ['tony.pitluga@gmail.com', 'paul.t.hinze@gmail.com']
10
+ s.homepage = 'http://github.com/pitluga/keepassx'
11
+ s.summary = 'Ruby API access for KeePassX databases'
12
+ s.description = 'See http://github.com/pitluga/keepassx'
13
+ s.license = 'MIT'
10
14
 
11
- s.add_dependency "fast-aes", "~> 0.1"
15
+ s.files = `git ls-files`.split("\n")
12
16
 
13
- s.add_development_dependency "rspec", "2.11.0"
17
+ s.add_development_dependency 'factory_bot'
18
+ s.add_development_dependency 'guard'
19
+ s.add_development_dependency 'guard-rspec'
20
+ s.add_development_dependency 'rake', '~> 10.4'
21
+ s.add_development_dependency 'respect'
22
+ s.add_development_dependency 'rspec'
23
+ s.add_development_dependency 'simplecov'
14
24
  end
@@ -1,13 +1,52 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
  require 'stringio'
3
5
  require 'openssl'
6
+ require 'securerandom'
4
7
  require 'digest/sha2'
5
- require 'fast-aes'
8
+ require 'yaml'
6
9
 
10
+ require 'keepassx/database/dumper'
11
+ require 'keepassx/database/loader'
12
+ require 'keepassx/database/finder'
7
13
  require 'keepassx/database'
14
+ require 'keepassx/field/base'
15
+ require 'keepassx/field/entry'
16
+ require 'keepassx/field/group'
17
+ require 'keepassx/fieldable'
8
18
  require 'keepassx/entry'
9
- require 'keepassx/entry_field'
10
19
  require 'keepassx/group'
11
- require 'keepassx/group_field'
12
20
  require 'keepassx/header'
13
21
  require 'keepassx/aes_crypt'
22
+
23
+ module Keepassx
24
+ class << self
25
+
26
+ # Create Keepassx database
27
+ #
28
+ # @param opts [Hash] Keepassx database options.
29
+ # @yield [opts]
30
+ # @yieldreturn [Fixnum]
31
+ # @return [Keepassx::Database]
32
+ def new(opts)
33
+ db = Database.new(opts)
34
+ yield db if block_given?
35
+ db
36
+ end
37
+
38
+
39
+ # Read Keepassx database from file storage.
40
+ #
41
+ # @param opts [Hash] Keepassx database options.
42
+ # @yield [opts]
43
+ # @yieldreturn [Fixnum]
44
+ # @return [Keepassx::Database]
45
+ def open(opts)
46
+ db = Database.new(opts)
47
+ yield db if block_given?
48
+ db
49
+ end
50
+
51
+ end
52
+ end
@@ -1,19 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Keepassx
2
4
  module AESCrypt
3
- def self.decrypt(encrypted_data, key, iv, cipher_type)
4
- aes = OpenSSL::Cipher::Cipher.new(cipher_type)
5
+ module_function
6
+
7
+ # rubocop:disable Naming/UncommunicativeMethodParamName
8
+ def decrypt(encrypted_data, key, iv, cipher_type)
9
+ aes = OpenSSL::Cipher.new(cipher_type)
5
10
  aes.decrypt
6
11
  aes.key = key
7
- aes.iv = iv unless iv.nil?
12
+ aes.iv = iv unless iv.nil?
8
13
  aes.update(encrypted_data) + aes.final
9
14
  end
15
+ # rubocop:enable Naming/UncommunicativeMethodParamName
10
16
 
11
- def self.encrypt(data, key, iv, cipher_type)
12
- aes = OpenSSL::Cipher::Cipher.new(cipher_type)
17
+
18
+ # rubocop:disable Naming/UncommunicativeMethodParamName
19
+ def encrypt(data, key, iv, cipher_type)
20
+ aes = OpenSSL::Cipher.new(cipher_type)
13
21
  aes.encrypt
14
22
  aes.key = key
15
- aes.iv = iv unless iv.nil?
23
+ aes.iv = iv unless iv.nil?
16
24
  aes.update(data) + aes.final
17
25
  end
26
+ # rubocop:enable Naming/UncommunicativeMethodParamName
27
+
18
28
  end
19
29
  end
@@ -1,45 +1,236 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Keepassx
2
4
  class Database
3
5
 
4
- attr_reader :header, :groups, :entries
6
+ include Database::Dumper
7
+ include Database::Loader
8
+ include Database::Finder
5
9
 
6
- def self.open(path)
7
- content = File.respond_to?(:binread) ? File.binread(path) : File.read(path)
8
- self.new(content)
9
- end
10
10
 
11
- def initialize(raw_db)
12
- @header = Header.new(raw_db[0..124])
13
- @encrypted_payload = raw_db[124..-1]
11
+ # Check database validity.
12
+ #
13
+ # @return [Boolean]
14
+ def valid?
15
+ header.valid?
14
16
  end
15
17
 
16
- def entry(title)
17
- @entries.detect { |e| e.title == title }
18
+
19
+ # Get lock state
20
+ #
21
+ # @return [Boolean]
22
+ def locked?
23
+ @locked
18
24
  end
19
25
 
20
- def unlock(master_password)
21
- @final_key = header.final_key(master_password)
22
- decrypt_payload
23
- payload_io = StringIO.new(@payload)
24
- @groups = Group.extract_from_payload(header, payload_io)
25
- @entries = Entry.extract_from_payload(header, payload_io)
26
- true
27
- rescue OpenSSL::Cipher::CipherError
28
- false
26
+
27
+ # Add new group to database.
28
+ #
29
+ # @param opts [Hash] Options that will be passed to Keepassx::Group#new.
30
+ # @return [Keepassx::Group]
31
+ # rubocop:disable Metrics/MethodLength
32
+ def add_group(opts)
33
+ raise ArgumentError, "Expected Hash or Keepassx::Group, got #{opts.class}" unless valid_group?(opts)
34
+
35
+ if opts.is_a?(Keepassx::Group)
36
+ # Assign parent group
37
+ parent = opts.parent
38
+ index = last_sibling_index(parent) + 1
39
+ @groups.insert(index, opts)
40
+
41
+ # Increment counter
42
+ header.groups_count += 1
43
+
44
+ # Return group
45
+ opts
46
+
47
+ elsif opts.is_a?(Hash)
48
+ opts = deep_copy(opts)
49
+ opts = build_group_options(opts)
50
+
51
+ # Create group
52
+ group = create_group(opts)
53
+
54
+ # Increment counter
55
+ header.groups_count += 1
56
+
57
+ # Return group
58
+ group
59
+ end
29
60
  end
61
+ # rubocop:enable Metrics/MethodLength
62
+
63
+
64
+ # Add new entry to database.
65
+ #
66
+ # @param opts [Hash] Options that will be passed to Keepassx::Entry#new.
67
+ # @return [Keepassx::Entry]
68
+ def add_entry(opts)
69
+ raise ArgumentError, "Expected Hash or Keepassx::Entry, got #{opts.class}" unless valid_entry?(opts)
30
70
 
31
- def search(pattern)
32
- backup = groups.detect { |g| g.name == "Backup" }
33
- backup_group_id = backup && backup.group_id
34
- entries.select { |e| e.group_id != backup_group_id && e.title =~ /#{pattern}/i }
71
+ if opts.is_a?(Keepassx::Entry)
72
+ # Add entry
73
+ @entries << opts
74
+
75
+ # Increment counter
76
+ header.entries_count += 1
77
+
78
+ # Return entry
79
+ opts
80
+
81
+ elsif opts.is_a?(Hash)
82
+ opts = deep_copy(opts)
83
+ opts = build_entry_options(opts)
84
+
85
+ # Create entry
86
+ entry = create_entry(opts)
87
+
88
+ # Increment counter
89
+ header.entries_count += 1
90
+
91
+ # Return entry
92
+ entry
93
+ end
35
94
  end
36
95
 
37
- def valid?
38
- @header.valid?
96
+
97
+ def delete_group(item)
98
+ # Get group entries and delete them
99
+ group_entries = entries.select { |e| e.group == item }
100
+ group_entries.each { |e| delete_entry(e) }
101
+
102
+ # Recursively delete ancestor groups
103
+ group_ancestors = groups.select { |g| g.parent == item }
104
+ group_ancestors.each { |g| delete_group(g) }
105
+
106
+ item = groups.delete(item)
107
+ header.groups_count -= 1
108
+ item
39
109
  end
40
110
 
41
- def decrypt_payload
42
- @payload = AESCrypt.decrypt(@encrypted_payload, @final_key, header.encryption_iv, 'AES-256-CBC')
111
+
112
+ def delete_entry(item)
113
+ item = entries.delete(item)
114
+ header.entries_count -= 1
115
+ item
43
116
  end
117
+
118
+
119
+ private
120
+
121
+
122
+ # Make deep copy of Hash
123
+ def deep_copy(opts)
124
+ Marshal.load Marshal.dump(opts)
125
+ end
126
+
127
+
128
+ # Get next group ID number.
129
+ #
130
+ # @return [Fixnum]
131
+ def next_group_id
132
+ if @groups.empty?
133
+ # Start each time from 1 to make sure groups get the same id's for the
134
+ # same input data
135
+ 1
136
+ else
137
+ id = @groups.last.id
138
+ loop do
139
+ id += 1
140
+ break id if @groups.find { |g| g.id == id }.nil?
141
+ end
142
+ end
143
+ end
144
+
145
+
146
+ # Retrieves last sibling index
147
+ #
148
+ # @param parent [Keepassx::Group] Last sibling group.
149
+ # @return [Integer] index Group index.
150
+ def last_sibling_index(parent)
151
+ return -1 if groups.empty?
152
+
153
+ if parent.nil?
154
+ parent_index = 0
155
+ sibling_level = 1
156
+ else
157
+ parent_index = groups.find_index(parent)
158
+ sibling_level = parent.level + 1
159
+ end
160
+
161
+ raise "Could not find group #{parent.name}" if parent_index.nil?
162
+
163
+ (parent_index..(header.groups_count - 1)).each do |i|
164
+ break i unless groups[i].level == sibling_level
165
+ end
166
+ end
167
+
168
+
169
+ def create_group(opts = {})
170
+ group = Keepassx::Group.new(opts)
171
+ if group.parent.nil?
172
+ @groups << group
173
+ else
174
+ index = last_sibling_index(group.parent) + 1
175
+ @groups.insert(index, group)
176
+ end
177
+ group
178
+ end
179
+
180
+
181
+ def build_group_options(opts = {})
182
+ opts[:id] = next_group_id unless opts.key?(:id)
183
+
184
+ # Replace parent, which is specified by symbol with actual group
185
+ if opts[:parent].is_a?(Symbol)
186
+ group = find_group(opts[:parent])
187
+ raise "Group #{opts[:parent].inspect} does not exist" if group.nil?
188
+
189
+ opts[:parent] = group
190
+ end
191
+ opts
192
+ end
193
+
194
+
195
+ def create_entry(opts = {})
196
+ entry = Keepassx::Entry.new(opts)
197
+ @entries << entry
198
+ entry
199
+ end
200
+
201
+
202
+ # rubocop:disable Metrics/MethodLength, Style/SafeNavigation
203
+ def build_entry_options(opts = {})
204
+ if opts[:group]
205
+ if opts[:group].is_a?(String) || opts[:group].is_a?(Hash)
206
+ group = find_group(opts[:group])
207
+ raise "Group #{opts[:group].inspect} does not exist" if group.nil?
208
+
209
+ opts[:group] = group
210
+ opts[:group_id] = group.id
211
+ elsif opts[:group].is_a?(Keepassx::Group)
212
+ opts[:group_id] = opts[:group].id
213
+ end
214
+
215
+ elsif opts[:group_id] && opts[:group_id].is_a?(Integer)
216
+ group = find_group(id: opts[:group_id])
217
+ raise "Group #{opts[:group_id].inspect} does not exist" if group.nil?
218
+
219
+ opts[:group] = group
220
+ end
221
+ opts
222
+ end
223
+ # rubocop:enable Metrics/MethodLength, Style/SafeNavigation
224
+
225
+
226
+ def valid_group?(object)
227
+ object.is_a?(Keepassx::Group) || object.is_a?(Hash)
228
+ end
229
+
230
+
231
+ def valid_entry?(object)
232
+ object.is_a?(Keepassx::Entry) || object.is_a?(Hash)
233
+ end
234
+
44
235
  end
45
236
  end