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.
@@ -0,0 +1,22 @@
1
+ module Keepassx
2
+ class GroupField
3
+ include Field
4
+
5
+ FIELD_TYPES = [
6
+ [0x0, 'ignored', :null],
7
+ [0x1, 'groupid', :int],
8
+ [0x2, 'group_name', :string],
9
+ [0x3, 'creation_time', :date],
10
+ [0x4, 'lastmod_time', :date],
11
+ [0x5, 'lastacc_time', :date],
12
+ [0x6, 'expire_time', :date],
13
+ [0x7, 'imageid', :int],
14
+ [0x8, 'level', :short],
15
+ [0x9, 'flags', :int],
16
+ [0xFFFF, 'terminator', :null]
17
+ ]
18
+
19
+ attr_reader :name, :data_type, :type_code
20
+
21
+ end
22
+ end
@@ -0,0 +1,178 @@
1
+ # The keepass file header.
2
+ #
3
+ # From the KeePass doc:
4
+ #
5
+ # Database header: [DBHDR]
6
+ #
7
+ # [ 4 bytes] DWORD dwSignature1 = 0x9AA2D903
8
+ # [ 4 bytes] DWORD dwSignature2 = 0xB54BFB65
9
+ # [ 4 bytes] DWORD dwFlags
10
+ # [ 4 bytes] DWORD dwVersion { Ve.Ve.Mj.Mj:Mn.Mn.Bl.Bl }
11
+ # [16 bytes] BYTE{16} aMasterSeed
12
+ # [16 bytes] BYTE{16} aEncryptionIV
13
+ # [ 4 bytes] DWORD dwGroups Number of groups in database
14
+ # [ 4 bytes] DWORD dwEntries Number of entries in database
15
+ # [32 bytes] BYTE{32} aContentsHash SHA-256 hash value of the plain contents
16
+ # [32 bytes] BYTE{32} aMasterSeed2 Used for the dwKeyEncRounds AES
17
+ # master key transformations
18
+ # [ 4 bytes] DWORD dwKeyEncRounds See above; number of transformations
19
+ #
20
+ # Notes:
21
+ #
22
+ # - dwFlags is a bitmap, which can include:
23
+ # * PWM_FLAG_SHA2 (1) for SHA-2.
24
+ # * PWM_FLAG_RIJNDAEL (2) for AES (Rijndael).
25
+ # * PWM_FLAG_ARCFOUR (4) for ARC4.
26
+ # * PWM_FLAG_TWOFISH (8) for Twofish.
27
+ # - aMasterSeed is a salt that gets hashed with the transformed user master key
28
+ # to form the final database data encryption/decryption key.
29
+ # * FinalKey = SHA-256(aMasterSeed, TransformedUserMasterKey)
30
+ # - aEncryptionIV is the initialization vector used by AES/Twofish for
31
+ # encrypting/decrypting the database data.
32
+ # - aContentsHash: "plain contents" refers to the database file, minus the
33
+ # database header, decrypted by FinalKey.
34
+ # * PlainContents = Decrypt_with_FinalKey(DatabaseFile - DatabaseHeader)
35
+ module Keepassx
36
+ class Header
37
+
38
+ ENCRYPTION_FLAGS = [
39
+ [1, 'SHA2'],
40
+ [2, 'Rijndael'],
41
+ [2, 'AES'],
42
+ [4, 'ArcFour'],
43
+ [8, 'TwoFish']
44
+ ]
45
+
46
+ FIELDS = [
47
+ :signature1,
48
+ :signature2,
49
+ :flags,
50
+ :version,
51
+ :master_seed,
52
+ :encryption_iv,
53
+ :group_number,
54
+ :entry_number,
55
+ :contents_hash,
56
+ :master_seed2,
57
+ :rounds
58
+ ]
59
+
60
+ SIGNATURES = [0x9AA2D903, 0xB54BFB65]
61
+
62
+ attr_accessor :group_number, :entry_number, :encryption_iv, :contents_hash
63
+ attr_reader :signature1, :signature2, :flags, :version, :master_seed,
64
+ :master_seed2, :rounds
65
+
66
+
67
+ def initialize(header_bytes = nil)
68
+ if header_bytes.nil?
69
+ self.signature1, self.signature2 = SIGNATURES
70
+ self.flags = 3 # SHA2 hashing, AES encryption
71
+ self.version = 0x30002
72
+
73
+ self.master_seed = SecureRandom.random_bytes(16)
74
+ self.encryption_iv = SecureRandom.random_bytes(16)
75
+ self.master_seed2 = SecureRandom.random_bytes(32)
76
+
77
+ self.group_number = 0
78
+ self.entry_number = 0
79
+ self.rounds = 50000
80
+ else
81
+ header_bytes = StringIO.new header_bytes
82
+ self.signature1 = header_bytes.read(4).unpack('L*').first
83
+ self.signature2 = header_bytes.read(4).unpack('L*').first
84
+ self.flags = header_bytes.read(4).unpack('L*').first
85
+ self.version = header_bytes.read(4).unpack('L*').first
86
+ self.master_seed = header_bytes.read(16)
87
+ self.encryption_iv = header_bytes.read(16)
88
+ self.group_number = header_bytes.read(4).unpack('L*').first
89
+ self.entry_number = header_bytes.read(4).unpack('L*').first
90
+ self.contents_hash = header_bytes.read(32)
91
+ self.master_seed2 = header_bytes.read(32)
92
+ self.rounds = header_bytes.read(4).unpack('L*').first
93
+ end
94
+ end
95
+
96
+
97
+ def length
98
+ length = 0
99
+ FIELDS.each do |field|
100
+ length += field.length
101
+ end
102
+ length
103
+ end
104
+
105
+
106
+ # Return encoded header
107
+ #
108
+ # @return [String] Encoded header representation.
109
+ def encode
110
+ [signature1].pack('L*') <<
111
+ [signature2].pack('L*') <<
112
+ [flags].pack('L*') <<
113
+ [version].pack('L*') <<
114
+ master_seed <<
115
+ encryption_iv <<
116
+ [group_number].pack('L*') <<
117
+ [entry_number].pack('L*') <<
118
+ contents_hash <<
119
+ master_seed2 <<
120
+ [rounds].pack('L*')
121
+
122
+ end
123
+
124
+
125
+ def valid?
126
+ signature1 == SIGNATURES[0] && signature2 == SIGNATURES[1]
127
+ end
128
+
129
+
130
+ def encryption_type
131
+ ENCRYPTION_FLAGS.each do |(flag_mask, encryption_type)|
132
+ return encryption_type if flags & flag_mask
133
+ end
134
+ 'Unknown'
135
+ end
136
+
137
+
138
+ def final_key(master_key, keyfile_data=nil)
139
+ key = Digest::SHA2.new.update(master_key).digest
140
+
141
+ if keyfile_data
142
+ if keyfile_data.size == 64 # Hex encoded key
143
+ keyfile_hash = [keyfile_data].pack("H*")
144
+ elsif keyfile_data.size == 32 # Raw key
145
+ keyfile_hash = keyfile_data
146
+ else
147
+ keyfile_hash = Digest::SHA2.new.update(keyfile_data).digest
148
+ end
149
+
150
+ if master_key == ""
151
+ key = keyfile_hash
152
+ else
153
+ key = Digest::SHA2.new.update(key + keyfile_hash).digest
154
+ end
155
+ end
156
+
157
+ aes = OpenSSL::Cipher::Cipher.new('AES-256-ECB')
158
+ aes.encrypt
159
+ aes.key = master_seed2
160
+ aes.padding = 0
161
+
162
+ rounds.times do
163
+ key = aes.update(key) + aes.final
164
+ end
165
+
166
+ key = Digest::SHA2.new.update(key).digest
167
+ key = Digest::SHA2.new.update(master_seed + key).digest
168
+
169
+ end
170
+
171
+
172
+ private
173
+
174
+ attr_writer :signature1, :signature2, :flags, :version, :master_seed,
175
+ :master_seed2, :rounds
176
+
177
+ end
178
+ end
@@ -0,0 +1,128 @@
1
+ module Keepassx
2
+
3
+ module Item
4
+
5
+
6
+ def initialize _
7
+ @fields = []
8
+ end
9
+
10
+ def length
11
+ @fields.map(&:length).reduce(&:+)
12
+ end
13
+
14
+
15
+ def encode
16
+ buffer = ''
17
+ # FIXME: Check if terminator field not in @fields
18
+ @fields.each do |field|
19
+ buffer << field.encode
20
+ end
21
+ buffer
22
+ end
23
+
24
+
25
+ # TODO: Remove debug method
26
+ def dump
27
+ @fields
28
+ end
29
+
30
+
31
+ def to_hash
32
+ result = {}
33
+ self.class.fields.each do |field|
34
+
35
+ filed = field.to_sym
36
+ if self.respond_to? field
37
+ data = self.send field
38
+ # Skip empty value
39
+ if data.respond_to? :empty?
40
+ result[field] = data unless data.empty?
41
+ else
42
+ unless data.nil?
43
+ # FIXME: Implement field export method
44
+ if data.is_a? Time
45
+ result[field] = data.to_i
46
+ else
47
+ result[field] = data
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+ # warn "result: #{result}"
55
+ result
56
+ end
57
+
58
+
59
+ def terminator= _
60
+ self.class::FIELD_CLASS.new :name => :terminator
61
+ end
62
+
63
+
64
+ def inspect
65
+ output = "<#{self.class} "
66
+ default_fields.each_key do |field_name|
67
+ if field_name.eql? :password
68
+ output << ', password=[FILTERED]'
69
+ else
70
+ output << ", #{field_name}=#{self.send(field_name)}" unless
71
+ field_name.eql? :terminator
72
+ end
73
+ end
74
+ output << '>'
75
+ end
76
+
77
+
78
+ def to_xml
79
+ section_name = (self.is_a? Keepassx::Group) && 'group' || 'entry'
80
+ main_section = REXML::Element.new section_name
81
+ default_fields.each_key do |field_name|
82
+ unless field_name.eql? :terminator
83
+ filed_section = main_section.add_element field_name.to_s
84
+ filed_section.text = self.send field_name
85
+ end
86
+ end
87
+
88
+ main_section
89
+ end
90
+
91
+
92
+ private
93
+
94
+ def decode(buffer)
95
+ @fields = []
96
+ loop do
97
+ field = self.class::FIELD_CLASS.new(buffer)
98
+ @fields << field
99
+ break if field.terminator?
100
+ end
101
+ @fields
102
+ end
103
+
104
+
105
+ def set(name, value)
106
+ field = @fields.detect { |field| field.name.eql? name.to_s }
107
+ if field.nil?
108
+ field = self.class::FIELD_CLASS.new :name => name, :data => value
109
+ else
110
+ field.data = value
111
+ end
112
+ field
113
+ end
114
+
115
+
116
+ def get(name)
117
+ field = @fields.detect { |field| field.name.eql? name.to_s }
118
+ # if field.nil?
119
+ # # FIXME: Put proper field name into 'Field doesn't exists' exception
120
+ # # fail "Field '#{name}' doesn't exists or not yet created"
121
+ # else
122
+ # field.data
123
+ # end
124
+ field.data unless field.nil? # Return nil, if field doesn't exist
125
+ end
126
+
127
+ end
128
+ end
@@ -0,0 +1,226 @@
1
+ module Keepassx
2
+ module Utilities
3
+
4
+
5
+ private
6
+
7
+ # Organize descendants in proper structure for given group.
8
+ # @param [Keepassx::Group] group Root group, branch is build for.
9
+ def build_branch group
10
+ group_hash = group.to_hash
11
+ group_entries = entries :group => group
12
+ descendant_groups = groups :parent => group
13
+
14
+ unless group_entries.nil?
15
+ group_hash[:entries] = []
16
+ entries.each { |e| group_hash[:entries] << e.to_hash }
17
+ end
18
+
19
+ unless descendant_groups.nil?
20
+ group_hash[:groups] = []
21
+ # Recursively build branch for descendant groups
22
+ descendant_groups.each { |g| group_hash[:groups] << build_branch(g) }
23
+ end
24
+
25
+ group_hash
26
+ end
27
+
28
+
29
+ def deep_copy opts
30
+ Marshal.load Marshal.dump opts # Make deep copy
31
+ end
32
+
33
+
34
+ def decrypt
35
+ @payload = AESCrypt.decrypt @encrypted_payload, final_key,
36
+ header.encryption_iv, 'AES-256-CBC'
37
+ current_checksum = checksum
38
+ unless current_checksum.eql? header.contents_hash
39
+ raise Keepassx::HashError.new "Hash test failed, expected " \
40
+ "#{header.contents_hash.inspect}, got #{current_checksum.inspect}"
41
+ end
42
+
43
+ @payload
44
+ end
45
+
46
+
47
+ def encrypt
48
+ @encrypted_payload = AESCrypt.encrypt @payload, final_key,
49
+ header.encryption_iv, 'AES-256-CBC'
50
+ end
51
+
52
+
53
+ def read opts
54
+ read_method = File.respond_to?(:binread) && :binread || :read
55
+ File.send read_method, opts
56
+
57
+ # FIXME: Implement exceptions
58
+ rescue IOError => e
59
+ warn ">>>> IOError in database.rb"
60
+ fail
61
+ rescue SystemCallError => e
62
+ warn ">>>> SystemCallError in database.rb"
63
+ fail
64
+ end
65
+
66
+
67
+ def final_key
68
+ fail "No master password specified" if password.nil?
69
+
70
+ key_file_data = nil
71
+ if File.exists? key_file
72
+ read_method = File.respond_to?(:binread) ? :binread : :read
73
+ key_file_data = File.send read_method
74
+ end unless key_file.nil?
75
+
76
+ header.final_key(password, key_file_data)
77
+ end
78
+
79
+
80
+ def initialize_database raw_db
81
+ if raw_db.empty?
82
+ @header = Header.new
83
+ @locked = false
84
+ else
85
+ @header = Header.new raw_db[0..124]
86
+ @encrypted_payload = raw_db[124..-1]
87
+ end
88
+
89
+ @locked
90
+ end
91
+
92
+
93
+ # Set parents for groups
94
+ #
95
+ # @param [Array] list Array of groups.
96
+ # @return [Array] Updated array of groups.
97
+ def initialize_groups list
98
+
99
+ list.each_with_index do |group, index|
100
+
101
+ if index.eql? 0
102
+ previous_group = nil
103
+ else
104
+ previous_group = list[index - 1]
105
+ end
106
+
107
+ # If group is first entry or has level equal 0,
108
+ # it gets parent set to nil
109
+ if previous_group.nil? or group.level.eql? 0
110
+ group.parent = nil
111
+
112
+ # If group has level greater than parent's level by one,
113
+ # it gets parent set to the first previous group with level less
114
+ # than group's level by one
115
+ elsif group.level == previous_group.level + 1 or
116
+ group.level == previous_group.level
117
+
118
+ group.parent = previous_group
119
+
120
+ # If group has level less than or equal the level of the previous
121
+ # group and its level is no less than zero, then need to backward
122
+ # search for the first group which level is less than group's
123
+ # level by 1 and set it as a parent of the group
124
+ elsif 0 < group.level and group.level <= previous_group.level
125
+ group.parent = (index - 2).downto 0 do |i|
126
+ parent_candidate = list[i]
127
+ break parent_candidate if parent_candidate.level - 1 == group.level
128
+ end
129
+
130
+ # Invalid level
131
+ else
132
+ fail "Unexpected level '#{group.level}' for group '#{group.title}'"
133
+ end
134
+
135
+ end
136
+
137
+ @groups = list
138
+ end
139
+
140
+
141
+ def initialize_payload
142
+ result = ''
143
+ @groups.each { |group| result << group.encode }
144
+ @entries.each { |entry| result << entry.encode }
145
+ result
146
+ end
147
+
148
+
149
+ def payload
150
+ @payload ||= initialize_payload
151
+ end
152
+
153
+
154
+ # Retrieves last sibling index
155
+ #
156
+ # @param [Keepassx::Group] parent Last sibling group.
157
+ # @return [Integer] index Group index.
158
+ def last_sibling_index parent
159
+
160
+ if groups.empty?
161
+ return -1
162
+
163
+ elsif parent.nil?
164
+ parent_index = 0
165
+ sibling_level = 1
166
+
167
+ else
168
+ parent_index = groups.find_index parent
169
+ sibling_level = parent.level + 1
170
+
171
+ end
172
+
173
+ fail "Could not find group #{parent.title}" if parent_index.nil?
174
+
175
+ (parent_index..(header.group_number - 1)).each do |i|
176
+ break i unless groups[i].level.eql? sibling_level
177
+ end
178
+
179
+ end
180
+
181
+
182
+ # FIXME: Rename the method
183
+ # See spec/fixtures/test_data_array.yaml for data example
184
+ def parse_data_array opts
185
+ groups, entries = opts[:groups], opts[:entries]
186
+
187
+ # Remove groups and entries from options, so new group could be
188
+ # initialized from incoming Hash
189
+ fields = Keepassx::Group.fields
190
+ group_opts = opts.reject { |k, _| !fields.include? k }
191
+ group = add_group group_opts
192
+
193
+ entries.each do |e|
194
+ entry = e.clone
195
+ add_entry entry.merge(:group => group)
196
+ end unless entries.nil?
197
+
198
+ # Recursively proceed each child group
199
+ groups.each { |g| parse_data_array g } unless groups.nil?
200
+ end
201
+
202
+
203
+ def delete_entry item
204
+ item = entries.delete item
205
+ header.entry_number -= 1
206
+ item
207
+ end
208
+
209
+
210
+ # Recursively delete group
211
+ def delete_group item
212
+ group_entries = entries.select { |e| e.group.equal? item }
213
+ # Delete entries, which belongs to group
214
+ group_entries.each { |e| delete_entry e }
215
+
216
+ group_ancestors = groups.select { |g| g.parent.equal? item }
217
+ # Recursively delete ancestor groups
218
+ group_ancestors.each { |g| delete_group g }
219
+
220
+ item = groups.delete item
221
+ header.group_number -= 1
222
+ item
223
+ end
224
+
225
+ end
226
+ end