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_GET_APP_IDS
- CMD_GET_CARD_VERSION
- 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_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
ISO144434::new
# File lib/mifare/des_fire.rb, line 160 def initialize(pcd, uid, sak) super invalid_auth @cmac_buffer = [] @selected_app = false end
Public Instance Methods
abort_transaction()
click to toggle source
# File lib/mifare/des_fire.rb, line 579 def abort_transaction transceive(cmd: CMD_ABORT_TRANSACTION, tx: :cmac, rx: :cmac, expect: ST_SUCCESS) end
app_exist?(id)
click to toggle source
# File lib/mifare/des_fire.rb, line 327 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 267 def auth(key_number, auth_key) cmd = (auth_key.type == :des) ? CMD_DES_AUTH : CMD_AES_AUTH auth_key.clear_iv # Ask for authentication received_data = transceive(cmd: cmd, data: key_number, expect: ST_ADDITIONAL_FRAME) # Receive challenge from DESFire challenge = auth_key.decrypt(received_data) challenge_rot = challenge.rotate # Generate random number and encrypt it with rotated challenge random_number = SecureRandom.random_bytes(received_data.size).bytes response = auth_key.encrypt(random_number + challenge_rot) # Send challenge response received_data = transceive(cmd: CMD_ADDITIONAL_FRAME, data: response, expect: ST_SUCCESS) # Check if verification matches rotated random_number verification = auth_key.decrypt(received_data) 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 167 def authed? @authed.is_a? Numeric end
change_file_setting(id, file_setting)
click to toggle source
# File lib/mifare/des_fire.rb, line 458 def change_file_setting(id, file_setting) buffer = [] buffer.append_uint(FILE_COMMUNICATION.fetch(file_setting.communication), 1) buffer.append_uint(file_setting.permission.to_uint, 2) transceive(cmd: CMD_CHANGE_FILE_SETTING, plain_data: id, data: buffer, tx: :encrypt, rx: :cmac, expect: ST_SUCCESS) end
change_key(key_number, new_key, curr_key = nil)
click to toggle source
# File lib/mifare/des_fire.rb, line 375 def change_key(key_number, new_key, curr_key = nil) raise UnauthenticatedError unless @authed raise UnexpectedDataError, '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.zip(curr_key.key).map{|x, y| x ^ y } 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 buffer = [key_number] + @session_key.encrypt(cryptogram) # Change current used key will revoke authentication invalid_auth if same_key transceive(cmd: CMD_CHANGE_KEY, data: buffer, rx: :cmac, expect: ST_SUCCESS) end
change_key_setting(key_setting)
click to toggle source
# File lib/mifare/des_fire.rb, line 419 def change_key_setting(key_setting) raise UnauthenticatedError unless @authed transceive(cmd: CMD_CHANGE_KEY_SETTING, data: key_setting.to_uint, tx: :encrypt, rx: :cmac, expect: ST_SUCCESS) end
clear_record(id)
click to toggle source
# File lib/mifare/des_fire.rb, line 571 def clear_record(id) transceive(cmd: CMD_CLEAR_RECORD_FILE, data: id, tx: :cmac, rx: :cmac, expect: ST_SUCCESS) end
commit_transaction()
click to toggle source
# File lib/mifare/des_fire.rb, line 575 def commit_transaction transceive(cmd: CMD_COMMIT_TRANSACTION, tx: :cmac, rx: :cmac, expect: ST_SUCCESS) end
create_app(id, key_setting, key_count, cipher_suite)
click to toggle source
# File lib/mifare/des_fire.rb, line 338 def create_app(id, key_setting, key_count, cipher_suite) raise UnauthenticatedError unless @authed raise UnexpectedDataError, 'An application can only hold up to 14 keys.' if key_count > 14 buffer = convert_app_id(id) + [key_setting.to_uint, KEY_TYPE.fetch(cipher_suite) | key_count] transceive(cmd: CMD_CREATE_APP, data: buffer, tx: :cmac, rx: :cmac, expect: ST_SUCCESS) end
create_file(id, file_setting)
click to toggle source
# File lib/mifare/des_fire.rb, line 466 def create_file(id, file_setting) buffer = [id] buffer.append_uint(FILE_COMMUNICATION.fetch(file_setting.communication), 1) buffer.append_uint(file_setting.permission.to_uint, 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, tx: :cmac, rx: :cmac, expect: ST_SUCCESS) end
credit_value(id, delta)
click to toggle source
# File lib/mifare/des_fire.rb, line 526 def credit_value(id, delta) raise UnexpectedDataError, '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 535 def debit_value(id, delta) raise UnexpectedDataError, '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 347 def delete_app(id) raise UnauthenticatedError unless @authed transceive(cmd: CMD_DELETE_APP, data: convert_app_id(id), tx: :cmac, rx: :cmac, expect: ST_SUCCESS) end
delete_file(id)
click to toggle source
# File lib/mifare/des_fire.rb, line 489 def delete_file(id) transceive(cmd: CMD_DELETE_FILE, data: id, tx: :cmac, rx: :cmac, expect: ST_SUCCESS) end
deselect()
click to toggle source
Calls superclass method
ISO144434#deselect
# File lib/mifare/des_fire.rb, line 171 def deselect super invalid_auth end
file_exist?(id)
click to toggle source
# File lib/mifare/des_fire.rb, line 429 def file_exist?(id) get_file_ids.include?(id) end
format_card()
click to toggle source
# File lib/mifare/des_fire.rb, line 363 def format_card raise UnauthenticatedError unless @authed transceive(cmd: CMD_FORMAT_CARD, tx: :cmac, rx: :cmac, expect: ST_SUCCESS) end
get_app_ids()
click to toggle source
# File lib/mifare/des_fire.rb, line 316 def get_app_ids ids = transceive(cmd: CMD_GET_APP_IDS, tx: :cmac, rx: :cmac, expect: ST_SUCCESS, return_data: true, receive_all: true) return ids if ids.empty? ids = ids.each_slice(3).to_a ids.map do |id| id.to_uint end end
get_card_version()
click to toggle source
# File lib/mifare/des_fire.rb, line 353 def get_card_version version = transceive(cmd: CMD_GET_CARD_VERSION, tx: :cmac, rx: :cmac, expect: ST_SUCCESS, receive_all: true) 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_file_ids()
click to toggle source
# File lib/mifare/des_fire.rb, line 425 def get_file_ids transceive(cmd: CMD_GET_FILE_IDS, tx: :cmac, rx: :cmac, expect: ST_SUCCESS, return_data: true) end
get_file_setting(id)
click to toggle source
# File lib/mifare/des_fire.rb, line 433 def get_file_setting(id) received_data = transceive(cmd: CMD_GET_FILE_SETTING, data: id, tx: :cmac, rx: :cmac, expect: ST_SUCCESS) 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.new.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_key_setting()
click to toggle source
# File lib/mifare/des_fire.rb, line 411 def get_key_setting received_data = transceive(cmd: CMD_GET_KEY_SETTING, tx: :cmac, rx: :cmac, expect: ST_SUCCESS) { key_setting: KEY_SETTING.new.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 369 def get_key_version(key_number) received_data = transceive(cmd: CMD_GET_KEY_VERSION, data: key_number, tx: :cmac, rx: :cmac, expect: ST_SUCCESS) received_data[0] end
limited_credit_value(id, delta)
click to toggle source
# File lib/mifare/des_fire.rb, line 544 def limited_credit_value(id, delta) raise UnexpectedDataError, '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 504 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 493 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, tx: :cmac, rx: convert_file_communication(file_setting.communication), expect: ST_SUCCESS, receive_all: true, receive_length: length) end
read_records(id, offset, length)
click to toggle source
# File lib/mifare/des_fire.rb, line 553 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 522 def read_value(id) read_file(id, CMD_GET_VALUE, id, 4).to_sint end
select_app(id)
click to toggle source
# File lib/mifare/des_fire.rb, line 331 def select_app(id) transceive(cmd: CMD_SELECT_APP, data: convert_app_id(id), expect: ST_SUCCESS) invalid_auth @selected_app = id end
transceive(cmd: , plain_data: [], data: [], tx: nil, rx: nil, expect: nil, return_data: nil, receive_all: nil, receive_length: nil)
click to toggle source
Calls superclass method
ISO144434#transceive
# File lib/mifare/des_fire.rb, line 176 def transceive(cmd: , plain_data: [], data: [], tx: nil, rx: nil, expect: nil, return_data: nil, receive_all: nil, receive_length: 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 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 == :cmac || tx == :send_cmac) && cmd != CMD_ADDITIONAL_FRAME && @authed @cmac_buffer = buffer cmac = @session_key.calculate_cmac(@cmac_buffer) # Only first 8 bytes of CMAC are transmitted buffer.concat(cmac[0..7]) if tx == :send_cmac end received_data = [] card_status = nil loop do receive_buffer = super(buffer.shift(@max_inf_size)) card_status = receive_buffer.shift received_data.concat(receive_buffer) break if card_status != ST_ADDITIONAL_FRAME || (buffer.empty? && !receive_all) 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_s(16).rjust(2, '0').upcase} - #{error_msg}" end if expect && expect != card_status raise UnexpectedDataError, 'Card status does not match expected value' end if (rx == :cmac || rx == :send_cmac) && (card_status == ST_SUCCESS || card_status == ST_ADDITIONAL_FRAME) && @authed @cmac_buffer = [] if cmd != CMD_ADDITIONAL_FRAME @cmac_buffer.concat(received_data) if card_status == ST_ADDITIONAL_FRAME if received_data.size >= 8 && card_status == ST_SUCCESS received_cmac = received_data.pop(8) @cmac_buffer.concat(received_data + [card_status]) cmac = @session_key.calculate_cmac(@cmac_buffer) # Only first 8 bytes of CMAC are transmitted if cmac[0..7] != received_cmac raise ReceiptIntegrityError end end end if rx == :encrypt if receive_length.nil? raise UnexpectedDataError, 'Lack of receive length for removing padding' end received_data = @session_key.decrypt(received_data) received_data = remove_padding_bytes(received_data, receive_length) received_crc = received_data.pop(4).to_uint crc = crc32(received_data, card_status) if crc != received_crc raise ReceiptIntegrityError end end if expect if received_data.empty? && !return_data return true else return received_data end end return card_status, received_data end
write_data(id, offset, data)
click to toggle source
# File lib/mifare/des_fire.rb, line 513 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 499 def write_file(id, cmd, plain_data, data) file_setting = get_file_setting(id) transceive(cmd: cmd, plain_data: plain_data, data: data, tx: convert_file_communication(file_setting.communication), rx: :cmac, expect: ST_SUCCESS) end
write_record(id, offset, data)
click to toggle source
# File lib/mifare/des_fire.rb, line 562 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 638 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 591 def convert_app_id(id) raise UnexpectedDataError, 'Application ID overflow' if id < 0 || id >= (1 << 24) [].append_uint(id, 3) end
convert_file_communication(communication)
click to toggle source
# File lib/mifare/des_fire.rb, line 627 def convert_file_communication(communication) case communication when :plain :cmac when :mac :send_cmac when :encrypt :encrypt end end
crc32(*datas)
click to toggle source
# File lib/mifare/des_fire.rb, line 597 def crc32(*datas) crc = 0xFFFFFFFF datas.each do |data| data = [data] unless data.is_a? Array data.each do |byte| crc ^= byte 8.times do flag = crc & 0x01 > 0 crc >>= 1 crc ^= 0xEDB88320 if flag end end end crc end
invalid_auth()
click to toggle source
# File lib/mifare/des_fire.rb, line 585 def invalid_auth @authed = false @session_key = nil @cmac_buffer = [] end
remove_padding_bytes(data, length)
click to toggle source
Remove trailing padding bytes
# File lib/mifare/des_fire.rb, line 615 def remove_padding_bytes(data, length) if length == 0 # padding format according to ISO 9797-1 str = data.pack('C*') str.sub! /#{0x80.chr}#{0x00.chr}*\z/, '' str.bytes else # data length + 4 bytes CRC data[0...length + 4] end end