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