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