passvault 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,124 @@
1
+ module Rights
2
+
3
+ # Used in communication settings in place of a key, to indicate that an
4
+ # authentication with the key being changed should be performed in order to
5
+ # change a key.
6
+ CHANGED_KEY = 0xE
7
+
8
+ # Used in communication settings in place of a key, to indicate that a key
9
+ # cannot be changed.
10
+ IMMUTABLE = 0xF
11
+
12
+ # The key settings determine the level of authentication needed to perform
13
+ # operations on the tag (DESFire) top-level, or on an application.
14
+ class KeySettings
15
+
16
+ # Is the application/tag master key modifiable? Takes values 0 or 1.
17
+ attr_reader :master_change
18
+
19
+ # Can the commands (+GetFileIDs+, +GetFileSettings+, +GetKeySettings+) /
20
+ # (+GetApplicationIDs+, +GetKeySettings+) be performed without application/tag
21
+ # master key authentication? Takes values 0 or 1.
22
+ attr_reader :auth_get
23
+
24
+ # Can files/applications be created or deleted / created without
25
+ # application/tag master key authentication? Takes values 0 or 1.
26
+ attr_reader :auth_create
27
+
28
+ # Can the key settings for this application/tag be changed again? Takes values
29
+ # 0 or 1.
30
+ attr_reader :settings_change
31
+
32
+ # Only defined for application keys settings. Defines the key needed to change
33
+ # application keys. It can be an application key number between 0x0 and 0xE
34
+ # (inclusive, 0x0 being the application master key) or one of the constants
35
+ # +CHANGED_KEY+ (authenticated with the key that will be changed) or
36
+ # +IMMUTABLE+ (keys cannot be changed) (constants defined in the +Rights+
37
+ # module). Takes a value +key+ such that (0x0 <= +key+ <= 0xF).
38
+ attr_reader :change_key
39
+
40
+ # The constructor takes a hash of parameters, which correspond to the class
41
+ # attributes.
42
+ def initialize(hash={})
43
+ @master_change = hash[:master_change] || 1
44
+ @auth_get = hash[:auth_get] || 1
45
+ @auth_create = hash[:auth_create] || 0
46
+ @settings_change = hash[:settings_change] || 1
47
+ @change_key = hash[:change_key] || 0x0
48
+ end
49
+
50
+ # Creates a +KeySettings+ object from a byte representing the key settings.
51
+ def self.from_byte(byte)
52
+ KeySettings.new(
53
+ :master_change => (byte & 0x1) % 2,
54
+ :auth_get => (byte & 0x2) % 2,
55
+ :auth_create => (byte & 0x4) % 2,
56
+ :settings_change => (byte & 0x8) % 2,
57
+ :change_key => byte >> 4)
58
+ end
59
+
60
+ # Returns a byte representing the key settings, fit to be used as a
61
+ # command parameter.
62
+ def to_byte
63
+ 1 * @master_change +
64
+ 2 * @auth_get +
65
+ 4 * @auth_create +
66
+ 8 * @settings_change +
67
+ 16 * @change_key
68
+ end
69
+
70
+ # Can key +changing_key+ of application +aid+ be changed by authenticating
71
+ # with key +change_key+?
72
+ def can_change_key?(aid, changing_key, change_key)
73
+ if aid == 0
74
+ changing_key == 0 && change_key == 0
75
+ elsif @change_key == IMMUTABLE
76
+ false
77
+ elsif @change_key == CHANGED_KEY
78
+ change_key == changing_key
79
+ elsif @change_key == changing_key
80
+ change_key == 0
81
+ else
82
+ change_key == @change_key
83
+ end
84
+ end
85
+
86
+ # Can we perform the application-level +GetFileIDs+, +GetFileSettings+ and
87
+ # +GetKeySettings+ commands if authenticated with the given key on the given
88
+ # application?
89
+ def can_get?(key_no)
90
+ @auth_get == 1 or key_no == 0
91
+ end
92
+
93
+ # Can we perform the top-level +GetApplicationIDs+ and
94
+ # +GetKeySettings+) commands if authenticated with the given key?
95
+ def can_get0?(aid, key_no)
96
+ aid == 0 and (@auth_get == 1 or key_no == 0)
97
+ end
98
+
99
+ # Can we create or delete a file if authenticated with the given key?
100
+ def can_edit_file?(key_no)
101
+ @auth_create == 1 or key_no == 0
102
+ end
103
+
104
+ # Can we create an application if authenticated with the given key on the
105
+ # given application?
106
+ def can_create_app?(aid, key_no)
107
+ aid == 0 and (@auth_create = 1 or key_no = 0)
108
+ end
109
+
110
+ # Can we delete an application if authenticated with the given key on the given
111
+ # application?
112
+ def can_delete_app?(aid, key_no)
113
+ aid == 0 and key_no == 0
114
+ end
115
+
116
+ # Can we change this key settings on the tag's currently selected application
117
+ # if authenticated with the given key?
118
+ def can_change_key_settings?(key_no)
119
+ @settings_change and key_no == 0
120
+ end
121
+
122
+ end
123
+
124
+ end
@@ -0,0 +1,6 @@
1
+ # This module contains classes that represent permission for various parts of
2
+ # the applications.
3
+ module Rights ; end
4
+
5
+ require_relative 'file_access'
6
+ require_relative 'key_settings'
@@ -0,0 +1,357 @@
1
+ require_relative 'connection'
2
+ require_relative 'crypto'
3
+ require_relative 'rights'
4
+
5
+ # The Vault class represents a vault stored on a tag. It implements all
6
+ # operations that can be performed on the vault.
7
+ #
8
+ # A vault stores credentials as <tt>[name, login, pass, hmac]</tt>
9
+ # quadruplets. When we say "a credential" we refer to this quadruplet. The name
10
+ # is ciphered with AES. The later three elements are ciphered together using
11
+ # AES. The three first elements are padded with zeroes to attain their mandated
12
+ # length (see constants). The HMAC is computed on unpadded (name || login ||
13
+ # pass). The three keys are derives from the vault password using PKBDF2,
14
+ # altough they use different salt. All the salt include the tag UID, and the
15
+ # salt for the ciphering of the last three elements include the credential name.
16
+ #
17
+ # For a credential, 112 bytes of data are stored of memory. This is a multiple
18
+ # of 8, so no padding is required in AES.
19
+ #
20
+ # When a vault object is initialized, it scans the tag to get a map from
21
+ # credential names to the file where the credential is stored. It also builds
22
+ # lists of unused applications ids and of unused file ids on existing
23
+ # applications.
24
+ class Vault
25
+ include Crypto
26
+ include Rights
27
+
28
+ # This is the default tag master key, and the initial key for new
29
+ # applications.
30
+ ZERO_KEY = Array.new(16, 0).pack('C*')
31
+
32
+ # These are the access rights we use when creating a new file.
33
+ FILE_RIGHTS = FileAccess.new(
34
+ read: 0, write: 0, rw: 0, change: 0, com_set: CS_CIPHERED)
35
+
36
+ # These are the key settings for the tag master key.
37
+ # This is set when creating the vault.
38
+ TAG_KEY_SETTINGS = KeySettings.new(change_key: 0x0,
39
+ master_change: 1, auth_get: 1, auth_create: 1, settings_change: 1)
40
+
41
+ # These are the key settings for new applications.
42
+ VAULT_KEY_SETTINGS = KeySettings.new(
43
+ master_change: 1, auth_get: 0, auth_create: 0, settings_change: 0)
44
+
45
+ # Number of files that a DESFire application can hold.
46
+ NB_FILES = 15
47
+
48
+ # The length of the name of a credential in the tag's memory. This is the
49
+ # maximum length for a name, and a padding of nul characters is added to reach
50
+ # this length if needed.
51
+ NAME_LEN = 16
52
+
53
+ # The length of the login in a credential in the tag's memory. This is the
54
+ # maximum length for a login, and a padding of nul characters is added to
55
+ # reach this length if needed.
56
+ LOGIN_LEN = 32
57
+
58
+ # The length of the password in a credential in the tag's memory. This is the
59
+ # maximum length for a password, and a padding of nul characters is added to
60
+ # reach this length if needed.
61
+ PASS_LEN = 32
62
+
63
+ # The total length on a credential in the tag's memory.
64
+ TOTAL_LEN = NAME_LEN + LOGIN_LEN + PASS_LEN + HMAC_LEN
65
+
66
+ # Raised if the tag contents changed while the program was running.
67
+ TAG_CHANGED_ERROR =
68
+ 'Tag content changed while the program was running.'
69
+
70
+ # Raised if the tag memory becomes full.
71
+ TAG_FULL_ERROR =
72
+ 'Tag full, remove some credentials to be able to add other.'
73
+
74
+ # Raised when we are supplied an unknown credential name.
75
+ UNKNOWN_NAME_ERROR =
76
+ 'The credential name is unknown. Use "list" to view existing names.'
77
+
78
+ # Raised if we are supplied an incorrect vault or tag password.
79
+ AUTHENTICATION_ERROR =
80
+ 'The supplied password is incorrect, please try again.'
81
+
82
+ # Raised if we detect that the tag memory has been tempered with.
83
+ TEMPER_ERROR =
84
+ 'The card has been tempered with (invalid signature).'
85
+
86
+ # Text used for credential errors where the name is already used.
87
+ NAME_IN_USE_TEXT =
88
+ 'Credential name already in use, choose another one.'
89
+
90
+ # Thrown when the user supplies an invalid credential (name, login or pass too
91
+ # long, or empty).
92
+ class CredentialError < StandardError ; end
93
+
94
+ public #######################################################################
95
+
96
+ # Erase the vault from the tag and restore it to a pristine state
97
+ # (memory wiped, tag master key set to +ZERO_KEY+).
98
+ def erase(tag_pass)
99
+ deauthenticate()
100
+ tag_key = derive_tag_key(tag_pass)
101
+ authentication { @conn.select_app_auth(0, tag_key) }
102
+ @conn.format()
103
+ @conn.change_key(0, tag_key, ZERO_KEY)
104
+ end
105
+
106
+ # Reset the vault: erases the tag and creates a new vault on it using the
107
+ # given vault master password.
108
+ def reset(tag_pass, vault_pass)
109
+ deauthenticate()
110
+ tag_key = derive_tag_key(tag_pass)
111
+ vault_key = derive_key(vault_pass, @uid, 16)
112
+ authentication { @conn.select_app_auth(0, tag_key) }
113
+ @conn.format()
114
+ @conn.change_key_settings(TAG_KEY_SETTINGS)
115
+ ensure_free_file([], [1], vault_key) # create application 1
116
+ end
117
+
118
+ # Returns a list of credentials names.
119
+ def credentials_names
120
+ return @locations.each_key()
121
+ end
122
+
123
+ # Returns the <tt>[login, pass]</tt> from the named credential.
124
+ def credential(name)
125
+ _ ,_ ,content = get_location(name)
126
+ triplet_aes = content.byteslice(NAME_LEN, LOGIN_LEN + PASS_LEN + HMAC_LEN)
127
+ triplet = aes_decipher(triplet_aes, derive_aes_key(name))
128
+
129
+ login = triplet.byteslice(0, LOGIN_LEN).tr("\x00", '')
130
+ pass = triplet.byteslice(LOGIN_LEN, PASS_LEN).tr("\x00", '')
131
+ hmac = triplet.byteslice(LOGIN_LEN + PASS_LEN, HMAC_LEN)
132
+
133
+ raise TEMPER_ERROR if hmac != hmac(name + login + pass, derive_aes_key('hmac'))
134
+ return [login, pass]
135
+ end
136
+
137
+ # Adds a new credential to the vault.
138
+ def add(name, login, pass)
139
+ ensure_free_file(@free_files, @free_apps, @key)
140
+ aid, fid = @free_files.first
141
+ raise CredentialError.new(NAME_IN_USE_TEXT) unless @locations[name] == nil
142
+ write_credentials(name, login, pass, aid, fid)
143
+ # Do this only after that potential exception have been thrown.
144
+ @locations[name] = @free_files.shift
145
+ end
146
+
147
+ # Replace the login and password for the named credential.
148
+ def edit(name, login, pass)
149
+ aid, fid = get_location(name)
150
+ write_credentials(name, login, pass, aid, fid)
151
+ end
152
+
153
+ # Remove the named credential from the vault.
154
+ def remove(name)
155
+ aid, fid, _ = get_location(name)
156
+ @free_files << [aid, fid]
157
+ @locations.delete(name)
158
+ @conn.select_app_auth(aid, @key)
159
+ # We do not remove the file, else the memory would be lost, instead
160
+ # we overwrite the previous credential with zeroes.
161
+ rand1 = Random.new(Random.new_seed).bytes(32)
162
+ rand2 = Random.new(Random.new_seed).bytes(32)
163
+ write_credentials('', rand1, rand2, aid, fid, true)
164
+ end
165
+
166
+ # Set the password and derive the key, then scans the card for available
167
+ # credentials (see <tt>scan()</tt>).
168
+ def authenticate(pass)
169
+ key = derive_desfire_key(pass, @uid)
170
+ authentication { @conn.select_app_auth(1, key) }
171
+ @pass = pass
172
+ @key = key
173
+ @conn.select_app(0)
174
+ scan()
175
+ end
176
+
177
+ # Forget all that is known about the card password, key and content.
178
+ def deauthenticate
179
+ @pass = nil
180
+ @key = nil
181
+ unscan()
182
+ end
183
+
184
+ # Indicate if we know the key to access the card.
185
+ def authenticated?
186
+ return @pass != nil
187
+ end
188
+
189
+ # "Destroy" the +Vault+ object by releasing the connection to the tag. Leaves
190
+ # the object in an unusable state.
191
+ def destroy
192
+ deauthenticate()
193
+ @conn.disconnect
194
+ end
195
+
196
+ private ######################################################################
197
+
198
+ # Creates the +Vault+ object by establishing a connection to the tag,
199
+ # retrieving its UID, and setting some default values.
200
+ def initialize
201
+ @conn = Connection.new
202
+ @uid = @conn.get_uid().pack('C*')
203
+ @continue = true
204
+ @pass = nil
205
+ @key = nil
206
+ end
207
+
208
+ # Try to run some authentication code. In case of failure, catch the throwed
209
+ # symbol and convert it into an exception.
210
+ def authentication(&block)
211
+ raise AUTHENTICATION_ERROR if
212
+ catch(:AUTHENTICATION_ERROR) do
213
+ raise AUTHENTICATION_ERROR if
214
+ catch(:authentication_failed) { block.call() ; false }
215
+ end
216
+ end
217
+
218
+ # * Sets <tt>@locations</tt> to a map from credential names to <tt>[aid,
219
+ # fid]</tt> pair.
220
+ #
221
+ # * Sets <tt>@free_files</tt> to a list of <tt>[aid, fid]</tt>
222
+ # pairs representing unused files.
223
+ #
224
+ # * Sets <tt>@free_apps</tt> to a list of unused applications ids.
225
+ def scan
226
+ @locations = {}
227
+ @free_files = []
228
+ @free_apps = (1..28).to_a
229
+ @conn.get_app_ids().each do |aid|
230
+ @free_apps.delete(aid)
231
+ add_free_files(aid)
232
+ end
233
+ end
234
+
235
+ # Adds the free files in the application +aid+ to @free_files.
236
+ def add_free_files(aid)
237
+ @conn.select_app_auth(aid, @key)
238
+ @free_files += Array.new(NB_FILES, aid).zip(1..NB_FILES)
239
+ @conn.get_file_ids.each { |fid| check_free_file(aid, fid) }
240
+ end
241
+
242
+ # If the file <tt>[aid, fid]</tt> isn't free, remove it from the list of free
243
+ # files and update <tt>@locations</tt> according to the file's content.
244
+ def check_free_file(aid, fid)
245
+ content = @conn.read_whole_file(fid)
246
+ name = get_name(content)
247
+ if name != '' # if not an erased file
248
+ @free_files.delete([aid, fid])
249
+ @locations[name] = [aid, fid]
250
+ end
251
+ end
252
+
253
+ # Get the name of a credential from the stored credential.
254
+ def get_name(content)
255
+ name_ciphered = content.byteslice(0, NAME_LEN)
256
+ name_deciphered = aes_decipher(name_ciphered, derive_aes_key(''))
257
+ return name_deciphered.tr("\x00", '')
258
+ end
259
+
260
+ # Erases all informations registered by <tt>scan()</tt>.
261
+ def unscan
262
+ @locations = nil
263
+ @free_files = nil
264
+ @free_apps = nil
265
+ end
266
+
267
+ # Derives the tag master key from the tag password. The password may have the
268
+ # special value <tt>:default</tt>, in which case +ZERO_KEY+ is used.
269
+ def derive_tag_key(tag_pass)
270
+ return ZERO_KEY if tag_pass == :default
271
+ return derive_desfire_key(tag_pass, @uid)
272
+ end
273
+
274
+ # Returns the memory location where the named credential is stored and the
275
+ # encrypted credential stored there as an <tt>[aid, fid, content]</tt>
276
+ # array. Also authenticates ourselves with the returned aid, and ensures that
277
+ # the tag content did not change since we last scanned the tag.
278
+ def get_location(name)
279
+ raise UNKNOWN_NAME_ERROR if @locations[name] == nil
280
+ aid, fid = @locations[name]
281
+ @conn.select_app_auth(aid, @key)
282
+ content = @conn.read_whole_file(fid)
283
+ tag_name = get_name(content)
284
+ raise TAG_CHANGED_ERROR if name != tag_name
285
+ return [aid, fid, content]
286
+ end
287
+
288
+ # Derives the AES key used to store a credential from the credential name.
289
+ # The credential name is used as salt in addition to the tag's UID. The key is
290
+ # derived from the vault master password.
291
+ def derive_aes_key(name)
292
+ return derive_key(@pass, @uid + name, 32)
293
+ end
294
+
295
+ # Ensure there is an unused <tt>[aid, fid]</tt> pair in <tt>@free_files</tt>
296
+ # available to add a credential to the vault (this does not actually create
297
+ # the file, but might create an application). Note that this does *not* ensure
298
+ # that there is memory left on the vault, as this can only be detected by
299
+ # writing to the vault.
300
+ def ensure_free_file(free_files, free_apps, vault_key)
301
+ return unless free_files.empty?
302
+ raise TAG_FULL_ERROR if free_apps.empty?
303
+
304
+ new_app = free_apps.shift
305
+ @conn.select_app(0)
306
+ @conn.create_app(new_app, 1, VAULT_KEY_SETTINGS)
307
+ @conn.select_app_auth(new_app, ZERO_KEY)
308
+ @conn.change_key(0, ZERO_KEY, vault_key)
309
+ free_files += Array.new(NB_FILES, new_app).zip(1..NB_FILES)
310
+ end
311
+
312
+ # Write the supplied credential to the supplied location on the tag. This
313
+ # does not modify any class variable.
314
+ def write_credentials(name, login, pass, aid, fid, bypass_check=false)
315
+ check_credential_sanity(name, login, pass) unless bypass_check
316
+ @conn.select_app_auth(aid, @key)
317
+
318
+ padded_name = name.ljust(NAME_LEN, "\x00")
319
+ padded_login = login.ljust(LOGIN_LEN, "\x00")
320
+ padded_pass = pass.ljust(PASS_LEN, "\x00")
321
+ hmac_data = hmac(name + login + pass, derive_aes_key('hmac'))
322
+ triplet = padded_login + padded_pass + hmac_data
323
+ ciph_name = aes_encipher(padded_name, derive_aes_key(''))
324
+ ciph_triplet = aes_encipher(triplet, derive_aes_key(name))
325
+ content = ciph_name + ciph_triplet
326
+
327
+ raise TAG_FULL_ERROR if
328
+ catch :OUT_OF_EEPROM_ERROR do
329
+ # If the file already exiting (e.g. when editing it), there is no need
330
+ # to create the file.
331
+ catch :DUPLICATE_ERROR do
332
+ @conn.create_file(fid, FILE_RIGHTS, TOTAL_LEN)
333
+ end
334
+ false
335
+ end
336
+ @conn.write_file(fid, 0, TOTAL_LEN, content)
337
+ end
338
+
339
+ # Check if the supplied name, login and password are neither too long nor too
340
+ # short.
341
+ def check_credential_sanity(name, login, pass)
342
+ if name.length > NAME_LEN
343
+ raise CredentialError.new("Name too long (max #{NAME_LEN} chars.)")
344
+ elsif login.length > LOGIN_LEN
345
+ raise CredentialError.new("Login too long (max #{LOGIN_LEN} chars.)")
346
+ elsif pass.length > PASS_LEN
347
+ raise CredentialError.new("Pass too long (max #{PASS_LEN} chars.)")
348
+ elsif name.length == 0
349
+ raise CredentialError.new('Empty name')
350
+ elsif pass.length == 0
351
+ raise CredentialError.new('Empty pass')
352
+ elsif login.length == 0
353
+ raise CredentialError.new('Empty login')
354
+ end
355
+ end
356
+
357
+ end