Class: Connection
- Inherits:
-
Smartcard::Iso::PcscTransport
- Object
- Smartcard::Iso::PcscTransport
- Connection
- Includes:
- BytesManipulation, Crypto, Rights
- Defined in:
- lib/connection.rb,
lib/conn_cmds.rb,
lib/conn_init.rb,
lib/conn_transmit.rb,
lib/conn_constants.rb
Overview
Uncommented constants are DESFire comments.
Constant Summary collapse
- WRITE_FFSIZ =
Size of the first frame for the write operation.
52
- WRITE_SFSIZ =
Size of the subsequent frame for the write operation.
59
- CARD_CONTACT_LOST_ERROR =
Thrown when the contact with the card is lost.
'Contact with the tag was lost, please verify that the card is in the ' \ 'reader, then relaunch the application.'
- CARD_CLA =
CLA field for DESFire commands.
0x90
- SW1 =
SW1 field for DESFire answers.
0x91
- STATUS =
Hash from DESFire response codes name (symbols) to the codes (integers).
{ :OPERATION_OK => 0x00, :ADDITIONAL_FRAME => 0xAF, :NO_CHANGES => 0x0C, :ILLEGAL_COMMAND_CODE => 0x1C, :INTEGRITY_ERROR => 0x1E, :NO_SUCH_KEY => 0x40, :LENGTH_ERROR => 0x7E, :PERMISSION_DENIED => 0x9D, :PARAMETER_ERROR => 0x9E, :APPLICATION_NOT_FOUND => 0xA0, :APPLICATION_INTEGRITY_ERROR => 0xA1, :AUTHENTICATION_ERROR => 0xAE, :BOUNDARY_ERROR => 0xBE, :COMMAND_ABORTED => 0xCA, :DUPLICATE_ERROR => 0xDE, :FILE_NOT_FOUND => 0xF0, :OUT_OF_EEPROM_ERROR => 0x0E, }
- CHANGE_KEY =
0xC4
- CREATE_APPLICATION =
0xCA
- CREATE_STD_DATA_FILE =
0xCD
- GET_CIPHERED_NONCE =
0x0A
- SELECT_APPLICATION =
0x5A
- SEND_MORE_DATA =
0xAF
- FORMAT_PICC =
0xFC
- GET_APP_IDS =
0x6A
- GET_FILE_IDS =
0x6F
- DELETE_APP =
0xDA
- DELETE_FILE =
0xDF
- READ_DATA =
0xBD
- WRITE_DATA =
0x3D
- GET_KEY_SETTINGS =
0x45
- GET_FILE_SETTINGS =
0xF5
- GET_VERSION =
0x60
- CHANGE_KEY_SETTINGS =
0x54
- WHOLE_FILE_PAD_MARKER =
Padding beginning marker when reading a whole file with encrypted communication.
"\x80"
- TO_CHIP =
Communication direction: send data to the chip.
0xD4
- FROM_CHIP =
Communicaiton direction: receive data from the chip.
0xD5
- LIST_PASSIVE_TARGET =
PN532 command: poll for tags.
0x4A
- DATA_EXCHANGE =
PN532 command: send data to/from the chip.
0x40
- LIST_RESPONSE =
PN532 response code (prefix).
0x4B
- DATA_RESPONSE =
PN532 data response codes (array) (prefix).
[0x41, 0x00]
- SUCCESS_SUFFIX =
PN532 success response codes (array) (suffix).
[0x90, 0x00]
- READER_CLA =
ACR112: class field for pseudo-APDU commands.
0xFF
- DIRECT_TRANSMIT =
ACR112 pseudo-APDU commands: send data.
0x00
- GET_RESPONSE =
ACR112 pseudo-APDU commands: retrieve an answer.
0xC0
- SUCCESS =
ACR112 response code (SW1 field).
0x61
- ERROR =
ACR112 response code (SW1 field).
0x63
- OPERATION_FAILED =
ACR112 response codes (SW2 field for SW1=ERROR).
0x00
- NO_ANSWER =
ACR112 response codes (SW2 field for SW1=ERROR).
0x01
Constants included from Crypto
Crypto::DES_BLEN, Crypto::HMAC_LEN, Crypto::ZERO_IV
Constants included from Rights
Rights::CHANGED_KEY, Rights::CS_CIPHERED, Rights::CS_PLAIN, Rights::CS_PLAIN_MAC, Rights::DENY_ACCESS, Rights::FREE_ACCESS, Rights::IMMUTABLE
Instance Method Summary collapse
-
#authenticate(key_no, key) ⇒ Object
Authenticate ourselves with key number ‘key_no`, which is provided in `key`, authenticate the tag and sets @sesskey.
-
#change_key(key_no, old_key, new_key) ⇒ Object
Change key number
key_no
to benew_key
, the old key beingold_key
. -
#change_key_settings(new_key_settings) ⇒ Object
Sets the given key settings (a
KeySettings
object) for the currently selected application. -
#create_app(aid, num_keys, key_settings) ⇒ Object
Create an application with given application ID, given number of keys and given key settings.
-
#create_file(file_no, rights, file_size) ⇒ Object
Create a new file in the selected application with given access rights, size, and communication settings.
-
#delete_app(aid) ⇒ Object
Delete the given application from the tag.
-
#delete_file(file_no) ⇒ Object
Delete the given file from the currently selected application from the tag.
-
#disconnect ⇒ Object
Release all resources associated with the card.
-
#format ⇒ Object
Erase all of the tag memory (except the tag master key).
-
#get_app_ids ⇒ Object
Returns an array containing all application IDs.
-
#get_file_ids ⇒ Object
Returns an array containg all files IDs on the current application.
-
#get_file_rights(file_no) ⇒ Object
Retrieve and return the file access rights (as a
FileAccess
object) for the given file. -
#get_key_settings ⇒ Object
Retrieve and returns the key settings (as a
KeySettings
object) for currently selected application. -
#get_uid ⇒ Object
Returns the UID of the tag.
-
#initialize ⇒ Connection
constructor
Connect to a card trough a reader.
-
#poll(max_tags = 1, baud = 0) ⇒ Object
Search for a card.
-
#read_file(file_no, offset, length) ⇒ Object
Read the up to
length
bytes in the given file (in the currently selected application), starting at the given offet. -
#read_whole_file(file_no) ⇒ Object
Read the entire file identified by file_no from the selected application.
-
#select_app(aid) ⇒ Object
Select the application with given application ID for future operations.
-
#select_app_auth(aid, key) ⇒ Object
Selects an application, and performs authentication and key settings retrieval for this application.
-
#send_card_apdu(card_apdu, tag_no = 1) ⇒ Object
Send a card APDU to the card and return the card’s answer.
-
#send_card_cmd(cmd, args = []) ⇒ Object
Send a command to the card and return an array whose first element is the command response (not comprising the card response codes) and the second is a successful status (needed to know if additional frames are available).
-
#send_chip_apdu(chip_apdu) ⇒ Object
Send a chip APDU trough the reader (by wrapping the chip APDU inside a reader pseudo-APDU) and return the chip response.
-
#send_chip_cmd(cmd, args) ⇒ Object
Send a chip command and return the command response (not comprising the chip response codes).
-
#write_file(file_no, offset, length, data) ⇒ Object
Write
length
bytes ofdata
(a byte string) within the given file (in the currently selected application), starting at the given offset in the file.
Methods included from Crypto
#aes_decipher, #aes_encipher, #decipher_receive, #decipher_send, #derive_desfire_key, #derive_key, #encipher_receive, #encipher_send, #hmac
Methods included from BytesManipulation
#bs_rotate, #bs_xor, #desfire_crc, #to_hex_array, #to_le_array
Constructor Details
#initialize ⇒ Connection
Connect to a card trough a reader.
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/conn_init.rb', line 6 def initialize() super({}) # no options @context = PCSC::Context.new # find readers begin reader_names = @context.readers rescue Exception => e puts "No smartcard readers were detected." disconnect exit end # select a single reader if reader_names.length == 1 @reader_name = reader_names.first else puts "Multiple readers available, please select one by number." @reader_name = nil while @reader_name == nil reader_names.each_with_index do |r,i| puts "#{i}) #{r.strip}" end begin @reader_name = reader_names[gets.strip.to_i] rescue puts "Invalid selection." end end end # The following two lines are lifted from the supermethod, since they were # the only thing of interest there, due to idiosyncraties of the ACR122. # The card object is used to transmit data, tough we don't need to use it # directly, as data transmission is wrapped by the exchange_apdu(apdu) # function. @card = @context.card(@reader_name, :shared) # the Answer To Reset for the card @atr = @card.info[:atr] # Wait until a card is inserted in the reader. Normally the super() call # takes care of this, but the ACR122 is a broken beast which always # indicates that a card is present, even if it isn't the case. msg_displayed = false while true begin poll break rescue Smartcard::PCSC::Exception => e status = e.pcsc_status_code if status == Smartcard::PCSC::FFILib::Status[:comm_data_lost] puts "No cards detected, please insert your card." if !msg_displayed msg_displayed = true else puts "error: #{e.}" disconnect exit end end end end |
Instance Method Details
#authenticate(key_no, key) ⇒ Object
Authenticate ourselves with key number ‘key_no`, which is provided in `key`, authenticate the tag and sets @sesskey. In case of success, @sesskey is set to the derived session key and is returned; @key_no and @key are set to the values of `key_no` and `key`. Any previous authentication is lost when running this function.
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/conn_cmds.rb', line 191 def authenticate(key_no, key) # Below, after an underscore, t is for "tag", r is for "reader" (first # letter) or for "rotated" (second letter) and c is for "ciphered". # get tag nonce, decipher it, then rotate it nonce_tc = send_card_cmd(GET_CIPHERED_NONCE, [key_no])[0].pack('C*') nonce_t = decipher_receive(nonce_tc, key) nonce_tr = bs_rotate(nonce_t, 1) # pick a nonce nonce_r = Random.new(Random.new_seed).bytes(8) # obtain our proof of identity using our key proof = decipher_send(nonce_r + nonce_tr, key) response = send_card_cmd(SEND_MORE_DATA, proof.unpack('C*')) [0] response = response.pack('C*') # verify the tag proof of identity proved = bs_rotate(decipher_receive(response, key), -1) == nonce_r # derive and return the session key, or an exception if authentication failed if proved @key = key @key_no = key_no if key.byteslice(0, 8) != key.byteslice(8, 8) return @sesskey = nonce_r.byteslice(0, 4) + nonce_t.byteslice(0, 4) + nonce_r.byteslice(4, 4) + nonce_t.byteslice(4, 4) else return @sesskey = nonce_r.byteslice(0, 4) + nonce_t.byteslice(0, 4) + nonce_r.byteslice(0, 4) + nonce_t.byteslice(0, 4) end else @key = nil @key_no = nil @sesskey = nil throw :authentication_failed, true end end |
#change_key(key_no, old_key, new_key) ⇒ Object
Change key number key_no
to be new_key
, the old key being old_key
. After a successful change of the key used to reach the current authentication status, that authentication is invalidated: an authentication with the new key is necessary to subsequent operations.
52 53 54 55 56 57 58 59 60 61 |
# File 'lib/conn_cmds.rb', line 52 def change_key(key_no, old_key, new_key) fail if @key_settings and not @key_settings.can_change_key?(@aid, key_no, @key_no) if key_no == @key_no or @key_settings.change_key == Rights::FREE_ACCESS data = encipher_data(new_key) else data = encipher_old_new_data(old_key, new_key) end send_card_cmd(CHANGE_KEY, [key_no, *data.unpack("C*")])[0] end |
#change_key_settings(new_key_settings) ⇒ Object
Sets the given key settings (a KeySettings
object) for the currently selected application.
14 15 16 17 18 19 |
# File 'lib/conn_cmds.rb', line 14 def change_key_settings(new_key_settings) fail if @key_settings and not @key_settings.can_change_key_settings?(@key_no) data = encipher_data([new_key_settings.to_byte].pack('C*')) response = send_card_cmd(CHANGE_KEY_SETTINGS, data.unpack('C*')) end |
#create_app(aid, num_keys, key_settings) ⇒ Object
Create an application with given application ID, given number of keys and given key settings. The application should not already exist.
79 80 81 82 83 |
# File 'lib/conn_cmds.rb', line 79 def create_app(aid, num_keys, key_settings) fail if @key_settings and not @key_settings.can_create_app?(@aid, @key_no) args = [*to_le_array(aid, 3), key_settings.to_byte, num_keys] send_card_cmd(CREATE_APPLICATION, args)[0] end |
#create_file(file_no, rights, file_size) ⇒ Object
Create a new file in the selected application with given access rights, size, and communication settings. The file should not already exists.
100 101 102 103 104 |
# File 'lib/conn_cmds.rb', line 100 def create_file(file_no, rights, file_size) fail if @key_settings and not @key_settings.can_edit_file?(@key_no) args = [file_no, rights.com_set, *rights.to_a, *to_le_array(file_size, 3)] send_card_cmd(CREATE_STD_DATA_FILE, args)[0] end |
#delete_app(aid) ⇒ Object
Delete the given application from the tag. Beware that the memory lost will not be reusable before the tag is formatted. It is not advised to use this.
87 88 89 90 |
# File 'lib/conn_cmds.rb', line 87 def delete_app(aid) fail if @key_settings and not @key_settings.can_delete_app?(@aid, @key_no) send_card_cmd(DELETE_APP, to_le_array(aid, 3)) end |
#delete_file(file_no) ⇒ Object
Delete the given file from the currently selected application from the tag. Beware that the memory lost will not be reusable before the tag is formatted. It is not advised to use this (overwrite the file instead).
109 110 111 112 |
# File 'lib/conn_cmds.rb', line 109 def delete_file(file_no) fail if @key_settings and not @key_settings.can_edit_file?(@key_no) send_card_cmd(DELETE_FILE, [file_no]) end |
#disconnect ⇒ Object
Release all resources associated with the card.
82 83 84 85 86 87 88 89 90 |
# File 'lib/conn_init.rb', line 82 def disconnect # :unpower is necessary if we want to relaunch the program without unpluging # the reader. unless @card.nil? @card.disconnect(:unpower) @card = nil end super end |
#format ⇒ Object
Erase all of the tag memory (except the tag master key).
167 168 169 170 |
# File 'lib/conn_cmds.rb', line 167 def format fail unless @aid === 0 && @key_no == 0 send_card_cmd(FORMAT_PICC)[0] end |
#get_app_ids ⇒ Object
Returns an array containing all application IDs.
64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/conn_cmds.rb', line 64 def get_app_ids fail if @key_settings and not @key_settings.can_get0?(@aid, @key_no) aids = [] begin response, status = send_card_cmd(GET_APP_IDS) response.each_slice(3) do |aid| # note: on the DESFire, aid[1] and aid[2] should be 0 aids << aid[0] + 256 * aid[1] + 256*256 * aid[2] end end while (status == STATUS[:ADDITIONAL_FRAME]) return aids end |
#get_file_ids ⇒ Object
Returns an array containg all files IDs on the current application.
93 94 95 96 |
# File 'lib/conn_cmds.rb', line 93 def get_file_ids fail if @key_settings and not @key_settings.can_get?(@key_no) send_card_cmd(GET_FILE_IDS)[0] end |
#get_file_rights(file_no) ⇒ Object
Retrieve and return the file access rights (as a FileAccess
object) for the given file.
43 44 45 46 |
# File 'lib/conn_cmds.rb', line 43 def get_file_rights(file_no) response = send_card_cmd(GET_FILE_SETTINGS, [file_no])[0] return Rights::FileAccess.from_a(response) end |
#get_key_settings ⇒ Object
Retrieve and returns the key settings (as a KeySettings
object) for currently selected application. Sets @max_keys
and @key_settings.
23 24 25 26 27 28 29 |
# File 'lib/conn_cmds.rb', line 23 def get_key_settings # We can't check for the permission to perform the operation since # this would require knowing the key settings already. response = send_card_cmd(GET_KEY_SETTINGS)[0] @max_keys = response[1] return @key_settings = KeySettings.from_byte(response[0]) end |
#get_uid ⇒ Object
Returns the UID of the tag.
32 33 34 35 36 37 38 39 |
# File 'lib/conn_cmds.rb', line 32 def get_uid # GET_VERSION can always be performed without authentication. # We don't care about the other information returned by GET_VERSION. send_card_cmd(GET_VERSION) send_card_cmd(SEND_MORE_DATA) response = send_card_cmd(SEND_MORE_DATA)[0] return response[1..8] end |
#poll(max_tags = 1, baud = 0) ⇒ Object
Search for a card. This succeeds if a card is detected, else it throws a Smartcard::PCSC::Exception
with pcsc_status_code
field set to the :comm_data_lost
status after some time.
76 77 78 79 |
# File 'lib/conn_init.rb', line 76 def poll( = 1, baud = 0) response = send_chip_cmd LIST_PASSIVE_TARGET, [, baud] throw :protocol_error unless response[0] == LIST_RESPONSE end |
#read_file(file_no, offset, length) ⇒ Object
Read the up to length
bytes in the given file (in the currently selected application), starting at the given offet. If length
is 0, the file read up to its end. A byte string is returned.
122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/conn_cmds.rb', line 122 def read_file(file_no, offset, length) args = [file_no, *to_le_array(offset, 3), *to_le_array(length, 3)] response, status = send_card_cmd(READ_DATA, args) data = response while status == STATUS[:ADDITIONAL_FRAME] response, status= send_card_cmd(SEND_MORE_DATA) data += response end fail 'protocol_error' if length != 0 and data.length != length handler = get_access_rights_read_handler(file_no) return handler.call(length, data) end |
#read_whole_file(file_no) ⇒ Object
Read the entire file identified by file_no from the selected application.
115 116 117 |
# File 'lib/conn_cmds.rb', line 115 def read_whole_file(file_no) return read_file(file_no, 0, 0) end |
#select_app(aid) ⇒ Object
Select the application with given application ID for future operations.
181 182 183 184 |
# File 'lib/conn_cmds.rb', line 181 def select_app(aid) @aid = aid send_card_cmd(SELECT_APPLICATION, to_le_array(aid, 3))[0] end |
#select_app_auth(aid, key) ⇒ Object
Selects an application, and performs authentication and key settings retrieval for this application.
174 175 176 177 178 |
# File 'lib/conn_cmds.rb', line 174 def select_app_auth(aid, key) select_app(aid) authenticate(0, key) get_key_settings() end |
#send_card_apdu(card_apdu, tag_no = 1) ⇒ Object
Send a card APDU to the card and return the card’s answer. This takes care of chip command DATA_EXCHANGE return codes.
72 73 74 75 76 |
# File 'lib/conn_transmit.rb', line 72 def send_card_apdu(card_apdu, tag_no=1) response = send_chip_cmd(DATA_EXCHANGE, [tag_no, *card_apdu]) raise CARD_CONTACT_LOST_ERROR unless response[0..1] == DATA_RESPONSE return response[2..response.length-1] end |
#send_card_cmd(cmd, args = []) ⇒ Object
Send a command to the card and return an array whose first element is the command response (not comprising the card response codes) and the second is a successful status (needed to know if additional frames are available).
81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/conn_transmit.rb', line 81 def send_card_cmd(cmd, args=[]) apdu = args.length == 0 \ ? [CARD_CLA, cmd, 0x00, 0x00, 0x00] : [CARD_CLA, cmd, 0x00, 0x00, args.length, *args, 0x00] response = send_card_apdu(apdu) len = response.length throw :protocol_error unless response[len-2] == SW1 status = response[len-1] throw STATUS.key(status), true unless status == STATUS[:OPERATION_OK] || status == STATUS[:ADDITIONAL_FRAME] return [len < 3 ? [] : response[0..len-3], status] end |
#send_chip_apdu(chip_apdu) ⇒ Object
Send a chip APDU trough the reader (by wrapping the chip APDU inside a reader pseudo-APDU) and return the chip response. This takes care of the reader response codes.
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/conn_transmit.rb', line 34 def send_chip_apdu(chip_apdu) nbytes = chip_apdu.length pseudo_apdu = [READER_CLA, DIRECT_TRANSMIT, 0x00, 0x00, nbytes, *chip_apdu] reader_response = exchange_apdu(pseudo_apdu) # check the reader response case reader_response[0] when ERROR throw :operation_failed if reader_response[1] == OPERATION_FAILED throw :no_answer if reader_response[1] == NO_ANSWER throw :unknown_error when SUCCESS # fetch the actual card/chip response fetch_len = reader_response[1] pseudo_apdu = [READER_CLA, GET_RESPONSE, 0x00, 0x00, fetch_len] exchange_apdu(pseudo_apdu) else throw :protocol_error end end |
#send_chip_cmd(cmd, args) ⇒ Object
Send a chip command and return the command response (not comprising the chip response codes).
There are two useful commands: DATA_EXCHANGE
(used in send_car_apdu()) and LIST_PASSIVE_TARGET
(used by poll()
).
60 61 62 63 64 65 66 67 68 |
# File 'lib/conn_transmit.rb', line 60 def send_chip_cmd(cmd, args) chip_apdu = [TO_CHIP, cmd, *args] response = send_chip_apdu(chip_apdu) len = response.length throw :protocol_error unless response[0] == FROM_CHIP && response[len-2..len-1] == SUCCESS_SUFFIX return response[1..len-3] end |
#write_file(file_no, offset, length, data) ⇒ Object
Write length
bytes of data
(a byte string) within the given file (in the currently selected application), starting at the given offset in the file.
Pre: data
is a byte array 0 <= length
<= data.size
<= 52+59
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/conn_cmds.rb', line 146 def write_file(file_no, offset, length, data) handler = get_access_rights_write_handler(file_no) # ! the line below may make data.length > length data = handler.call(length, data.byteslice(0, length)) # The first frame contains at most WRITE_FFSIZ bytes. block_size = [data.length, WRITE_FFSIZ].min block = data.byteslice(0, block_size).unpack('C*') args = [file_no, *to_le_array(offset, 3), *to_le_array(length, 3), *block] send_card_cmd(WRITE_DATA, args) # If there is more data to send, send subsequent frames. remaining = data.length while (remaining -= block.length) > 0 block_size = [remaining, WRITE_SFSIZ].min block = data.byteslice(data.length - remaining, block_size).unpack('C*') send_card_cmd(SEND_MORE_DATA, block) end end |