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,3 @@
1
+ *.aux
2
+ *.log
3
+ *.synctex.gz
@@ -0,0 +1,65 @@
1
+ # Passworld Vault
2
+
3
+ ## Manual
4
+
5
+ Before launching the application, ensure the reader is plugged in. If a card is
6
+ not inserted in the reader before launching the application, the application
7
+ will ask you to insert your card.
8
+
9
+ ## System requirements
10
+
11
+ * Ruby 1.9.x (with rubygems)
12
+ * an internet connection to download the dependencies
13
+
14
+ ## Installation
15
+
16
+ Simply unzip passvault.zip, open your system command-line interpreter, go to the
17
+ unzipped directory and type: `gem install passvault-0.1.0.gem` (you might need
18
+ `sudo` in front of that).
19
+
20
+ ## User Guide
21
+
22
+ Our password vault comes with two different user interfaces. One is a command
23
+ line interface and the other is graphical interface. Everyone can use the
24
+ command line interface, but it is the only way to run administration commands.
25
+
26
+ The user interfaces are provided with a timeout system that requires the
27
+ user to re-type its password every five minutes. This mechanism allows to
28
+ prevent a potential attacker to access the vault content if the user is away and
29
+ forgets to close the application properly.
30
+
31
+ Before launching one of the the application, ensure the reader is plugged in. If
32
+ a card is not inserted in the reader before launching the application, the
33
+ application will ask you to insert your card.
34
+
35
+ ### Command-line interface
36
+
37
+ Command: `passvault`
38
+
39
+ Available commands (copy of the inline help):
40
+
41
+ -- user commands --
42
+ list : list the names of registered credentials
43
+ display <name> : display a registered credential
44
+ clip <name> : same as "display", but copies the password to the clipboard
45
+ instead of displaying it
46
+ add <name> : register a new credential'
47
+ remove <name> : remove a registered credential'
48
+ edit <name> : change a registered credential'
49
+ quit : exit the program'
50
+ help : display this help message'
51
+ -- administration commands --
52
+ reset : reinitialize the vault'
53
+ erase : erase the vault from the tag'
54
+
55
+ ### Graphical user interface
56
+
57
+ Command: `passvault-gui`
58
+
59
+ Note: the graphical user interface does not seem to work on Linux.
60
+
61
+ Using our GUI is quite straightforward: when the interface is launched, the user
62
+ is prompted to type its vault password. If its password is correct, the user
63
+ access to the main part of the application, a window that shows the list of the
64
+ entries name currently in the vault. The user simply has to double-click on an
65
+ entry name to consult the associated password and login.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/console_ui'
3
+ ConsoleUI.run
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/gui'
3
+ VaultGUI::GUILauncher.new.main_loop
@@ -0,0 +1,47 @@
1
+ # Convenience methods to manipulate bytes.
2
+ module BytesManipulation
3
+
4
+ # Converts a number to a little-endian array of n bytes.
5
+ def to_le_array(num, n)
6
+ out = Array.new(n)
7
+ out.each_index do |i|
8
+ out[i] = num % 256
9
+ num /= 256
10
+ end
11
+ end
12
+
13
+ # Converts a byte array to an array holding strings representing the numbers
14
+ # in base 16. For debugging purposes.
15
+ def to_hex_array(array)
16
+ array.flatten.map { |e| e.to_s(16) }
17
+ end
18
+
19
+ # Xors together two byte strings of the same length.
20
+ def bs_xor(bs1, bs2)
21
+ fail unless bs1.bytesize == bs2.bytesize
22
+ out = String.new(bs1)
23
+ (0 .. bs1.bytesize - 1).each do |i|
24
+ out.setbyte(i, bs1.getbyte(i) ^ bs2.getbyte(i))
25
+ end
26
+ out
27
+ end
28
+
29
+ # Rotate a byte string by n bytes to the right. n can be negative, and efault
30
+ # to 1.
31
+ def bs_rotate(bs, n=1)
32
+ bs.unpack('C*').rotate(n).pack('C*')
33
+ end
34
+
35
+ # DESFire-specific CRC algorithm taking a byte string as input and returning a
36
+ # byte string of length 2 as output.
37
+ def desfire_crc(data)
38
+ wCRC = 0x6363
39
+ data.each_byte do |byte|
40
+ byte = (byte ^ (wCRC & 0x00FF)) % 256
41
+ byte = (byte ^ (byte << 4)) % 256
42
+ wCRC = ((wCRC >> 8) ^ (byte << 8) ^ (byte << 3) ^ (byte >> 4)) % 65536
43
+ end
44
+ [wCRC & 0xFF, (wCRC >> 8) & 0xFF].pack("C*")
45
+ end
46
+
47
+ end
@@ -0,0 +1,337 @@
1
+ require_relative 'bytes_manip'
2
+ require_relative 'rights'
3
+
4
+ class Connection
5
+ # This file contains functions wrapping card commands. Usually one function
6
+ # implements one card command. However, sometimes performing one "logical"
7
+ # command requires to send multiple card commands (e.g. <tt>authenticate()</tt>).
8
+
9
+ include BytesManipulation
10
+ include Rights
11
+
12
+ # Sets the given key settings (a +KeySettings+ object) for the currently
13
+ # selected application.
14
+ def change_key_settings(new_key_settings)
15
+ fail if
16
+ @key_settings and not @key_settings.can_change_key_settings?(@key_no)
17
+ data = encipher_data([new_key_settings.to_byte].pack('C*'))
18
+ response = send_card_cmd(CHANGE_KEY_SETTINGS, data.unpack('C*'))
19
+ end
20
+
21
+ # Retrieve and returns the key settings (as a +KeySettings+ object) for currently
22
+ # selected application. Sets <tt>@max_keys</tt> and <tt>@key_settings.</tt>
23
+ def get_key_settings
24
+ # We can't check for the permission to perform the operation since
25
+ # this would require knowing the key settings already.
26
+ response = send_card_cmd(GET_KEY_SETTINGS)[0]
27
+ @max_keys = response[1]
28
+ return @key_settings = KeySettings.from_byte(response[0])
29
+ end
30
+
31
+ # Returns the UID of the tag.
32
+ def get_uid
33
+ # GET_VERSION can always be performed without authentication.
34
+ # We don't care about the other information returned by GET_VERSION.
35
+ send_card_cmd(GET_VERSION)
36
+ send_card_cmd(SEND_MORE_DATA)
37
+ response = send_card_cmd(SEND_MORE_DATA)[0]
38
+ return response[1..8]
39
+ end
40
+
41
+ # Retrieve and return the file access rights (as a +FileAccess+ object) for the
42
+ # given file.
43
+ def get_file_rights(file_no)
44
+ response = send_card_cmd(GET_FILE_SETTINGS, [file_no])[0]
45
+ return Rights::FileAccess.from_a(response)
46
+ end
47
+
48
+ # Change key number +key_no+ to be +new_key+, the old key being +old_key+.
49
+ # After a successful change of the key used to reach the current
50
+ # authentication status, that authentication is invalidated: an authentication
51
+ # with the new key is necessary to subsequent operations.
52
+ def change_key(key_no, old_key, new_key)
53
+ fail if
54
+ @key_settings and not @key_settings.can_change_key?(@aid, key_no, @key_no)
55
+ if key_no == @key_no or @key_settings.change_key == Rights::FREE_ACCESS
56
+ data = encipher_data(new_key)
57
+ else
58
+ data = encipher_old_new_data(old_key, new_key)
59
+ end
60
+ send_card_cmd(CHANGE_KEY, [key_no, *data.unpack("C*")])[0]
61
+ end
62
+
63
+ # Returns an array containing all application IDs.
64
+ def get_app_ids
65
+ fail if @key_settings and not @key_settings.can_get0?(@aid, @key_no)
66
+ aids = []
67
+ begin
68
+ response, status = send_card_cmd(GET_APP_IDS)
69
+ response.each_slice(3) do |aid|
70
+ # note: on the DESFire, aid[1] and aid[2] should be 0
71
+ aids << aid[0] + 256 * aid[1] + 256*256 * aid[2]
72
+ end
73
+ end while (status == STATUS[:ADDITIONAL_FRAME])
74
+ return aids
75
+ end
76
+
77
+ # Create an application with given application ID, given number of keys and
78
+ # given key settings. The application should not already exist.
79
+ def create_app(aid, num_keys, key_settings)
80
+ fail if @key_settings and not @key_settings.can_create_app?(@aid, @key_no)
81
+ args = [*to_le_array(aid, 3), key_settings.to_byte, num_keys]
82
+ send_card_cmd(CREATE_APPLICATION, args)[0]
83
+ end
84
+
85
+ # Delete the given application from the tag. Beware that the memory lost will
86
+ # not be reusable before the tag is formatted. It is not advised to use this.
87
+ def delete_app(aid)
88
+ fail if @key_settings and not @key_settings.can_delete_app?(@aid, @key_no)
89
+ send_card_cmd(DELETE_APP, to_le_array(aid, 3))
90
+ end
91
+
92
+ # Returns an array containg all files IDs on the current application.
93
+ def get_file_ids
94
+ fail if @key_settings and not @key_settings.can_get?(@key_no)
95
+ send_card_cmd(GET_FILE_IDS)[0]
96
+ end
97
+
98
+ # Create a new file in the selected application with given access rights,
99
+ # size, and communication settings. The file should not already exists.
100
+ def create_file(file_no, rights, file_size)
101
+ fail if @key_settings and not @key_settings.can_edit_file?(@key_no)
102
+ args = [file_no, rights.com_set, *rights.to_a, *to_le_array(file_size, 3)]
103
+ send_card_cmd(CREATE_STD_DATA_FILE, args)[0]
104
+ end
105
+
106
+ # Delete the given file from the currently selected application from the
107
+ # tag. Beware that the memory lost will not be reusable before the tag is
108
+ # formatted. It is not advised to use this (overwrite the file instead).
109
+ def delete_file(file_no)
110
+ fail if @key_settings and not @key_settings.can_edit_file?(@key_no)
111
+ send_card_cmd(DELETE_FILE, [file_no])
112
+ end
113
+
114
+ # Read the entire file identified by file_no from the selected application.
115
+ def read_whole_file(file_no)
116
+ return read_file(file_no, 0, 0)
117
+ end
118
+
119
+ # Read the up to +length+ bytes in the given file (in the currently selected
120
+ # application), starting at the given offet. If +length+ is 0, the file read
121
+ # up to its end. A byte string is returned.
122
+ def read_file(file_no, offset, length)
123
+ args = [file_no, *to_le_array(offset, 3), *to_le_array(length, 3)]
124
+ response, status = send_card_cmd(READ_DATA, args)
125
+
126
+ data = response
127
+ while status == STATUS[:ADDITIONAL_FRAME]
128
+ response, status= send_card_cmd(SEND_MORE_DATA)
129
+ data += response
130
+ end
131
+ fail 'protocol_error' if length != 0 and data.length != length
132
+ handler = get_access_rights_read_handler(file_no)
133
+ return handler.call(length, data)
134
+ end
135
+
136
+ # Size of the first frame for the write operation.
137
+ WRITE_FFSIZ = 52
138
+
139
+ # Size of the subsequent frame for the write operation.
140
+ WRITE_SFSIZ = 59
141
+
142
+ # Write +length+ bytes of +data+ (a byte string) within the given file (in the
143
+ # currently selected application), starting at the given offset in the file.
144
+ #
145
+ # Pre: +data+ is a byte array 0 <= +length+ <= <tt>data.size</tt> <= 52+59
146
+ def write_file(file_no, offset, length, data)
147
+ handler = get_access_rights_write_handler(file_no)
148
+ # ! the line below may make data.length > length
149
+ data = handler.call(length, data.byteslice(0, length))
150
+
151
+ # The first frame contains at most WRITE_FFSIZ bytes.
152
+ block_size = [data.length, WRITE_FFSIZ].min
153
+ block = data.byteslice(0, block_size).unpack('C*')
154
+ args = [file_no, *to_le_array(offset, 3), *to_le_array(length, 3), *block]
155
+ send_card_cmd(WRITE_DATA, args)
156
+
157
+ # If there is more data to send, send subsequent frames.
158
+ remaining = data.length
159
+ while (remaining -= block.length) > 0
160
+ block_size = [remaining, WRITE_SFSIZ].min
161
+ block = data.byteslice(data.length - remaining, block_size).unpack('C*')
162
+ send_card_cmd(SEND_MORE_DATA, block)
163
+ end
164
+ end
165
+
166
+ # Erase all of the tag memory (except the tag master key).
167
+ def format
168
+ fail unless @aid === 0 && @key_no == 0
169
+ send_card_cmd(FORMAT_PICC)[0]
170
+ end
171
+
172
+ # Selects an application, and performs authentication and key settings
173
+ # retrieval for this application.
174
+ def select_app_auth(aid, key)
175
+ select_app(aid)
176
+ authenticate(0, key)
177
+ get_key_settings()
178
+ end
179
+
180
+ # Select the application with given application ID for future operations.
181
+ def select_app(aid)
182
+ @aid = aid
183
+ send_card_cmd(SELECT_APPLICATION, to_le_array(aid, 3))[0]
184
+ end
185
+
186
+ # Authenticate ourselves with key number `key_no`, which is provided in `key`,
187
+ # authenticate the tag and sets @sesskey. In case of success, @sesskey is set
188
+ # to the derived session key and is returned; @key_no and @key are set to the
189
+ # values of `key_no` and `key`. Any previous authentication is lost when
190
+ # running this function.
191
+ def authenticate(key_no, key)
192
+ # Below, after an underscore, t is for "tag", r is for "reader" (first
193
+ # letter) or for "rotated" (second letter) and c is for "ciphered".
194
+
195
+ # get tag nonce, decipher it, then rotate it
196
+ nonce_tc = send_card_cmd(GET_CIPHERED_NONCE, [key_no])[0].pack('C*')
197
+ nonce_t = decipher_receive(nonce_tc, key)
198
+ nonce_tr = bs_rotate(nonce_t, 1)
199
+
200
+ # pick a nonce
201
+ nonce_r = Random.new(Random.new_seed).bytes(8)
202
+
203
+ # obtain our proof of identity using our key
204
+ proof = decipher_send(nonce_r + nonce_tr, key)
205
+ response = send_card_cmd(SEND_MORE_DATA, proof.unpack('C*')) [0]
206
+ response = response.pack('C*')
207
+
208
+ # verify the tag proof of identity
209
+ proved = bs_rotate(decipher_receive(response, key), -1) == nonce_r
210
+
211
+ # derive and return the session key, or an exception if authentication failed
212
+ if proved
213
+ @key = key
214
+ @key_no = key_no
215
+ if key.byteslice(0, 8) != key.byteslice(8, 8)
216
+ return @sesskey = nonce_r.byteslice(0, 4) + nonce_t.byteslice(0, 4) +
217
+ nonce_r.byteslice(4, 4) + nonce_t.byteslice(4, 4)
218
+ else
219
+ return @sesskey = nonce_r.byteslice(0, 4) + nonce_t.byteslice(0, 4) +
220
+ nonce_r.byteslice(0, 4) + nonce_t.byteslice(0, 4)
221
+ end
222
+ else
223
+ @key = nil
224
+ @key_no = nil
225
+ @sesskey = nil
226
+ throw :authentication_failed, true
227
+ end
228
+ end
229
+
230
+ private ######################################################################
231
+
232
+ # "Enciphers" the given data for the DESFire tag (decipher in 3DES send mode,
233
+ # with CRC and padding).
234
+ def encipher_data(data)
235
+ pad_len = 8 - ((data.length + 2) % 8)
236
+ padded = data + desfire_crc(data) + Array.new(pad_len, 0).pack('C*')
237
+ return decipher_send(padded, @sesskey)
238
+ end
239
+
240
+ # "Enciphers" the given data, given the old value of the data for the DESFire
241
+ # tag (involves 3DES send mode, xoring, two CRCs and padding).
242
+ def encipher_old_new_data(old, new)
243
+ fail unless old.length == new.length
244
+ pad_len = 8 - ((old.length + 4) % 8)
245
+ xored = bs_xor(old, new)
246
+ with_crc = xored + desfire_crc(xored) + desfire_crc(new)
247
+ padded = with_crc + Array.new(pad_len, 0).pack('C*')
248
+ return decipher_send(padded, @sesskey)
249
+ end
250
+
251
+ # Return a function that will extract readable file data from the transmitted
252
+ # file data, according the the file access permissions of the given file of
253
+ # the currently selected application.
254
+ def get_access_rights_read_handler(file_no)
255
+ rights = get_file_rights(file_no)
256
+ read_type = rights.read_type(@key_no)
257
+ fail 'access denied' unless read_type
258
+
259
+ case read_type
260
+ when Rights::CS_PLAIN
261
+ return ->(length, data) { data }
262
+ when Rights:: CS_PLAIN_MAC
263
+ fail 'MAC handling not implemented.'
264
+ when Rights::CS_CIPHERED
265
+ return ->(length, data) { decipher_data(length, data) }
266
+ end
267
+ end
268
+
269
+ # Decipher ciphered data received from the tag, also stripping the padding and
270
+ # the CRC at the end of the deciphered data. (functional)
271
+ def decipher_data(length, data)
272
+ fail unless data.length % 8 == 0
273
+ data = decipher_receive(data.pack('C*'), @sesskey)
274
+ if length == 0
275
+ data = strip_padding_whole_file(data)
276
+ else
277
+ data = strip_padding_partial_file(length + 2, data)
278
+ end
279
+ return remove_crc(data)
280
+ end
281
+
282
+ # Strip the padding at the end of deciphered file data received from the tag,
283
+ # when reading a whole file. (functional)
284
+ def strip_padding_whole_file(data)
285
+ i = data.rindex(WHOLE_FILE_PAD_MARKER)
286
+ return data if i == nil
287
+ pad_len = data.length - (i + 1)
288
+ if data.slice(i+1, pad_len) == Array.new(pad_len, 0).pack('C*')
289
+ return data.slice(0, i)
290
+ else
291
+ return data
292
+ end
293
+ end
294
+
295
+ # Strip the padding at the end of deciphered file data received from the tag,
296
+ # when reading a fixed chunk of a file. (functional)
297
+ def strip_padding_partial_file(length_with_crc, data)
298
+ pad_len = data.length - length_with_crc
299
+ if data.slice(length_with_crc, pad_len) == Array.new(pad_len, 0).pack('C*')
300
+ return data.slice(0, length_with_crc)
301
+ else
302
+ fail 'no padding when expected'
303
+ end
304
+ end
305
+
306
+ # Remove the CRC at the end of deciphered and "unpadded" file
307
+ # data. (functional)
308
+ def remove_crc(data)
309
+ len_with_crc = data.length
310
+ data.chomp!(desfire_crc(data[0..-3]))
311
+ throw Exception.new('Bad CRC.') if len_with_crc - 2 != data.length
312
+ return data
313
+ end
314
+
315
+ # Returns a function that will wrap readable file data in order to send it to
316
+ # the tag, according the the file access permissions of the given file of
317
+ # the currently selected application.
318
+ def get_access_rights_write_handler(file_no)
319
+ rights = get_file_rights(file_no)
320
+ write_type = rights.write_type(@key_no)
321
+ fail 'access denied' unless write_type
322
+
323
+ case write_type
324
+ when Rights::CS_PLAIN
325
+ return ->(length, data) { data }
326
+ when Rights::CS_PLAIN_MAC
327
+ fail 'MAC handling not implemented'
328
+ when Rights::CS_CIPHERED
329
+ return ->(length, data) {
330
+ data += desfire_crc(data)
331
+ data += Array.new(8 - ((length + 2) % 8), 0).pack('C*')
332
+ decipher_send(data, @sesskey)
333
+ }
334
+ end
335
+ end
336
+
337
+ end