Files
seader/sam_api.c
T

1521 lines
54 KiB
C

#include "sam_api.h"
#include "hf_read_lifecycle.h"
#include "hf_sam_response_view.h"
#include "seader_i.h"
#include "protocol/rfal_picopass.h"
#include "sam_key_label.h"
#include "trace_log.h"
#include "uhf_snmp_probe.h"
#include "card_details_builder.h"
#include "uhf_status_label.h"
#include "allocation_policy.h"
#include <toolbox/path.h>
#include <toolbox/version.h>
// #define ASN1_DEBUG true
#define TAG "SAMAPI"
#define ASN1_PREFIX 6
#define SEADER_ICLASS_SR_SIO_BASE_BLOCK 10
#define SEADER_SERIAL_FILE_NAME "sam_serial"
#define SEADER_SNMP_MAX_REQUEST_SIZE 176U
const uint8_t picopass_iclass_key[] = {0xaf, 0xa7, 0x85, 0xa7, 0xda, 0xb3, 0x33, 0x78};
const uint8_t seader_oid[] =
{0x2B, 0x06, 0x01, 0x04, 0x01, 0x81, 0xE4, 0x38, 0x01, 0x01, 0x02, 0x04};
static void log_hex(const char* prefix, const uint8_t* data, size_t len) {
char hex[256];
size_t i;
for(i = 0; i < len && i < 120; i++) {
snprintf(hex + (i * 2), sizeof(hex) - (i * 2), "%02X", data[i]);
}
hex[i * 2] = '\0';
FURI_LOG_W(TAG, "%s len=%zu: %s", prefix, len, hex);
}
static void seader_sam_set_state(
Seader* seader,
SeaderSamState state,
SeaderSamIntent intent,
SamCommand_PR command);
static void seader_snmp_probe_finish(Seader* seader);
static const char* seader_snmp_probe_stage_name(SeaderUhfSnmpProbeStage stage) {
switch(stage) {
case SeaderUhfSnmpProbeStageDiscovery:
return "discovery";
case SeaderUhfSnmpProbeStageReadIce:
return "read_ice";
case SeaderUhfSnmpProbeStageReadStandardEncryptionKey:
return "read_std_enc_key";
case SeaderUhfSnmpProbeStageReadStandardSignatureKey:
return "read_std_sig_key";
case SeaderUhfSnmpProbeStageReadTagConfig:
return "read_tag_config";
case SeaderUhfSnmpProbeStageReadMonza4QtKey:
return "read_monza4qt_key";
case SeaderUhfSnmpProbeStageReadHiggs3Key:
return "read_higgs3_key";
case SeaderUhfSnmpProbeStageDone:
return "done";
case SeaderUhfSnmpProbeStageFailed:
return "failed";
default:
return "unknown";
}
}
static void seader_publish_sam_status(Seader* seader) {
if(seader && seader->view_dispatcher) {
view_dispatcher_send_custom_event(
seader->view_dispatcher, SeaderCustomEventSamStatusUpdated);
}
}
static void seader_update_sam_key_label(Seader* seader, const uint8_t* value, size_t value_len) {
if(!seader) {
return;
}
seader_sam_key_label_format(
seader->sam_present,
seader->sam_key_probe_status,
value,
value_len,
seader->snmp_probe.standard_pacs_keys_probed,
seader_uhf_snmp_probe_standard_pacs_keys_present(&seader->snmp_probe),
seader->sam_key_label,
sizeof(seader->sam_key_label));
seader_publish_sam_status(seader);
}
static void seader_update_uhf_status_label(Seader* seader) {
if(!seader) {
return;
}
const SeaderUhfProbeStatus probe_status =
seader_board_class_supports_uhf(seader->board_class) ? seader->uhf_probe_status :
SeaderUhfProbeStatusHidden;
seader_uhf_status_label_format(
probe_status,
seader->snmp_probe.has_monza4qt,
seader->snmp_probe.monza4qt_key_present,
seader->snmp_probe.has_higgs3,
seader->snmp_probe.higgs3_key_present,
seader->uhf_status_label,
sizeof(seader->uhf_status_label));
seader_publish_sam_status(seader);
}
static void seader_set_sam_key_probe_status_from_snmp(Seader* seader, bool probe_succeeded) {
if(!seader) {
return;
}
seader->sam_key_probe_status = seader_sam_key_probe_status_from_snmp_result(
probe_succeeded, seader->snmp_probe.ice_value_storage, seader->snmp_probe.ice_value_len);
}
static void seader_finish_failed_snmp_probe(Seader* seader) {
if(!seader) {
return;
}
seader->uhf_probe_status = SeaderUhfProbeStatusFailed;
seader_update_uhf_status_label(seader);
seader_set_sam_key_probe_status_from_snmp(seader, false);
seader_update_sam_key_label(
seader, seader->snmp_probe.ice_value_storage, seader->snmp_probe.ice_value_len);
seader_snmp_probe_finish(seader);
}
static SeaderWorker* seader_get_active_worker(Seader* seader) {
return seader ? seader->worker : NULL;
}
static SeaderUartBridge* seader_require_uart(Seader* seader) {
furi_check(seader);
furi_check(seader->uart);
return seader->uart;
}
/* A newly inserted SAM should never inherit the previous card's cached firmware/UHF status
while maintenance probes for the new card are still pending. */
static void seader_reset_cached_sam_metadata(Seader* seader) {
if(!seader) {
return;
}
seader->sam_key_probe_status = SeaderSamKeyProbeStatusUnknown;
seader->uhf_probe_status = seader_board_class_supports_uhf(seader->board_class) ?
SeaderUhfProbeStatusUnknown :
SeaderUhfProbeStatusHidden;
seader->sam_version[0] = 0U;
seader->sam_version[1] = 0U;
seader->uhf_status_label[0] = '\0';
seader_uhf_snmp_probe_init(&seader->snmp_probe);
seader->snmp_probe.supports_uhf = seader_board_class_supports_uhf(seader->board_class);
}
static bool seader_snmp_probe_send_next_request(Seader* seader) {
SeaderUartBridge* seader_uart = seader_require_uart(seader);
uint8_t* scratch = seader_uart->tx_buf + MAX_FRAME_HEADERS;
uint8_t message[SEADER_SNMP_MAX_REQUEST_SIZE] = {0};
size_t message_len = 0U;
if(!seader_uhf_snmp_probe_build_next_request(
&seader->snmp_probe,
scratch,
SEADER_UART_RX_BUF_SIZE - MAX_FRAME_HEADERS,
message,
sizeof(message),
&message_len)) {
return false;
}
return seader_worker_send_process_snmp_message(seader, message, message_len);
}
/* Finishing the maintenance probe returns mode ownership to the normal app flow and leaves
the SAM state machine idle for the next command. */
static void seader_snmp_probe_finish(Seader* seader) {
if(!seader) {
return;
}
if(seader->mode_runtime == SeaderModeRuntimeUHF) {
seader->mode_runtime = SeaderModeRuntimeNone;
}
seader_sam_set_state(seader, SeaderSamStateIdle, SeaderSamIntentNone, SamCommand_PR_NOTHING);
}
/* UHF maintenance is only legal when the SAM is present and HF runtime is fully unloaded.
The helper enforces that ownership boundary before any SNMP request is sent. */
static void seader_start_snmp_probe(Seader* seader) {
if(!seader || !seader->sam_present) {
return;
}
if(seader->hf_session_state != SeaderHfSessionStateUnloaded ||
seader->mode_runtime != SeaderModeRuntimeNone) {
seader_snmp_probe_finish(seader);
return;
}
seader->mode_runtime = SeaderModeRuntimeUHF;
seader_uhf_snmp_probe_init(&seader->snmp_probe);
seader->snmp_probe.supports_uhf = seader_board_class_supports_uhf(seader->board_class);
if(seader->sam_key_probe_status != SeaderSamKeyProbeStatusVerifiedStandard &&
seader->sam_key_probe_status != SeaderSamKeyProbeStatusVerifiedValue) {
seader->sam_key_probe_status = SeaderSamKeyProbeStatusUnknown;
seader_update_sam_key_label(seader, NULL, 0U);
}
if(seader->uhf_probe_status != SeaderUhfProbeStatusSuccess) {
seader->uhf_probe_status = seader->snmp_probe.supports_uhf ? SeaderUhfProbeStatusUnknown :
SeaderUhfProbeStatusHidden;
seader_update_uhf_status_label(seader);
}
seader_sam_set_state(
seader,
SeaderSamStateCapabilityPending,
SeaderSamIntentMaintenance,
SamCommand_PR_processSNMPMessage);
if(!seader_snmp_probe_send_next_request(seader)) {
seader->uhf_probe_status = seader->snmp_probe.supports_uhf ? SeaderUhfProbeStatusFailed :
SeaderUhfProbeStatusHidden;
seader_update_uhf_status_label(seader);
seader_snmp_probe_finish(seader);
}
}
#ifdef ASN1_DEBUG
char asn1_log[SEADER_UART_RX_BUF_SIZE] = {0};
#endif
void* calloc(size_t count, size_t size) {
size_t total_size = 0U;
if(!seader_size_multiply_checked(count, size, &total_size)) {
return NULL;
}
void* ptr = malloc(total_size);
if(ptr) {
memset(ptr, 0, total_size);
}
return ptr;
}
// Forward declarations
static void seader_abort_active_read(Seader* seader);
static void seader_sam_set_state(
Seader* seader,
SeaderSamState state,
SeaderSamIntent intent,
SamCommand_PR command) {
seader->sam_state = state;
seader->sam_intent = intent;
seader->samCommand = command;
seader_trace(TAG, "sam state=%d intent=%d cmd=%d", state, intent, command);
}
static SeaderSamIntent seader_sam_card_intent(const Seader* seader) {
if(seader->credential->type == SeaderCredentialTypeConfig) {
return SeaderSamIntentConfig;
} else {
return SeaderSamIntentReadPacs2;
}
}
bool seader_sam_can_accept_card(const Seader* seader) {
return seader->sam_state == SeaderSamStateIdle;
}
bool seader_sam_has_active_card(const Seader* seader) {
return seader->sam_state == SeaderSamStateDetectPending ||
seader->sam_state == SeaderSamStateConversation ||
seader->sam_state == SeaderSamStateFinishing;
}
void seader_sam_force_idle_for_recovery(Seader* seader) {
if(!seader) {
return;
}
FURI_LOG_W(TAG, "Force SAM idle state=%d intent=%d", seader->sam_state, seader->sam_intent);
seader_sam_set_state(seader, SeaderSamStateIdle, SeaderSamIntentNone, SamCommand_PR_NOTHING);
if(seader->worker) {
seader_worker_reset_poller_session(seader->worker);
}
}
PicopassError seader_worker_fake_epurse_update(BitBuffer* tx_buffer, BitBuffer* rx_buffer) {
const uint8_t* buffer = bit_buffer_get_data(tx_buffer);
uint8_t fake_response[8];
memset(fake_response, 0, sizeof(fake_response));
memcpy(fake_response + 0, buffer + 6, 4);
memcpy(fake_response + 4, buffer + 2, 4);
bit_buffer_append_bytes(rx_buffer, fake_response, sizeof(fake_response));
iso13239_crc_append(Iso13239CrcTypePicopass, rx_buffer);
SEADER_VERBOSE_HEX(
FuriLogLevelDebug,
TAG,
"Fake update E-Purse response",
bit_buffer_get_data(rx_buffer),
bit_buffer_get_size_bytes(rx_buffer));
return PicopassErrorNone;
}
/* Saved credentials store the SIO from index zero no matter which block it started on, and the
replay always presents an SR-shaped card, so the SAM's block numbers are relative to the SR
base block. Returns NULL when the requested block falls outside the saved SIO, which happens
for SE credentials (they start at block 6) and for any block the SAM probes speculatively. */
static const uint8_t* seader_virtual_sio_fragment(
const SeaderCredential* credential,
uint8_t block_num,
size_t fragment_len) {
if(block_num < SEADER_ICLASS_SR_SIO_BASE_BLOCK) {
return NULL;
}
const size_t offset =
(size_t)(block_num - SEADER_ICLASS_SR_SIO_BASE_BLOCK) * PICOPASS_BLOCK_LEN;
if(offset > sizeof(credential->sio) || sizeof(credential->sio) - offset < fragment_len) {
return NULL;
}
return credential->sio + offset;
}
void seader_virtual_picopass_state_machine(Seader* seader, uint8_t* buffer, size_t len) {
BitBuffer* tx_buffer = bit_buffer_alloc(len);
BitBuffer* rx_buffer = bit_buffer_alloc(SEADER_POLLER_MAX_BUFFER_SIZE);
if(!tx_buffer || !rx_buffer) {
FURI_LOG_E(TAG, "Failed to allocate virtual Picopass buffers");
if(tx_buffer) bit_buffer_free(tx_buffer);
if(rx_buffer) bit_buffer_free(rx_buffer);
return;
}
bit_buffer_append_bytes(tx_buffer, buffer, len);
uint8_t config[PICOPASS_BLOCK_LEN] = {0x12, 0xff, 0xff, 0xff, 0x7f, 0x1f, 0xff, 0x3c};
uint8_t sr_aia[PICOPASS_BLOCK_LEN] = {0xFF, 0xff, 0xff, 0xff, 0xFF, 0xFf, 0xff, 0xFF};
uint8_t epurse[PICOPASS_BLOCK_LEN] = {0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff};
uint8_t pacs_sr_cfg[PICOPASS_BLOCK_LEN] = {0xA3, 0x03, 0x03, 0x03, 0x00, 0x03, 0xe0, 0x14};
uint8_t zeroes[PICOPASS_BLOCK_LEN] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t tmac[4] = {};
uint8_t cc_p[12] = {};
uint8_t div_key[PICOPASS_BLOCK_LEN] = {};
do {
switch(buffer[0]) {
case RFAL_PICOPASS_CMD_READ_OR_IDENTIFY:
if(buffer[1] == AIA_INDEX) {
bit_buffer_append_bytes(rx_buffer, sr_aia, sizeof(sr_aia));
} else if(buffer[1] == PACS_CFG_INDEX) {
bit_buffer_append_bytes(rx_buffer, pacs_sr_cfg, sizeof(pacs_sr_cfg));
} else { // What i've seen is 0c 12
const uint8_t* fragment =
seader_virtual_sio_fragment(seader->credential, buffer[1], PICOPASS_BLOCK_LEN);
if(!fragment) {
FURI_LOG_W(TAG, "Virtual read of block %02x outside saved SIO", buffer[1]);
break;
}
bit_buffer_append_bytes(rx_buffer, fragment, PICOPASS_BLOCK_LEN);
}
iso13239_crc_append(Iso13239CrcTypePicopass, rx_buffer);
break;
case RFAL_PICOPASS_CMD_UPDATE:
seader_worker_fake_epurse_update(tx_buffer, rx_buffer);
break;
case RFAL_PICOPASS_CMD_READCHECK_KD:
if(buffer[1] == EPURSE_INDEX) {
bit_buffer_append_bytes(rx_buffer, epurse, sizeof(epurse));
}
break;
case RFAL_PICOPASS_CMD_CHECK:
loclass_iclass_calc_div_key(
seader->credential->diversifier, picopass_iclass_key, div_key, false);
memcpy(cc_p, epurse, PICOPASS_BLOCK_LEN);
memcpy(cc_p + 8, buffer + 1, PICOPASS_MAC_LEN);
loclass_opt_doTagMAC(cc_p, div_key, tmac);
bit_buffer_append_bytes(rx_buffer, tmac, sizeof(tmac));
break;
case RFAL_PICOPASS_CMD_READ4:
if(buffer[1] < SEADER_ICLASS_SR_SIO_BASE_BLOCK) {
if(buffer[1] == PACS_CFG_INDEX) {
bit_buffer_append_bytes(rx_buffer, pacs_sr_cfg, sizeof(pacs_sr_cfg));
bit_buffer_append_bytes(rx_buffer, zeroes, sizeof(zeroes));
bit_buffer_append_bytes(rx_buffer, zeroes, sizeof(zeroes));
bit_buffer_append_bytes(rx_buffer, zeroes, sizeof(zeroes));
}
} else {
const uint8_t* fragment = seader_virtual_sio_fragment(
seader->credential, buffer[1], PICOPASS_BLOCK_LEN * 4);
if(!fragment) {
FURI_LOG_W(TAG, "Virtual read4 of block %02x outside saved SIO", buffer[1]);
break;
}
bit_buffer_append_bytes(rx_buffer, fragment, PICOPASS_BLOCK_LEN * 4);
}
iso13239_crc_append(Iso13239CrcTypePicopass, rx_buffer);
break;
case RFAL_PICOPASS_CMD_PAGESEL:
// this should be considered an attempt, but realisticly not working
bit_buffer_append_bytes(rx_buffer, config, sizeof(config));
iso13239_crc_append(Iso13239CrcTypePicopass, rx_buffer);
break;
}
seader_send_nfc_rx(
seader,
(uint8_t*)bit_buffer_get_data(rx_buffer),
bit_buffer_get_size_bytes(rx_buffer));
} while(false);
bit_buffer_free(tx_buffer);
bit_buffer_free(rx_buffer);
}
bool seader_send_apdu(
Seader* seader,
uint8_t CLA,
uint8_t INS,
uint8_t P1,
uint8_t P2,
uint8_t* payload,
uint8_t payloadLen,
bool in_scratchpad) {
SeaderUartBridge* seader_uart = seader_require_uart(seader);
bool extended = seader_uart->T == 1;
uint8_t header_len = extended ? 7 : 5;
// Must account for MAX_FRAME_HEADERS headroom in scratchpad mode
if(MAX_FRAME_HEADERS + header_len + payloadLen > SEADER_UART_RX_BUF_SIZE) {
FURI_LOG_E(TAG, "Cannot send message, too long: %d", header_len + payloadLen);
return false;
}
uint8_t length = header_len + payloadLen;
uint8_t* apdu;
uint8_t local_apdu_buf[262];
uintptr_t tx_start = (uintptr_t)seader_uart->tx_buf;
uintptr_t tx_end = tx_start + SEADER_UART_RX_BUF_SIZE;
uintptr_t payload_addr = (uintptr_t)payload;
bool scratchpad_payload = false;
// in_scratchpad is only valid when the full payload range is inside tx_buf.
if(in_scratchpad && payload_addr >= tx_start + header_len && payload_addr <= tx_end) {
size_t available = (size_t)(tx_end - payload_addr);
scratchpad_payload = payloadLen <= available;
}
if(scratchpad_payload) {
apdu = (uint8_t*)(payload_addr - header_len);
} else {
apdu = local_apdu_buf;
memcpy(apdu + header_len, payload, payloadLen);
}
apdu[0] = CLA;
apdu[1] = INS;
apdu[2] = P1;
apdu[3] = P2;
if(extended) {
apdu[4] = 0x00;
apdu[5] = 0x00;
apdu[6] = payloadLen;
} else {
apdu[4] = payloadLen;
}
log_hex("RAW TX APDU", apdu, length);
if(seader_uart->T == 1) {
seader_send_t1(seader_uart, apdu, length);
} else {
seader_ccid_XfrBlock(seader_uart, apdu, length);
}
return true;
}
#ifdef ASN1_DEBUG
static int seader_print_struct_callback(const void* buffer, size_t size, void* app_key) {
if(app_key) {
char* str = (char*)app_key;
size_t next = strlen(str);
strncpy(str + next, buffer, size);
} else {
uint8_t next = strlen(asn1_log);
strncpy(asn1_log + next, buffer, size);
}
return 0;
}
#endif
void seader_send_payload(
Seader* seader,
Payload_t* payload,
uint8_t from,
uint8_t to,
uint8_t replyTo) {
SeaderUartBridge* seader_uart = seader_require_uart(seader);
uint8_t* scratchpad = seader_uart->tx_buf + MAX_FRAME_HEADERS;
size_t scratchpad_size = SEADER_UART_RX_BUF_SIZE - MAX_FRAME_HEADERS;
size_t max_der_len = UINT8_MAX - ASN1_PREFIX;
uint8_t* payload_buf = scratchpad;
bool payload_in_scratchpad = true;
uint8_t fallback_buf[255];
asn_enc_rval_t er = der_encode_to_buffer(
&asn_DEF_Payload, payload, scratchpad + ASN1_PREFIX, scratchpad_size - ASN1_PREFIX);
if(er.encoded < 0 || ((size_t)er.encoded + ASN1_PREFIX) > UINT8_MAX) {
payload_buf = fallback_buf;
payload_in_scratchpad = false;
er = der_encode_to_buffer(
&asn_DEF_Payload, payload, payload_buf + ASN1_PREFIX, max_der_len);
}
if(er.encoded < 0) {
FURI_LOG_E(TAG, "Failed to encode payload");
return;
}
size_t apdu_payload_len = ASN1_PREFIX + (size_t)er.encoded;
if(apdu_payload_len > UINT8_MAX) {
FURI_LOG_E(TAG, "Encoded payload too large for APDU: %d", (int)apdu_payload_len);
return;
}
#ifdef ASN1_DEBUG
if(er.encoded > -1) {
char payloadDebug[384] = {0};
memset(payloadDebug, 0, sizeof(payloadDebug));
(&asn_DEF_Payload)
->op->print_struct(
&asn_DEF_Payload, payload, 1, seader_print_struct_callback, payloadDebug);
if(strlen(payloadDebug) > 0) {
FURI_LOG_D(TAG, "Sending payload[%d %d %d]: %s", to, from, replyTo, payloadDebug);
}
} else {
FURI_LOG_W(TAG, "Failed to print_struct payload");
}
#endif
//0xa0, 0xda, 0x02, 0x63, 0x00, 0x00, 0x0a,
//0x44, 0x0a, 0x44, 0x00, 0x00, 0x00, 0xa0, 0x02, 0x96, 0x00
payload_buf[0] = from;
payload_buf[1] = to;
payload_buf[2] = replyTo;
payload_buf[3] = 0x00;
payload_buf[4] = 0x00;
payload_buf[5] = 0x00;
seader_send_apdu(
seader,
0xA0,
0xDA,
0x02,
0x63,
payload_buf,
(uint8_t)apdu_payload_len,
payload_in_scratchpad);
}
void seader_send_process_config_card(Seader* seader) {
SamCommand_t samCommand = {0};
Payload_t payload = {0};
samCommand.present = SamCommand_PR_processConfigCard;
seader_sam_set_state(
seader, SeaderSamStateConversation, SeaderSamIntentConfig, samCommand.present);
payload.present = Payload_PR_samCommand;
payload.choice.samCommand = samCommand;
seader_send_payload(seader, &payload, 0x44, 0x0a, 0x44);
}
void seader_send_response(
Seader* seader,
Response_t* response,
uint8_t from,
uint8_t to,
uint8_t replyTo) {
Payload_t payload = {0};
payload.present = Payload_PR_response;
payload.choice.response = *response;
seader_send_payload(seader, &payload, from, to, replyTo);
}
void seader_send_request_pacs2(Seader* seader) {
OCTET_STRING_t oid = {
.buf = (uint8_t*)seader_oid,
.size = sizeof(seader_oid),
};
RequestPacs_t requestPacs = {0};
requestPacs.contentElementTag = ContentElementTag_implicitFormatPhysicalAccessBits;
requestPacs.oid = &oid;
SamCommand_t samCommand = {0};
samCommand.present = SamCommand_PR_requestPacs2;
seader_sam_set_state(
seader, SeaderSamStateConversation, SeaderSamIntentReadPacs2, samCommand.present);
samCommand.choice.requestPacs2 = requestPacs;
Payload_t payload = {0};
payload.present = Payload_PR_samCommand;
payload.choice.samCommand = samCommand;
seader_send_payload(
seader, &payload, ExternalApplicationA, SAMInterface, ExternalApplicationA);
}
void seader_worker_send_serial_number(Seader* seader) {
SamCommand_t samCommand = {0};
samCommand.present = SamCommand_PR_serialNumber;
seader_sam_set_state(
seader, SeaderSamStateSerialPending, SeaderSamIntentMaintenance, samCommand.present);
Payload_t payload = {0};
payload.present = Payload_PR_samCommand;
payload.choice.samCommand = samCommand;
seader_send_payload(
seader, &payload, ExternalApplicationA, SAMInterface, ExternalApplicationA);
}
void seader_worker_send_version(Seader* seader) {
SamCommand_t samCommand = {0};
samCommand.present = SamCommand_PR_version;
if(!seader->sam_present) {
seader_reset_cached_sam_metadata(seader);
seader->sam_key_probe_status = SeaderSamKeyProbeStatusUnknown;
seader_update_sam_key_label(seader, NULL, 0U);
}
seader->sam_present = true;
seader_sam_set_state(
seader, SeaderSamStateVersionPending, SeaderSamIntentMaintenance, samCommand.present);
Payload_t payload = {0};
payload.present = Payload_PR_samCommand;
payload.choice.samCommand = samCommand;
seader_send_payload(
seader, &payload, ExternalApplicationA, SAMInterface, ExternalApplicationA);
}
bool seader_worker_send_process_snmp_message(
Seader* seader,
const uint8_t* message,
size_t message_len) {
furi_check(seader);
furi_check(message);
if(message_len == 0U || message_len > UINT16_MAX) return false;
SamCommand_t samCommand = {0};
samCommand.present = SamCommand_PR_processSNMPMessage;
samCommand.choice.processSNMPMessage.buf = (uint8_t*)message;
samCommand.choice.processSNMPMessage.size = message_len;
Payload_t payload = {0};
payload.present = Payload_PR_samCommand;
payload.choice.samCommand = samCommand;
seader_send_payload(
seader, &payload, ExternalApplicationA, SAMInterface, ExternalApplicationA);
return true;
}
void seader_send_card_detected(Seader* seader, CardDetails_t* cardDetails) {
furi_check(seader);
furi_check(cardDetails);
furi_check(cardDetails->csn.buf);
CardDetected_t cardDetected = {
.detectedCardDetails = *cardDetails,
};
SamCommand_t samCommand = {0};
samCommand.present = SamCommand_PR_cardDetected;
samCommand.choice.cardDetected = cardDetected;
Payload_t payload = {0};
payload.present = Payload_PR_samCommand;
payload.choice.samCommand = samCommand;
seader_trace(
TAG, "send cardDetected state=%d intent=%d", seader->sam_state, seader->sam_intent);
FURI_LOG_D(
TAG,
"Send cardDetected csn_len=%zu has_sak=%d has_ats=%d protocol_len=%zu",
cardDetails->csn.size,
cardDetails->sak != NULL,
cardDetails->atsOrAtqbOrAtr != NULL,
cardDetails->protocol.size);
seader_send_payload(
seader, &payload, ExternalApplicationA, SAMInterface, ExternalApplicationA);
}
void seader_send_no_card_detected(Seader* seader) {
furi_assert(seader);
CardDetails_t cardDetails = {0};
uint8_t protocol_bytes[] = {0x00, FrameProtocol_none};
OCTET_STRING_fromBuf(
&cardDetails.protocol, (const char*)protocol_bytes, sizeof(protocol_bytes));
OCTET_STRING_fromBuf(&cardDetails.csn, "", 0);
seader_sam_set_state(
seader, SeaderSamStateClearPending, SeaderSamIntentNone, SamCommand_PR_cardDetected);
seader_trace(TAG, "send no-card cardDetected");
seader_send_card_detected(seader, &cardDetails);
ASN_STRUCT_FREE_CONTENTS_ONLY(asn_DEF_CardDetails, &cardDetails);
}
static bool seader_store_pacs_bits(
SeaderCredential* credential,
const uint8_t* payload,
size_t payload_size,
uint8_t unused_bits) {
if(!credential || !payload || payload_size == 0 ||
payload_size > sizeof(credential->credential) || unused_bits > 7) {
return false;
}
const uint8_t bit_length = payload_size * 8 - unused_bits;
if(bit_length == 0) {
return false;
}
uint64_t credential_val = 0;
memcpy(&credential_val, payload, payload_size);
credential_val = __builtin_bswap64(credential_val);
credential->bit_length = bit_length;
credential->credential = credential_val >> (64 - bit_length);
return true;
}
static bool seader_unpack_pacs2_bits(Seader* seader, const OCTET_STRING_t* pacs_bits) {
SeaderCredential* seader_credential = seader->credential;
if(!pacs_bits || !pacs_bits->buf || pacs_bits->size < 2) {
FURI_LOG_W(TAG, "Malformed pacs2 bits: pacs_bits=%p", (void*)pacs_bits);
if(pacs_bits) {
FURI_LOG_W(TAG, " buf=%p, size=%zu", (void*)pacs_bits->buf, pacs_bits->size);
if(pacs_bits->buf) {
for(size_t i = 0; i < pacs_bits->size && i < 16; i++) {
FURI_LOG_W(TAG, " byte[%zu] = 0x%02x", i, pacs_bits->buf[i]);
}
}
}
return false;
}
SEADER_VERBOSE_HEX(FuriLogLevelDebug, TAG, "PACS2 bits", pacs_bits->buf, pacs_bits->size);
if(seader_credential->sio[0] == 0x30) {
SEADER_VERBOSE_HEX(
FuriLogLevelDebug, TAG, "SIO", seader_credential->sio, seader_credential->sio_len);
#ifdef ASN1_DEBUG
asn_dec_rval_t rval;
SIO_t sio = {0};
SIO_t* sio_p = &sio;
rval = asn_decode(
0,
ATS_DER,
&asn_DEF_SIO,
(void**)&sio_p,
seader_credential->sio,
seader_credential->sio_len);
if(rval.code == RC_OK) {
SEADER_VERBOSE_D(TAG, "Decoded SIO");
char sioDebug[384] = {0};
(&asn_DEF_SIO)
->op->print_struct(&asn_DEF_SIO, &sio, 1, seader_print_struct_callback, sioDebug);
if(strlen(sioDebug) > 0) {
SEADER_VERBOSE_D(TAG, "SIO: %s", sioDebug);
}
} else {
FURI_LOG_W(TAG, "Failed to decode SIO %d consumed", rval.consumed);
}
ASN_STRUCT_FREE_CONTENTS_ONLY(asn_DEF_SIO, &sio);
#endif
}
const uint8_t unused_bits = pacs_bits->buf[0];
const uint8_t* payload = pacs_bits->buf + 1;
const size_t payload_size = pacs_bits->size - 1;
SEADER_VERBOSE_D(TAG, "PACS2 unused_bits=%u payload_size=%zu", unused_bits, payload_size);
if(!seader_store_pacs_bits(seader_credential, payload, payload_size, unused_bits)) {
FURI_LOG_W(TAG, "Failed to store PACS2 bits");
return false;
}
SEADER_VERBOSE_D(
TAG,
"credential (%d) %016llx",
seader_credential->bit_length,
seader_credential->credential);
return true;
}
// 800201298106683d052026b6820101
//300F800201298106683D052026B6820101
// ATR3:
// 800207358106793D81F9F385820104A51E8004000000018106053000000000820B323330353139313232395A830152
#define MAX_VERSION_SIZE 60
bool seader_parse_version(Seader* seader, uint8_t* buf, size_t size) {
bool rtn = false;
if(size > MAX_VERSION_SIZE) {
// Too large to handle now
FURI_LOG_W(TAG, "Version of %d is too long to parse", size);
return false;
}
SamVersion_t version = {0};
SamVersion_t* version_p = &version;
// Add sequence prefix
uint8_t seq[MAX_VERSION_SIZE + 2] = {0x30};
seq[1] = (uint8_t)size;
memcpy(seq + 2, buf, size);
asn_dec_rval_t rval =
asn_decode(0, ATS_DER, &asn_DEF_SamVersion, (void**)&version_p, seq, size + 2);
if(rval.code == RC_OK) {
#ifdef ASN1_DEBUG
char versionDebug[128] = {0};
(&asn_DEF_SamVersion)
->op->print_struct(
&asn_DEF_SamVersion, &version, 1, seader_print_struct_callback, versionDebug);
if(strlen(versionDebug) > 0) {
SEADER_VERBOSE_D(TAG, "Received version: %s", versionDebug);
}
#endif
if(version.version.size == 2) {
memcpy(seader->sam_version, version.version.buf, version.version.size);
SEADER_VERBOSE_I(
TAG, "SAM Version: %d.%d", seader->sam_version[0], seader->sam_version[1]);
}
rtn = true;
} else {
FURI_LOG_W(TAG, "Failed to decode SamVersion %d consumed, size %d", rval.consumed, size);
}
ASN_STRUCT_FREE_CONTENTS_ONLY(asn_DEF_SamVersion, &version);
return rtn;
}
bool seader_sam_save_serial(Seader* seader, uint8_t* buf, size_t size) {
SeaderCredential* cred = seader->credential;
const char* file_header = "SAM Serial Number";
const uint32_t file_version = 1;
bool use_load_path = true;
bool saved = false;
FlipperFormat* file = flipper_format_file_alloc(cred->storage);
FuriString* temp_str;
temp_str = furi_string_alloc();
do {
if(use_load_path && !furi_string_empty(cred->load_path)) {
// Get directory name
path_extract_dirname(furi_string_get_cstr(cred->load_path), temp_str);
// Make path to file to save
furi_string_cat_printf(temp_str, "/%s%s", SEADER_SERIAL_FILE_NAME, ".txt");
} else {
furi_string_printf(
temp_str, "%s/%s%s", STORAGE_APP_DATA_PATH_PREFIX, SEADER_SERIAL_FILE_NAME, ".txt");
}
// Open file
if(!flipper_format_file_open_always(file, furi_string_get_cstr(temp_str))) break;
if(!flipper_format_write_header_cstr(file, file_header, file_version)) break;
if(!flipper_format_write_hex(file, "Chip Serial Number", buf, size)) break;
saved = true;
} while(false);
if(!saved) {
dialog_message_show_storage_error(cred->dialogs, "Can not save\nserial file");
}
furi_string_free(temp_str);
flipper_format_free(file);
return saved;
}
bool seader_sam_save_serial_QR(Seader* seader, char* serial) {
SeaderCredential* cred = seader->credential;
const char* file_header = "QRCode";
const uint32_t file_version = 0;
bool saved = false;
FlipperFormat* file = flipper_format_file_alloc(cred->storage);
FuriString* temp_str;
temp_str = furi_string_alloc();
do {
storage_simply_mkdir(cred->storage, EXT_PATH("qrcodes"));
furi_string_printf(
temp_str, "%s/%s%s", EXT_PATH("qrcodes"), "seader_sam_serial", ".qrcode");
// Open file
if(!flipper_format_file_open_always(file, furi_string_get_cstr(temp_str))) break;
if(!flipper_format_write_header_cstr(file, file_header, file_version)) break;
if(!flipper_format_write_string_cstr(file, "Message", serial)) break;
saved = true;
} while(false);
if(!saved) {
dialog_message_show_storage_error(cred->dialogs, "Can not save\nQR file");
}
furi_string_free(temp_str);
flipper_format_free(file);
return saved;
}
bool seader_parse_serial_number(Seader* seader, uint8_t* buf, size_t size) {
// Create hex string for QR code (needs to be persistent)
char hex_string[size * 2 + 1];
for(size_t i = 0; i < size; i++) {
snprintf(hex_string + (i * 2), sizeof(hex_string) - (i * 2), "%02x", buf[i]);
}
hex_string[size * 2] = '\0';
SEADER_VERBOSE_HEX(FuriLogLevelDebug, TAG, "Received serial", buf, size);
seader_sam_save_serial_QR(seader, hex_string);
return seader_sam_save_serial(seader, buf, size);
}
void seader_abort_active_read_with_reason(
Seader* seader,
SeaderHfReadFailureReason reason,
const char* detail) {
SeaderWorker* seader_worker = seader_get_active_worker(seader);
const int stage = seader_worker ? (int)seader_worker->stage : -1;
FURI_LOG_W(TAG, "Abort active read stage=%d sam=%d", stage, seader->samCommand);
seader_trace(
TAG,
"abort stage=%d sam=%d state=%d intent=%d",
stage,
seader->samCommand,
seader->sam_state,
seader->sam_intent);
if(reason != SeaderHfReadFailureReasonNone) {
seader->hf_read_failure_reason = reason;
if(detail && detail[0] != '\0') {
strlcpy(seader->read_error, detail, sizeof(seader->read_error));
} else {
strlcpy(
seader->read_error,
seader_hf_read_failure_reason_text(reason),
sizeof(seader->read_error));
}
}
if(seader_worker) {
seader_worker->stage = SeaderPollerEventTypeFail;
}
seader->hf_read_state = SeaderHfReadStateTerminalFail;
if(!seader_sam_has_active_card(seader) && seader->sam_state != SeaderSamStateClearPending) {
seader_sam_force_idle_for_recovery(seader);
}
view_dispatcher_send_custom_event(seader->view_dispatcher, SeaderCustomEventWorkerExit);
}
static void seader_abort_active_read(Seader* seader) {
seader_abort_active_read_with_reason(seader, SeaderHfReadFailureReasonNone, NULL);
}
bool seader_parse_sam_response2(Seader* seader, SamResponse2_t* samResponse) {
switch(samResponse->present) {
case SamResponse2_PR_pacs:
SEADER_VERBOSE_I(TAG, "samResponse2 SamResponse2_PR_pacs");
if((seader->sam_state != SeaderSamStateConversation &&
seader->sam_state != SeaderSamStateFinishing) ||
seader->sam_intent != SeaderSamIntentReadPacs2) {
FURI_LOG_W(
TAG,
"Unexpected pacs2 response in state=%d intent=%d",
seader->sam_state,
seader->sam_intent);
seader_abort_active_read(seader);
break;
}
Pacs2_t pacs2 = samResponse->choice.pacs;
OCTET_STRING_t* pacs = pacs2.bits;
seader->credential->has_pacs_media_type = pacs2.type != NULL;
seader->credential->pacs_media_type = pacs2.type ? (SeaderPacsMediaType)(*pacs2.type) :
SeaderPacsMediaTypeUnknown;
const bool sam_keys_missing = seader_pacs2_indicates_sam_keys_missing(
seader->credential->has_pacs_media_type,
pacs ? pacs->buf : NULL,
pacs ? pacs->size : 0U);
if(sam_keys_missing) {
char read_error[SEADER_TEXT_STORE_SIZE + 1] = {0};
seader_hf_read_format_sam_keys_missing_error(
seader->credential->has_pacs_media_type,
(SeaderHfPacsMediaType)seader->credential->pacs_media_type,
seader->snmp_probe.standard_pacs_keys_probed,
seader_uhf_snmp_probe_standard_pacs_keys_present(&seader->snmp_probe),
read_error,
sizeof(read_error));
FURI_LOG_W(
TAG,
"Empty PACS2 after card read: media=%d probe=%d",
seader->credential->pacs_media_type,
seader->sam_key_probe_status);
seader_abort_active_read_with_reason(
seader, SeaderHfReadFailureReasonSamKeysMissing, read_error);
break;
}
if(seader_unpack_pacs2_bits(seader, pacs)) {
SeaderWorker* seader_worker = seader_get_active_worker(seader);
if(seader_worker) {
seader_worker->stage = SeaderPollerEventTypeComplete;
}
seader->hf_read_state = SeaderHfReadStateTerminalSuccess;
seader_sam_set_state(
seader, SeaderSamStateIdle, SeaderSamIntentNone, SamCommand_PR_NOTHING);
} else {
seader_abort_active_read_with_reason(
seader, SeaderHfReadFailureReasonProtocolError, NULL);
}
break;
case SamResponse2_PR_NOTHING:
SEADER_VERBOSE_I(TAG, "samResponse2 SamResponse2_PR_NOTHING");
seader_abort_active_read(seader);
break;
default:
SEADER_VERBOSE_I(TAG, "Unknown samResponse2 %d", samResponse->present);
seader_abort_active_read(seader);
break;
}
return false;
}
bool seader_parse_sam_response(Seader* seader, SamResponse_t* samResponse) {
SeaderWorker* seader_worker = seader_get_active_worker(seader);
switch(seader->sam_state) {
case SeaderSamStateConversation:
case SeaderSamStateFinishing:
if(seader->sam_intent == SeaderSamIntentConfig) {
FURI_LOG_I(TAG, "samResponse config");
if(seader_worker) {
seader_worker->stage = SeaderPollerEventTypeFail;
}
seader_sam_set_state(
seader, SeaderSamStateIdle, SeaderSamIntentNone, SamCommand_PR_NOTHING);
} else {
FURI_LOG_W(TAG, "Unexpected samResponse intent=%d", seader->sam_intent);
seader_abort_active_read(seader);
}
break;
case SeaderSamStateVersionPending:
FURI_LOG_I(TAG, "samResponse version");
seader_parse_version(seader, samResponse->buf, samResponse->size);
seader_worker_send_serial_number(seader);
break;
case SeaderSamStateSerialPending:
FURI_LOG_I(TAG, "samResponse serial");
seader_parse_serial_number(seader, samResponse->buf, samResponse->size);
seader_start_snmp_probe(seader);
break;
case SeaderSamStateCapabilityPending:
SEADER_VERBOSE_I(TAG, "samResponse processSNMPMessage");
if(!seader_uhf_snmp_probe_consume_response(
&seader->snmp_probe, samResponse->buf, samResponse->size)) {
seader_finish_failed_snmp_probe(seader);
break;
}
if(seader->snmp_probe.ice_value_len > 0U) {
seader_set_sam_key_probe_status_from_snmp(seader, true);
}
if(seader->snmp_probe.stage >= SeaderUhfSnmpProbeStageReadStandardEncryptionKey ||
seader->snmp_probe.stage == SeaderUhfSnmpProbeStageDone) {
seader_update_sam_key_label(
seader, seader->snmp_probe.ice_value_storage, seader->snmp_probe.ice_value_len);
}
if(seader->snmp_probe.stage >= SeaderUhfSnmpProbeStageReadTagConfig ||
seader->snmp_probe.stage == SeaderUhfSnmpProbeStageDone) {
if(seader->snmp_probe.supports_uhf) {
seader->uhf_probe_status = SeaderUhfProbeStatusSuccess;
}
seader_update_uhf_status_label(seader);
}
if(seader->snmp_probe.stage == SeaderUhfSnmpProbeStageDone) {
seader_snmp_probe_finish(seader);
} else if(
seader->snmp_probe.stage == SeaderUhfSnmpProbeStageFailed ||
!seader_snmp_probe_send_next_request(seader)) {
seader_finish_failed_snmp_probe(seader);
}
break;
case SeaderSamStateDetectPending:
SEADER_VERBOSE_I(TAG, "samResponse cardDetected");
if(seader->sam_intent == SeaderSamIntentConfig) {
seader_send_process_config_card(seader);
} else if(seader->sam_intent == SeaderSamIntentReadPacs2) {
seader_send_request_pacs2(seader);
} else {
FURI_LOG_W(TAG, "Unexpected detect intent=%d", seader->sam_intent);
seader_abort_active_read(seader);
}
break;
case SeaderSamStateClearPending:
SEADER_VERBOSE_I(TAG, "samResponse clear-detected-card ack");
seader_trace(
TAG,
"cardDetected ack clear stage=%d",
seader_worker ? (int)seader_worker->stage : -1);
seader_sam_set_state(
seader, SeaderSamStateIdle, SeaderSamIntentNone, SamCommand_PR_NOTHING);
break;
case SeaderSamStateIdle:
FURI_LOG_W(TAG, "Unexpected samResponse while idle");
SEADER_VERBOSE_HEX(
FuriLogLevelDebug, TAG, "Unexpected samResponse", samResponse->buf, samResponse->size);
break;
default:
FURI_LOG_W(TAG, "Unhandled sam state %d", seader->sam_state);
seader_abort_active_read(seader);
break;
}
return false;
}
bool seader_parse_response(Seader* seader, Response_t* response) {
switch(response->present) {
case Response_PR_samResponse:
seader_parse_sam_response(seader, &response->choice.samResponse);
break;
case Response_PR_samResponse2:
seader_parse_sam_response2(seader, &response->choice.samResponse2);
break;
default:
SEADER_VERBOSE_D(TAG, "non-sam response");
break;
};
return false;
}
void seader_send_nfc_rx_status(
Seader* seader,
uint8_t* buffer,
size_t len,
SeaderHfBridgeRfStatus status) {
OCTET_STRING_t rxData = {.buf = buffer, .size = len};
uint8_t status_bytes[2] = {0};
seader_hf_bridge_rf_status_bytes(status, status_bytes);
RfStatus_t rfStatus = {.buf = status_bytes, .size = sizeof(status_bytes)};
NFCRx_t nfcRx = {0};
nfcRx.rfStatus = rfStatus;
if(buffer && len > 0U) {
nfcRx.data = &rxData;
}
NFCResponse_t nfcResponse = {0};
nfcResponse.present = NFCResponse_PR_nfcRx;
nfcResponse.choice.nfcRx = nfcRx;
Response_t response = {0};
response.present = Response_PR_nfcResponse;
response.choice.nfcResponse = nfcResponse;
seader_send_response(seader, &response, NFCInterface, SAMInterface, 0x0);
}
void seader_send_nfc_rx(Seader* seader, uint8_t* buffer, size_t len) {
seader_send_nfc_rx_status(seader, buffer, len, SeaderHfBridgeRfStatusSuccess);
}
static void seader_dispatch_nfc_send(
Seader* seader,
uint8_t* data,
size_t data_len,
uint32_t timeout_us,
const uint8_t* format,
size_t format_len) {
PluginHfAction action = {
.data = data,
.len = data_len,
.timeout = timeout_us,
};
if(format) {
const size_t copied_format_len =
format_len < sizeof(action.format) ? format_len : sizeof(action.format);
memcpy(action.format, format, copied_format_len);
}
if(seader->credential->type == SeaderCredentialTypeVirtual) {
seader_virtual_picopass_state_machine(seader, data, data_len);
} else if(seader->plugin_hf && seader->hf_plugin_ctx) {
if(seader->credential->type == SeaderCredentialTypePicopass) {
action.type = PluginHfActionTypePicopassTx;
} else if(seader->credential->type == SeaderCredentialTypeMifareClassic) {
action.type = PluginHfActionTypeMfClassicTx;
} else {
action.type = PluginHfActionTypeIso14443Tx;
}
SEADER_VERBOSE_D(
TAG,
"Dispatch HF action type=%d len=%u timeout=%lu",
action.type,
action.len,
(unsigned long)action.timeout);
if(!seader->plugin_hf->handle_action(seader->hf_plugin_ctx, &action)) {
FURI_LOG_W(TAG, "HF plugin failed to handle action");
SeaderWorker* seader_worker = seader_get_active_worker(seader);
if(seader_worker) {
seader_worker->stage = SeaderPollerEventTypeFail;
}
}
} else {
FURI_LOG_W(TAG, "No HF plugin available for nfcSend");
}
}
void seader_parse_nfc_command_transmit(Seader* seader, NFCSend_t* nfcSend) {
#ifdef ASN1_DEBUG
SEADER_VERBOSE_HEX(
FuriLogLevelDebug, TAG, "Transmit data", nfcSend->data.buf, nfcSend->data.size);
#endif
const long sam_timeout_us = nfcSend->timeOut;
const uint32_t timeout_us = sam_timeout_us > 0L ? (uint32_t)sam_timeout_us : 0U;
const uint8_t* format = nfcSend->format ? nfcSend->format->buf : NULL;
const size_t format_len = nfcSend->format ? (size_t)nfcSend->format->size : 0U;
seader_dispatch_nfc_send(
seader, nfcSend->data.buf, nfcSend->data.size, timeout_us, format, format_len);
}
void seader_parse_nfc_off(Seader* seader) {
SEADER_VERBOSE_D(TAG, "Set Field Off");
seader_trace(TAG, "nfcOff state=%d intent=%d", seader->sam_state, seader->sam_intent);
NFCResponse_t nfcResponse = {0};
nfcResponse.present = NFCResponse_PR_nfcAck;
Response_t response = {0};
response.present = Response_PR_nfcResponse;
response.choice.nfcResponse = nfcResponse;
seader_send_response(seader, &response, ExternalApplicationA, SAMInterface, 0);
if(seader->sam_state == SeaderSamStateConversation &&
(seader->sam_intent == SeaderSamIntentReadPacs2 ||
seader->sam_intent == SeaderSamIntentConfig)) {
seader->hf_read_state = SeaderHfReadStateFinishing;
seader->hf_read_last_progress_tick = furi_get_tick();
seader_sam_set_state(
seader, SeaderSamStateFinishing, seader->sam_intent, seader->samCommand);
}
}
void seader_parse_nfc_command(Seader* seader, NFCCommand_t* nfcCommand, SeaderPollerContainer* spc) {
switch(nfcCommand->present) {
case NFCCommand_PR_nfcSend:
seader_parse_nfc_command_transmit(seader, &nfcCommand->choice.nfcSend);
break;
case NFCCommand_PR_nfcOff:
seader_parse_nfc_off(seader);
if(spc != NULL) {
SeaderWorker* seader_worker = seader_get_active_worker(seader);
if(seader_worker) {
seader_worker->stage = SeaderPollerEventTypeComplete;
}
}
break;
default:
FURI_LOG_W(TAG, "unparsed NFCCommand");
break;
};
}
bool seader_worker_state_machine(
Seader* seader,
Payload_t* payload,
bool online,
SeaderPollerContainer* spc) {
bool processed = false;
switch(payload->present) {
case Payload_PR_response:
SEADER_VERBOSE_D(TAG, "Payload_PR_response");
seader_parse_response(seader, &payload->choice.response);
processed = true;
break;
case Payload_PR_nfcCommand:
SEADER_VERBOSE_D(TAG, "Payload_PR_nfcCommand");
if(online) {
seader_parse_nfc_command(seader, &payload->choice.nfcCommand, spc);
processed = true;
} else if(payload->choice.nfcCommand.present == NFCCommand_PR_nfcOff) {
seader_parse_nfc_command(seader, &payload->choice.nfcCommand, NULL);
processed = true;
} else {
seader_trace(
TAG,
"defer offline nfcSend state=%d intent=%d",
seader->sam_state,
seader->sam_intent);
}
break;
case Payload_PR_errorResponse:
processed = true;
if(seader->sam_state == SeaderSamStateCapabilityPending) {
ErrorResponse_t* err = &payload->choice.errorResponse;
SeaderUhfSnmpProbeStage previous_stage = seader->snmp_probe.stage;
if(seader_uhf_snmp_probe_consume_error(
&seader->snmp_probe, err->errorCode, err->data.buf, err->data.size)) {
SEADER_VERBOSE_I(
TAG,
"SNMP probe handled error stage=%s code=0x%02lx data=%02x%02x len=%zu",
seader_snmp_probe_stage_name(previous_stage),
(unsigned long)err->errorCode,
err->data.size > 0U ? err->data.buf[0] : 0U,
err->data.size > 1U ? err->data.buf[1] : 0U,
err->data.size);
if(seader->snmp_probe.ice_value_len > 0U) {
seader_set_sam_key_probe_status_from_snmp(seader, true);
}
if(seader->snmp_probe.stage >= SeaderUhfSnmpProbeStageReadStandardEncryptionKey ||
seader->snmp_probe.stage == SeaderUhfSnmpProbeStageDone) {
seader_update_sam_key_label(
seader,
seader->snmp_probe.ice_value_storage,
seader->snmp_probe.ice_value_len);
}
if(seader->snmp_probe.stage >= SeaderUhfSnmpProbeStageReadTagConfig ||
seader->snmp_probe.stage == SeaderUhfSnmpProbeStageDone) {
if(seader->snmp_probe.supports_uhf) {
seader->uhf_probe_status = SeaderUhfProbeStatusSuccess;
}
seader_update_uhf_status_label(seader);
}
if(seader->snmp_probe.stage == SeaderUhfSnmpProbeStageDone) {
seader_snmp_probe_finish(seader);
} else if(!seader_snmp_probe_send_next_request(seader)) {
seader_finish_failed_snmp_probe(seader);
}
} else {
FURI_LOG_W(
TAG,
"SNMP probe unhandled error stage=%s code=0x%02lx data=%02x%02x len=%zu",
seader_snmp_probe_stage_name(previous_stage),
(unsigned long)err->errorCode,
err->data.size > 0U ? err->data.buf[0] : 0U,
err->data.size > 1U ? err->data.buf[1] : 0U,
err->data.size);
seader_finish_failed_snmp_probe(seader);
}
} else {
FURI_LOG_W(TAG, "Payload_PR_errorResponse");
view_dispatcher_send_custom_event(
seader->view_dispatcher, SeaderCustomEventWorkerExit);
}
break;
default:
FURI_LOG_W(TAG, "unhandled payload");
break;
};
return processed;
}
bool seader_process_success_response_i(
Seader* seader,
uint8_t* apdu,
size_t len,
bool online,
SeaderPollerContainer* spc) {
log_hex("RAW RX APDU", apdu, len);
Payload_t payload = {0};
Payload_t* payload_p = &payload;
bool processed = false;
SeaderHfSamNfcSendView nfc_send_view = {0};
if(seader_hf_sam_response_view_parse_nfc_send(apdu, len, &nfc_send_view)) {
if(online) {
seader_dispatch_nfc_send(
seader,
(uint8_t*)nfc_send_view.data,
nfc_send_view.data_len,
nfc_send_view.timeout_us,
nfc_send_view.format,
nfc_send_view.format_len);
return true;
}
seader_trace(
TAG, "defer offline nfcSend state=%d intent=%d", seader->sam_state, seader->sam_intent);
return false;
}
if(len < ASN1_PREFIX) {
SEADER_VERBOSE_HEX(FuriLogLevelDebug, TAG, "Short APDU payload", apdu, len);
return false;
}
/* Seader wraps each ASN.1 payload with a 6-byte application header
{from, to, replyTo, 0x00, 0x00, 0x00}. Skip that prefix before decoding. */
asn_dec_rval_t rval =
asn_decode(0, ATS_DER, &asn_DEF_Payload, (void**)&payload_p, apdu + 6, len - 6);
if(rval.code == RC_OK) {
#ifdef ASN1_DEBUG
if(online == false) {
SEADER_VERBOSE_HEX(FuriLogLevelDebug, TAG, "incoming APDU", apdu + 6, len - 6);
char payloadDebug[384] = {0};
memset(payloadDebug, 0, sizeof(payloadDebug));
(&asn_DEF_Payload)
->op->print_struct(
&asn_DEF_Payload, &payload, 1, seader_print_struct_callback, payloadDebug);
if(strlen(payloadDebug) > 0) {
SEADER_VERBOSE_D(TAG, "Received Payload: %s", payloadDebug);
} else {
SEADER_VERBOSE_D(TAG, "Received empty Payload");
}
} else {
SEADER_VERBOSE_D(TAG, "Online mode");
}
#endif
processed = seader_worker_state_machine(seader, &payload, online, spc);
} else {
SEADER_VERBOSE_HEX(FuriLogLevelDebug, TAG, "Failed to decode APDU payload", apdu, len);
}
ASN_STRUCT_FREE_CONTENTS_ONLY(asn_DEF_Payload, &payload);
return processed;
}
NfcCommand seader_worker_card_detect(
Seader* seader,
uint8_t sak,
uint8_t* atqa,
const uint8_t* uid,
uint8_t uid_len,
uint8_t* ats,
uint8_t ats_len) {
UNUSED(atqa);
furi_check(seader);
furi_check(seader->credential);
furi_check(uid);
furi_check(uid_len > 0U);
SeaderCredential* credential = seader->credential;
CardDetails_t cardDetails = {0};
SEADER_VERBOSE_D(
TAG, "Build card_detect sak=%02x uid_len=%u ats_len=%u", sak, uid_len, ats_len);
/* The UID is reused as the current diversifier seed for formats that need one. This is
not universal across all media, but it is the intentional behavior for the cards Seader
currently supports on this read path. */
size_t diversifier_len = uid_len;
if(diversifier_len > sizeof(credential->diversifier)) {
FURI_LOG_W(
TAG, "Clamp diversifier uid_len=%u to %zu", uid_len, sizeof(credential->diversifier));
diversifier_len = sizeof(credential->diversifier);
}
memcpy(credential->diversifier, uid, diversifier_len);
credential->diversifier_len = diversifier_len;
if(!seader_card_details_build(&cardDetails, sak, uid, uid_len, ats, ats_len)) {
FURI_LOG_E(TAG, "Failed to build card details");
return NfcCommandStop;
}
seader_sam_set_state(
seader,
SeaderSamStateDetectPending,
seader_sam_card_intent(seader),
SamCommand_PR_cardDetected);
/* cardDetails must remain valid until the SAM payload is encoded, then it can be released
through the ASN.1-owned reset helper. */
seader_send_card_detected(seader, &cardDetails);
SEADER_VERBOSE_D(TAG, "cardDetected sent");
// Print version information for app and firmware for later review in log
SEADER_VERBOSE_I(
TAG,
"Firmware origin: %s firmware version: %s app version: %s",
version_get_firmware_origin(version_get()),
version_get_version(version_get()),
FAP_VERSION);
seader_card_details_reset(&cardDetails);
return NfcCommandContinue;
}