ruby-keepassx 0.2.0beta11
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 +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +9 -0
- data/.rvmrc +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +5 -0
- data/README.md +28 -0
- data/Rakefile +5 -0
- data/lib/keepassx.rb +56 -0
- data/lib/keepassx/aes_crypt.rb +19 -0
- data/lib/keepassx/database.rb +431 -0
- data/lib/keepassx/entry.rb +155 -0
- data/lib/keepassx/entry_field.rb +27 -0
- data/lib/keepassx/exceptions.rb +4 -0
- data/lib/keepassx/field.rb +205 -0
- data/lib/keepassx/group.rb +138 -0
- data/lib/keepassx/group_field.rb +22 -0
- data/lib/keepassx/header.rb +178 -0
- data/lib/keepassx/item.rb +128 -0
- data/lib/keepassx/utilities.rb +226 -0
- data/ruby-keepassx.gemspec +23 -0
- data/spec/fixtures/test_data_array.yaml +54 -0
- data/spec/fixtures/test_data_hash.yaml +36 -0
- data/spec/fixtures/test_database.kdb +0 -0
- data/spec/keepassx/database_spec.rb +338 -0
- data/spec/keepassx/entry_spec.rb +72 -0
- data/spec/keepassx/group_spec.rb +47 -0
- data/spec/spec_helper.rb +64 -0
- metadata +126 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA512:
|
3
|
+
metadata.gz: 23df857ea8fd01dacd6b60497d8f95075c0f827767835eca41258166fb98504198eefe9c3bb75cf67f42ec1abdff0410b2f9a38aaad314b6fe8e9feaf871abaa
|
4
|
+
data.tar.gz: 078c8430644ba977faf74ee6646adb1c039355819d975ff4e584ad4ac84d0dd3f897ce31b861a2e2bac3dfdf9cfb9b4d56b05d183c47bc680632f730b1b915c9
|
5
|
+
SHA1:
|
6
|
+
metadata.gz: a3fa2dedf5c4ad9d9ee9c742d4daff03044a6ed2
|
7
|
+
data.tar.gz: d9e4c27365a9772e81486e23b9da7f4f602a48d3
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
repo_token: kYWPOllQ5cMu6gvQ9Nbyu2LxSiGCM8gj2
|
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm ruby-1.8.7@keepassx --create
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Keepassx
|
2
|
+
|
3
|
+
This is fork of Tony Pitluga's Ruby API for [keepassx](http://www.keepassx.org/) with read-write support.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```
|
8
|
+
gem install ruby-keepassx
|
9
|
+
```
|
10
|
+
or if you use bundler
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'ruby-keepassx'
|
14
|
+
```
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
require 'keepassx'
|
20
|
+
|
21
|
+
database = Keepassx::Database.open("/path/to/database.kdb")
|
22
|
+
database.unlock("the master password")
|
23
|
+
puts database.entry("entry's title").password
|
24
|
+
```
|
25
|
+
|
26
|
+
## Security Warning
|
27
|
+
|
28
|
+
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
ADDED
data/lib/keepassx.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'stringio'
|
3
|
+
require 'openssl'
|
4
|
+
require 'digest/sha2'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'rexml/document'
|
7
|
+
|
8
|
+
# Add backward compatibility stuff
|
9
|
+
if RUBY_VERSION =~ /1\.8/
|
10
|
+
require 'backports/tools'
|
11
|
+
require 'backports/1.9.1/symbol/empty'
|
12
|
+
require 'backports/1.9.3/io/write'
|
13
|
+
require 'time' # Get Time.parse
|
14
|
+
|
15
|
+
unless SecureRandom.method_defined? :uuid
|
16
|
+
module SecureRandom
|
17
|
+
# Based on this post https://www.ruby-forum.com/topic/3171049#1035902
|
18
|
+
def self.uuid
|
19
|
+
s = hex 16
|
20
|
+
[s[0..7], s[8..11], s[12..15], s[16..19], s[20..-1]].join '-'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
require 'keepassx/exceptions'
|
28
|
+
require 'keepassx/header'
|
29
|
+
require 'keepassx/utilities'
|
30
|
+
require 'keepassx/database'
|
31
|
+
require 'keepassx/field'
|
32
|
+
require 'keepassx/entry_field'
|
33
|
+
require 'keepassx/group_field'
|
34
|
+
require 'keepassx/item'
|
35
|
+
require 'keepassx/entry'
|
36
|
+
require 'keepassx/group'
|
37
|
+
require 'keepassx/aes_crypt'
|
38
|
+
|
39
|
+
module Keepassx
|
40
|
+
|
41
|
+
class << self
|
42
|
+
def new opts
|
43
|
+
db = Database.new opts
|
44
|
+
return db unless block_given?
|
45
|
+
yield db
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def open opts
|
50
|
+
db = Database.open opts
|
51
|
+
return db unless block_given?
|
52
|
+
yield db
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Keepassx
|
2
|
+
module AESCrypt
|
3
|
+
def self.decrypt(encrypted_data, key, iv, cipher_type)
|
4
|
+
aes = OpenSSL::Cipher::Cipher.new(cipher_type)
|
5
|
+
aes.decrypt
|
6
|
+
aes.key = key
|
7
|
+
aes.iv = iv unless iv.nil?
|
8
|
+
aes.update(encrypted_data) + aes.final
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.encrypt(data, key, iv, cipher_type)
|
12
|
+
aes = OpenSSL::Cipher::Cipher.new(cipher_type)
|
13
|
+
aes.encrypt
|
14
|
+
aes.key = key
|
15
|
+
aes.iv = iv unless iv.nil?
|
16
|
+
aes.update(data) + aes.final
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,431 @@
|
|
1
|
+
module Keepassx
|
2
|
+
class Database
|
3
|
+
|
4
|
+
include Keepassx::Utilities
|
5
|
+
|
6
|
+
# BACKUP_GROUP_OPTIONS = { :title => :Backup, :icon => 4, :id => 0 }
|
7
|
+
|
8
|
+
attr_reader :header
|
9
|
+
attr_accessor :password, :key_file, :path
|
10
|
+
|
11
|
+
|
12
|
+
def self.open opts
|
13
|
+
path = opts.to_s
|
14
|
+
fail "File #{path} does not exist." unless File.exist? path
|
15
|
+
db = self.new path
|
16
|
+
return db unless block_given?
|
17
|
+
yield db
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def initialize opts
|
22
|
+
|
23
|
+
raw_db, @groups, @entries, @locked = '', [], [], true
|
24
|
+
|
25
|
+
if opts.is_a? File
|
26
|
+
self.path = opts.path
|
27
|
+
raw_db = read opts
|
28
|
+
initialize_database raw_db
|
29
|
+
|
30
|
+
elsif opts.is_a? String
|
31
|
+
self.path = opts
|
32
|
+
raw_db = read opts if File.exist? opts
|
33
|
+
initialize_database raw_db
|
34
|
+
|
35
|
+
elsif opts.is_a? REXML::Document
|
36
|
+
# TODO: Implement import method
|
37
|
+
fail NotImplementedError
|
38
|
+
|
39
|
+
elsif opts.is_a? Array
|
40
|
+
initialize_database '' # Pass empty data to get header initialized
|
41
|
+
# Avoid opts change by parse_data_array method
|
42
|
+
opts.each { |item| parse_data_array item }
|
43
|
+
# initialize_payload # Make sure payaload is available for checksum
|
44
|
+
|
45
|
+
else
|
46
|
+
fail TypeError, "Expected one of the File, String, " \
|
47
|
+
"REXML::Document or Hast, got #{opts.class}"
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def dump password = nil, key_file = nil
|
54
|
+
# FIXME: Figure out what this is needed for
|
55
|
+
# my $e = ($self->find_entries({title => 'Meta-Info', username => 'SYSTEM', comment => 'KPX_GROUP_TREE_STATE', url => '$'}))[0] || $self->add_entry({
|
56
|
+
# comment => 'KPX_GROUP_TREE_STATE',
|
57
|
+
# title => 'Meta-Info',
|
58
|
+
# username => 'SYSTEM',
|
59
|
+
# url => '$',
|
60
|
+
# id => '0000000000000000',
|
61
|
+
# group => $g[0],
|
62
|
+
# binary => {'bin-stream' => $bin},
|
63
|
+
# });
|
64
|
+
|
65
|
+
self.password = password unless password.nil?
|
66
|
+
self.key_file = key_file unless key_file.nil?
|
67
|
+
|
68
|
+
initialize_payload
|
69
|
+
header.contents_hash = checksum
|
70
|
+
encrypt
|
71
|
+
|
72
|
+
header.encode << @encrypted_payload.to_s
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
# TODO: Switch to rails style, i.e. save(:password => 'pass')
|
77
|
+
def save password = nil, key_file = nil
|
78
|
+
fail TypeError, 'File path is not set' if path.nil?
|
79
|
+
File.write path, dump(password, key_file)
|
80
|
+
|
81
|
+
# FIXME: Implement exceptions
|
82
|
+
rescue IOError => e
|
83
|
+
warn ">>>> IOError in database.rb"
|
84
|
+
fail
|
85
|
+
rescue SystemCallError => e
|
86
|
+
warn ">>>> SystemCallError in database.rb"
|
87
|
+
fail
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
# Search for items, using AND statement for the search conditions
|
92
|
+
def get item_type, opts = {}
|
93
|
+
|
94
|
+
case item_type
|
95
|
+
when :entry
|
96
|
+
item_list = @entries
|
97
|
+
when :group
|
98
|
+
item_list = @groups
|
99
|
+
else
|
100
|
+
fail "Unknown item type '#{item_type}'"
|
101
|
+
end
|
102
|
+
|
103
|
+
if opts.empty?
|
104
|
+
# Return all items if no selection condition was provided
|
105
|
+
items = item_list
|
106
|
+
|
107
|
+
else
|
108
|
+
opts = {:title => opts.to_s} if opts.is_a? String or opts.is_a? Symbol
|
109
|
+
|
110
|
+
match_number = opts.length
|
111
|
+
items = []
|
112
|
+
opts.each do |k, v|
|
113
|
+
items += Array(item_list.select { |e| e.send(k).eql?(v) })
|
114
|
+
end
|
115
|
+
|
116
|
+
buffer = Hash.new 0
|
117
|
+
items.each do |e|
|
118
|
+
buffer[e] += 1
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
# Select only items which matches all conditions
|
123
|
+
items = []
|
124
|
+
buffer.each do |k, v|
|
125
|
+
items << k if v.eql? match_number
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
if block_given?
|
130
|
+
items.each do |i|
|
131
|
+
yield i
|
132
|
+
end
|
133
|
+
|
134
|
+
else
|
135
|
+
items
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
# Get first matching entry
|
142
|
+
def entry opts = {}
|
143
|
+
entries = get :entry, opts
|
144
|
+
if entries.empty?
|
145
|
+
nil
|
146
|
+
else
|
147
|
+
entries.first
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
# Get all matching entries
|
154
|
+
def entries opts = {}
|
155
|
+
get :entry, opts
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
# Get first matching group
|
160
|
+
def group opts = {}
|
161
|
+
groups = get :group, opts
|
162
|
+
if groups.empty?
|
163
|
+
nil
|
164
|
+
else
|
165
|
+
groups.first
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
# Get all matching groups
|
172
|
+
def groups opts = {}
|
173
|
+
get :group, opts
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
def add item, opts = {}
|
178
|
+
if item.is_a? Symbol
|
179
|
+
|
180
|
+
if item.eql? :group
|
181
|
+
return add_group opts
|
182
|
+
elsif item.eql? :entry
|
183
|
+
return add_entry opts
|
184
|
+
else
|
185
|
+
fail "Unknown item type '#{item.to_s}'"
|
186
|
+
end
|
187
|
+
|
188
|
+
elsif item.is_a? Keepassx::Group
|
189
|
+
return add_group item
|
190
|
+
|
191
|
+
elsif item.is_a? Keepassx::Entry
|
192
|
+
return add_entry item
|
193
|
+
|
194
|
+
else
|
195
|
+
fail "Could not add '#{item.inspect}'"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
def add_group opts
|
201
|
+
|
202
|
+
if opts.is_a? Hash
|
203
|
+
opts = deep_copy opts
|
204
|
+
opts[:id] = next_group_id unless opts.has_key? :id
|
205
|
+
|
206
|
+
if opts[:parent].is_a? Symbol
|
207
|
+
group = self.group opts[:parent]
|
208
|
+
fail "Group #{opts[:parent].inspect} does not exist" if group.nil?
|
209
|
+
opts[:parent] = group
|
210
|
+
end
|
211
|
+
|
212
|
+
group = Keepassx::Group.new(opts)
|
213
|
+
if group.parent.nil?
|
214
|
+
@groups << group
|
215
|
+
else
|
216
|
+
@groups.insert last_sibling_index(group.parent) + 1, group
|
217
|
+
end
|
218
|
+
header.group_number += 1
|
219
|
+
|
220
|
+
group
|
221
|
+
elsif opts.is_a? Keepassx::Group
|
222
|
+
# Assign parent group
|
223
|
+
parent = opts.parent || nil
|
224
|
+
@groups.insert last_sibling_index(parent) + 1, item
|
225
|
+
header.group_number += 1
|
226
|
+
opts
|
227
|
+
|
228
|
+
else
|
229
|
+
fail TypeError, "Expected Hash or Keepassx::Group, got #{opts.class}"
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
def add_entry opts
|
236
|
+
if opts.is_a? Hash
|
237
|
+
opts = deep_copy opts
|
238
|
+
|
239
|
+
# FIXME: Remove this feature as it has unpredictable behavior when groups with duplicate title are present
|
240
|
+
if opts[:group].is_a? Symbol
|
241
|
+
group = self.group opts[:group]
|
242
|
+
fail "Group #{opts[:group].inspect} does not exist" if group.nil?
|
243
|
+
opts[:group] = group
|
244
|
+
end
|
245
|
+
|
246
|
+
entry = Keepassx::Entry.new opts
|
247
|
+
@entries << entry
|
248
|
+
header.entry_number += 1
|
249
|
+
entry
|
250
|
+
|
251
|
+
elsif opts.is_a? Keepassx::Entry
|
252
|
+
@entries << opts
|
253
|
+
header.entry_number += 1
|
254
|
+
opts
|
255
|
+
else
|
256
|
+
fail TypeError, "Expected Hash or Keepassx::Entry, got #{opts.class}"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
|
261
|
+
# Delete item from database
|
262
|
+
#
|
263
|
+
# @param [Keepassx::Group, Keepassx::Entry, Symbol] item Item to delete.
|
264
|
+
# @param [Hash] opts If first parameter is a Symbol, then this is used to
|
265
|
+
# determine which item to delete.
|
266
|
+
def delete item, opts = {}
|
267
|
+
if item.is_a? Keepassx::Group
|
268
|
+
delete_group item
|
269
|
+
|
270
|
+
elsif item.is_a? Keepassx::Entry
|
271
|
+
delete_entry item
|
272
|
+
|
273
|
+
elsif item.is_a? Symbol
|
274
|
+
if item.eql? :group
|
275
|
+
delete_group group(opts)
|
276
|
+
|
277
|
+
elsif item.eql? :entry
|
278
|
+
delete_entry entry(opts)
|
279
|
+
|
280
|
+
else
|
281
|
+
fail "Unknown item type '#{item.to_s}'"
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
end
|
286
|
+
|
287
|
+
|
288
|
+
# Unlock database
|
289
|
+
#
|
290
|
+
# @param [String] password Datbase password
|
291
|
+
# @param [String] key_file Key file path
|
292
|
+
# @return [Boolean] Whether or not password validation successfull
|
293
|
+
def unlock password, key_file = nil
|
294
|
+
|
295
|
+
return true unless locked?
|
296
|
+
|
297
|
+
self.password = password unless password.nil?
|
298
|
+
self.key_file = key_file unless key_file.nil?
|
299
|
+
decrypt
|
300
|
+
payload_io = StringIO.new payload
|
301
|
+
|
302
|
+
initialize_groups Group.extract_from_payload header, payload_io
|
303
|
+
@entries = Entry.extract_from_payload header, groups, payload_io
|
304
|
+
@locked = false
|
305
|
+
true
|
306
|
+
rescue OpenSSL::Cipher::CipherError
|
307
|
+
false
|
308
|
+
rescue Keepassx::MalformedDataError
|
309
|
+
fail
|
310
|
+
end
|
311
|
+
|
312
|
+
|
313
|
+
# FIXME: Seqrch by any atribute by pattern
|
314
|
+
# Searn entry by title
|
315
|
+
#
|
316
|
+
# @param [String] pattern Entry's title to search for
|
317
|
+
# @return [Keepassx::Entry]
|
318
|
+
def search pattern
|
319
|
+
backup = group 'Backup'
|
320
|
+
|
321
|
+
entries.select do |e|
|
322
|
+
e.group != backup && e.title =~ /#{pattern}/i
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
|
327
|
+
# Check database validity
|
328
|
+
#
|
329
|
+
# @return [Boolean]
|
330
|
+
def valid?
|
331
|
+
header.valid?
|
332
|
+
end
|
333
|
+
|
334
|
+
|
335
|
+
# Get lock state
|
336
|
+
#
|
337
|
+
# @return [Boolean]
|
338
|
+
def locked?
|
339
|
+
@locked
|
340
|
+
end
|
341
|
+
|
342
|
+
|
343
|
+
# Get Group/Entry index in storage
|
344
|
+
#
|
345
|
+
# @return [Integer]
|
346
|
+
def index v
|
347
|
+
if v.is_a? Keepassx::Group
|
348
|
+
groups.find_index v
|
349
|
+
|
350
|
+
elsif v.is_a? Keepassx::Entry
|
351
|
+
entries.find_index v
|
352
|
+
|
353
|
+
else
|
354
|
+
fail "Cannot get index for #{v.class}"
|
355
|
+
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
|
360
|
+
# Get Enries and Groups total number
|
361
|
+
#
|
362
|
+
# @return [Integer]
|
363
|
+
def length
|
364
|
+
length = 0
|
365
|
+
[@groups, @entries].each do |items|
|
366
|
+
items.each do |item|
|
367
|
+
length += item.length
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
length
|
372
|
+
end
|
373
|
+
|
374
|
+
|
375
|
+
# Get actual payload checksum
|
376
|
+
#
|
377
|
+
# @return [String]
|
378
|
+
def checksum
|
379
|
+
Digest::SHA256.digest payload
|
380
|
+
end
|
381
|
+
|
382
|
+
|
383
|
+
# Get next group ID number
|
384
|
+
#
|
385
|
+
# @return [Integer]
|
386
|
+
def next_group_id
|
387
|
+
if groups.empty?
|
388
|
+
# Start each time from 1 to make sure groups get the same id's for the
|
389
|
+
# same input data
|
390
|
+
1
|
391
|
+
else
|
392
|
+
id = groups.last.id
|
393
|
+
loop do
|
394
|
+
id += 1
|
395
|
+
break id if groups.detect { |g| g.id.eql? id }.nil?
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
|
401
|
+
# Dump database in XML.
|
402
|
+
#
|
403
|
+
# @return [REXML::Document] XML database representation.
|
404
|
+
def to_xml
|
405
|
+
|
406
|
+
document = REXML::Document.new '<!DOCTYPE KEEPASSX_DATABASE><database/>'
|
407
|
+
|
408
|
+
parent_element = document.root
|
409
|
+
groups.each do |group|
|
410
|
+
# xml = group.to_xml
|
411
|
+
# parent_element = parent_element.add xml if group.parent.nil?
|
412
|
+
section = parent_element.add group.to_xml
|
413
|
+
entries(:group => group).each { |e| section.add e.to_xml }
|
414
|
+
parent_element.add section
|
415
|
+
end
|
416
|
+
|
417
|
+
document
|
418
|
+
end
|
419
|
+
|
420
|
+
|
421
|
+
# Dump Array representation of database.
|
422
|
+
#
|
423
|
+
# @return [Array]
|
424
|
+
def to_a
|
425
|
+
result = []
|
426
|
+
groups(:level => 0).each { |group| result << build_branch(group) }
|
427
|
+
result
|
428
|
+
end
|
429
|
+
|
430
|
+
end
|
431
|
+
end
|