Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

picopass: Add support for non-secure cards #106

Merged
merged 2 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion picopass/.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 picopass/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 picopass/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/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/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 picopass/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 picopass/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 picopass/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