Class: Connection

Inherits:
Smartcard::Iso::PcscTransport
  • Object
show all
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

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

#initializeConnection

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.message}"
        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

#disconnectObject

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

#formatObject

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_idsObject

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_idsObject

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_settingsObject

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_uidObject

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(max_tags = 1, baud = 0)
  response = send_chip_cmd LIST_PASSIVE_TARGET, [max_tags, 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