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.
- data/.gitignore +3 -0
- data/Laurent_Vansteenberghe.zip +0 -0
- data/README.md +65 -0
- data/bin/passvault +3 -0
- data/bin/passvault-gui +3 -0
- data/lib/bytes_manip.rb +47 -0
- data/lib/conn_cmds.rb +337 -0
- data/lib/conn_constants.rb +116 -0
- data/lib/conn_init.rb +92 -0
- data/lib/conn_transmit.rb +96 -0
- data/lib/connection.rb +28 -0
- data/lib/console_ui.rb +243 -0
- data/lib/crypto.rb +146 -0
- data/lib/file_access.rb +103 -0
- data/lib/gui.rb +270 -0
- data/lib/key_settings.rb +124 -0
- data/lib/rights.rb +6 -0
- data/lib/vault.rb +357 -0
- data/passvault.gemspec +30 -0
- data/pres/secu.pdf +0 -0
- data/pres/secu.pptx +0 -0
- metadata +136 -0
data/.gitignore
ADDED
Binary file
|
data/README.md
ADDED
@@ -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.
|
data/bin/passvault
ADDED
data/bin/passvault-gui
ADDED
data/lib/bytes_manip.rb
ADDED
@@ -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
|
data/lib/conn_cmds.rb
ADDED
@@ -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
|