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,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