Skip to content

Commit

Permalink
picopass: Add support for non-secure cards (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
nvx committed Jan 12, 2024
1 parent efe1a38 commit 95ae7f2
Show file tree
Hide file tree
Showing 15 changed files with 153 additions and 58 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
dist/*
.vscode
.clang-format
.editorconfig
.editorconfig
.env
.ufbt
6 changes: 3 additions & 3 deletions lib/loclass/optimized_cipher.c
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ void loclass_opt_doReaderMAC(uint8_t* cc_nr_p, uint8_t* div_key_p, uint8_t mac[4

void loclass_opt_doReaderMAC_2(
LoclassState_t _init,
uint8_t* nr,
const uint8_t* nr,
uint8_t mac[4],
const uint8_t* div_key_p) {
loclass_opt_suc(div_key_p, &_init, nr, 4, false);
Expand Down Expand Up @@ -268,7 +268,7 @@ LoclassState_t loclass_opt_doTagMAC_1(uint8_t* cc_p, const uint8_t* div_key_p) {
*/
void loclass_opt_doTagMAC_2(
LoclassState_t _init,
uint8_t* nr,
const uint8_t* nr,
uint8_t mac[4],
const uint8_t* div_key_p) {
loclass_opt_suc(div_key_p, &_init, nr, 4, true);
Expand All @@ -286,7 +286,7 @@ void loclass_opt_doTagMAC_2(
*/
void loclass_opt_doBothMAC_2(
LoclassState_t _init,
uint8_t* nr,
const uint8_t* nr,
uint8_t rmac[4],
uint8_t tmac[4],
const uint8_t* div_key_p) {
Expand Down
6 changes: 3 additions & 3 deletions lib/loclass/optimized_cipher.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ void loclass_opt_doReaderMAC(uint8_t* cc_nr_p, uint8_t* div_key_p, uint8_t mac[4

void loclass_opt_doReaderMAC_2(
LoclassState_t _init,
uint8_t* nr,
const uint8_t* nr,
uint8_t mac[4],
const uint8_t* div_key_p);

Expand Down Expand Up @@ -89,7 +89,7 @@ LoclassState_t loclass_opt_doTagMAC_1(uint8_t* cc_p, const uint8_t* div_key_p);
*/
void loclass_opt_doTagMAC_2(
LoclassState_t _init,
uint8_t* nr,
const uint8_t* nr,
uint8_t mac[4],
const uint8_t* div_key_p);

Expand All @@ -103,7 +103,7 @@ void loclass_opt_doTagMAC_2(
*/
void loclass_opt_doBothMAC_2(
LoclassState_t _init,
uint8_t* nr,
const uint8_t* nr,
uint8_t rmac[4],
uint8_t tmac[4],
const uint8_t* div_key_p);
Expand Down
4 changes: 2 additions & 2 deletions picopass_device.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
// Crypt1 // 1+1 (crypt1+crypt0) means secured and keys changable
#define PICOPASS_FUSE_CRYPT1 0x10
// Crypt0 // 1+0 means secure and keys locked, 0+1 means not secured, 0+0 means disable auth entirely
#define PICOPASS_FUSE_CRTPT0 0x08
#define PICOPASS_FUSE_CRYPT10 (PICOPASS_FUSE_CRYPT1 | PICOPASS_FUSE_CRTPT0)
#define PICOPASS_FUSE_CRYPT0 0x08
#define PICOPASS_FUSE_CRYPT10 (PICOPASS_FUSE_CRYPT1 | PICOPASS_FUSE_CRYPT0)
// Read Access, 1 meanns anonymous read enabled, 0 means must auth to read applicaion
#define PICOPASS_FUSE_RA 0x01

Expand Down
1 change: 1 addition & 0 deletions picopass_i.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ struct Picopass {
PicopassPoller* poller;
PicopassListener* listener;
KeysDict* dict;
uint32_t last_error_notify_ticks;

char text_store[PICOPASS_TEXT_STORE_SIZE];
FuriString* text_box_store;
Expand Down
63 changes: 42 additions & 21 deletions protocol/picopass_listener.c
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,15 @@ PicopassListenerCommand
uint8_t block_num = bit_buffer_get_byte(buf, 1);
if(block_num > PICOPASS_MAX_APP_LIMIT) break;

bool secured = (instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[7] &
PICOPASS_FUSE_CRYPT10) != PICOPASS_FUSE_CRYPT0;

// TODO: Check CRC?
// TODO: Check auth?

bit_buffer_reset(instance->tx_buffer);
if((block_num == PICOPASS_SECURE_KD_BLOCK_INDEX) ||
(block_num == PICOPASS_SECURE_KC_BLOCK_INDEX)) {
if(secured && ((block_num == PICOPASS_SECURE_KD_BLOCK_INDEX) ||
(block_num == PICOPASS_SECURE_KC_BLOCK_INDEX))) {
for(size_t i = 0; i < PICOPASS_BLOCK_LEN; i++) {
bit_buffer_append_byte(instance->tx_buffer, 0xff);
}
Expand Down Expand Up @@ -193,6 +199,8 @@ static PicopassListenerCommand
uint8_t block_num = bit_buffer_get_byte(buf, 1);
if(block_num != PICOPASS_SECURE_EPURSE_BLOCK_INDEX) break;

// note that even non-secure chips seem to reply to READCHECK still

// loclass mode doesn't do any card side crypto, just logs the readers crypto, so no-op in this mode
// we can also no-op if the key block is the same, CHECK re-inits if it failed already
if((instance->key_block_num != key_block_num) &&
Expand Down Expand Up @@ -238,8 +246,7 @@ PicopassListenerCommand

PicopassBlock key = instance->data->AA1[PICOPASS_SECURE_KD_BLOCK_INDEX];
uint8_t rmac[4];
uint8_t rx_data[9] = {};
bit_buffer_write_bytes(buf, rx_data, sizeof(rx_data));
const uint8_t* rx_data = bit_buffer_get_data(buf);
loclass_opt_doReaderMAC_2(instance->cipher_state, &rx_data[1], rmac, key.data);

if(!memcmp(&rx_data[5], rmac, 4)) {
Expand Down Expand Up @@ -300,7 +307,8 @@ PicopassListenerCommand
return command;
}

PicopassListenerCommand picopass_listener_save_mac(PicopassListener* instance, uint8_t* rx_data) {
PicopassListenerCommand
picopass_listener_save_mac(PicopassListener* instance, const uint8_t* rx_data) {
PicopassListenerCommand command = PicopassListenerCommandSilent;
Picopass* picopass = instance->context;

Expand Down Expand Up @@ -360,13 +368,15 @@ PicopassListenerCommand
PicopassListenerCommand command = PicopassListenerCommandSilent;

do {
bool secured = (instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[7] &
PICOPASS_FUSE_CRYPT10) != PICOPASS_FUSE_CRYPT0;
if(!secured) break;

uint8_t rmac[4] = {};
uint8_t tmac[4] = {};
const uint8_t* key = instance->data->AA1[instance->key_block_num].data;
// Since nr isn't const in loclass_opt_doBothMAC_2() copy buffer
uint8_t rx_data[9] = {};
bit_buffer_write_bytes(buf, rx_data, sizeof(rx_data));
bool no_key = picopass_is_memset(key, 0x00, PICOPASS_BLOCK_LEN);
const uint8_t* rx_data = bit_buffer_get_data(buf);

if(no_key) {
// We're emulating a partial dump of an iClass SE card and should capture the NR and MAC
Expand Down Expand Up @@ -424,27 +434,31 @@ PicopassListenerCommand

PicopassBlock config_block = instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX];
bool pers_mode = PICOPASS_LISTENER_HAS_MASK(config_block.data[7], PICOPASS_FUSE_PERS);
bool secured = (instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[7] &
PICOPASS_FUSE_CRYPT10) != PICOPASS_FUSE_CRYPT0;

const uint8_t* rx_data = bit_buffer_get_data(buf);
uint8_t block_num = rx_data[1];
if(block_num == PICOPASS_CSN_BLOCK_INDEX) break; // CSN is always read only
if(!pers_mode && PICOPASS_LISTENER_HAS_MASK(config_block.data[3], 0x80))
break; // Chip is in RO mode, no updated possible (even ePurse)
if(!pers_mode && (block_num == PICOPASS_SECURE_AIA_BLOCK_INDEX))
if(!pers_mode && ((secured && block_num == PICOPASS_SECURE_AIA_BLOCK_INDEX) ||
(!secured && block_num == PICOPASS_NONSECURE_AIA_BLOCK_INDEX)))
break; // AIA can only be set in personalisation mode
if(!pers_mode &&
if(!pers_mode && secured &&
((block_num == PICOPASS_SECURE_KD_BLOCK_INDEX ||
block_num == PICOPASS_SECURE_KC_BLOCK_INDEX) &&
(!PICOPASS_LISTENER_HAS_MASK(config_block.data[7], PICOPASS_FUSE_CRYPT10))))
break;
break; // TODO: Is this the right response?

if(block_num >= 6 && block_num <= 12) {
// bit0 is block6, up to bit6 being block12
if(!PICOPASS_LISTENER_HAS_MASK(config_block.data[3], (1 << (block_num - 6)))) {
// Block is marked as read-only, deny writing
break;
break; // TODO: Is this the right response?
}
}

// TODO: Check CRC/SIGN depending on if in secure mode
// Check correct key
// -> Kd only allows decrementing e-Purse
Expand Down Expand Up @@ -477,15 +491,17 @@ PicopassListenerCommand
break;

case PICOPASS_SECURE_EPURSE_BLOCK_INDEX:
// ePurse updates swap first and second half of the block each update
memcpy(&new_block.data[4], &rx_data[2], 4);
memcpy(&new_block.data[0], &rx_data[6], 4);
if(secured) {
// ePurse updates swap first and second half of the block each update on secure cards
memcpy(&new_block.data[4], &rx_data[2], 4);
memcpy(&new_block.data[0], &rx_data[6], 4);
}
break;

case PICOPASS_SECURE_KD_BLOCK_INDEX:
// fallthrough
case PICOPASS_SECURE_KC_BLOCK_INDEX:
if(!pers_mode) {
if(!pers_mode && secured) {
new_block = instance->data->AA1[block_num];
for(size_t i = 0; i < sizeof(PicopassBlock); i++) {
new_block.data[i] ^= rx_data[i + 2];
Expand All @@ -500,14 +516,14 @@ PicopassListenerCommand
}

instance->data->AA1[block_num] = new_block;
if((block_num == instance->key_block_num) ||
(block_num == PICOPASS_SECURE_EPURSE_BLOCK_INDEX)) {
if(secured && ((block_num == instance->key_block_num) ||
(block_num == PICOPASS_SECURE_EPURSE_BLOCK_INDEX))) {
picopass_listener_init_cipher_state(instance);
}

bit_buffer_reset(instance->tx_buffer);
if((block_num == PICOPASS_SECURE_KD_BLOCK_INDEX) ||
(block_num == PICOPASS_SECURE_KC_BLOCK_INDEX)) {
if(secured && ((block_num == PICOPASS_SECURE_KD_BLOCK_INDEX) ||
(block_num == PICOPASS_SECURE_KC_BLOCK_INDEX))) {
// Key updates always return FF's
for(size_t i = 0; i < PICOPASS_BLOCK_LEN; i++) {
bit_buffer_append_byte(instance->tx_buffer, 0xff);
Expand Down Expand Up @@ -539,12 +555,16 @@ PicopassListenerCommand
uint8_t block_start = bit_buffer_get_byte(buf, 1);
if(block_start + 4 >= PICOPASS_MAX_APP_LIMIT) break;

bool secured = (instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[7] &
PICOPASS_FUSE_CRYPT10) != PICOPASS_FUSE_CRYPT0;

// TODO: Check CRC?
// TODO: Check auth?

bit_buffer_reset(instance->tx_buffer);
for(uint8_t i = block_start; i < block_start + 4; i++) {
if((i == PICOPASS_SECURE_KD_BLOCK_INDEX) || (i == PICOPASS_SECURE_KC_BLOCK_INDEX)) {
if(secured &&
((i == PICOPASS_SECURE_KD_BLOCK_INDEX) || (i == PICOPASS_SECURE_KC_BLOCK_INDEX))) {
for(size_t j = 0; j < sizeof(PicopassBlock); j++) {
bit_buffer_append_byte(instance->tx_buffer, 0xff);
}
Expand Down Expand Up @@ -622,6 +642,7 @@ static const PicopassListenerCmd picopass_listener_cmd_handlers[] = {
.cmd_len_bits = 8 * 4,
.handler = picopass_listener_read4_handler,
},
// TODO: RFAL_PICOPASS_CMD_DETECT
};

PicopassListener* picopass_listener_alloc(Nfc* nfc, const PicopassDeviceData* data) {
Expand Down
64 changes: 52 additions & 12 deletions protocol/picopass_poller.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,7 @@ NfcCommand picopass_poller_select_handler(PicopassPoller* instance) {
break;
}

if(instance->mode == PicopassPollerModeRead) {
instance->state = PicopassPollerStatePreAuth;
} else {
instance->state = PicopassPollerStateAuth;
}
instance->state = PicopassPollerStatePreAuth;
} while(false);

return command;
Expand Down Expand Up @@ -138,7 +134,7 @@ NfcCommand picopass_poller_pre_auth_handler(PicopassPoller* instance) {
instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data[6],
instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data[7]);

error = picopass_poller_read_block(instance, 5, &block);
error = picopass_poller_read_block(instance, PICOPASS_SECURE_AIA_BLOCK_INDEX, &block);
if(error != PicopassErrorNone) {
instance->state = PicopassPollerStateFail;
break;
Expand Down Expand Up @@ -168,6 +164,28 @@ NfcCommand picopass_poller_pre_auth_handler(PicopassPoller* instance) {
NfcCommand picopass_poller_check_security(PicopassPoller* instance) {
NfcCommand command = NfcCommandContinue;

instance->secured = true;

uint8_t crypt =
(instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[7] & PICOPASS_FUSE_CRYPT10);
switch(crypt) {
case 0:
FURI_LOG_D(TAG, "Secured page - Authentication disabled");
// Well this is awkward... We can try anyway though I guess...
break;
case PICOPASS_FUSE_CRYPT0:
FURI_LOG_D(TAG, "Non-secured page, skipping auth");
instance->secured = false;
picopass_poller_prepare_read(instance);
instance->state = PicopassPollerStateReadBlock;
return command;
case PICOPASS_FUSE_CRYPT0 | PICOPASS_FUSE_CRYPT1:
FURI_LOG_D(TAG, "Secured page - keys modifiable");
break;
case PICOPASS_FUSE_CRYPT1:
FURI_LOG_D(TAG, "Secured page - keys locked");
}

// Thank you proxmark!
PicopassBlock temp_block = {};
memset(temp_block.data, 0xff, sizeof(PicopassBlock));
Expand All @@ -188,8 +206,14 @@ NfcCommand picopass_poller_check_security(PicopassPoller* instance) {
if(instance->data->pacs.se_enabled) {
FURI_LOG_D(TAG, "SE enabled");
}
// Always try the NR-MAC auth in case we have the file.
instance->state = PicopassPollerStateNrMacAuth;

if(instance->mode == PicopassPollerModeRead) {
// Always try the NR-MAC auth in case we have the file.
instance->state = PicopassPollerStateNrMacAuth;
} else {
// NR-MAC auth doesn't allow for writing, so don't try
instance->state = PicopassPollerStateAuth;
}
return command;
}

Expand Down Expand Up @@ -222,7 +246,7 @@ NfcCommand picopass_poller_nr_mac_auth(PicopassPoller* instance) {

// Set next state so breaking do/while will jump to it. If successful, do/while will set to ReadBlock
if(instance->data->pacs.se_enabled) {
instance->state = PicopassPollerStateFail;
instance->state = PicopassPollerStateAuthFail;
} else {
// For non-SE, run through normal key check
instance->state = PicopassPollerStateAuth;
Expand Down Expand Up @@ -319,7 +343,7 @@ NfcCommand picopass_poller_auth_handler(PicopassPoller* instance) {
if(command != NfcCommandContinue) break;

if(!instance->event_data.req_key.is_key_provided) {
instance->state = PicopassPollerStateFail;
instance->state = PicopassPollerStateAuthFail;
break;
}

Expand Down Expand Up @@ -397,11 +421,15 @@ NfcCommand picopass_poller_read_block_handler(PicopassPoller* instance) {

do {
if(instance->current_block == instance->app_limit) {
instance->state = PicopassPollerStateParseCredential;
if(instance->secured) {
instance->state = PicopassPollerStateParseCredential;
} else {
instance->state = PicopassPollerStateSuccess;
}
break;
}

if(instance->current_block == PICOPASS_SECURE_KD_BLOCK_INDEX) {
if(instance->secured && instance->current_block == PICOPASS_SECURE_KD_BLOCK_INDEX) {
// Skip over Kd block which is populated earlier (READ of Kd returns all FF's)
instance->current_block++;
}
Expand Down Expand Up @@ -587,6 +615,17 @@ NfcCommand picopass_poller_fail_handler(PicopassPoller* instance) {
return command;
}

NfcCommand picopass_poller_auth_fail_handler(PicopassPoller* instance) {
NfcCommand command = NfcCommandReset;

instance->event.type = PicopassPollerEventTypeAuthFail;
command = instance->callback(instance->event, instance->context);
picopass_poller_reset(instance);
instance->state = PicopassPollerStateDetect;

return command;
}

static const PicopassPollerStateHandler picopass_poller_state_handler[PicopassPollerStateNum] = {
[PicopassPollerStateRequestMode] = picopass_poller_request_mode_handler,
[PicopassPollerStateDetect] = picopass_poller_detect_handler,
Expand All @@ -602,6 +641,7 @@ static const PicopassPollerStateHandler picopass_poller_state_handler[PicopassPo
[PicopassPollerStateParseWiegand] = picopass_poller_parse_wiegand_handler,
[PicopassPollerStateSuccess] = picopass_poller_success_handler,
[PicopassPollerStateFail] = picopass_poller_fail_handler,
[PicopassPollerStateAuthFail] = picopass_poller_auth_fail_handler,
};

static NfcCommand picopass_poller_callback(NfcEvent event, void* context) {
Expand Down
1 change: 1 addition & 0 deletions protocol/picopass_poller.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ typedef enum {
PicopassPollerEventTypeRequestWriteKey,
PicopassPollerEventTypeSuccess,
PicopassPollerEventTypeFail,
PicopassPollerEventTypeAuthFail,
} PicopassPollerEventType;

typedef enum {
Expand Down
Loading

0 comments on commit 95ae7f2

Please sign in to comment.