keepassx 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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