passvault 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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