class MIFARE::DESFire

Constants

CARD_VERSION
CMD_ABORT_TRANSACTION
CMD_AES_AUTH
CMD_CHANGE_FILE_SETTING
CMD_CHANGE_KEY
CMD_CHANGE_KEY_SETTING
CMD_CLEAR_RECORD_FILE
CMD_COMMIT_TRANSACTION
CMD_CREATE_APP

PICC Level Commands

CMD_CREATE_BACKUP_DATA_FILE
CMD_CREATE_CYCLIC_RECORD_FILE
CMD_CREATE_LINEAR_RECORD_FILE
CMD_CREATE_STD_DATA_FILE
CMD_CREATE_VALUE_FILE
CMD_CREDIT
CMD_DEBIT
CMD_DELETE_APP
CMD_DELETE_FILE
CMD_DES_AUTH

Security Related Commands

CMD_FORMAT_CARD
CMD_FREE_MEMORY
CMD_GET_APP_IDS
CMD_GET_CARD_UID
CMD_GET_CARD_VERSION
CMD_GET_DF_NAMES
CMD_GET_FILE_IDS

Application Level Commands

CMD_GET_FILE_SETTING
CMD_GET_KEY_SETTING
CMD_GET_KEY_VERSION
CMD_GET_VALUE
CMD_LIMITED_CREDIT
CMD_READ_DATA

Data Manipulation Commands

CMD_READ_RECORDS
CMD_SELECT_APP
CMD_SET_CONFIGURATION
CMD_WRITE_DATA
CMD_WRITE_RECORD
FILE_COMMUNICATION
FILE_PERMISSION

value 0x00 ~ 0x0D are key numbers, 0x0E grants free access, 0x0F always denies access

FILE_SETTING
FILE_TYPE
KEY_SETTING
KEY_TYPE
ST_ADDITIONAL_FRAME
ST_APPL_INTEGRITY_ERROR
ST_APP_NOT_FOUND
ST_AUTHENTICATION_ERROR
ST_BOUNDARY_ERROR
ST_COMMAND_ABORTED
ST_COUNT_ERROR
ST_DUPLICATE_ERROR
ST_EEPROM_ERROR
ST_FILE_INTEGRITY_ERROR
ST_FILE_NOT_FOUND
ST_ILLEGAL_COMMAND
ST_INCORRECT_PARAM
ST_INTEGRITY_ERROR
ST_KEY_NOT_EXIST
ST_NO_CHANGES
ST_OUT_OF_MEMORY
ST_PERMISSION_DENIED
ST_PICC_DISABLED_ERROR
ST_PICC_INTEGRITY_ERROR
ST_SUCCESS

Status code returned by DESFire

ST_WRONG_COMMAND_LEN

Attributes

selected_app[R]

Public Class Methods

new(pcd, uid, sak) click to toggle source
Calls superclass method PICC::new
# File lib/mifare/des_fire.rb, line 166
def initialize(pcd, uid, sak)
  super
  invalid_auth
  @selected_app = false
end

Public Instance Methods

abort_transaction() click to toggle source
# File lib/mifare/des_fire.rb, line 620
def abort_transaction
  transceive(cmd: CMD_ABORT_TRANSACTION)
end
app_exist?(id) click to toggle source
# File lib/mifare/des_fire.rb, line 326
def app_exist?(id)
  get_app_ids.include?(id)
end
auth(key_number, auth_key) click to toggle source
# File lib/mifare/des_fire.rb, line 264
def auth(key_number, auth_key)
  cmd = (auth_key.type == :des) ? CMD_DES_AUTH : CMD_AES_AUTH
  rand_size = (auth_key.cipher_suite == 'des-ede-cbc') ? 8 : 16
  auth_key.clear_iv
  auth_key.padding_mode(1)

  # Ask for authentication
  received_data = transceive(cmd: cmd, data: key_number, expect: ST_ADDITIONAL_FRAME, tx: :none, rx: :none)

  # Receive challenge from DESFire
  challenge = auth_key.decrypt(received_data, data_length: rand_size)
  challenge_rot = challenge.rotate

  # Generate random number and encrypt it with rotated challenge
  random_number = SecureRandom.random_bytes(rand_size).bytes
  response = auth_key.encrypt(random_number + challenge_rot)

  # Send challenge response
  received_data = transceive(cmd: CMD_ADDITIONAL_FRAME, data: response, tx: :none, rx: :none)

  # Check if verification matches rotated random_number
  verification = auth_key.decrypt(received_data, data_length: rand_size)

  if random_number.rotate != verification
    halt
    @authed = false

    raise ReceiptIntegrityError, 'Authentication Failed'
  end

  # Generate session key from generated random number(RndA) and challenge(RndB)
  session_key = random_number[0..3] + challenge[0..3]

  if auth_key.key_size > 8
    if auth_key.cipher_suite == 'des-ede-cbc'
      session_key.concat(random_number[4..7] + challenge[4..7])
    elsif auth_key.cipher_suite == 'des-ede3-cbc'
      session_key.concat(random_number[6..9] + challenge[6..9])
      session_key.concat(random_number[12..15] + challenge[12..15])
    elsif auth_key.cipher_suite == 'aes-128-cbc'
      session_key.concat(random_number[12..15] + challenge[12..15])
    end
  end

  @session_key = Key.new(auth_key.type, session_key)
  @session_key.generate_cmac_subkeys
  @authed = key_number

  authed?
end
authed?() click to toggle source
# File lib/mifare/des_fire.rb, line 172
def authed?
  @authed.is_a? Numeric
end
change_file_setting(id, file_setting) click to toggle source
# File lib/mifare/des_fire.rb, line 499
def change_file_setting(id, file_setting)
  buffer = []
  buffer.append_uint(FILE_COMMUNICATION.fetch(file_setting.communication), 1)
  buffer.append_uint(file_setting.permission.export, 2)

  transceive(cmd: CMD_CHANGE_FILE_SETTING, plain_data: id, data: buffer, tx: :encrypt)
end
change_key(key_number, new_key, curr_key = nil) click to toggle source
# File lib/mifare/des_fire.rb, line 415
def change_key(key_number, new_key, curr_key = nil)
  raise UnauthenticatedError unless @authed
  raise UsageError, 'Invalid key number' if key_number > 13

  cryptogram = new_key.key

  same_key = (key_number == @authed)

  # Only Master Key can change its key type
  key_number |= KEY_TYPE.fetch(new_key.cipher_suite) if @selected_app == 0

  # XOR new key if we're using different one
  unless same_key
    cryptogram = cryptogram.xor(curr_key.key)
  end

  # AES stores key version separately
  if new_key.type == :aes
    cryptogram.append_uint(new_key.version, 1)
  end

  cryptogram.append_uint(crc32([CMD_CHANGE_KEY, key_number], cryptogram), 4)

  unless same_key
    cryptogram.append_uint(crc32(new_key.key), 4)
  end

  # Encrypt cryptogram
  @session_key.padding_mode(1)
  buffer = [key_number] + @session_key.encrypt(cryptogram)

  transceive(cmd: CMD_CHANGE_KEY, data: buffer, tx: :none, rx: :mac)

  # Change current used key will revoke authentication
  invalid_auth if same_key
end
change_key_setting(key_setting) click to toggle source
# File lib/mifare/des_fire.rb, line 460
def change_key_setting(key_setting)
  raise UnauthenticatedError unless @authed

  transceive(cmd: CMD_CHANGE_KEY_SETTING, data: key_setting.export, tx: :encrypt, rx: :mac)
end
clear_record(id) click to toggle source
# File lib/mifare/des_fire.rb, line 612
def clear_record(id)
  transceive(cmd: CMD_CLEAR_RECORD_FILE, data: id)
end
commit_transaction() click to toggle source
# File lib/mifare/des_fire.rb, line 616
def commit_transaction
  transceive(cmd: CMD_COMMIT_TRANSACTION)
end
create_app(id, key_setting, key_count, cipher_suite) click to toggle source
# File lib/mifare/des_fire.rb, line 337
def create_app(id, key_setting, key_count, cipher_suite)
  raise UnauthenticatedError unless @authed
  raise UsageError, 'An application can only hold up to 14 keys.' if key_count > 14

  buffer = convert_app_id(id) + [key_setting.export, KEY_TYPE.fetch(cipher_suite) | key_count]

  transceive(cmd: CMD_CREATE_APP, data: buffer, rx: :mac)
end
create_file(id, file_setting) click to toggle source
# File lib/mifare/des_fire.rb, line 507
def create_file(id, file_setting)
  buffer = [id]
  buffer.append_uint(FILE_COMMUNICATION.fetch(file_setting.communication), 1)
  buffer.append_uint(file_setting.permission.export, 2)

  case file_setting.type
  when :std_data_file, :backup_data_file
    buffer.append_uint(file_setting.size, 3) # PICC will allocate n * 32 bytes memory internally
  when :value_file
    buffer.append_sint(file_setting.lower_limit, 4)
    buffer.append_sint(file_setting.upper_limit, 4)
    buffer.append_sint(file_setting.limited_credit_value, 4)
    buffer.append_uint(file_setting.limited_credit, 1)
  when :linear_record_file, :cyclic_record_file
    buffer.append_uint(file_setting.record_size, 3)
    buffer.append_uint(file_setting.max_record_number, 3)
  end

  cmd = self.class.const_get("CMD_CREATE_#{file_setting.type.to_s.upcase}")

  transceive(cmd: cmd, data: buffer)
end
credit_value(id, delta) click to toggle source
# File lib/mifare/des_fire.rb, line 567
def credit_value(id, delta)
  raise UsageError, 'Negative number is not allowed.' if delta < 0

  buffer = []
  buffer.append_sint(delta, 4)

  write_file(id, CMD_CREDIT, id, buffer)
end
debit_value(id, delta) click to toggle source
# File lib/mifare/des_fire.rb, line 576
def debit_value(id, delta)
  raise UsageError, 'Negative number is not allowed.' if delta < 0

  buffer = []
  buffer.append_sint(delta, 4)

  write_file(id, CMD_DEBIT, id, buffer)
end
delete_app(id) click to toggle source
# File lib/mifare/des_fire.rb, line 346
def delete_app(id)
  raise UnauthenticatedError unless @authed

  transceive(cmd: CMD_DELETE_APP, data: convert_app_id(id))
end
delete_file(id) click to toggle source
# File lib/mifare/des_fire.rb, line 530
def delete_file(id)
  transceive(cmd: CMD_DELETE_FILE, data: id)
end
deselect() click to toggle source
# File lib/mifare/des_fire.rb, line 180
def deselect
  invalid_auth
  iso_deselect
end
file_exist?(id) click to toggle source
# File lib/mifare/des_fire.rb, line 470
def file_exist?(id)
  get_file_ids.include?(id)
end
format_card() click to toggle source
# File lib/mifare/des_fire.rb, line 378
def format_card
  raise UnauthenticatedError unless @authed

  transceive(cmd: CMD_FORMAT_CARD)
end
get_app_ids() click to toggle source
# File lib/mifare/des_fire.rb, line 315
def get_app_ids
  ids = transceive(cmd: CMD_GET_APP_IDS, rx: :mac)

  return ids if ids.empty?

  ids = ids.each_slice(3).to_a
  ids.map do |id|
    id.to_uint
  end
end
get_card_uid() click to toggle source
# File lib/mifare/des_fire.rb, line 372
def get_card_uid
  raise UnauthenticatedError unless @authed

  transceive(cmd: CMD_GET_CARD_UID, rx: :encrypt, receive_length: 7)
end
get_card_version() click to toggle source
# File lib/mifare/des_fire.rb, line 352
def get_card_version
  version = transceive(cmd: CMD_GET_CARD_VERSION)

  CARD_VERSION.new(
    version[0], version[1], version[2], version[3], version[4], 1 << (version[5] / 2), version[6],
    version[7], version[8], version[9], version[10], version[11], 1 << (version[12] / 2), version[13],
    version[14..20], version[21..25], version[26].to_s(16).to_i, 2000 + version[27].to_s(16).to_i
  )
end
get_df_names() click to toggle source
# File lib/mifare/des_fire.rb, line 366
def get_df_names
  raise UsageError, 'App 0 should be selected before calling' unless @selected_app == 0

  transceive(cmd: CMD_GET_DF_NAMES)
end
get_file_ids() click to toggle source
# File lib/mifare/des_fire.rb, line 466
def get_file_ids
  transceive(cmd: CMD_GET_FILE_IDS)
end
get_file_setting(id) click to toggle source
# File lib/mifare/des_fire.rb, line 474
def get_file_setting(id)
  received_data = transceive(cmd: CMD_GET_FILE_SETTING, data: id)

  file_setting = FILE_SETTING.new
  file_setting.type = FILE_TYPE.key(received_data.shift)
  file_setting.communication = FILE_COMMUNICATION.key(received_data.shift)
  file_setting.permission = FILE_PERMISSION.import(received_data.shift(2).to_uint)

  case file_setting.type
  when :std_data_file, :backup_data_file
    file_setting.size = received_data.shift(3).to_uint
  when :value_file
    file_setting.lower_limit = received_data.shift(4).to_uint
    file_setting.upper_limit = received_data.shift(4).to_uint
    file_setting.limited_credit_value = received_data.shift(4).to_uint
    file_setting.limited_credit = received_data.shift & 0x01
  when :linear_record_file, :cyclic_record_file
    file_setting.record_size = received_data.shift(3).to_uint
    file_setting.max_record_number = received_data.shift(3).to_uint
    file_setting.current_record_number = received_data.shift(3).to_uint
  end

  file_setting
end
get_free_memory() click to toggle source
# File lib/mifare/des_fire.rb, line 362
def get_free_memory
  transceive(cmd: CMD_FREE_MEMORY)
end
get_key_setting() click to toggle source
# File lib/mifare/des_fire.rb, line 452
def get_key_setting
  received_data = transceive(cmd: CMD_GET_KEY_SETTING)

  { key_setting: KEY_SETTING.import(received_data[0]),
    key_count: received_data[1] & 0x0F,
    key_type: KEY_TYPE.key(received_data[1] & 0xF0) }
end
get_key_version(key_number) click to toggle source
# File lib/mifare/des_fire.rb, line 409
def get_key_version(key_number)
  received_data = transceive(cmd: CMD_GET_KEY_VERSION, data: key_number, rx: :mac)

  received_data[0]
end
limited_credit_value(id, delta) click to toggle source
# File lib/mifare/des_fire.rb, line 585
def limited_credit_value(id, delta)
  raise UsageError, 'Negative number is not allowed.' if delta < 0

  buffer = []
  buffer.append_sint(delta, 4)

  write_file(id, CMD_LIMITED_CREDIT, id, buffer)
end
read_data(id, offset, length) click to toggle source
# File lib/mifare/des_fire.rb, line 545
def read_data(id, offset, length)
  buffer = []
  buffer.append_uint(id, 1)
  buffer.append_uint(offset, 3)
  buffer.append_uint(length, 3)

  read_file(id, CMD_READ_DATA, buffer, length)
end
read_file(id, cmd, data, length) click to toggle source
# File lib/mifare/des_fire.rb, line 534
def read_file(id, cmd, data, length)
  file_setting = get_file_setting(id)
  length *= file_setting.record_size if file_setting.record_size
  transceive(cmd: cmd, data: data, rx: file_setting.communication, receive_length: length)
end
read_records(id, offset, length) click to toggle source
# File lib/mifare/des_fire.rb, line 594
def read_records(id, offset, length)
  buffer = []
  buffer.append_uint(id, 1)
  buffer.append_uint(offset, 3)
  buffer.append_uint(length, 3)

  read_file(id, CMD_READ_RECORDS, buffer, length)
end
read_value(id) click to toggle source
# File lib/mifare/des_fire.rb, line 563
def read_value(id)
  read_file(id, CMD_GET_VALUE, id, 4).to_sint
end
select() click to toggle source
# File lib/mifare/des_fire.rb, line 176
def select
  iso_select
end
select_app(id) click to toggle source
# File lib/mifare/des_fire.rb, line 330
def select_app(id)
  transceive(cmd: CMD_SELECT_APP, data: convert_app_id(id))

  invalid_auth
  @selected_app = id
end
set_ats(ats) click to toggle source
# File lib/mifare/des_fire.rb, line 403
def set_ats(ats)
  raise UnauthenticatedError unless @authed

  transceive(cmd: CMD_SET_CONFIGURATION, plain_data: 0x02, data: ats, tx: :encrypt, encrypt_padding: 2)
end
set_configuration_byte(disable_format, enable_random_uid) click to toggle source
# File lib/mifare/des_fire.rb, line 384
def set_configuration_byte(disable_format, enable_random_uid)
  raise UnauthenticatedError unless @authed

  flag = 0
  flag |= 0x01 if disable_format
  flag |= 0x02 if enable_random_uid

  transceive(cmd: CMD_SET_CONFIGURATION, plain_data: 0x00, data: [flag], tx: :encrypt)
end
set_default_key(key) click to toggle source
# File lib/mifare/des_fire.rb, line 394
def set_default_key(key)
  raise UnauthenticatedError unless @authed

  buffer = key.key
  buffer.append_uint(key.version, 1)

  transceive(cmd: CMD_SET_CONFIGURATION, plain_data: 0x01, data: buffer, tx: :encrypt)
end
transceive(cmd: , plain_data: [], data: [], tx: nil, rx: nil, expect: nil, return_data: nil, receive_length: nil, encrypt_padding: nil) click to toggle source
# File lib/mifare/des_fire.rb, line 185
def transceive(cmd: , plain_data: [], data: [], tx: nil, rx: nil, expect: nil, return_data: nil, receive_length: nil, encrypt_padding: nil)
  # Session key is needed for encryption
  if (tx == :encrypt || rx == :encrypt) && !@authed
    raise UnauthenticatedError
  end

  # Separate objects and be compatable with single byte input
  plain_data = plain_data.is_a?(Array) ? plain_data.dup : [plain_data]
  data = data.is_a?(Array) ? data.dup : [data]

  buffer = [cmd] + plain_data

  if @authed
    @session_key.padding_mode(encrypt_padding || 1)
  end

  if tx == :encrypt
    # Calculate CRC on whole frame
    data.append_uint(crc32(buffer, data), 4)
    # Encrypt data only
    data = @session_key.encrypt(data)
  end

  buffer.concat(data)

  if tx != :encrypt && tx != :none && cmd != CMD_ADDITIONAL_FRAME && @authed
    cmac = @session_key.calculate_cmac(buffer)
    # Only first 8 bytes of CMAC are transmitted
    buffer.concat(cmac[0..7]) if tx == :mac
  end

  received_data = []
  card_status = nil
  loop do
    receive_buffer = iso_transceive(buffer.shift(@max_inf_size))

    card_status = receive_buffer.shift
    received_data.concat(receive_buffer)

    break if card_status != ST_ADDITIONAL_FRAME || (buffer.empty? && expect == ST_ADDITIONAL_FRAME)

    buffer.unshift(CMD_ADDITIONAL_FRAME)
  end

  error_msg = check_status_code(card_status)

  unless error_msg.empty?
    invalid_auth
    raise ReceiptStatusError, "0x#{card_status.to_bytehex} - #{error_msg}"
  end

  if expect && expect != card_status
    raise UnexpectedDataError, 'Card status does not match expected value'
  end

  if rx == :encrypt
    if receive_length.nil?
      raise UsageError, 'Lack of receive length for removing padding'
    end
    @session_key.padding_mode((receive_length > 0) ? 1 : 2)
    receive_length += 4 # CRC32
    received_data = @session_key.decrypt(received_data, data_length: receive_length)
    received_crc = received_data.pop(4).to_uint
    crc = crc32(received_data, card_status)
    if crc != received_crc
      raise ReceiptIntegrityError
    end
  elsif rx != :none && @authed && received_data.size >= 8 && card_status == ST_SUCCESS
    received_cmac = received_data.pop(8)
    cmac = @session_key.calculate_cmac(received_data + [card_status])
    # Only first 8 bytes of CMAC are transmitted
    if rx == :mac && cmac[0..7] != received_cmac
      raise ReceiptIntegrityError
    end
  end

  received_data
end
write_data(id, offset, data) click to toggle source
# File lib/mifare/des_fire.rb, line 554
def write_data(id, offset, data)
  buffer = []
  buffer.append_uint(id, 1)
  buffer.append_uint(offset, 3)
  buffer.append_uint(data.size, 3)

  write_file(id, CMD_WRITE_DATA, buffer, data)
end
write_file(id, cmd, plain_data, data) click to toggle source
# File lib/mifare/des_fire.rb, line 540
def write_file(id, cmd, plain_data, data)
  file_setting = get_file_setting(id)
  transceive(cmd: cmd, plain_data: plain_data, data: data, tx: file_setting.communication)
end
write_record(id, offset, data) click to toggle source
# File lib/mifare/des_fire.rb, line 603
def write_record(id, offset, data)
  buffer = []
  buffer.append_uint(id, 1)
  buffer.append_uint(offset, 3)
  buffer.append_uint(data.size, 3)

  write_file(id, CMD_WRITE_RECORD, buffer, data)
end

Private Instance Methods

check_status_code(code) click to toggle source
# File lib/mifare/des_fire.rb, line 637
def check_status_code(code)
  case code
  when ST_SUCCESS, ST_ADDITIONAL_FRAME
    ''
  when ST_NO_CHANGES
    'No changes have been made, authenticate state revoked.'
  when ST_OUT_OF_MEMORY
    'Insufficient NV-Memory to complete command.'
  when ST_ILLEGAL_COMMAND
    'Command code not supported.'
  when ST_INTEGRITY_ERROR
    'CRC or MAC does not match data. Padding bytes not valid.'
  when ST_KEY_NOT_EXIST
    'Invalid key number specified.'
  when ST_WRONG_COMMAND_LEN
    'Length of command string invalid.'
  when ST_PERMISSION_DENIED
    'Current configuration / status does not allow the requested command.'
  when ST_INCORRECT_PARAM
    'Value of the parameter(s) invalid.'
  when ST_APP_NOT_FOUND
    'Requested AID not present on PICC.'
  when ST_APPL_INTEGRITY_ERROR
    'Unrecoverable error within application, application will be disabled.'
  when ST_AUTHENTICATION_ERROR
    'Authentication error or insufficient privilege.'
  when ST_BOUNDARY_ERROR
    'Attempt to read/write data from/to beyond the file\'s/record\'s limits.'
  when ST_PICC_INTEGRITY_ERROR
    'Unrecoverable error within PICC, PICC will be disabled.'
  when ST_COMMAND_ABORTED
    'Previous Command was not fully completed. Not all Frames were requested or provided by the PCD.'
  when ST_PICC_DISABLED_ERROR
    'PICC was disabled by an unrecoverable error.'
  when ST_COUNT_ERROR
    'Number of Applications limited to 28, no additional CreateApplication possible.'
  when ST_DUPLICATE_ERROR
    'Creation of file/application failed because file/application with same number already exists.'
  when ST_EEPROM_ERROR
    'Could not complete NV-write operation due to loss of power, internal backup/rollback mechanism activated.'
  when ST_FILE_NOT_FOUND
    'Specified file number does not exist.'
  when ST_FILE_INTEGRITY_ERROR
    'Unrecoverable error within file, file will be disabled.'
  else
    'Unknown Error Code.'
  end
end
convert_app_id(id) click to toggle source
# File lib/mifare/des_fire.rb, line 631
def convert_app_id(id)
  raise UsageError, 'Application ID overflow' if id < 0 || id >= (1 << 24)

  [].append_uint(id, 3)
end
invalid_auth() click to toggle source
# File lib/mifare/des_fire.rb, line 626
def invalid_auth
  @authed = false
  @session_key = nil
end