ruby-keepassx 0.2.0beta11

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