Merge pull request #3300 from Antiklesys/master

Added SC Operations
This commit is contained in:
Iceman
2026-05-11 10:49:50 +02:00
committed by GitHub
12 changed files with 1228 additions and 30 deletions
+1
View File
@@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file.
This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log...
## [unreleased][unreleased]
- Added secure channel operations to `hf iclass sam` and fixed/improved I2C bus operations (@antiklesys)
- Added `hf calypso list` command (@kormax)
- Added `hf mfd verifycert` command (@kormax)
- Added `hf calypso dump` command (@kormax)
+1 -1
View File
@@ -36,7 +36,7 @@ APP_CFLAGS = $(PLATFORM_DEFS)
SRC_LF = lfops.c lfsampling.c pcf7931.c lfdemod.c lfadc.c
SRC_HF = hfops.c
SRC_ISO15693 = iso15693.c iso15693tools.c
SRC_ISO14443a = iso14443a.c secc.c mifareutil.c mifarecmd.c epa.c mifaresim.c sam_common.c sam_mfc.c sam_seos.c
SRC_ISO14443a = iso14443a.c secc.c mifareutil.c mifarecmd.c epa.c mifaresim.c sam_common.c sam_mfc.c sam_seos.c sam_sc.c
#UNUSED: mifaresniff.c
SRC_ISO14443b = iso14443b.c
+6
View File
@@ -69,6 +69,7 @@
#include "sam_picopass.h"
#include "sam_seos.h"
#include "sam_mfc.h"
#include "sam_sc.h"
#include "cmac_calc.h"
#ifdef WITH_LCD
@@ -2530,6 +2531,11 @@ static void PacketReceived(PacketCommandNG *packet) {
break;
}
case CMD_HF_SAM_SC: {
sam_sc_handler(packet);
break;
}
#endif
#ifdef WITH_FPC_USART
+43 -16
View File
@@ -187,9 +187,8 @@ static bool WaitSCL_L(void) {
return WaitSCL_L_delay(5000);
}
// Wait max 1800ms or until SCL goes LOW.
// It timeout reading response from card
// Which ever comes first
// Wait up to 1200ms or until SCL goes LOW, whichever comes first.
// Used to bound the timeout while reading a response from the card.
static bool WaitSCL_L_timeout(void) {
volatile uint32_t delay = 1200;
while (delay--) {
@@ -199,7 +198,7 @@ static bool WaitSCL_L_timeout(void) {
WaitMS(1);
}
return (delay == 0);
return false;
}
static bool I2C_Start(void) {
@@ -489,8 +488,9 @@ bool I2C_BufferWrite(const uint8_t *data, uint16_t len, uint8_t device_cmd, uint
// len = uint16 because we need to read up to 256bytes
int16_t I2C_BufferRead(uint8_t *data, uint16_t len, uint8_t device_cmd, uint8_t device_address) {
// sanity check
if (data == NULL || len == 0) {
// sanity check - need at least 2 bytes for the SIM-module length header
// (the response format prepends a 2-byte BE length); fewer cannot be parsed.
if (data == NULL || len < 2) {
return 0;
}
@@ -858,7 +858,11 @@ void SmartCardRaw(const smart_card_raw_t *p) {
uint16_t len = 0;
uint8_t *resp = BigBuf_calloc(ISO7816_MAX_FRAME);
// check if alloacted...
if (resp == NULL) {
reply_ng(CMD_SMART_RAW, PM3_EMALLOC, NULL, 0);
LEDsoff();
return;
}
smartcard_command_t flags = p->flags;
if ((flags & SC_CLEARLOG) == SC_CLEARLOG)
@@ -888,7 +892,10 @@ void SmartCardRaw(const smart_card_raw_t *p) {
uint32_t wait = SIM_WAIT_DELAY;
if ((flags & SC_WAIT) == SC_WAIT) {
wait = (uint32_t)((p->wait_delay * 1000) / 3.07);
// wait_delay is in ms; one WaitSCL_H_delay iteration is ~3.07us.
// Integer-only conversion via uint64_t to avoid soft-float and avoid
// overflow at large wait_delay values: (ms * 100000 + 153) / 307.
wait = (uint32_t)(((uint64_t)p->wait_delay * 100000U + 153U) / 307U);
}
LogTrace(p->data, p->len, 0, 0, NULL, true);
@@ -900,8 +907,10 @@ void SmartCardRaw(const smart_card_raw_t *p) {
I2C_DEVICE_ADDRESS_MAIN
);
if (res == false && g_dbglevel > 3) {
DbpString(I2C_ERROR);
if (res == false) {
if (g_dbglevel > 3) {
DbpString(I2C_ERROR);
}
reply_ng(CMD_SMART_RAW, PM3_ESOFT, NULL, 0);
goto OUT;
}
@@ -937,7 +946,12 @@ void SmartCardUpgrade(uint64_t arg0) {
bool isOK = true;
uint16_t length = arg0, pos = 0;
const uint8_t *fwdata = BigBuf_get_addr();
uint8_t *verfiydata = BigBuf_calloc(I2C_BLOCK_SIZE);
uint8_t *verifydata = BigBuf_calloc(I2C_BLOCK_SIZE);
if (verifydata == NULL) {
reply_ng(CMD_SMART_UPGRADE, PM3_EMALLOC, NULL, 0);
LED_C_OFF();
return;
}
while (length) {
@@ -951,7 +965,7 @@ void SmartCardUpgrade(uint64_t arg0) {
// write
int16_t res = I2C_WriteFW(fwdata + pos, size, msb, lsb, I2C_DEVICE_ADDRESS_BOOT);
if (!res) {
DbpString("Writing failed");
Dbprintf("Writing failed at offset 0x%04X", pos);
isOK = false;
break;
}
@@ -960,16 +974,16 @@ void SmartCardUpgrade(uint64_t arg0) {
WaitMS(50);
// read
res = I2C_ReadFW(verfiydata, size, msb, lsb, I2C_DEVICE_ADDRESS_BOOT);
res = I2C_ReadFW(verifydata, size, msb, lsb, I2C_DEVICE_ADDRESS_BOOT);
if (res <= 0) {
DbpString("Reading back failed");
Dbprintf("Reading back failed at offset 0x%04X", pos);
isOK = false;
break;
}
// cmp
if (0 != memcmp(fwdata + pos, verfiydata, size)) {
DbpString("not equal data");
if (0 != memcmp(fwdata + pos, verifydata, size)) {
Dbprintf("Verify mismatch at offset 0x%04X", pos);
isOK = false;
break;
}
@@ -983,7 +997,20 @@ void SmartCardUpgrade(uint64_t arg0) {
BigBuf_free();
}
// Send a single byte to the SIM module's CMD_SETBAUD opcode (0x04).
// The 8051 firmware uses this to reload Timer1 (UART0 baud generator).
// Until 2026 the implementation was an empty stub; the SIM module silently
// ignored any host-driven baud renegotiation. Some smart cards (notably the
// HID Artemis SLE88 SAM family) advertise non-default Fi/Di in TA1 and need
// PPS to switch the bridge baud post-ATR.
void SmartCardSetBaud(uint64_t arg0) {
LED_D_ON();
I2C_Reset_EnterMainProgram();
bool ok = I2C_WriteByte((uint8_t)(arg0 & 0xFF),
I2C_DEVICE_CMD_SETBAUD,
I2C_DEVICE_ADDRESS_MAIN);
reply_ng(CMD_SMART_SETBAUD, ok ? PM3_SUCCESS : PM3_ESOFT, NULL, 0);
LEDsoff();
}
void SmartCardSetClock(uint64_t arg0) {
+5 -3
View File
@@ -34,9 +34,11 @@
#define ISO7816_MAX_FRAME 270
// 8051 speaks with smart card.
// 1000*50*3.07 = 153.5ms
// 1 byte transfer == 1ms with max frame being 256 bytes
#define SIM_WAIT_DELAY 150000 // about 270ms delay // 109773 -- about 337.7ms delay
// 1 byte transfer == 1ms with max frame being 256 bytes.
// SIM_WAIT_DELAY is the iteration count passed to WaitSCL_H_delay(); each iter
// is ~3.07us, so 150000 * 3.07us = ~460ms - the upper bound we wait for the
// SIM module to assert SCL after a SIM-side operation.
#define SIM_WAIT_DELAY 150000 // ~460ms total wait via WaitSCL_H_delay
void I2C_recovery(void);
+34 -7
View File
@@ -36,13 +36,25 @@
#include "i2c.h"
#include "i2c_direct.h"
static void SmartCardDirectSend(uint8_t prepend, const smart_card_raw_t *p, uint8_t *output, uint16_t *olen) {
// Maximum chained ISO 7816-4 GET RESPONSE (61xx) follow-ups before bailing.
// A misbehaving card that always returns 61xx would otherwise recurse without
// bound, allocating fresh smart_card_raw_t payloads from BigBuf each time
// until the device wedges. Eight rounds is more than any legitimate APDU
// chain would need.
#define SC_DIRECT_MAX_DEPTH 8
static void SmartCardDirectSend(uint8_t prepend, const smart_card_raw_t *p, uint8_t *output, uint16_t *olen, uint8_t depth) {
LED_D_ON();
uint16_t len = 0;
uint8_t *resp = BigBuf_calloc(ISO7816_MAX_FRAME);
if (resp == NULL) {
Dbprintf("SmartCardDirectSend: BigBuf_calloc failed");
if (olen) *olen = 0;
LEDsoff();
return;
}
resp[0] = prepend;
// check if alloacted...
smartcard_command_t flags = p->flags;
if ((flags & SC_LOG) == SC_LOG)
@@ -70,7 +82,10 @@ static void SmartCardDirectSend(uint8_t prepend, const smart_card_raw_t *p, uint
if (((flags & SC_RAW) == SC_RAW) || ((flags & SC_RAW_T0) == SC_RAW_T0)) {
if ((flags & SC_WAIT) == SC_WAIT) {
wait = (uint32_t)((p->wait_delay * 1000) / 3.07);
// wait_delay is in ms; one WaitSCL_H_delay iteration is ~3.07us.
// Integer-only conversion via uint64_t to avoid soft-float and
// avoid overflow at large wait_delay values.
wait = (uint32_t)(((uint64_t)p->wait_delay * 100000U + 153U) / 307U);
}
LogTrace(p->data, p->len, 0, 0, NULL, true);
@@ -82,8 +97,10 @@ static void SmartCardDirectSend(uint8_t prepend, const smart_card_raw_t *p, uint
I2C_DEVICE_ADDRESS_MAIN
);
if (res == false && g_dbglevel > 3) {
Dbprintf("SmartCardDirectSend: I2C_BufferWrite failed\n");
if (res == false) {
if (g_dbglevel > 3) {
Dbprintf("SmartCardDirectSend: I2C_BufferWrite failed");
}
goto OUT;
}
@@ -98,15 +115,25 @@ static void SmartCardDirectSend(uint8_t prepend, const smart_card_raw_t *p, uint
}
if (len == 2 && resp[1] == 0x61) {
if (depth >= SC_DIRECT_MAX_DEPTH) {
Dbprintf("SmartCardDirectSend: GET RESPONSE chain depth (%u) exceeded; aborting", depth);
goto OUT;
}
uint8_t cmd_getresp[] = {0x00, ISO7816_GET_RESPONSE, 0x00, 0x00, resp[2]};
smart_card_raw_t *payload = (smart_card_raw_t *)BigBuf_calloc(sizeof(smart_card_raw_t) + sizeof(cmd_getresp));
if (payload == NULL) {
Dbprintf("SmartCardDirectSend: GET RESPONSE alloc failed");
goto OUT;
}
payload->flags = SC_RAW | SC_LOG;
payload->len = sizeof(cmd_getresp);
payload->wait_delay = 0;
memcpy(payload->data, cmd_getresp, sizeof(cmd_getresp));
SmartCardDirectSend(prepend, payload, output, olen);
SmartCardDirectSend(prepend, payload, output, olen, depth + 1);
} else if (len == 2) {
Dbprintf("***** BAD response from card (response unsupported)...");
Dbhexdump(3, &resp[0], false);
@@ -199,7 +226,7 @@ int CmdSmartRaw(const uint8_t prepend, const uint8_t *data, int dlen, uint8_t *o
payload->flags |= SC_RAW;
}
SmartCardDirectSend(prepend, payload, output, olen);
SmartCardDirectSend(prepend, payload, output, olen, 0);
return PM3_SUCCESS;
}
+19 -1
View File
@@ -158,6 +158,23 @@ int sam_send_payload(
const uint8_t *const payload,
const uint16_t *payload_len,
uint8_t *response,
uint16_t *response_len
) {
return sam_send_payload_ex(addr_src, addr_dest, addr_reply, 0x00,
payload, payload_len,
response, response_len);
}
int sam_send_payload_ex(
const uint8_t addr_src,
const uint8_t addr_dest,
const uint8_t addr_reply,
const uint8_t scFlag,
const uint8_t *const payload,
const uint16_t *payload_len,
uint8_t *response,
uint16_t *response_len
) {
@@ -171,13 +188,14 @@ int sam_send_payload(
buf[3] = 0x63; // P2
buf[4] = SAM_TX_ASN1_PREFIX_LENGTH + (uint8_t) * payload_len; // LEN
// Grace routing header: FROM, TO, REPLY-TO, 0x00, 0x00, scFlag
buf[5] = addr_src;
buf[6] = addr_dest;
buf[7] = addr_reply;
buf[8] = 0x00;
buf[9] = 0x00;
buf[10] = 0x00;
buf[10] = scFlag;
memcpy(
&buf[11],
+23
View File
@@ -27,6 +27,7 @@ int sam_rxtx(const uint8_t *data, uint16_t n, uint8_t *resp, uint16_t *resplen);
void switch_clock_to_ticks(void);
void switch_clock_to_countsspclk(void);
// Backwards-compatible wrapper that calls sam_send_payload_ex with scFlag=0x00.
int sam_send_payload(
const uint8_t addr_src,
const uint8_t addr_dest,
@@ -39,6 +40,28 @@ int sam_send_payload(
uint16_t *response_len
);
// Extended variant that lets the caller set the Grace routing scFlag byte.
//
// The Grace routing header is 6 bytes: FROM, TO, REPLY-TO, 0x00, 0x00, scFlag.
// During InitAuth the scFlag is 0x00; after the SAM authenticates the host it
// returns a session-bound scFlag (typically 0x81) that must be echoed in the
// routing header of every subsequent wrapped APDU (ContinueAuth, wrap/unwrap).
// The original sam_send_payload hardcoded scFlag=0x00 which made it impossible
// to drive a real secure-channel session. New SC code paths must use this _ex
// variant instead.
int sam_send_payload_ex(
const uint8_t addr_src,
const uint8_t addr_dest,
const uint8_t addr_reply,
const uint8_t scFlag,
const uint8_t *const payload,
const uint16_t *payload_len,
uint8_t *response,
uint16_t *response_len
);
int sam_get_version(bool info);
int sam_get_serial_number(void);
+160
View File
@@ -0,0 +1,160 @@
//-----------------------------------------------------------------------------
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// See LICENSE.txt for the text of the license.
//-----------------------------------------------------------------------------
// HID Artemis SAM secure-channel transport (see sam_sc.h for design notes).
//-----------------------------------------------------------------------------
#include "sam_sc.h"
#include <string.h>
#include "BigBuf.h"
#include "appmain.h"
#include "cmd.h"
#include "dbprint.h"
#include "i2c.h" // ISO7816_MAX_FRAME, I2C_Reset_EnterMainProgram
#include "proxmark3_arm.h"
#include "sam_common.h"
#include "ticks.h"
#include "util.h" // LED_D_ON, LEDsoff
// Tracks whether the SIM module has been initialised since the last reset.
// Set true after the first successful sam_sc_handler() invocation; cleared by
// sam_sc_session_invalidate() (called from any other firmware path that takes
// over the SIM module - currently a manual hook left for future wiring) and
// by SAM_SC_FLAG_FORCE_RESET / SAM_SC_FLAG_RELEASE.
static bool s_sam_sc_session_active = false;
void sam_sc_session_invalidate(void) {
s_sam_sc_session_active = false;
}
void sam_sc_handler(const PacketCommandNG *c) {
if (c == NULL || c->length < SAM_SC_HEADER_LEN) {
reply_ng(CMD_HF_SAM_SC, PM3_EINVARG, NULL, 0);
return;
}
const uint8_t *body = c->data.asBytes;
const uint8_t flags = body[SAM_SC_OFF_FLAGS];
const uint8_t addr_src = body[SAM_SC_OFF_ADDR_SRC];
const uint8_t addr_dest = body[SAM_SC_OFF_ADDR_DEST];
const uint8_t addr_reply = body[SAM_SC_OFF_ADDR_REPLY];
const uint8_t scFlag = body[SAM_SC_OFF_SCFLAG];
const bool force_reset = !!(flags & SAM_SC_FLAG_FORCE_RESET);
const bool release = !!(flags & SAM_SC_FLAG_RELEASE);
const bool no_payload = !!(flags & SAM_SC_FLAG_NO_PAYLOAD);
const uint8_t *payload = body + SAM_SC_HEADER_LEN;
uint16_t payload_len = (uint16_t)(c->length - SAM_SC_HEADER_LEN);
if (no_payload) {
// Caller is just managing session state (open/close); no SAM traffic.
payload_len = 0;
} else if (payload_len == 0) {
reply_ng(CMD_HF_SAM_SC, PM3_EINVARG, NULL, 0);
return;
}
LED_D_ON();
set_tracing(true);
// Reset the SAM only if the caller asked for it OR this is the first SC
// op since boot / since the previous session was released. Crucially
// this dispatcher does NOT reset on every call the way sam_picopass_get_pacs
// does, so the SAM-side session-flag binding established by ContinueAuth
// survives across multiple CMD_HF_SAM_SC invocations.
//
// After every reset we issue a sam_get_version() warmup ping. This
// mirrors what sam_picopass_get_pacs does (which is what `hf iclass sam
// --info` runs through). Without this warmup, the FIRST sam_send_payload_ex
// after I2C_Reset can time out - the 8051<->SAM UART link needs a
// sacrificial round-trip to settle. The version response is discarded.
if (force_reset || s_sam_sc_session_active == false) {
I2C_Reset_EnterMainProgram();
StartTicks();
sam_get_version(false);
s_sam_sc_session_active = true;
}
int res = PM3_SUCCESS;
if (no_payload == false) {
uint8_t *response = BigBuf_calloc(ISO7816_MAX_FRAME);
if (response == NULL) {
res = PM3_EMALLOC;
goto out;
}
uint16_t response_len = ISO7816_MAX_FRAME;
res = sam_send_payload_ex(
addr_src, addr_dest, addr_reply, scFlag,
payload, &payload_len,
response, &response_len
);
if (res != PM3_SUCCESS) {
// Whatever happened on the wire, the session may be in an
// inconsistent state. Mark dirty so the next call re-opens.
s_sam_sc_session_active = false;
}
if (release) {
// Caller requested an explicit teardown after this op (typically
// after a samCommandSecureChannelTerminate). Do a full reset to
// bring the SAM back to a clean idle state.
I2C_Reset_EnterMainProgram();
s_sam_sc_session_active = false;
}
// Reformat the buffer for the host: prepend the SAM-assigned scFlag
// (firmware-side index 4 of the routing tail), then the SAM payload
// (firmware-side index 5 onward). See sam_sc.h for the wire layout.
// memmove is safe across the overlapping ranges (dst < src by 4).
if (res == PM3_SUCCESS && response_len >= 6) {
uint8_t sc_flag = response[4];
uint16_t sam_payload_len = (uint16_t)(response_len - 5);
memmove(response + 1, response + 5, sam_payload_len);
response[0] = sc_flag;
response_len = (uint16_t)(1 + sam_payload_len);
reply_ng(CMD_HF_SAM_SC, PM3_SUCCESS, response, response_len);
} else if (res == PM3_SUCCESS) {
// sam_send_payload_ex succeeded but the response is too short
// to contain a routing tail + SAM payload. Treat as exchange
// error so the host knows the result is unusable.
reply_ng(CMD_HF_SAM_SC, PM3_ECARDEXCHANGE, NULL, 0);
} else {
// sam_send_payload_ex failed. Propagate the error; no payload.
reply_ng(CMD_HF_SAM_SC, res, NULL, 0);
}
BigBuf_free();
goto done;
}
// SAM_SC_FLAG_NO_PAYLOAD path: caller wants to manage session state only.
if (release) {
I2C_Reset_EnterMainProgram();
s_sam_sc_session_active = false;
}
out:
reply_ng(CMD_HF_SAM_SC, res, NULL, 0);
done:
set_tracing(false);
LEDsoff();
}
+96
View File
@@ -0,0 +1,96 @@
//-----------------------------------------------------------------------------
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// See LICENSE.txt for the text of the license.
//-----------------------------------------------------------------------------
// HID Artemis SAM secure-channel transport.
//
// CMD_HF_SAM_PICOPASS is structurally unsuitable for sustaining an SCP02 /
// Grace secure channel because it (a) hard-resets the SAM at the start of
// every CLI invocation, wiping the SAM's internal session-flag binding, and
// (b) routes raw payloads through sam_send_request_iso15 whose loop is
// designed to relay 0x61-tagged SAM responses to an iCLASS card via NFC.
//
// This handler is a separate dispatcher that:
// - Resets the SAM only on the very first call after device boot, OR when
// the host explicitly asks for a reset.
// - Skips the sam_get_version sanity ping.
// - Sends a single SAM payload with a host-supplied scFlag and returns the
// SAM's raw response - no NFC card-edge involvement at all.
//
// The host owns the SCP02 / Grace KDF + wrap/unwrap state machine; this
// firmware module is a thin transport pipe so that state can survive across
// CLI invocations on the SAM side.
//-----------------------------------------------------------------------------
#ifndef __SAM_SC_H
#define __SAM_SC_H
#include "common.h"
#include "pm3_cmd.h"
// CMD_HF_SAM_SC payload layout:
//
// [0] flags byte
// BITMASK(0) SAM_SC_FLAG_FORCE_RESET - I2C_Reset before this op,
// marks session uninitialised
// BITMASK(1) SAM_SC_FLAG_RELEASE - I2C_Reset after this op,
// marks session uninitialised
// BITMASK(2) SAM_SC_FLAG_NO_PAYLOAD - send no payload, just
// manage session state
// (open / close)
// [1] addr_src Grace routing FROM byte (typically 0x44)
// [2] addr_dest Grace routing TO byte (typically 0x0A = SAM)
// [3] addr_reply Grace routing REPLY-TO (typically 0x44)
// [4] scFlag Grace routing scFlag (0x00 for InitAuth;
// server-assigned thereafter)
// [5...] SAM payload bytes starting with 0xA0 (or whatever SAM TLV the
// host wants delivered raw; the firmware does not interpret).
//
// Reply: reply_ng(CMD_HF_SAM_SC, status, payload, payload_len)
// payload[0] = SAM-assigned scFlag (the byte the host MUST echo in the
// routing header of the next request - 0x00 during
// InitAuth, server-assigned thereafter)
// payload[1..] = raw SAM response starting at the first byte after the
// routing tail (typically 0xBD for Path A/B, 0xBE for
// Path C errorResponse)
// payload_len = 1 + len(SAM response)
//
// The scFlag is the load-bearing piece of state for sustaining a Grace
// secure channel: the SAM assigns it during InitAuth and binds the
// authenticated session to it. Subsequent ContinueAuth and wrapped APDUs
// MUST carry the same scFlag in their outgoing routing header or the SAM
// will reject them. Surfacing it as the first byte of the reply lets the
// host save and replay it on the next CMD_HF_SAM_SC call without any
// additional probing.
#define SAM_SC_FLAG_FORCE_RESET (1 << 0)
#define SAM_SC_FLAG_RELEASE (1 << 1)
#define SAM_SC_FLAG_NO_PAYLOAD (1 << 2)
// Wire-layout offsets within the CMD_HF_SAM_SC packet body.
#define SAM_SC_OFF_FLAGS 0
#define SAM_SC_OFF_ADDR_SRC 1
#define SAM_SC_OFF_ADDR_DEST 2
#define SAM_SC_OFF_ADDR_REPLY 3
#define SAM_SC_OFF_SCFLAG 4
#define SAM_SC_HEADER_LEN 5
void sam_sc_handler(const PacketCommandNG *c);
// Forces the next sam_sc_handler() call to perform an I2C reset before sending
// its payload. Intended to be called by other firmware paths that may have
// taken over the SIM module (e.g. CMD_HF_SAM_PICOPASS, CMD_SMART_*) and would
// otherwise leave a stale "session active" flag visible to sam_sc_handler.
void sam_sc_session_invalidate(void);
#endif
+834 -2
View File
@@ -46,6 +46,7 @@
#include "proxendian.h"
#include "iclass_cmd.h"
#include "crypto/asn1utils.h" // ASN1 decoder
#include "crypto/libpcrypto.h" // aes_encode, aes_decode (for SAM SC)
#include "preferences.h"
#include "generator.h"
#include "cmdhw.h"
@@ -7744,7 +7745,14 @@ static bool match_with_wildcard(const uint8_t *data, const uint8_t *pattern, con
}
static int CmdHFiClassSAM(const char *Cmd) {
// ---------------------------------------------------------------------------
// Legacy "hf iclass sam" PACS-extraction implementation.
//
// Reached via the CmdHFiClassSAM dispatcher (below) when no SC subcommand
// keyword is given - so `hf iclass sam`, `hf iclass sam --info`,
// `hf iclass sam -p -d ...`, `hf iclass sam -f ...` etc. all land here.
// ---------------------------------------------------------------------------
static int CmdHFiClassSAMExtract(const char *Cmd) {
CLIParserContext *ctx;
CLIParserInit(&ctx, "hf iclass sam",
"Extract PACS via a HID SAM\n",
@@ -7752,6 +7760,7 @@ static int CmdHFiClassSAM(const char *Cmd) {
"hf iclass sam -p -d a005a103800104 -> get PACS data, prevent epurse update\n"
"hf iclass sam --break -> get Nr-MAC for extracting encrypted SIO\n"
"hf iclass sam -f hf-iclass-dump.bin -> emulate card from dump file to SAM\n"
"hf iclass sam --info -> get SAM version + serial (also warms up the SAM)\n"
);
void *argtable[] = {
@@ -7947,6 +7956,829 @@ static int CmdHFiClassSAM(const char *Cmd) {
return PM3_SUCCESS;
}
// ===========================================================================
// HID Artemis secure-channel session - persistent across CLI calls
// ===========================================================================
//
// Sits on top of the new CMD_HF_SAM_SC firmware dispatcher (armsrc/sam_sc.c).
// The firmware is just a transport pipe - host owns the SCP02 / Grace crypto
// state (sEnc / sMAC1 / sMAC2 / rolling C-MAC + R-MAC) and the SAM-assigned
// scFlag. State persists across CLI invocations in the static s_sam_sc.
//
// All bit patterns mirror UTILITIES_TOOLS/cp1000_client/secure_channel.py.
// Mirror of armsrc/sam_sc.h - duplicated locally so the client doesn't have
// to pull armsrc/ headers. Keep in sync if those flags ever change.
#define SAM_SC_FLAG_FORCE_RESET (1 << 0)
#define SAM_SC_FLAG_RELEASE (1 << 1)
#define SAM_SC_FLAG_NO_PAYLOAD (1 << 2)
// Mirror of armsrc/i2c.h::ISO7816_MAX_FRAME (270). Not exported to the
// client; used as an upper bound for SC plaintext / wrap buffers.
#define SAM_SC_MAX_FRAME 270
// All-zero placeholder master key. Real master keys depend on the target
// SAM's provisioning state and must be supplied with --key. Slot 0x85 is the
// canonical HidUserAdmin slot index on CP1000-class encoders.
static const uint8_t SAM_SC_DEFAULT_MASTER_KEY[16] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
static const uint8_t SAM_SC_DEFAULT_KREF = 0x85;
typedef struct {
bool open;
uint8_t kref;
uint8_t master_key[16];
uint8_t suid[8];
uint8_t rnd_a[8];
uint8_t rnd_b[8];
uint8_t scbk[16];
uint8_t s_enc[16];
uint8_t s_mac1[16];
uint8_t s_mac2[16];
uint8_t c_mac[16]; // rolling client MAC (advanced by Wrap)
uint8_t r_mac[16]; // rolling server MAC (advanced by Unwrap)
uint8_t sc_flag; // SAM-assigned secure-channel routing byte
} sam_sc_session_t;
static sam_sc_session_t s_sam_sc = {0};
// HID's custom CBC-MAC variant. Zero-pad msg to a 16B multiple; AES-ECB
// the first N-1 blocks with sMAC1, the last with sMAC2. Initial mac = iv.
// Output is 16 bytes (no truncation). AES-ECB single block done via
// aes_encode() with a zero CBC-IV.
static void sam_sc_hid_mac(const uint8_t s_mac1[16], const uint8_t s_mac2[16],
const uint8_t iv[16],
const uint8_t *msg, size_t msg_len,
uint8_t out[16]) {
uint8_t mac[16];
memcpy(mac, iv, 16);
if (msg_len == 0) msg_len = 16;
size_t pad = (16 - (msg_len % 16)) % 16;
size_t total = msg_len + pad;
size_t blocks = total / 16;
for (size_t i = 0; i < blocks; i++) {
uint8_t block[16];
size_t off = i * 16;
size_t copy = (off + 16 <= msg_len) ? 16 : (msg_len > off ? msg_len - off : 0);
if (copy > 0) memcpy(block, msg + off, copy);
if (copy < 16) memset(block + copy, 0x00, 16 - copy);
for (size_t j = 0; j < 16; j++) block[j] ^= mac[j];
uint8_t zero_iv[16] = {0}, key_copy[16];
memcpy(key_copy, (i + 1 == blocks) ? s_mac2 : s_mac1, 16);
aes_encode(zero_iv, key_copy, block, mac, 16);
}
memcpy(out, mac, 16);
}
// Send a payload via CMD_HF_SAM_SC. Returns SAM-assigned scFlag and the SAM
// response bytes (BD/BE-prefixed) per the reply layout in armsrc/sam_sc.h.
static int sam_sc_dispatch(uint8_t flags, uint8_t scflag_in,
const uint8_t *payload, uint16_t payload_len,
uint8_t *sc_flag_out, uint8_t *sam_resp,
uint16_t *sam_resp_len) {
if (sc_flag_out == NULL || sam_resp == NULL || sam_resp_len == NULL)
return PM3_EINVARG;
if ((size_t)payload_len + 5 > PM3_CMD_DATA_SIZE)
return PM3_EINVARG;
uint8_t pkt[PM3_CMD_DATA_SIZE] = {0};
pkt[0] = flags;
pkt[1] = 0x44; // ipcNodeIdExternalApplicationA (us)
pkt[2] = 0x0A; // ipcNodeIdPrimarySam
pkt[3] = 0x44; // reply-to = us
pkt[4] = scflag_in;
if (payload_len > 0)
memcpy(pkt + 5, payload, payload_len);
clearCommandBuffer();
SendCommandNG(CMD_HF_SAM_SC, pkt, payload_len + 5);
PacketResponseNG resp;
if (WaitForResponse(CMD_HF_SAM_SC, &resp) == false)
return PM3_ETIMEOUT;
if (resp.status != PM3_SUCCESS)
return resp.status;
if (resp.length < 1)
return PM3_ESOFT;
*sc_flag_out = resp.data.asBytes[0];
uint16_t body_len = (uint16_t)(resp.length - 1);
if (body_len > *sam_resp_len) body_len = *sam_resp_len;
if (body_len > 0) memcpy(sam_resp, resp.data.asBytes + 1, body_len);
*sam_resp_len = body_len;
return PM3_SUCCESS;
}
// Build the InitAuth SAM payload (23 bytes):
// A0 15 AF 13 80 01 <ver> 81 01 <kref> 82 08 <rnd_a> 83 01 <tca>
static uint16_t sam_sc_build_init_auth(uint8_t kref, const uint8_t rnd_a[8],
uint8_t out[23]) {
out[0] = 0xA0; out[1] = 0x15;
out[2] = 0xAF; out[3] = 0x13;
out[4] = 0x80; out[5] = 0x01; out[6] = 0x00; // version=0
out[7] = 0x81; out[8] = 0x01; out[9] = kref;
out[10] = 0x82; out[11] = 0x08;
memcpy(out + 12, rnd_a, 8);
out[20] = 0x83; out[21] = 0x01; out[22] = 0x00; // tca=0
return 23;
}
// Build the ContinueAuth SAM payload (40 bytes):
// A0 26 B0 24 80 10 <clientCryptogram> 81 10 <clientCmac>
static uint16_t sam_sc_build_continue_auth(const uint8_t client_crypto[16],
const uint8_t client_cmac[16],
uint8_t out[40]) {
out[0] = 0xA0; out[1] = 0x26;
out[2] = 0xB0; out[3] = 0x24;
out[4] = 0x80; out[5] = 0x10;
memcpy(out + 6, client_crypto, 16);
out[22] = 0x81; out[23] = 0x10;
memcpy(out + 24, client_cmac, 16);
return 40;
}
// out = AES-CBC(scbk, IV=0, prefix(2) || rnd_b[0..2] || zeros[12]) (1 block).
static void sam_sc_kdf_derive(const uint8_t scbk[16], const uint8_t prefix[2],
const uint8_t rnd_b[8], uint8_t out[16]) {
uint8_t in[16];
in[0] = prefix[0];
in[1] = prefix[1];
in[2] = rnd_b[0];
in[3] = rnd_b[1];
memset(in + 4, 0, 12);
uint8_t iv[16] = {0}, key_copy[16];
memcpy(key_copy, scbk, 16);
aes_encode(iv, key_copy, in, out, 16);
}
// Wrap plaintext for transmission on the open SC. Returns wrapped length.
// padded = plaintext || 0x80 || zeros (to next 16B)
// iv_enc = ~r_mac
// ciphertext = AES-CBC(s_enc, iv_enc, padded)
// new_cmac = HID-MAC(s_mac1, s_mac2, IV=r_mac, ciphertext)
// c_mac <- new_cmac (R-MAC unchanged; only Unwrap advances it)
// return ciphertext || new_cmac
static uint16_t sam_sc_wrap(const uint8_t *plaintext, uint16_t plaintext_len,
uint8_t *out, uint16_t out_cap) {
if (s_sam_sc.open == false) return 0;
uint16_t padded_len = plaintext_len + 1;
if ((padded_len % 16) != 0) padded_len += 16 - (padded_len % 16);
if ((uint32_t)padded_len + 16 > out_cap) return 0;
uint8_t padded[SAM_SC_MAX_FRAME];
if (padded_len > sizeof(padded)) return 0;
if (plaintext_len > 0) memcpy(padded, plaintext, plaintext_len);
padded[plaintext_len] = 0x80;
if (padded_len > plaintext_len + 1)
memset(padded + plaintext_len + 1, 0x00, padded_len - plaintext_len - 1);
uint8_t iv_enc[16], key_copy[16];
for (int i = 0; i < 16; i++) iv_enc[i] = (uint8_t)(s_sam_sc.r_mac[i] ^ 0xFF);
memcpy(key_copy, s_sam_sc.s_enc, 16);
aes_encode(iv_enc, key_copy, padded, out, padded_len);
uint8_t new_cmac[16];
sam_sc_hid_mac(s_sam_sc.s_mac1, s_sam_sc.s_mac2, s_sam_sc.r_mac,
out, padded_len, new_cmac);
memcpy(out + padded_len, new_cmac, 16);
memcpy(s_sam_sc.c_mac, new_cmac, 16);
return (uint16_t)(padded_len + 16);
}
// Unwrap ciphertext+MAC. Verifies MAC under c_mac, decrypts under sEnc with
// iv=~c_mac, strips 0x80+zeros padding, advances r_mac. Returns plaintext
// length on success, -1 on MAC mismatch or padding error.
static int sam_sc_unwrap(const uint8_t *data, uint16_t data_len,
uint8_t *plaintext_out, uint16_t out_cap) {
if (s_sam_sc.open == false) return -1;
if (data_len < 16) return -1;
uint16_t ct_len = data_len - 16;
const uint8_t *ciphertext = data;
const uint8_t *received_mac = data + ct_len;
uint8_t expected_mac[16];
sam_sc_hid_mac(s_sam_sc.s_mac1, s_sam_sc.s_mac2, s_sam_sc.c_mac,
ciphertext, ct_len, expected_mac);
if (memcmp(expected_mac, received_mac, 16) != 0) return -1;
if (ct_len == 0) {
memcpy(s_sam_sc.r_mac, received_mac, 16);
return 0;
}
if (ct_len > out_cap) return -1;
uint8_t iv_dec[16], key_copy[16];
for (int i = 0; i < 16; i++) iv_dec[i] = (uint8_t)(s_sam_sc.c_mac[i] ^ 0xFF);
memcpy(key_copy, s_sam_sc.s_enc, 16);
aes_decode(iv_dec, key_copy, (uint8_t *)ciphertext, plaintext_out, ct_len);
int i = ct_len - 1;
while (i >= 0 && plaintext_out[i] == 0x00) i--;
if (i < 0 || plaintext_out[i] != 0x80) return -1;
int plaintext_len = i;
memcpy(s_sam_sc.r_mac, received_mac, 16);
return plaintext_len;
}
// Peel BD <len> 8A <len> or BD <len> B3 <len> or BE <len> envelopes.
// Returns pointer to inner ciphertext+MAC bytes + its length. *path is set
// to 'A', 'B', or 'C'. Returns 0 on success, -1 on shape error. Supports
// short-form and long-form (0x81/0x82) BER lengths.
static int sam_sc_peel_envelope(const uint8_t *resp, uint16_t resp_len,
const uint8_t **inner, uint16_t *inner_len,
char *path) {
if (resp_len < 2) return -1;
#define SAM_SC_READ_BER_LEN(buf, buflen, off, out_len) \
do { \
if ((off) >= (buflen)) return -1; \
uint8_t b0 = (buf)[(off)++]; \
if (b0 < 0x80) { (out_len) = b0; } \
else if (b0 == 0x81) { \
if ((off) >= (buflen)) return -1; \
(out_len) = (buf)[(off)++]; \
} else if (b0 == 0x82) { \
if ((off) + 1 >= (buflen)) return -1; \
(out_len) = ((uint16_t)(buf)[(off)] << 8) | (buf)[(off)+1]; \
(off) += 2; \
} else return -1; \
} while (0)
uint16_t off = 0;
uint8_t outer_tag = resp[off++];
uint16_t outer_len;
SAM_SC_READ_BER_LEN(resp, resp_len, off, outer_len);
if ((uint32_t)off + outer_len > resp_len) return -1;
if (outer_tag == 0xBE) {
*path = 'C';
*inner = resp + off;
*inner_len = outer_len;
return 0;
}
if (outer_tag != 0xBD) return -1;
if (off >= resp_len) return -1;
uint8_t inner_tag = resp[off++];
uint16_t in_len;
SAM_SC_READ_BER_LEN(resp, resp_len, off, in_len);
if ((uint32_t)off + in_len > resp_len) return -1;
if (inner_tag == 0x8A) *path = 'A';
else if (inner_tag == 0xB3) *path = 'B';
else return -1;
*inner = resp + off;
*inner_len = in_len;
return 0;
#undef SAM_SC_READ_BER_LEN
}
// Encode a BER length (short form < 128, otherwise 0x81/0x82).
static uint8_t sam_sc_emit_ber_len(uint8_t *out, uint16_t len) {
if (len < 0x80) { out[0] = (uint8_t)len; return 1; }
if (len < 0x100) { out[0] = 0x81; out[1] = (uint8_t)len; return 2; }
out[0] = 0x82; out[1] = (uint8_t)(len >> 8); out[2] = (uint8_t)(len & 0xFF);
return 3;
}
// ---------------------------------------------------------------------------
// hf iclass sam scopen
// ---------------------------------------------------------------------------
static int CmdHFiClassSAMSCOpen(const char *Cmd) {
CLIParserContext *ctx;
CLIParserInit(&ctx, "hf iclass sam scopen",
"Open a HID Artemis secure channel: InitAuth + ContinueAuth.\n"
"Persists the session state across CLI commands so subsequent\n"
"scsend / scclose operations can wrap/unwrap APDUs against\n"
"the same SAM-side session.\n"
"Defaults: --key 00000000000000000000000000000000 --kref 0x85",
"hf iclass sam scopen --key 00000000000000000000000000000000\n");
void *argtable[] = {
arg_param_begin,
arg_str0(NULL, "key", "<hex>", "16-byte AES master key (default: all zeros - override with real key)"),
arg_int0(NULL, "kref", "<dec>", "key reference slot 0..255 (default: 0x85)"),
arg_lit0("v", "verbose", "verbose output"),
arg_param_end
};
CLIExecWithReturn(ctx, Cmd, argtable, true);
uint8_t master_key[16];
int key_len = 0;
if (CLIParamHexToBuf(arg_get_str(ctx, 1), master_key, sizeof(master_key), &key_len) != PM3_SUCCESS) {
CLIParserFree(ctx);
return PM3_EINVARG;
}
if (key_len == 0) {
memcpy(master_key, SAM_SC_DEFAULT_MASTER_KEY, 16);
} else if (key_len != 16) {
PrintAndLogEx(FAILED, "--key must be exactly 16 bytes (32 hex chars), got %d", key_len);
CLIParserFree(ctx);
return PM3_EINVARG;
}
int kref = arg_get_int_def(ctx, 2, SAM_SC_DEFAULT_KREF);
bool verbose = arg_get_lit(ctx, 3);
CLIParserFree(ctx);
if (kref < 0 || kref > 0xFF) {
PrintAndLogEx(FAILED, "--kref must be 0..255");
return PM3_EINVARG;
}
memset(&s_sam_sc, 0, sizeof(s_sam_sc));
memcpy(s_sam_sc.master_key, master_key, 16);
s_sam_sc.kref = (uint8_t)kref;
PrintAndLogEx(INFO, "KREF : 0x%02X", s_sam_sc.kref);
PrintAndLogEx(INFO, "Master key : %s", sprint_hex_inrow(master_key, 16));
// ---------------- Initialize the SAM ----------------
// Run the equivalent of `hf iclass sam --info` first. This:
// - Resets the SAM via I2C (CMD_HF_SAM_PICOPASS dispatcher)
// - Sends sam_get_version + sam_get_serial_number as a warmup
// - Prints the version + serial to the user
// The InitAuth that follows then reaches a SAM in a known clean state.
PrintAndLogEx(INFO, "");
PrintAndLogEx(INFO, "--- Initializing SAM ('hf iclass sam --info') ---");
int init_rc = CmdHFiClassSAMExtract("--info");
if (init_rc != PM3_SUCCESS) {
PrintAndLogEx(WARNING, "SAM init returned rc=%d; continuing with InitAuth anyway", init_rc);
}
PrintAndLogEx(INFO, "--- Opening secure channel ---");
PrintAndLogEx(INFO, "");
// ---------------- InitAuth ----------------
uint8_t init_auth[23];
sam_sc_build_init_auth(s_sam_sc.kref, s_sam_sc.rnd_a, init_auth);
uint8_t resp[PM3_CMD_DATA_SIZE];
uint16_t resp_len = sizeof(resp);
uint8_t sam_flag = 0;
int rc = sam_sc_dispatch(SAM_SC_FLAG_FORCE_RESET, 0x00,
init_auth, sizeof(init_auth),
&sam_flag, resp, &resp_len);
if (rc != PM3_SUCCESS) {
PrintAndLogEx(FAILED, "InitAuth dispatch failed (rc=%d)", rc);
return rc;
}
if (verbose)
PrintAndLogEx(DEBUG, "InitAuth scFlag=0x%02X resp(%u): %s",
sam_flag, resp_len, sprint_hex_inrow(resp, resp_len));
if (resp_len < 36) {
PrintAndLogEx(FAILED, "InitAuth response too short (%u bytes)", resp_len);
if (verbose) PrintAndLogEx(DEBUG, "raw: %s", sprint_hex_inrow(resp, resp_len));
return PM3_ESOFT;
}
if (resp[0] != 0xBD || resp[2] != 0x8A || resp[3] != 0x20) {
PrintAndLogEx(FAILED, "InitAuth response shape unexpected: %02X %02X %02X %02X ...",
resp[0], resp[1], resp[2], resp[3]);
if (verbose) PrintAndLogEx(DEBUG, "raw: %s", sprint_hex_inrow(resp, resp_len));
return PM3_ESOFT;
}
memcpy(s_sam_sc.suid, resp + 4, 8);
memcpy(s_sam_sc.rnd_b, resp + 12, 8);
uint8_t server_cryptogram[16];
memcpy(server_cryptogram, resp + 20, 16);
s_sam_sc.sc_flag = sam_flag;
PrintAndLogEx(INFO, "scFlag : 0x%02X (SAM-assigned)", s_sam_sc.sc_flag);
PrintAndLogEx(INFO, "Server UID : %s", sprint_hex_inrow(s_sam_sc.suid, 8));
PrintAndLogEx(INFO, "Server RNDB: %s", sprint_hex_inrow(s_sam_sc.rnd_b, 8));
PrintAndLogEx(INFO, "ServerCrypt: %s", sprint_hex_inrow(server_cryptogram, 16));
// ---------------- Derive SCBK + session keys ----------------
{
uint8_t suid_data[16];
memcpy(suid_data, s_sam_sc.suid, 8);
for (int i = 0; i < 8; i++) suid_data[8 + i] = (uint8_t)(s_sam_sc.suid[i] ^ 0xFF);
uint8_t iv[16] = {0}, key_copy[16];
memcpy(key_copy, master_key, 16);
aes_encode(iv, key_copy, suid_data, s_sam_sc.scbk, 16);
}
static const uint8_t SC_PFX_MAC1[2] = {0x01, 0x01};
static const uint8_t SC_PFX_MAC2[2] = {0x01, 0x02};
static const uint8_t SC_PFX_ENC[2] = {0x01, 0x82};
sam_sc_kdf_derive(s_sam_sc.scbk, SC_PFX_MAC1, s_sam_sc.rnd_b, s_sam_sc.s_mac1);
sam_sc_kdf_derive(s_sam_sc.scbk, SC_PFX_MAC2, s_sam_sc.rnd_b, s_sam_sc.s_mac2);
sam_sc_kdf_derive(s_sam_sc.scbk, SC_PFX_ENC, s_sam_sc.rnd_b, s_sam_sc.s_enc);
if (verbose) {
PrintAndLogEx(DEBUG, "SCBK : %s", sprint_hex_inrow(s_sam_sc.scbk, 16));
PrintAndLogEx(DEBUG, "sEnc : %s", sprint_hex_inrow(s_sam_sc.s_enc, 16));
PrintAndLogEx(DEBUG, "sMAC1: %s", sprint_hex_inrow(s_sam_sc.s_mac1, 16));
PrintAndLogEx(DEBUG, "sMAC2: %s", sprint_hex_inrow(s_sam_sc.s_mac2, 16));
}
uint8_t expected_srv[16];
{
uint8_t in[16], iv[16] = {0}, key_copy[16];
memcpy(in, s_sam_sc.rnd_a, 8);
memcpy(in + 8, s_sam_sc.rnd_b, 8);
memcpy(key_copy, s_sam_sc.s_enc, 16);
aes_encode(iv, key_copy, in, expected_srv, 16);
}
if (memcmp(expected_srv, server_cryptogram, 16) != 0) {
PrintAndLogEx(FAILED, _RED_("Server cryptogram mismatch") " - master key wrong for KREF 0x%02X",
s_sam_sc.kref);
if (verbose) {
PrintAndLogEx(DEBUG, "expected: %s", sprint_hex_inrow(expected_srv, 16));
PrintAndLogEx(DEBUG, "got : %s", sprint_hex_inrow(server_cryptogram, 16));
}
memset(&s_sam_sc, 0, sizeof(s_sam_sc));
return PM3_ESOFT;
}
PrintAndLogEx(SUCCESS, _GREEN_("InitAuth OK") " - server cryptogram verified");
// ---------------- ContinueAuth ----------------
uint8_t client_crypto[16];
{
uint8_t in[16], iv[16] = {0}, key_copy[16];
memcpy(in, s_sam_sc.rnd_b, 8);
memcpy(in + 8, s_sam_sc.rnd_a, 8);
memcpy(key_copy, s_sam_sc.s_enc, 16);
aes_encode(iv, key_copy, in, client_crypto, 16);
}
uint8_t client_cmac[16];
{
uint8_t iv0[16] = {0};
sam_sc_hid_mac(s_sam_sc.s_mac1, s_sam_sc.s_mac2, iv0, client_crypto, 16, client_cmac);
}
if (verbose) {
PrintAndLogEx(DEBUG, "client_crypto: %s", sprint_hex_inrow(client_crypto, 16));
PrintAndLogEx(DEBUG, "client_cmac : %s", sprint_hex_inrow(client_cmac, 16));
}
uint8_t cont_auth[40];
sam_sc_build_continue_auth(client_crypto, client_cmac, cont_auth);
resp_len = sizeof(resp);
rc = sam_sc_dispatch(0, s_sam_sc.sc_flag, cont_auth, sizeof(cont_auth),
&sam_flag, resp, &resp_len);
if (rc != PM3_SUCCESS) {
PrintAndLogEx(FAILED, "ContinueAuth dispatch failed (rc=%d)", rc);
memset(&s_sam_sc, 0, sizeof(s_sam_sc));
return rc;
}
if (verbose) {
PrintAndLogEx(DEBUG, "ContinueAuth scFlag=0x%02X resp(%u): %s",
sam_flag, resp_len, sprint_hex_inrow(resp, resp_len));
}
if (resp_len < 20) {
PrintAndLogEx(FAILED, "ContinueAuth response too short (%u bytes)", resp_len);
if (verbose) PrintAndLogEx(DEBUG, "raw: %s", sprint_hex_inrow(resp, resp_len));
memset(&s_sam_sc, 0, sizeof(s_sam_sc));
return PM3_ESOFT;
}
if (resp[0] != 0xBD || resp[2] != 0x8A || resp[3] != 0x10) {
PrintAndLogEx(FAILED, "ContinueAuth response shape unexpected: %02X %02X %02X %02X ...",
resp[0], resp[1], resp[2], resp[3]);
if (verbose) PrintAndLogEx(DEBUG, "raw: %s", sprint_hex_inrow(resp, resp_len));
memset(&s_sam_sc, 0, sizeof(s_sam_sc));
return PM3_ESOFT;
}
uint8_t server_rmac[16];
memcpy(server_rmac, resp + 4, 16);
uint8_t expected_rmac[16];
{
uint8_t pad_block[16] = {0};
pad_block[0] = 0x80;
sam_sc_hid_mac(s_sam_sc.s_mac1, s_sam_sc.s_mac2, client_cmac,
pad_block, 16, expected_rmac);
}
if (memcmp(expected_rmac, server_rmac, 16) != 0) {
PrintAndLogEx(FAILED, _RED_("ContinueAuth R-MAC mismatch") " - SAM did not authenticate");
if (verbose) {
PrintAndLogEx(DEBUG, "expected: %s", sprint_hex_inrow(expected_rmac, 16));
PrintAndLogEx(DEBUG, "got : %s", sprint_hex_inrow(server_rmac, 16));
}
memset(&s_sam_sc, 0, sizeof(s_sam_sc));
return PM3_ESOFT;
}
memcpy(s_sam_sc.c_mac, client_cmac, 16);
memcpy(s_sam_sc.r_mac, server_rmac, 16);
s_sam_sc.sc_flag = sam_flag;
s_sam_sc.open = true;
PrintAndLogEx(SUCCESS, _GREEN_("Secure channel OPEN") " (scFlag=0x%02X, KREF=0x%02X)",
s_sam_sc.sc_flag, s_sam_sc.kref);
PrintAndLogEx(INFO, "Use 'hf iclass sam scsend' to send wrapped APDUs;");
PrintAndLogEx(INFO, "use 'hf iclass sam scclose' to terminate.");
return PM3_SUCCESS;
}
// ---------------------------------------------------------------------------
// hf iclass sam scsend --payload <hex>
// ---------------------------------------------------------------------------
static int CmdHFiClassSAMSCSend(const char *Cmd) {
CLIParserContext *ctx;
CLIParserInit(&ctx, "hf iclass sam scsend",
"Send a plaintext SAMCommand body through the open secure\n"
"channel. The client wraps it (encrypt + MAC + chain RMAC),\n"
"sends via CMD_HF_SAM_SC, unwraps the response, and prints\n"
"the plaintext. Requires 'scopen' to have been called first.\n"
"Test payload: --payload 8200 (samCommandGetSamVersion)",
"hf iclass sam scsend --payload 8200\n"
"hf iclass sam scsend --payload b9028a00 -v\n"
"hf iclass sam scsend --payload b903850100");
void *argtable[] = {
arg_param_begin,
arg_str1(NULL, "payload", "<hex>", "plaintext SAMCommand body (the bytes inside A0 <len>)"),
arg_lit0("v", "verbose", "verbose output"),
arg_param_end
};
CLIExecWithReturn(ctx, Cmd, argtable, false);
if (s_sam_sc.open == false) {
PrintAndLogEx(FAILED, "no open secure channel - run 'scopen' first");
CLIParserFree(ctx);
return PM3_ESOFT;
}
uint8_t plaintext[SAM_SC_MAX_FRAME];
int plaintext_len = 0;
if (CLIParamHexToBuf(arg_get_str(ctx, 1), plaintext, sizeof(plaintext), &plaintext_len) != PM3_SUCCESS) {
CLIParserFree(ctx);
return PM3_EINVARG;
}
if (plaintext_len < 1) {
PrintAndLogEx(FAILED, "--payload must be at least 1 byte");
CLIParserFree(ctx);
return PM3_EINVARG;
}
bool verbose = arg_get_lit(ctx, 2);
CLIParserFree(ctx);
PrintAndLogEx(INFO, "Plaintext : %s", sprint_hex_inrow(plaintext, plaintext_len));
uint8_t wrapped[SAM_SC_MAX_FRAME];
uint16_t wrapped_len = sam_sc_wrap(plaintext, (uint16_t)plaintext_len,
wrapped, sizeof(wrapped));
if (wrapped_len == 0) {
PrintAndLogEx(FAILED, "wrap() failed");
return PM3_ESOFT;
}
if (verbose)
PrintAndLogEx(DEBUG, "wrapped (%u): %s", wrapped_len, sprint_hex_inrow(wrapped, wrapped_len));
uint8_t sam_payload[SAM_SC_MAX_FRAME + 4];
uint16_t off = 0;
sam_payload[off++] = 0xA0;
off += sam_sc_emit_ber_len(sam_payload + off, wrapped_len);
if ((uint32_t)off + wrapped_len > sizeof(sam_payload)) {
PrintAndLogEx(FAILED, "wrapped payload too large for buffer");
return PM3_ESOFT;
}
memcpy(sam_payload + off, wrapped, wrapped_len);
off += wrapped_len;
uint8_t resp[PM3_CMD_DATA_SIZE];
uint16_t resp_len = sizeof(resp);
uint8_t sam_flag = 0;
int rc = sam_sc_dispatch(0, s_sam_sc.sc_flag,
sam_payload, off, &sam_flag, resp, &resp_len);
if (rc != PM3_SUCCESS) {
PrintAndLogEx(FAILED, "dispatch failed (rc=%d)", rc);
return rc;
}
if (verbose) {
PrintAndLogEx(DEBUG, "scFlag=0x%02X resp(%u): %s",
sam_flag, resp_len, sprint_hex_inrow(resp, resp_len));
}
if (sam_flag != s_sam_sc.sc_flag && verbose) {
PrintAndLogEx(WARNING, "SAM scFlag changed: was 0x%02X, now 0x%02X",
s_sam_sc.sc_flag, sam_flag);
}
const uint8_t *inner = NULL;
uint16_t inner_len = 0;
char path = '?';
if (sam_sc_peel_envelope(resp, resp_len, &inner, &inner_len, &path) != 0) {
PrintAndLogEx(FAILED, "could not peel SAM response envelope");
if (verbose) PrintAndLogEx(DEBUG, "raw: %s", sprint_hex_inrow(resp, resp_len));
return PM3_ESOFT;
}
if (verbose)
PrintAndLogEx(DEBUG, "envelope=Path %c, inner ciphertext+MAC (%u): %s",
path, inner_len, sprint_hex_inrow(inner, inner_len));
uint8_t plain_resp[SAM_SC_MAX_FRAME];
int plain_len = sam_sc_unwrap(inner, inner_len, plain_resp, sizeof(plain_resp));
if (plain_len < 0) {
PrintAndLogEx(FAILED, _RED_("unwrap failed") " - MAC mismatch or padding error");
if (verbose) PrintAndLogEx(DEBUG, "raw response: %s", sprint_hex_inrow(resp, resp_len));
return PM3_ESOFT;
}
if (path == 'C') {
PrintAndLogEx(WARNING, _YELLOW_("Path C errorResponse") " - SAM returned an error");
} else {
PrintAndLogEx(SUCCESS, _GREEN_("Decrypt OK") " (Path %c)", path);
}
PrintAndLogEx(INFO, "Plaintext response (%d): %s",
plain_len, sprint_hex_inrow(plain_resp, plain_len));
return PM3_SUCCESS;
}
// ---------------------------------------------------------------------------
// hf iclass sam scclose
// ---------------------------------------------------------------------------
static int CmdHFiClassSAMSCClose(const char *Cmd) {
CLIParserContext *ctx;
CLIParserInit(&ctx, "hf iclass sam scclose",
"Send Terminate (91 00) over the open SC and release the\n"
"firmware-side SAM session.",
"hf iclass sam scclose");
void *argtable[] = {
arg_param_begin,
arg_lit0("v", "verbose", "verbose output"),
arg_param_end
};
CLIExecWithReturn(ctx, Cmd, argtable, true);
bool verbose = arg_get_lit(ctx, 1);
CLIParserFree(ctx);
if (s_sam_sc.open == false) {
PrintAndLogEx(WARNING, "no open secure channel - nothing to close");
return PM3_SUCCESS;
}
uint8_t terminate[2] = {0x91, 0x00};
uint8_t wrapped[64];
uint16_t wrapped_len = sam_sc_wrap(terminate, sizeof(terminate),
wrapped, sizeof(wrapped));
if (wrapped_len == 0) {
PrintAndLogEx(FAILED, "wrap() of Terminate failed - forcing local close");
memset(&s_sam_sc, 0, sizeof(s_sam_sc));
return PM3_ESOFT;
}
uint8_t sam_payload[80];
uint16_t off = 0;
sam_payload[off++] = 0xA0;
off += sam_sc_emit_ber_len(sam_payload + off, wrapped_len);
memcpy(sam_payload + off, wrapped, wrapped_len);
off += wrapped_len;
uint8_t resp[PM3_CMD_DATA_SIZE];
uint16_t resp_len = sizeof(resp);
uint8_t sam_flag = 0;
int rc = sam_sc_dispatch(SAM_SC_FLAG_RELEASE, s_sam_sc.sc_flag,
sam_payload, off, &sam_flag, resp, &resp_len);
if (verbose) {
PrintAndLogEx(DEBUG, "Terminate scFlag=0x%02X resp(%u): %s",
sam_flag, resp_len, sprint_hex_inrow(resp, resp_len));
}
memset(&s_sam_sc, 0, sizeof(s_sam_sc));
if (rc != PM3_SUCCESS) {
PrintAndLogEx(WARNING, "Terminate dispatch returned rc=%d - host state cleared anyway", rc);
return rc;
}
PrintAndLogEx(SUCCESS, _GREEN_("Secure channel closed"));
return PM3_SUCCESS;
}
// ---------------------------------------------------------------------------
// hf iclass sam dispatcher
// ---------------------------------------------------------------------------
//
// Backwards-compat router: when invoked without a subcommand keyword (or
// with a flag-style argument like `--info` / `-d ...` / `-f ...`), defers
// to the legacy PACS-extraction implementation (CmdHFiClassSAMExtract).
// When the first word matches a known SC subcommand, dispatches to the
// scopen / scsend / scclose / help handlers.
//
// So all of these continue to work:
// hf iclass sam (legacy default)
// hf iclass sam --info (legacy SAM info)
// hf iclass sam -p -d <hex> (legacy with flags)
// hf iclass sam -f hf-iclass-dump.bin (legacy emulate-from-file)
// And these are new:
// hf iclass sam scopen
// hf iclass sam scsend --payload <hex>
// hf iclass sam scclose
// hf iclass sam help (lists the SC subcommands)
// ---------------------------------------------------------------------------
static int CmdHFiClassSAMHelp(const char *Cmd);
// SC subcommand dispatch table. Note: no `help` entry here on purpose -
// use `hf iclass sam --help` (or `-h`) for the SAM-level help. The bare-word
// `help` keyword is still accepted as a deprecated alias (see sam_cmd_is_help_request).
static command_t SAMSubCommandTable[] = {
{"scopen", CmdHFiClassSAMSCOpen, IfPm3Smartcard, "Open Artemis SC (InitAuth + ContinueAuth); persists session"},
{"scsend", CmdHFiClassSAMSCSend, IfPm3Smartcard, "Send wrapped SAMCommand body through the open SC"},
{"scclose", CmdHFiClassSAMSCClose, IfPm3Smartcard, "Send Terminate; release the SC session"},
{NULL, NULL, NULL, NULL}
};
static int CmdHFiClassSAMHelp(const char *Cmd) {
(void)Cmd;
PrintAndLogEx(NORMAL, "");
PrintAndLogEx(NORMAL, _YELLOW_("hf iclass sam") " - HID SAM operations");
PrintAndLogEx(NORMAL, "");
PrintAndLogEx(NORMAL, "Secure-channel subcommands:");
CmdsHelp(SAMSubCommandTable);
PrintAndLogEx(NORMAL, "");
PrintAndLogEx(NORMAL, "Legacy PACS-extraction flags (no subcommand, applied to " _YELLOW_("hf iclass sam") " directly):");
PrintAndLogEx(NORMAL, " --info get SAM version + serial number (also warms up the SAM)");
PrintAndLogEx(NORMAL, " -d, --data <hex> DER-encoded SAMCommand to send (raw, no SC)");
PrintAndLogEx(NORMAL, " -s, --snmp --data is in SNMP format without the A0/94 headers");
PrintAndLogEx(NORMAL, " -p, --prevent fake the e-purse update during PACS extraction");
PrintAndLogEx(NORMAL, " --break stop tag interaction at nr-mac (for SIO extract)");
PrintAndLogEx(NORMAL, " -f, --file <fn> emulate from a dump file instead of a real card");
PrintAndLogEx(NORMAL, " -n, --nodetect skip card detect + SetDetectedCardInfo");
PrintAndLogEx(NORMAL, " -k, --keep keep the field active after the command");
PrintAndLogEx(NORMAL, " -t, --tlv decode the response as TLV");
PrintAndLogEx(NORMAL, " --shallow shallow modulation");
PrintAndLogEx(NORMAL, " -v, --verbose verbose output");
PrintAndLogEx(NORMAL, "");
PrintAndLogEx(NORMAL, "Examples:");
PrintAndLogEx(NORMAL, " " _YELLOW_("hf iclass sam") " extract PACS via SAM (defaults)");
PrintAndLogEx(NORMAL, " " _YELLOW_("hf iclass sam --info") " get SAM version + serial (warmup ping)");
PrintAndLogEx(NORMAL, " " _YELLOW_("hf iclass sam scopen") " open Artemis secure channel");
PrintAndLogEx(NORMAL, " " _YELLOW_("hf iclass sam scsend --payload 8200") " send wrapped SAMCommand");
PrintAndLogEx(NORMAL, " " _YELLOW_("hf iclass sam scclose") " terminate the SC session");
PrintAndLogEx(NORMAL, "");
return PM3_SUCCESS;
}
// Returns true if Cmd consists solely of "-h", "--help", or the bare-word
// "help" (with optional surrounding whitespace). Used to intercept help
// requests before the dispatcher would otherwise forward them to the legacy
// CLIParser. The bare-word "help" is kept as a deprecated alias - it's no
// longer advertised in the subcommand table, but existing scripts/muscle
// memory still resolve correctly.
static bool sam_cmd_is_help_request(const char *Cmd) {
while (*Cmd == ' ' || *Cmd == '\t') Cmd++;
size_t len = 0;
while (Cmd[len] && Cmd[len] != ' ' && Cmd[len] != '\t') len++;
if (len == 0) return false;
// ensure standalone (no trailing args)
const char *tail = Cmd + len;
while (*tail == ' ' || *tail == '\t') tail++;
if (*tail != '\0') return false;
if (len == 2 && memcmp(Cmd, "-h", 2) == 0) return true;
if (len == 6 && memcmp(Cmd, "--help", 6) == 0) return true;
if (len == 4 && memcmp(Cmd, "help", 4) == 0) return true;
return false;
}
static int CmdHFiClassSAM(const char *Cmd) {
// Skip leading whitespace.
while (*Cmd == ' ' || *Cmd == '\t') Cmd++;
// `--help` / `-h` (standalone) -> SC-aware help instead of the legacy
// CLIParser's auto-help (which would hide the SC subcommands). The
// bare-word `help` is also accepted as a deprecated alias (no longer
// listed in the subcommand table but still resolves here).
if (sam_cmd_is_help_request(Cmd)) {
return CmdHFiClassSAMHelp(Cmd);
}
// No args or a flag-style invocation -> legacy PACS-extract implementation.
// Preserves `hf iclass sam`, `hf iclass sam --info`, `hf iclass sam -d ...`,
// `hf iclass sam -f ...` etc.
if (*Cmd == '\0' || *Cmd == '-') {
return CmdHFiClassSAMExtract(Cmd);
}
// Match the first word against known SC subcommands. The CmdsParse
// dispatcher would print help on no match, but we want unknown first
// words to fall through to the legacy implementation, so we check first.
size_t word_len = 0;
while (Cmd[word_len] && Cmd[word_len] != ' ' && Cmd[word_len] != '\t')
word_len++;
for (size_t i = 0; SAMSubCommandTable[i].Name != NULL; i++) {
const char *name = SAMSubCommandTable[i].Name;
size_t nl = strlen(name);
if (nl == word_len && memcmp(name, Cmd, word_len) == 0) {
clearCommandBuffer();
return CmdsParse(SAMSubCommandTable, Cmd);
}
}
// Unknown first word - defer to legacy (which will either parse it as a
// free-form arg or error out cleanly with its own help).
return CmdHFiClassSAMExtract(Cmd);
}
// ---------------------------------------------------------------------------
// hf iclass liberate — detect and liberate MKF / iCopy-X cloned cards
// ---------------------------------------------------------------------------
@@ -8256,7 +9088,7 @@ static command_t CommandTable[] = {
{"managekeys", CmdHFiClassManageKeys, AlwaysAvailable, "Manage keys to use with iclass commands"},
{"permutekey", CmdHFiClassPermuteKey, AlwaysAvailable, "Permute function from 'heart of darkness' paper"},
{"-----------", CmdHelp, IfPm3Smartcard, "----------------------- " _CYAN_("SAM") " -----------------------"},
{"sam", CmdHFiClassSAM, IfPm3Smartcard, "SAM tests"},
{"sam", CmdHFiClassSAM, IfPm3Smartcard, "SAM ops: PACS extract + secure channel (scopen/scsend/scclose)"},
{NULL, NULL, NULL, NULL}
};
+6
View File
@@ -892,6 +892,12 @@ typedef struct {
#define CMD_HF_SEOS_SIMULATE 0x0903
// HID SAM secure-channel transport (separate dispatcher from CMD_HF_SAM_PICOPASS).
// Used to drive InitAuth / ContinueAuth / wrap-unwrap traffic where the SAM's
// session state must persist across calls (no I2C reset, no GetVersion ping,
// no iso15 NFC-relay loop) and the routing scFlag byte must be host-supplied.
#define CMD_HF_SAM_SC 0x0904
#define CMD_UNKNOWN 0xFFFF
// Mifare simulation flags