passvault 0.1.2

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