ruby-keepassx 0.2.0beta11

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