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 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
@@ -0,0 +1,9 @@
1
+ .bundle
2
+ vendor
3
+ .idea
4
+ spec/fixtures/new_database.kdb
5
+ atlassian-ide-plugin.xml
6
+ ruby-keepassx-0.2.0beta1.gem
7
+ *.gem
8
+ Gemfile.lock
9
+ .yardoc
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm ruby-1.8.7@keepassx --create
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.3
5
+ - 2.1.2
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'respect', :github => 'nicolasdespres/respect', :group => :development
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
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ task :default => :spec
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
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