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,155 @@
1
+ # One entry: [FIELDTYPE(FT)][FIELDSIZE(FS)][FIELDDATA(FD)]
2
+ # [FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)]...
3
+
4
+ # [ 2 bytes] FIELDTYPE
5
+ # [ 4 bytes] FIELDSIZE, size of FIELDDATA in bytes
6
+ # [ n bytes] FIELDDATA, n = FIELDSIZE
7
+
8
+ # Notes:
9
+ # - Strings are stored in UTF-8 encoded form and are null-terminated.
10
+ # - FIELDTYPE can be one of the following identifiers:
11
+ # * 0000: Invalid or comment block, block is ignored
12
+ # * 0001: UUID, uniquely identifying an entry, FIELDSIZE must be 16
13
+ # * 0002: Group ID, identifying the group of the entry, FIELDSIZE = 4
14
+ # It can be any 32-bit value except 0 and 0xFFFFFFFF
15
+ # * 0003: Image ID, identifying the image/icon of the entry, FIELDSIZE = 4
16
+ # * 0004: Title of the entry, FIELDDATA is an UTF-8 encoded string
17
+ # * 0005: URL string, FIELDDATA is an UTF-8 encoded string
18
+ # * 0006: UserName string, FIELDDATA is an UTF-8 encoded string
19
+ # * 0007: Password string, FIELDDATA is an UTF-8 encoded string
20
+ # * 0008: Notes string, FIELDDATA is an UTF-8 encoded string
21
+ # * 0009: Creation time, FIELDSIZE = 5, FIELDDATA = packed date/time
22
+ # * 000A: Last modification time, FIELDSIZE = 5, FIELDDATA = packed date/time
23
+ # * 000B: Last access time, FIELDSIZE = 5, FIELDDATA = packed date/time
24
+ # * 000C: Expiration time, FIELDSIZE = 5, FIELDDATA = packed date/time
25
+ # * 000D: Binary description UTF-8 encoded string
26
+ # * 000E: Binary data
27
+ # * FFFF: Entry terminator, FIELDSIZE must be 0
28
+ # '''
29
+
30
+ module Keepassx
31
+
32
+ class Entry
33
+
34
+ include Item
35
+
36
+ FIELD_CLASS = Keepassx::EntryField
37
+ FIELD_MAPPING = {
38
+ :title => :title,
39
+ :icon => :imageid,
40
+ :lastmod => :last_mod_time,
41
+ :lastaccess => :last_acc_time,
42
+ :creation => :creation_time,
43
+ :expire => :expiration_time,
44
+ :password => :password,
45
+ :username => :username,
46
+ :uuid => :uuid,
47
+ :url => :url,
48
+ :binary_desc => :binary_desc,
49
+ :binary_data => :binary_data,
50
+ :comment => :notes,
51
+ }
52
+
53
+
54
+ def self.extract_from_payload(header, groups, payload)
55
+ items = []
56
+ header.entry_number.times do
57
+ entry = Entry.new(payload)
58
+ entry.group = groups.detect { |g| g.id.eql? entry.group_id }
59
+ items << entry
60
+ end
61
+ items
62
+ end
63
+
64
+
65
+ def self.fields
66
+ FIELD_MAPPING.keys
67
+ end
68
+
69
+
70
+ def initialize(payload)
71
+ super
72
+
73
+ if payload.is_a? StringIO
74
+ decode payload
75
+
76
+ elsif payload.is_a? Hash
77
+ fail "'title' is required" if payload[:title].nil?
78
+ fail "'group' is required" if payload[:group].nil?
79
+ self.group = payload[:group]
80
+
81
+ fields = self.class.fields
82
+ data = payload.reject { |k, _| !fields.include? k }
83
+ data[:group_id] = group.id
84
+
85
+ @fields = []
86
+ default_fields.merge(data).each do |k, v|
87
+ fail "Unknown field: '#{k}'" unless self.respond_to? "#{k}=", true
88
+ @fields << self.send("#{k}=", v)
89
+ end
90
+
91
+ else
92
+ fail "Expecting StringIO or Hash, got #{payload.class}"
93
+ end
94
+ end
95
+
96
+
97
+ attr_reader :group
98
+
99
+
100
+ def group= v
101
+ if v.is_a? Keepassx::Group
102
+ self.group_id = v.id
103
+ @group = v
104
+ else
105
+ fail "Expected Keepassx::Group, got #{v.class}"
106
+ end
107
+ end
108
+
109
+
110
+ def group_id
111
+ get :groupid
112
+ end
113
+
114
+
115
+ FIELD_MAPPING.each do |method, field|
116
+ define_method method do
117
+ get field
118
+ end
119
+
120
+ define_method "#{method}=" do |v|
121
+ set field, v
122
+ end
123
+ end
124
+
125
+
126
+ private
127
+
128
+ def default_fields
129
+ @default_fields ||= {
130
+ :uuid => SecureRandom.uuid,
131
+ :group_id => nil,
132
+ :icon => 1,
133
+ :title => nil,
134
+ :url => nil,
135
+ :username => nil,
136
+ :password => nil,
137
+ :comment => nil,
138
+ :creation => Time.now,
139
+ :lastmod => Time.now,
140
+ :lastaccess => Time.now,
141
+ :expire => Time.local(2999, 12, 28, 23, 59, 59),
142
+ :binary_desc => nil,
143
+ :binary_data => nil,
144
+ :terminator => nil
145
+ }
146
+ end
147
+
148
+
149
+ # Keep this method private in order to avoid group/group_id divergence
150
+ def group_id= v
151
+ set :groupid, v
152
+ end
153
+
154
+ end
155
+ end
@@ -0,0 +1,27 @@
1
+ module Keepassx
2
+ class EntryField
3
+ include Field
4
+
5
+ FIELD_TYPES = [
6
+ [0x0, 'ignored', :null],
7
+ [0x1, 'uuid', :ascii],
8
+ [0x2, 'groupid', :int],
9
+ [0x3, 'imageid', :int],
10
+ [0x4, 'title', :string],
11
+ [0x5, 'url', :string],
12
+ [0x6, 'username', :string],
13
+ [0x7, 'password', :string],
14
+ [0x8, 'notes', :string],
15
+ [0x9, 'creation_time', :date],
16
+ [0xa, 'last_mod_time', :date],
17
+ [0xb, 'last_acc_time', :date],
18
+ [0xc, 'expiration_time', :date],
19
+ [0xd, 'binary_desc', :string],
20
+ [0xe, 'binary_data', :shunt],
21
+ [0xFFFF, 'terminator', :null]
22
+ ]
23
+
24
+ attr_reader :name, :data_type, :type_code
25
+
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ module Keepassx
2
+ class HashError < StandardError; end
3
+ class MalformedDataError < StandardError; end
4
+ end
@@ -0,0 +1,205 @@
1
+ module Keepassx
2
+
3
+ module Field
4
+
5
+ # FIELD_TERMINATOR = 0xFFFF # FIXME: Use it or remove it
6
+ TYPE_CODE_FIELD_SIZE = 2 # unsigned short integer
7
+ DATA_LENGTH_FIELD_SIZE = 4 # unsigned integer
8
+
9
+
10
+ def initialize(payload)
11
+
12
+ if payload.is_a? StringIO
13
+
14
+ @type_code, data_length = payload.read(
15
+ TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE).unpack('SI')
16
+ @name, @data_type = parse_type_code
17
+ fail "Failed to determine field name from '#{payload.inspect}'" if
18
+ @name.nil?
19
+ fail "Failed to determine field typefrom '#{payload.inspect}'" if
20
+ @data_type.nil?
21
+ # Not using setter because it should be raw data here
22
+ @data = payload.read(data_length) unless terminator?
23
+ set_export_import_methods
24
+
25
+ elsif payload.is_a? Hash
26
+ @name = payload[:name].to_s
27
+ type_set = self.class::FIELD_TYPES.detect do |_, name, _|
28
+ name.eql? @name.to_s
29
+ end
30
+ @type_code, _, @data_type = type_set
31
+
32
+ set_export_import_methods
33
+ self.data = payload[:data]
34
+ end
35
+
36
+ rescue NoMethodError => e
37
+ raise Keepassx::MalformedDataError.
38
+ new "Read error at #{payload.lineno}" if e.name.eql? :unpack
39
+ # fail
40
+ end
41
+
42
+
43
+ def encode
44
+ buffer = [@type_code, size].pack 'SI'
45
+ buffer << @data unless @data.nil? # Writing raw data
46
+ # [buffer].pack "a#{size}"
47
+ buffer
48
+ end
49
+
50
+
51
+ def terminator?
52
+ @name.eql? 'terminator'
53
+ end
54
+
55
+
56
+ # Return raw data length
57
+ def length
58
+ TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE + size
59
+ end
60
+
61
+
62
+ def size
63
+ case @data_type
64
+ when :null
65
+ 0
66
+ when :int
67
+ 4
68
+ when :date
69
+ 5
70
+ when :uuid
71
+ 16
72
+ else
73
+ (@data.nil?) && 0 || @data.length
74
+ end
75
+ end
76
+
77
+
78
+ def data
79
+ self.send @export_method
80
+ end
81
+
82
+
83
+ def data= value
84
+ self.send @import_method, value
85
+ end
86
+
87
+
88
+ def raw
89
+ @data
90
+ end
91
+
92
+
93
+ private
94
+
95
+ def set_export_import_methods
96
+ @export_method = "#{@data_type}".to_sym
97
+ @import_method = "#{@data_type}=".to_sym
98
+ end
99
+
100
+
101
+ def parse_type_code
102
+ (_, name, data_type) = self.class::FIELD_TYPES.detect do |(code, *rest)|
103
+ code == @type_code
104
+ end
105
+ [name, data_type]
106
+ end
107
+
108
+
109
+ def string
110
+ @data.chomp("\000")
111
+ end
112
+
113
+
114
+ def int
115
+ @data.unpack('I')[0]
116
+ end
117
+
118
+
119
+ def short
120
+ @data.unpack('S')[0]
121
+ end
122
+
123
+
124
+ def ascii
125
+ # TODO: Add spec
126
+ @data.unpack('H*')[0]
127
+ end
128
+
129
+
130
+ def date
131
+ buffer = @data.unpack('C5')
132
+ year = (buffer[0] << 6) | (buffer[1] >> 2)
133
+ month = ((buffer[1] & 0b11) << 2) | (buffer[2] >> 6)
134
+ day = ((buffer[2] & 0b111111) >> 1)
135
+ hour = ((buffer[2] & 0b1) << 4) | (buffer[3] >> 4)
136
+ min = ((buffer[3] & 0b1111) << 2) | (buffer[4] >> 6)
137
+ sec = ((buffer[4] & 0b111111))
138
+
139
+ Time.local(year, month, day, hour, min, sec)
140
+ end
141
+
142
+
143
+ def null
144
+ nil
145
+ end
146
+
147
+
148
+ def shunt
149
+ @data
150
+ end
151
+
152
+
153
+ def string= value
154
+ @data = "#{value}\000"
155
+ # @data.force_encoding 'ASCII-8BIT' if @data.respond_to? :force_encoding
156
+ end
157
+
158
+
159
+ def int= value
160
+ @data = [value].pack('I')
161
+ end
162
+
163
+
164
+ def short= value
165
+ @data = [value].pack('S')
166
+ end
167
+
168
+
169
+ def ascii= value
170
+ @data = [value].pack('H*')
171
+ end
172
+
173
+
174
+ def date= value
175
+ fail "Expected: Time, String or Fixnum, got: '#{value.class}'." unless
176
+ [Time, String, Fixnum].include? value.class
177
+
178
+ value = Time.parse value if value.is_a? String
179
+ value = Time.at value if value.is_a? Fixnum
180
+
181
+ sec, min, hour, day, month, year = value.to_a
182
+ @data = [
183
+ 0x0000FFFF & ((year >> 6) & 0x0000003F),
184
+ 0x0000FFFF & (((year & 0x0000003f) << 2) |
185
+ ((month >> 2) & 0x00000003)),
186
+ 0x0000FFFF & (((month & 0x00000003) << 6) |
187
+ ((day & 0x0000001F) << 1) | ((hour >> 4) & 0x00000001)),
188
+ 0x0000FFFF & (((hour & 0x0000000F) << 4) |
189
+ ((min >> 2) & 0x0000000F)),
190
+ 0x0000FFFF & (((min & 0x00000003) << 6) | (sec & 0x0000003F))
191
+ ].pack('<C5')
192
+ end
193
+
194
+
195
+ def null= _
196
+ @data = nil
197
+ end
198
+
199
+
200
+ def shunt= value
201
+ @data = value
202
+ end
203
+
204
+ end
205
+ end
@@ -0,0 +1,138 @@
1
+ # One group: [FIELDTYPE(FT)][FIELDSIZE(FS)][FIELDDATA(FD)]
2
+ # [FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)]...
3
+ #
4
+ # [ 2 bytes] FIELDTYPE
5
+ # [ 4 bytes] FIELDSIZE, size of FIELDDATA in bytes
6
+ # [ n bytes] FIELDDATA, n = FIELDSIZE
7
+ #
8
+ # Notes:
9
+ # - Strings are stored in UTF-8 encoded form and are null-terminated.
10
+ # - FIELDTYPE can be one of the following identifiers:
11
+ # * 0000: Invalid or comment block, block is ignored
12
+ # * 0001: Group ID, FIELDSIZE must be 4 bytes
13
+ # It can be any 32-bit value except 0 and 0xFFFFFFFF
14
+ # * 0002: Group name, FIELDDATA is an UTF-8 encoded string
15
+ # * 0003: Creation time, FIELDSIZE = 5, FIELDDATA = packed date/time
16
+ # * 0004: Last modification time, FIELDSIZE = 5, FIELDDATA = packed date/time
17
+ # * 0005: Last access time, FIELDSIZE = 5, FIELDDATA = packed date/time
18
+ # * 0006: Expiration time, FIELDSIZE = 5, FIELDDATA = packed date/time
19
+ # * 0007: Image ID, FIELDSIZE must be 4 bytes
20
+ # * 0008: Level, FIELDSIZE = 2
21
+ # * 0009: Flags, 32-bit value, FIELDSIZE = 4
22
+ # * FFFF: Group entry terminator, FIELDSIZE must be 0
23
+ module Keepassx
24
+
25
+ class Group
26
+
27
+ include Item
28
+
29
+ FIELD_CLASS = Keepassx::GroupField
30
+ FIELD_MAPPING = {
31
+ :id => :groupid,
32
+ :title => :group_name,
33
+ :icon => :imageid,
34
+ :lastmod => :lastmod_time,
35
+ :lastaccess => :lastacc_time,
36
+ :creation => :creation_time,
37
+ :expire => :expire_time,
38
+ :level => :level,
39
+ :flags => :flags
40
+ }
41
+
42
+
43
+ def self.extract_from_payload(header, payload_io)
44
+ groups = []
45
+ header.group_number.times { groups << Group.new(payload_io) }
46
+ groups
47
+ end
48
+
49
+ # return
50
+ def self.fields
51
+ FIELD_MAPPING.keys
52
+ end
53
+
54
+
55
+ attr_reader :parent
56
+
57
+
58
+ def initialize payload
59
+ super
60
+
61
+ if payload.is_a? StringIO
62
+ decode payload
63
+
64
+ elsif payload.is_a? Hash
65
+ fail "'title' is required" if payload[:title].nil?
66
+ fail "'id' is required" if payload[:id].nil?
67
+
68
+ group_parent = payload.delete :parent
69
+ @fields = []
70
+ default_fields.merge(payload).each do |k, v|
71
+ @fields << self.send("#{k.to_s}=", v)
72
+ end
73
+
74
+ self.parent = group_parent
75
+
76
+ else
77
+ fail "Expected StringIO or Hash, got #{payload.class}"
78
+ end
79
+ end
80
+
81
+
82
+ FIELD_MAPPING.each do |method, field|
83
+ define_method method do
84
+ get field
85
+ end
86
+
87
+ define_method "#{method}=" do |v|
88
+ set field, v
89
+ end
90
+ end
91
+
92
+
93
+ def parent= v
94
+ if v.is_a? Keepassx::Group
95
+ self.level = v.level + 1 # FIXME: If parent is nil, then set level to 0
96
+ @parent = v
97
+
98
+ elsif v.nil?
99
+ self.level = 0 # Assume group located on top level if has no parent
100
+
101
+ else
102
+ fail "Expected Keepassx::Group, got #{v.class}"
103
+ end
104
+ end
105
+
106
+
107
+ def level
108
+ value = get :level
109
+ value.nil? ? 0 : value
110
+ end
111
+
112
+
113
+ private
114
+
115
+ def level= v
116
+ set :level, v
117
+ end
118
+
119
+
120
+ def default_fields
121
+ @default_field ||= {
122
+ :id => :unknown,
123
+ :title => :unknown,
124
+ :icon => 1,
125
+ # Group's timestamps does not make sense to me,
126
+ # hence removing that from defaults
127
+ # :creation => Time.now,
128
+ # :lastmod => Time.now,
129
+ # :lastaccess => Time.now,
130
+ # :expire => Time.local(2999, 12, 28, 23, 59, 59),
131
+ :level => 0,
132
+ :flags => 0,
133
+ :terminator => nil
134
+ }
135
+ end
136
+
137
+ end
138
+ end