diff --git a/CHANGELOG.md b/CHANGELOG.md index b7e52289c..3046ebdd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ 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 `hf gst read` command (@kormax) +- Added `hf gst info` command (@kormax) - Added `hf 14b tearoff` - interactive ST25TB/SRx monotonic counter tear-off attack (@xNovyz) - Fixed missing `WDT_HIT()` in `Get14443bAnswerFromTag()` DMA polling loop causing hardware watchdog reboot on SSC clock stall (@xNovyz) - Added `hf vas info` command (@kormax) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 794c52646..e1232f08d 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -261,6 +261,8 @@ if (LZ4_INCLUDE_DIRS AND LZ4_LIBRARIES) set(LZ4_FOUND ON) endif (LZ4_INCLUDE_DIRS AND LZ4_LIBRARIES) +find_package(ZLIB QUIET) + if (NOT SKIPGD EQUAL 1) if (EMBED_GD) cmake_policy(SET CMP0114 NEW) @@ -386,6 +388,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdhffido.c ${PM3_ROOT}/client/src/cmdhffudan.c ${PM3_ROOT}/client/src/cmdhfgallagher.c + ${PM3_ROOT}/client/src/cmdhfgst.c ${PM3_ROOT}/client/src/cmdhfcipurse.c ${PM3_ROOT}/client/src/cmdhficlass.c ${PM3_ROOT}/client/src/cmdhfict.c @@ -584,6 +587,12 @@ if (LZ4_FOUND) set(ADDITIONAL_LNK ${LZ4_LIBRARIES} ${ADDITIONAL_LNK}) endif (LZ4_FOUND) +if (ZLIB_FOUND) + add_definitions("-DHAVE_ZLIB") + set(ADDITIONAL_DIRS ${ZLIB_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) + set(ADDITIONAL_LNK ${ZLIB_LIBRARIES} ${ADDITIONAL_LNK}) +endif (ZLIB_FOUND) + if (NOT SKIPGD EQUAL 1 AND GD_FOUND) set(ADDITIONAL_DIRS ${GD_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) set(ADDITIONAL_LNK ${GD_LIBRARIES} ${ADDITIONAL_LNK}) @@ -651,6 +660,12 @@ else (LZ4_FOUND) message(SEND_ERROR "LZ4 library: LZ4 not found") endif (LZ4_FOUND) +if (ZLIB_FOUND) + message(STATUS "Zlib library: system library found (enabled)") +else (ZLIB_FOUND) + message(STATUS "Zlib library: not found, disabled") +endif (ZLIB_FOUND) + if (SKIPGD EQUAL 1) message(STATUS "GD library: skipped") elseif (GD_FOUND) diff --git a/client/Makefile b/client/Makefile index e2e2499ef..7381c16b7 100644 --- a/client/Makefile +++ b/client/Makefile @@ -272,6 +272,15 @@ LDLIBS += -lbz2 ## LZ4 LDLIBS += -llz4 +## ZLIB +ZLIBINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags zlib 2>/dev/null) +ZLIBLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs zlib 2>/dev/null) +ifneq ($(ZLIBLDLIBS),) + LDLIBS += $(ZLIBLDLIBS) + PM3INCLUDES += $(ZLIBINCLUDES) + ZLIB_FOUND = 1 +endif + ## Bluez (optional) ifneq ($(SKIPBT),1) BTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags bluez 2>/dev/null) @@ -485,6 +494,10 @@ ifeq ($(GD_FOUND),1) PM3CFLAGS += -DHAVE_GD endif +ifeq ($(ZLIB_FOUND),1) + PM3CFLAGS += -DHAVE_ZLIB +endif + ifeq ($(SWIG_LUA_FOUND),1) PM3CFLAGS += -DHAVE_LUA_SWIG endif @@ -597,6 +610,12 @@ else endif endif +ifeq ($(ZLIB_FOUND),1) + $(info Zlib library: system library found, enabled) +else + $(info Zlib library: not found, disabled) +endif + ifeq ($(SKIPREADLINE),1) $(info Readline library: skipped) else @@ -674,6 +693,7 @@ SRCS = mifare/aiddesfire.c \ cmdhffido.c \ cmdhffudan.c \ cmdhfgallagher.c \ + cmdhfgst.c \ cmdhfksx6924.c \ cmdhfcipurse.c \ cmdhficlass.c \ diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index e3f9b5c53..885f6b2b2 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -181,6 +181,8 @@ if (LZ4_INCLUDE_DIRS AND LZ4_LIBRARIES) set(LZ4_FOUND ON) endif (LZ4_INCLUDE_DIRS AND LZ4_LIBRARIES) +find_package(ZLIB QUIET) + if (NOT SKIPGD EQUAL 1) if (EMBED_GD) cmake_policy(SET CMP0114 NEW) @@ -305,6 +307,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdhffido.c ${PM3_ROOT}/client/src/cmdhffudan.c ${PM3_ROOT}/client/src/cmdhfgallagher.c + ${PM3_ROOT}/client/src/cmdhfgst.c ${PM3_ROOT}/client/src/cmdhfcipurse.c ${PM3_ROOT}/client/src/cmdhficlass.c ${PM3_ROOT}/client/src/cmdhfict.c @@ -479,6 +482,12 @@ if (LZ4_FOUND) set(ADDITIONAL_LNK ${LZ4_LIBRARIES} ${ADDITIONAL_LNK}) endif (LZ4_FOUND) +if (ZLIB_FOUND) + add_definitions("-DHAVE_ZLIB") + set(ADDITIONAL_DIRS ${ZLIB_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) + set(ADDITIONAL_LNK ${ZLIB_LIBRARIES} ${ADDITIONAL_LNK}) +endif (ZLIB_FOUND) + if (NOT SKIPGD EQUAL 1 AND GD_FOUND) set(ADDITIONAL_DIRS ${GD_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) set(ADDITIONAL_LNK ${GD_LIBRARIES} ${ADDITIONAL_LNK}) @@ -533,6 +542,12 @@ else (LZ4_FOUND) message(SEND_ERROR "LZ4 library: LZ4 not found") endif (LZ4_FOUND) +if (ZLIB_FOUND) + message(STATUS "Zlib library: system library found (enabled)") +else (ZLIB_FOUND) + message(STATUS "Zlib library: not found, disabled") +endif (ZLIB_FOUND) + if (SKIPGD EQUAL 1) message(STATUS "GD library: skipped") elseif (GD_FOUND) diff --git a/client/resources/gst.google.der b/client/resources/gst.google.der new file mode 100644 index 000000000..8820c2562 Binary files /dev/null and b/client/resources/gst.google.der differ diff --git a/client/resources/gst.passkit.der b/client/resources/gst.passkit.der new file mode 100644 index 000000000..b8a9045d9 Binary files /dev/null and b/client/resources/gst.passkit.der differ diff --git a/client/src/cmdhf.c b/client/src/cmdhf.c index 2099a1fb6..ad37bdf59 100644 --- a/client/src/cmdhf.c +++ b/client/src/cmdhf.c @@ -34,6 +34,7 @@ #include "cmdhffido.h" // FIDO authenticators #include "cmdhffudan.h" // Fudan cards #include "cmdhfgallagher.h" // Gallagher DESFire cards +#include "cmdhfgst.h" // Google Smart Tap #include "cmdhficlass.h" // ICLASS #include "cmdhfict.h" // ICT MFC / DESfire cards #include "cmdhfjooki.h" // MFU based Jooki @@ -586,6 +587,7 @@ static command_t CommandTable[] = { {"fido", CmdHFFido, AlwaysAvailable, "{ FIDO and FIDO2 authenticators... }"}, {"fudan", CmdHFFudan, AlwaysAvailable, "{ Fudan RFIDs... }"}, {"gallagher", CmdHFGallagher, AlwaysAvailable, "{ Gallagher DESFire RFIDs... }"}, + {"gst", CmdHFGST, AlwaysAvailable, "{ Google Smart Tap passes... }"}, {"iclass", CmdHFiClass, AlwaysAvailable, "{ ICLASS RFIDs... }"}, {"ict", CmdHFICT, AlwaysAvailable, "{ ICT MFC/DESfire RFIDs... }"}, {"jooki", CmdHF_Jooki, AlwaysAvailable, "{ Jooki RFIDs... }"}, diff --git a/client/src/cmdhf14a.c b/client/src/cmdhf14a.c index ff21385be..2e12ce15e 100644 --- a/client/src/cmdhf14a.c +++ b/client/src/cmdhf14a.c @@ -243,6 +243,7 @@ static const hintAIDList_t hintAIDList[] = { { "\xA0\x00\x00\x09\x09\xAC\xCE\x55\x01", 9, "Aliro", "hf aliro" }, { "\xd2\x76\x00\x00\x85\x01\x00", 7, "desfire", "hf mfdes" }, { "\x4F\x53\x45\x2E\x56\x41\x53\x2E\x30\x31", 10, "OSE.VAS", "hf vas"}, + { "\xA0\x00\x00\x04\x76\xD0\x00\x01\x11", 9, "Google Smart Tap v2", "hf gst" }, }; // iso14a apdu input frame length diff --git a/client/src/cmdhfgst.c b/client/src/cmdhfgst.c new file mode 100644 index 000000000..4862a4f5d --- /dev/null +++ b/client/src/cmdhfgst.c @@ -0,0 +1,2448 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +// High frequency Google Smart Tap commands +//----------------------------------------------------------------------------- + +#include "cmdhfgst.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_ZLIB +#include +#endif +#include "cliparser.h" +#include "cmdparser.h" +#include "cmdtrace.h" +#include "commonutil.h" +#include "comms.h" +#include "crypto/libpcrypto.h" +#include "emv/tlv.h" +#include "iso7816/apduinfo.h" +#include "iso7816/iso7816core.h" +#include "protocols.h" +#include "ui.h" +#include "util.h" + +#define GST_MAX_BUFFER 4096 +#define GST_MAX_NDEF_BUFFER 2048 +#define GST_MAX_NONCE_LEN 64 +#define GST_MAX_SIGNATURE_LEN 96 +#define GST_MAX_DECOMPRESSED_PAYLOAD (256 * 1024) +#define GST_MAX_PRIVATE_KEY_ARG 8192 +#define GST_MAX_WALLET_TYPE_LEN 64 +#define GST_MAX_FEATURES_LEN 16 +#define GST_MAX_GET_DATA_CHAINED_RETRIES 10 + +static const uint8_t GST_OSE_AID[] = {'O', 'S', 'E', '.', 'V', 'A', 'S', '.', '0', '1'}; +static const uint8_t GST_SMART_TAP_V2_AID[] = {0xA0, 0x00, 0x00, 0x04, 0x76, 0xD0, 0x00, 0x01, 0x11}; + +static const uint8_t GST_TYPE_HANDSET_NONCE[] = {'m', 'd', 'n'}; +static const uint8_t GST_TYPE_SESSION[] = {'s', 'e', 's'}; +static const uint8_t GST_TYPE_NEGOTIATE_REQUEST[] = {'n', 'g', 'r'}; +static const uint8_t GST_TYPE_NEGOTIATE_RESPONSE[] = {'n', 'r', 's'}; +static const uint8_t GST_TYPE_CRYPTO_PARAMS[] = {'c', 'p', 'r'}; +static const uint8_t GST_TYPE_SIGNATURE[] = {'s', 'i', 'g'}; +static const uint8_t GST_TYPE_SERVICE_REQUEST[] = {'s', 'r', 'q'}; +static const uint8_t GST_TYPE_SERVICE_RESPONSE[] = {'s', 'r', 's'}; +static const uint8_t GST_TYPE_MERCHANT[] = {'m', 'e', 'r'}; +static const uint8_t GST_TYPE_COLLECTOR_ID[] = {'c', 'l', 'd'}; +static const uint8_t GST_TYPE_SERVICE_LIST[] = {'s', 'l', 'r'}; +static const uint8_t GST_TYPE_SERVICE_TYPE_REQUEST[] = {'s', 't', 'r'}; +static const uint8_t GST_TYPE_POS_CAPABILITIES[] = {'p', 'c', 'r'}; +static const uint8_t GST_TYPE_HANDSET_EPH_PUBKEY[] = {'d', 'p', 'k'}; +static const uint8_t GST_TYPE_RECORD_BUNDLE[] = {'r', 'e', 'b'}; +static const uint8_t GST_TYPE_SERVICE_VALUE[] = {'a', 's', 'v'}; +static const uint8_t GST_TYPE_ISSUER[] = {'i'}; +static const uint8_t GST_TYPE_CUSTOMER[] = {'c', 'u', 's'}; +static const uint8_t GST_TYPE_CUSTOMER_ID[] = {'c', 'i', 'd'}; +static const uint8_t GST_TYPE_CUSTOMER_LANGUAGE[] = {'c', 'p', 'l'}; +static const uint8_t GST_TYPE_OBJECT_ID[] = {'o', 'i', 'd'}; +static const uint8_t GST_TYPE_SERVICE_NUMBER[] = {'n'}; +static const uint8_t GST_OBJECT_TYPE_EVENT[] = {'e', 't'}; +static const uint8_t GST_OBJECT_TYPE_FLIGHT[] = {'f', 'l'}; +static const uint8_t GST_OBJECT_TYPE_GIFT_CARD[] = {'g', 'c'}; +static const uint8_t GST_OBJECT_TYPE_LOYALTY[] = {'l', 'y'}; +static const uint8_t GST_OBJECT_TYPE_OFFER[] = {'o', 'f'}; +static const uint8_t GST_OBJECT_TYPE_PRIVATE_LABEL_CARD[] = {'p', 'l'}; +static const uint8_t GST_OBJECT_TYPE_TRANSIT[] = {'t', 'r'}; +static const uint8_t GST_OBJECT_TYPE_GENERIC[] = {'g', 'r'}; +static const uint8_t GST_OBJECT_TYPE_GENERIC_PRIVATE[] = {'g', 'p'}; +static const uint8_t GST_RNG_PERSONALIZATION[] = "pm3-gst"; + +typedef enum { + GST_MODE_PASS_ONLY = 0x01, + GST_MODE_PAYMENT_ONLY = 0x02, + GST_MODE_PASS_AND_PAYMENT = 0x04, + GST_MODE_PASS_OVER_PAYMENT = 0x08, +} gst_mode_t; + +typedef enum { + GST_SYSTEM_ZLIB_SUPPORTED = 0x40, +} gst_system_flag_t; + +typedef enum { + GST_SELECT_AUTO = 0, + GST_SELECT_YES = 1, + GST_SELECT_NO = 2, +} gst_select_behavior_t; + +typedef struct { + bool have_wallet_type; + uint8_t wallet_type[GST_MAX_WALLET_TYPE_LEN]; + size_t wallet_type_len; + bool have_features; + uint8_t features[GST_MAX_FEATURES_LEN]; + size_t features_len; + bool have_capabilities; + uint8_t capabilities; + bool have_ose_device_nonce; + uint8_t ose_device_nonce[GST_MAX_NONCE_LEN]; + size_t ose_device_nonce_len; + bool have_min_version; + uint8_t min_version[2]; + bool have_max_version; + uint8_t max_version[2]; + bool have_directory_smart_tap_entry; + bool have_directory_device_nonce; + uint8_t directory_device_nonce[GST_MAX_NONCE_LEN]; + size_t directory_device_nonce_len; +} gst_ose_info_t; + +typedef struct { + bool have_min_version; + uint8_t min_version[2]; + bool have_max_version; + uint8_t max_version[2]; + bool have_handset_nonce; + uint8_t handset_nonce[GST_MAX_NONCE_LEN]; + size_t handset_nonce_len; +} gst_select_info_t; + +typedef struct { + uint8_t tnf; + const uint8_t *type; + size_t type_len; + const uint8_t *id; + size_t id_len; + const uint8_t *payload; + size_t payload_len; +} gst_ndef_record_t; + +typedef struct { + bool mb; + bool me; + uint8_t tnf; + const uint8_t *type; + size_t type_len; + const uint8_t *id; + size_t id_len; + const uint8_t *payload; + size_t payload_len; +} gst_ndef_record_view_t; + +typedef struct { + uint32_t collector_id; + uint32_t key_version; + uint8_t reader_private_key[32]; + bool have_session_id; + uint8_t session_id[8]; + bool have_reader_nonce; + uint8_t reader_nonce[32]; + bool have_reader_ephemeral_private_key; + uint8_t reader_ephemeral_private_key[32]; + gst_mode_t mode; + gst_select_behavior_t select_behavior; + bool live_authentication; + bool apdu_logging; +} gst_read_config_t; + +typedef struct { + const uint8_t *type; + size_t type_len; + const char *name; +} gst_object_type_t; + +static const gst_object_type_t gst_object_types[] = { + {GST_OBJECT_TYPE_EVENT, sizeof(GST_OBJECT_TYPE_EVENT), "event"}, + {GST_OBJECT_TYPE_FLIGHT, sizeof(GST_OBJECT_TYPE_FLIGHT), "flight"}, + {GST_OBJECT_TYPE_GIFT_CARD, sizeof(GST_OBJECT_TYPE_GIFT_CARD), "gift_card"}, + {GST_OBJECT_TYPE_LOYALTY, sizeof(GST_OBJECT_TYPE_LOYALTY), "loyalty"}, + {GST_OBJECT_TYPE_OFFER, sizeof(GST_OBJECT_TYPE_OFFER), "offer"}, + {GST_OBJECT_TYPE_PRIVATE_LABEL_CARD, sizeof(GST_OBJECT_TYPE_PRIVATE_LABEL_CARD), "private_label_card"}, + {GST_OBJECT_TYPE_TRANSIT, sizeof(GST_OBJECT_TYPE_TRANSIT), "transit"}, + {GST_OBJECT_TYPE_GENERIC, sizeof(GST_OBJECT_TYPE_GENERIC), "generic"}, + {GST_OBJECT_TYPE_GENERIC_PRIVATE, sizeof(GST_OBJECT_TYPE_GENERIC_PRIVATE), "generic_private"}, +}; + +static int CmdHelp(const char *Cmd); + +static bool gst_wallet_type_is_android_pay(const uint8_t *wallet_type, size_t wallet_type_len) { + static const uint8_t android_pay_wallet_type[] = "AndroidPay"; + return wallet_type != NULL + && wallet_type_len == (sizeof(android_pay_wallet_type) - 1) + && memcmp(wallet_type, android_pay_wallet_type, sizeof(android_pay_wallet_type) - 1) == 0; +} + +static uint32_t gst_version_value(const uint8_t version[2]) { + return ((uint32_t)version[0] << 8) | version[1]; +} + +static const char *gst_issuer_type_name(uint8_t issuer_type) { + switch (issuer_type) { + case 0x00: + return "unspecified"; + case 0x01: + return "merchant"; + case 0x02: + return "wallet"; + case 0x03: + return "manufacturer"; + default: + return "unknown"; + } +} + +static const char *gst_status_name(uint16_t sw) { + switch (sw) { + case 0x9000: + return "OK"; + case 0x9001: + return "OK_NO_PAYLOAD"; + case 0x9002: + return "OK_PRE_SIGNED_AUTH"; + case 0x9100: + return "OK_MORE_PAYLOAD"; + case 0x9201: + return "CRYPTO_FAILURE"; + case 0x9203: + return "EXECUTION_FAILURE"; + case 0x9300: + return "DEVICE_LOCKED"; + case 0x9302: + return "DISAMBIGUATION_SCREEN_SHOWN"; + case 0x9400: + return "UNKNOWN_TERMINAL_COMMAND"; + case 0x9402: + return "PARSING_FAILURE"; + case 0x9403: + return "INVALID_CRYPTO_INPUT"; + case 0x9404: + return "REQUEST_MORE_NOT_APPLICABLE"; + case 0x9405: + return "MORE_DATA_NOT_AVAILABLE"; + case 0x9406: + return "TOO_MANY_REQUESTS"; + case 0x9407: + return "NO_MERCHANT_SET"; + case 0x9408: + return "INVALID_PUSHBACK_URI"; + case 0x9500: + return "AUTH_FAILED"; + case 0x9502: + return "VERSION_NOT_SUPPORTED"; + default: + break; + } + if ((sw >> 8) == 0x92) { + return "UNKNOWN_TRANSIENT_FAILURE"; + } + if ((sw >> 8) == 0x93) { + return "UNKNOWN_USER_ACTION_NEEDED"; + } + if ((sw >> 8) == 0x94) { + return "UNKNOWN_TERMINAL_ERROR"; + } + if ((sw >> 8) == 0x95) { + return "UNKNOWN_PERMANENT_ERROR"; + } + return "UNKNOWN"; +} + +static bool gst_status_is_success(uint16_t sw) { + uint8_t sw1 = (uint8_t)(sw >> 8); + return (sw1 == 0x90) || (sw1 == 0x91); +} + +static bool gst_status_is_transient_failure(uint16_t sw) { + return ((sw >> 8) == 0x92); +} + +static void gst_print_status_line(const char *label, uint16_t sw) { + PrintAndLogEx(INFO, "%s " _YELLOW_("%04X") " (%s)", label, sw, gst_status_name(sw)); +} + +static int gst_ndef_encode_message(const gst_ndef_record_t *records, size_t record_count, + uint8_t *out, size_t out_size, size_t *out_len) { + if (records == NULL || out == NULL || out_len == NULL || record_count == 0) { + return PM3_EINVARG; + } + + size_t offset = 0; + for (size_t i = 0; i < record_count; i++) { + const gst_ndef_record_t *rec = &records[i]; + if (rec->type == NULL || rec->type_len == 0 || rec->type_len > 255 || rec->id_len > 255) { + return PM3_EINVARG; + } + + bool short_record = (rec->payload_len <= 255); + uint8_t header = (uint8_t)(rec->tnf & 0x07); + if (i == 0) { + header |= 0x80; + } + if (i == (record_count - 1)) { + header |= 0x40; + } + if (short_record) { + header |= 0x10; + } + if (rec->id_len > 0) { + header |= 0x08; + } + + if (buffer_append_bytes_with_offset(out, out_size, &offset, &header, 1) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + uint8_t type_len_u8 = (uint8_t)rec->type_len; + if (buffer_append_bytes_with_offset(out, out_size, &offset, &type_len_u8, 1) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + if (short_record) { + uint8_t payload_len_u8 = (uint8_t)rec->payload_len; + if (buffer_append_bytes_with_offset(out, out_size, &offset, &payload_len_u8, 1) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + } else { + uint8_t payload_len_u32[4]; + payload_len_u32[0] = (uint8_t)(rec->payload_len >> 24); + payload_len_u32[1] = (uint8_t)(rec->payload_len >> 16); + payload_len_u32[2] = (uint8_t)(rec->payload_len >> 8); + payload_len_u32[3] = (uint8_t)(rec->payload_len); + if (buffer_append_bytes_with_offset(out, out_size, &offset, payload_len_u32, sizeof(payload_len_u32)) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + } + + if (rec->id_len > 0) { + uint8_t id_len_u8 = (uint8_t)rec->id_len; + if (buffer_append_bytes_with_offset(out, out_size, &offset, &id_len_u8, 1) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + } + + if (buffer_append_bytes_with_offset(out, out_size, &offset, rec->type, rec->type_len) != PM3_SUCCESS || + buffer_append_bytes_with_offset(out, out_size, &offset, rec->id, rec->id_len) != PM3_SUCCESS || + buffer_append_bytes_with_offset(out, out_size, &offset, rec->payload, rec->payload_len) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + } + + *out_len = offset; + return PM3_SUCCESS; +} + +static int gst_ndef_parse_record(const uint8_t *message, size_t message_len, size_t *offset, + gst_ndef_record_view_t *out) { + if (message == NULL || offset == NULL || out == NULL || *offset >= message_len) { + return PM3_EINVARG; + } + + size_t pos = *offset; + uint8_t header = message[pos++]; + bool short_record = (header & 0x10) != 0; + bool id_len_present = (header & 0x08) != 0; + bool chunked = (header & 0x20) != 0; + if (chunked) { + return PM3_ENOTIMPL; + } + + if (pos >= message_len) { + return PM3_ESOFT; + } + size_t type_len = message[pos++]; + + size_t payload_len = 0; + if (short_record) { + if (pos >= message_len) { + return PM3_ESOFT; + } + payload_len = message[pos++]; + } else { + if ((pos + 4) > message_len) { + return PM3_ESOFT; + } + payload_len = ((size_t)message[pos] << 24) | + ((size_t)message[pos + 1] << 16) | + ((size_t)message[pos + 2] << 8) | + ((size_t)message[pos + 3]); + pos += 4; + } + + size_t id_len = 0; + if (id_len_present) { + if (pos >= message_len) { + return PM3_ESOFT; + } + id_len = message[pos++]; + } + + size_t required = type_len + id_len + payload_len; + if ((pos + required) > message_len) { + return PM3_ESOFT; + } + + out->mb = (header & 0x80) != 0; + out->me = (header & 0x40) != 0; + out->tnf = (uint8_t)(header & 0x07); + out->type = message + pos; + out->type_len = type_len; + pos += type_len; + out->id = message + pos; + out->id_len = id_len; + pos += id_len; + out->payload = message + pos; + out->payload_len = payload_len; + pos += payload_len; + + *offset = pos; + return PM3_SUCCESS; +} + +static bool gst_ndef_record_matches(const gst_ndef_record_view_t *record, const uint8_t *needle, size_t needle_len) { + if (record == NULL || needle == NULL || needle_len == 0) { + return false; + } + if (bytes_equal_not_null(record->type, record->type_len, needle, needle_len)) { + return true; + } + return bytes_equal_not_null(record->id, record->id_len, needle, needle_len); +} + +static int gst_ndef_find_by_type_or_id(const uint8_t *message, size_t message_len, + const uint8_t *needle, size_t needle_len, + gst_ndef_record_view_t *out_record) { + if (message == NULL || needle == NULL || out_record == NULL) { + return PM3_EINVARG; + } + + size_t offset = 0; + while (offset < message_len) { + gst_ndef_record_view_t record; + int res = gst_ndef_parse_record(message, message_len, &offset, &record); + if (res != PM3_SUCCESS) { + return res; + } + if (gst_ndef_record_matches(&record, needle, needle_len)) { + *out_record = record; + return PM3_SUCCESS; + } + if (record.me) { + break; + } + } + + return PM3_ENODATA; +} + +static int gst_exchange_chained(bool activate_field, bool leave_field_on, + uint8_t cla, uint8_t ins, uint8_t p1, uint8_t p2, + const uint8_t *data, size_t data_len, + uint8_t *result, size_t max_result_len, + size_t *result_len, uint16_t *sw) { + if (result == NULL || result_len == NULL || sw == NULL) { + return PM3_EINVARG; + } + if (data_len > 255) { + PrintAndLogEx(ERR, "APDU payload too long: %zu (max 255)", data_len); + return PM3_EINVARG; + } + + *result_len = 0; + *sw = 0; + + uint8_t chunk[APDU_RES_LEN] = {0}; + size_t chunk_len = 0; + sAPDU_t apdu = {cla, ins, p1, p2, (uint8_t)data_len, (uint8_t *)data}; + int res = Iso7816ExchangeEx(CC_CONTACTLESS, activate_field, leave_field_on, + apdu, true, 0, chunk, sizeof(chunk), &chunk_len, sw); + if (res != PM3_SUCCESS) { + return res; + } + + if ((chunk_len + *result_len) > max_result_len) { + return PM3_EOVFLOW; + } + memcpy(result + *result_len, chunk, chunk_len); + *result_len += chunk_len; + + while (((*sw >> 8) & 0xFF) == 0x61) { + uint16_t le = (*sw & 0xFF); + if (le == 0) { + le = 0x100; + } + + chunk_len = 0; + sAPDU_t get_response = {cla, ISO7816_GET_RESPONSE, 0x00, 0x00, 0x00, NULL}; + res = Iso7816ExchangeEx(CC_CONTACTLESS, false, leave_field_on, get_response, true, le, + chunk, sizeof(chunk), &chunk_len, sw); + if (res != PM3_SUCCESS) { + return res; + } + if ((chunk_len + *result_len) > max_result_len) { + return PM3_EOVFLOW; + } + memcpy(result + *result_len, chunk, chunk_len); + *result_len += chunk_len; + } + + return PM3_SUCCESS; +} + +static int gst_parse_ose_response(const uint8_t *buf, size_t len, gst_ose_info_t *info) { + if (buf == NULL || info == NULL || len == 0) { + return PM3_EINVARG; + } + + memset(info, 0, sizeof(*info)); + + const uint8_t *cursor = buf; + size_t left = len; + + struct tlv top = {0}; + while (left > 0) { + if (tlv_parse_tl(&cursor, &left, &top) == false || top.len > left) { + return PM3_ESOFT; + } + + const uint8_t *value = cursor; + size_t value_len = top.len; + cursor += top.len; + left -= top.len; + + if (top.tag != 0x6F) { + continue; + } + + const uint8_t *fci_cursor = value; + size_t fci_left = value_len; + struct tlv fci_tlv = {0}; + while (fci_left > 0) { + if (tlv_parse_tl(&fci_cursor, &fci_left, &fci_tlv) == false || fci_tlv.len > fci_left) { + return PM3_ESOFT; + } + const uint8_t *fci_value = fci_cursor; + size_t fci_value_len = fci_tlv.len; + fci_cursor += fci_tlv.len; + fci_left -= fci_tlv.len; + + if (fci_tlv.tag == 0x50 && fci_value_len > 0) { + size_t wallet_type_len = MIN(fci_value_len, sizeof(info->wallet_type)); + memcpy(info->wallet_type, fci_value, wallet_type_len); + info->wallet_type_len = wallet_type_len; + info->have_wallet_type = true; + } else if (fci_tlv.tag == 0xC1 && fci_value_len > 0) { + size_t features_len = MIN(fci_value_len, sizeof(info->features)); + memcpy(info->features, fci_value, features_len); + info->features_len = features_len; + info->have_features = true; + } else if (fci_tlv.tag == 0xC2 && fci_value_len > 0) { + size_t nonce_len = MIN(fci_value_len, sizeof(info->ose_device_nonce)); + memcpy(info->ose_device_nonce, fci_value, nonce_len); + info->ose_device_nonce_len = nonce_len; + info->have_ose_device_nonce = true; + } else if (fci_tlv.tag == 0xA5) { + const uint8_t *a5_cursor = fci_value; + size_t a5_left = fci_value_len; + struct tlv a5_tlv = {0}; + while (a5_left > 0) { + if (tlv_parse_tl(&a5_cursor, &a5_left, &a5_tlv) == false || a5_tlv.len > a5_left) { + return PM3_ESOFT; + } + const uint8_t *a5_value = a5_cursor; + size_t a5_value_len = a5_tlv.len; + a5_cursor += a5_tlv.len; + a5_left -= a5_tlv.len; + + if (a5_tlv.tag != 0xBF0C) { + continue; + } + + const uint8_t *dir_cursor = a5_value; + size_t dir_left = a5_value_len; + struct tlv dir_tlv = {0}; + while (dir_left > 0) { + if (tlv_parse_tl(&dir_cursor, &dir_left, &dir_tlv) == false || dir_tlv.len > dir_left) { + return PM3_ESOFT; + } + const uint8_t *dir_value = dir_cursor; + size_t dir_value_len = dir_tlv.len; + dir_cursor += dir_tlv.len; + dir_left -= dir_tlv.len; + + if (dir_tlv.tag != 0x61) { + continue; + } + + const uint8_t *entry_cursor = dir_value; + size_t entry_left = dir_value_len; + struct tlv entry_tlv = {0}; + const uint8_t *entry_aid = NULL; + size_t entry_aid_len = 0; + const uint8_t *entry_discretionary = NULL; + size_t entry_discretionary_len = 0; + + while (entry_left > 0) { + if (tlv_parse_tl(&entry_cursor, &entry_left, &entry_tlv) == false || entry_tlv.len > entry_left) { + return PM3_ESOFT; + } + const uint8_t *entry_value = entry_cursor; + size_t entry_value_len = entry_tlv.len; + entry_cursor += entry_tlv.len; + entry_left -= entry_tlv.len; + + if (entry_tlv.tag == 0x4F) { + entry_aid = entry_value; + entry_aid_len = entry_value_len; + } else if (entry_tlv.tag == 0x73) { + entry_discretionary = entry_value; + entry_discretionary_len = entry_value_len; + } + } + + if (entry_aid == NULL || entry_discretionary == NULL) { + continue; + } + if (!bytes_equal_not_null(entry_aid, entry_aid_len, GST_SMART_TAP_V2_AID, sizeof(GST_SMART_TAP_V2_AID))) { + continue; + } + + info->have_directory_smart_tap_entry = true; + + const uint8_t *disc_cursor = entry_discretionary; + size_t disc_left = entry_discretionary_len; + struct tlv disc_tlv = {0}; + while (disc_left > 0) { + if (tlv_parse_tl(&disc_cursor, &disc_left, &disc_tlv) == false || disc_tlv.len > disc_left) { + return PM3_ESOFT; + } + const uint8_t *disc_value = disc_cursor; + size_t disc_value_len = disc_tlv.len; + disc_cursor += disc_tlv.len; + disc_left -= disc_tlv.len; + + if (disc_tlv.tag == 0xDF6D && disc_value_len >= 2) { + info->have_min_version = true; + info->min_version[0] = disc_value[0]; + info->min_version[1] = disc_value[1]; + } else if (disc_tlv.tag == 0xDF4D && disc_value_len >= 2) { + info->have_max_version = true; + info->max_version[0] = disc_value[0]; + info->max_version[1] = disc_value[1]; + } else if (disc_tlv.tag == 0xDF6E && disc_value_len > 0) { + size_t nonce_len = MIN(disc_value_len, sizeof(info->directory_device_nonce)); + memcpy(info->directory_device_nonce, disc_value, nonce_len); + info->directory_device_nonce_len = nonce_len; + info->have_directory_device_nonce = true; + } else if (disc_tlv.tag == 0xDF62 && disc_value_len >= 1) { + info->have_capabilities = true; + info->capabilities = disc_value[0]; + } + } + break; + } + } + } + } + break; + } + + return PM3_SUCCESS; +} + +static int gst_parse_select_smart_tap_response(const uint8_t *buf, size_t len, gst_select_info_t *info) { + if (buf == NULL || info == NULL || len < 4) { + return PM3_EINVARG; + } + + memset(info, 0, sizeof(*info)); + info->have_min_version = true; + info->min_version[0] = buf[0]; + info->min_version[1] = buf[1]; + info->have_max_version = true; + info->max_version[0] = buf[2]; + info->max_version[1] = buf[3]; + + if (len == 4) { + return PM3_SUCCESS; + } + + const uint8_t *ndef = buf + 4; + size_t ndef_len = len - 4; + gst_ndef_record_view_t handset_nonce_record; + int res = gst_ndef_find_by_type_or_id(ndef, ndef_len, + GST_TYPE_HANDSET_NONCE, sizeof(GST_TYPE_HANDSET_NONCE), + &handset_nonce_record); + if (res != PM3_SUCCESS) { + return res; + } + + if (handset_nonce_record.payload_len < 1) { + return PM3_ESOFT; + } + + size_t nonce_len = handset_nonce_record.payload_len - 1; + nonce_len = MIN(nonce_len, sizeof(info->handset_nonce)); + memcpy(info->handset_nonce, handset_nonce_record.payload + 1, nonce_len); + info->handset_nonce_len = nonce_len; + info->have_handset_nonce = true; + return PM3_SUCCESS; +} + +static void gst_print_version_line(const char *label, bool have, const uint8_t version[2]) { + if (have) { + PrintAndLogEx(INFO, "%s " _YELLOW_("%" PRIu32 " (%u.%u)"), + label, + gst_version_value(version), + version[1], version[0]); + } else { + PrintAndLogEx(INFO, "%s not present", label); + } +} + +static void gst_print_feature_bit(const char *bits, uint8_t mask, uint8_t bit, + const char *enabled, const char *disabled) { + const bool is_enabled = (mask & (1U << bit)) != 0; + const int pad = 7 - bit; + PrintAndLogEx(INFO, " %s", + sprint_breakdown_bin(is_enabled ? C_GREEN : C_NONE, bits, 8, pad, 1, + is_enabled ? enabled : disabled)); +} + +static void gst_print_feature_bit_nested(const char *bits, uint8_t mask, uint8_t bit, + const char *enabled, const char *disabled) { + const bool is_enabled = (mask & (1U << bit)) != 0; + const int pad = 7 - bit; + PrintAndLogEx(INFO, " %s", + sprint_breakdown_bin(is_enabled ? C_GREEN : C_NONE, bits, 8, pad, 1, + is_enabled ? enabled : disabled)); +} + +static void gst_print_ose_transaction_mode(uint8_t mask) { + const char *bits = sprint_bin(&mask, 1); + PrintAndLogEx(INFO, " Transaction mode........ " _YELLOW_("%s") " (" _YELLOW_("0x%02X") ")", bits, mask); + gst_print_feature_bit_nested(bits, mask, 7, "Payment enabled", "Payment enabled not set"); + gst_print_feature_bit_nested(bits, mask, 6, "Payment requested", "Payment requested not set"); + gst_print_feature_bit_nested(bits, mask, 5, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + gst_print_feature_bit_nested(bits, mask, 4, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + gst_print_feature_bit_nested(bits, mask, 3, "Pass enabled", "Pass enabled not set"); + gst_print_feature_bit_nested(bits, mask, 2, "Pass requested", "Pass requested not set"); + gst_print_feature_bit_nested(bits, mask, 1, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + gst_print_feature_bit_nested(bits, mask, 0, "Reserved/unknown bit set", "Reserved/unknown bit clear"); +} + +static void gst_print_ose_capabilities(uint8_t mask) { + const char *bits = sprint_bin(&mask, 1); + PrintAndLogEx(INFO, "Capabilities (DF62)....... " _YELLOW_("%s") " (" _YELLOW_("0x%02X") ")", bits, mask); + gst_print_feature_bit(bits, mask, 1, "VAS support advertised", "VAS support not advertised"); + gst_print_feature_bit(bits, mask, 0, "Skipping second select allowed", "Skipping second select not allowed"); +} + +static void gst_print_bundle_flags(uint8_t mask) { + const char *bits = sprint_bin(&mask, 1); + PrintAndLogEx(INFO, "Bundle flags.............. " _YELLOW_("%s") " (" _YELLOW_("0x%02X") ")", bits, mask); + gst_print_feature_bit(bits, mask, 1, "Payload compressed", "Payload not compressed"); + gst_print_feature_bit(bits, mask, 0, "Payload encrypted", "Payload not encrypted"); + + const uint8_t reserved = (uint8_t)(mask & 0xFC); + if (reserved != 0) { + PrintAndLogEx(WARNING, "Reserved bundle bits....... " _YELLOW_("0x%02X"), reserved); + } +} + +static void gst_print_ose_info(const gst_ose_info_t *info) { + if (info == NULL) { + return; + } + + PrintAndLogInfoHeader("OSE Information (6F)"); + bool skip_gst_details = false; + if (info->have_wallet_type) { + PrintAndLogEx(INFO, "Wallet type............... " _YELLOW_("%s"), + sprint_ascii(info->wallet_type, info->wallet_type_len)); + if (gst_wallet_type_is_android_pay(info->wallet_type, info->wallet_type_len) == false) { + PrintAndLogEx(WARNING, "Wallet type is not AndroidPay. This likely isn't Google Smart Tap."); + skip_gst_details = true; + } + } else { + PrintAndLogEx(WARNING, "Wallet type............... " _YELLOW_("not present")); + } + if (skip_gst_details) { + return; + } + + if (info->have_features) { + PrintAndLogEx(INFO, "Features (C1)............. " _YELLOW_("%s"), + sprint_hex_inrow(info->features, info->features_len)); + gst_print_ose_transaction_mode(info->features[0]); + } else { + PrintAndLogEx(INFO, "Features (C1)............. not present"); + } + if (info->have_capabilities) { + gst_print_ose_capabilities(info->capabilities); + } else { + PrintAndLogEx(INFO, "Capabilities.............. not present"); + } + gst_print_version_line("Minimum version...........", info->have_min_version, info->min_version); + gst_print_version_line("Maximum version...........", info->have_max_version, info->max_version); + + if (info->have_ose_device_nonce) { + PrintAndLogEx(INFO, "Device nonce (C2)......... " _YELLOW_("%s"), + sprint_hex_inrow(info->ose_device_nonce, info->ose_device_nonce_len)); + } else { + PrintAndLogEx(INFO, "Device nonce (C2)......... not present"); + } + + if (info->have_directory_device_nonce) { + PrintAndLogEx(INFO, "Device nonce (DF6E)....... " _YELLOW_("%s"), + sprint_hex_inrow(info->directory_device_nonce, info->directory_device_nonce_len)); + } else { + PrintAndLogEx(INFO, "Device nonce (DF6E)....... not present"); + } +} + +static void gst_print_select_info(const gst_select_info_t *info) { + if (info == NULL) { + return; + } + PrintAndLogInfoHeader("Smart Tap applet information"); + gst_print_version_line("Minimum version...........", info->have_min_version, info->min_version); + gst_print_version_line("Maximum version...........", info->have_max_version, info->max_version); + if (info->have_handset_nonce) { + PrintAndLogEx(INFO, "Handset nonce............. " _YELLOW_("%s"), + sprint_hex_inrow(info->handset_nonce, info->handset_nonce_len)); + } else { + PrintAndLogEx(INFO, "Handset nonce............. not present"); + } +} + +static bool gst_should_select_smart_tap(gst_select_behavior_t behavior, const gst_ose_info_t *ose_info) { + if (behavior == GST_SELECT_YES) { + return true; + } + if (behavior == GST_SELECT_NO) { + return false; + } + + if (ose_info != NULL && ose_info->have_wallet_type && + gst_wallet_type_is_android_pay(ose_info->wallet_type, ose_info->wallet_type_len) == false) { + return false; + } + + bool allow_skip = (ose_info != NULL && ose_info->have_capabilities && (ose_info->capabilities & 0x01) != 0); + bool have_device_nonce = false; + if (ose_info != NULL) { + have_device_nonce = ose_info->have_directory_device_nonce || ose_info->have_ose_device_nonce; + } + return (!allow_skip || !have_device_nonce); +} + +static int gst_select_ose(gst_ose_info_t *info, bool keep_field_on) { + uint8_t response[GST_MAX_BUFFER] = {0}; + size_t response_len = 0; + uint16_t sw = 0; + + int res = gst_exchange_chained(true, keep_field_on, 0x00, ISO7816_SELECT_FILE, 0x04, 0x00, + GST_OSE_AID, sizeof(GST_OSE_AID), + response, sizeof(response), &response_len, &sw); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "SELECT OSE exchange error"); + return res; + } + if (sw != ISO7816_OK) { + if (sw) { + PrintAndLogEx(INFO, "OSE.VAS.01 not found. APDU response: %04x - %s", + sw, GetAPDUCodeDescription(sw >> 8, sw & 0xff)); + } else { + PrintAndLogEx(ERR, "APDU exchange error. Card returns 0x0000"); + } + return PM3_ESOFT; + } + + return gst_parse_ose_response(response, response_len, info); +} + +static int gst_select_smart_tap_v2(gst_select_info_t *info, bool keep_field_on) { + uint8_t response[GST_MAX_BUFFER] = {0}; + size_t response_len = 0; + uint16_t sw = 0; + + int res = gst_exchange_chained(false, keep_field_on, 0x00, ISO7816_SELECT_FILE, 0x04, 0x00, + GST_SMART_TAP_V2_AID, sizeof(GST_SMART_TAP_V2_AID), + response, sizeof(response), &response_len, &sw); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "SELECT Smart Tap v2 exchange error"); + return res; + } + if (sw != ISO7816_OK) { + if (sw) { + PrintAndLogEx(INFO, "Smart Tap v2 applet not found. APDU response: %04x - %s", + sw, GetAPDUCodeDescription(sw >> 8, sw & 0xff)); + } else { + PrintAndLogEx(ERR, "APDU exchange error. Card returns 0x0000"); + } + return PM3_ESOFT; + } + + return gst_parse_select_smart_tap_response(response, response_len, info); +} + +static int gst_load_private_key_raw(const uint8_t private_key_bytes[32], mbedtls_ecp_keypair *keypair, pcrypto_rng_t *rng) { + if (private_key_bytes == NULL || keypair == NULL || rng == NULL || rng->seeded == false) { + return PM3_EINVARG; + } + + mbedtls_ecp_keypair_init(keypair); + int ret = mbedtls_ecp_group_load(&keypair->grp, MBEDTLS_ECP_DP_SECP256R1); + if (ret != 0) { + return PM3_ESOFT; + } + ret = mbedtls_mpi_read_binary(&keypair->d, private_key_bytes, 32); + if (ret != 0) { + return PM3_ESOFT; + } + ret = mbedtls_ecp_check_privkey(&keypair->grp, &keypair->d); + if (ret != 0) { + return PM3_ESOFT; + } + + ret = mbedtls_ecp_mul(&keypair->grp, &keypair->Q, &keypair->d, &keypair->grp.G, + mbedtls_ctr_drbg_random, &rng->ctr_drbg); + if (ret != 0) { + return PM3_ESOFT; + } + ret = mbedtls_ecp_check_pubkey(&keypair->grp, &keypair->Q); + if (ret != 0) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int gst_generate_ephemeral_keypair(mbedtls_ecp_keypair *keypair, pcrypto_rng_t *rng) { + if (keypair == NULL || rng == NULL || rng->seeded == false) { + return PM3_EINVARG; + } + mbedtls_ecp_keypair_init(keypair); + return (mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1, keypair, mbedtls_ctr_drbg_random, &rng->ctr_drbg) == 0) + ? PM3_SUCCESS : PM3_ESOFT; +} + +static int gst_export_pubkey_uncompressed(const mbedtls_ecp_keypair *keypair, uint8_t out[65]) { + if (keypair == NULL || out == NULL) { + return PM3_EINVARG; + } + size_t written = 0; + int ret = mbedtls_ecp_point_write_binary(&keypair->grp, &keypair->Q, + MBEDTLS_ECP_PF_UNCOMPRESSED, &written, out, 65); + if (ret != 0 || written != 65) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int gst_export_pubkey_compressed(const mbedtls_ecp_keypair *keypair, uint8_t out[33]) { + if (keypair == NULL || out == NULL) { + return PM3_EINVARG; + } + size_t written = 0; + int ret = mbedtls_ecp_point_write_binary(&keypair->grp, &keypair->Q, + MBEDTLS_ECP_PF_COMPRESSED, &written, out, 33); + if (ret != 0 || written != 33) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int gst_load_public_key_any(const uint8_t *pubkey, size_t pubkey_len, mbedtls_ecp_keypair *keypair) { + if (pubkey == NULL || keypair == NULL) { + return PM3_EINVARG; + } + if (pubkey_len != 33 && pubkey_len != 65) { + return PM3_EINVARG; + } + + mbedtls_ecp_keypair_init(keypair); + int ret = mbedtls_ecp_group_load(&keypair->grp, MBEDTLS_ECP_DP_SECP256R1); + if (ret != 0) { + return PM3_ESOFT; + } + + uint8_t uncompressed[65] = {0}; + const uint8_t *to_read = pubkey; + size_t to_read_len = pubkey_len; + + if (pubkey_len == 33) { + size_t uncompressed_len = 0; + if (mbedtls_ecp_decompress(&keypair->grp, pubkey, pubkey_len, uncompressed, &uncompressed_len, sizeof(uncompressed)) != 0 || + uncompressed_len != sizeof(uncompressed)) { + return PM3_ESOFT; + } + to_read = uncompressed; + to_read_len = sizeof(uncompressed); + } + + ret = mbedtls_ecp_point_read_binary(&keypair->grp, &keypair->Q, to_read, to_read_len); + if (ret != 0) { + return PM3_ESOFT; + } + ret = mbedtls_ecp_check_pubkey(&keypair->grp, &keypair->Q); + if (ret != 0) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int gst_build_signature_input(const uint8_t reader_nonce[32], + const uint8_t *device_nonce, size_t device_nonce_len, + uint32_t collector_id, + const uint8_t reader_ephemeral_public_key[33], + uint8_t *out, size_t out_size, size_t *out_len) { + if (reader_nonce == NULL || device_nonce == NULL || reader_ephemeral_public_key == NULL || + out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + + size_t offset = 0; + uint8_t collector_id_be[4]; + Uint4byteToMemBe(collector_id_be, collector_id); + + if (buffer_append_bytes_with_offset(out, out_size, &offset, reader_nonce, 32) != PM3_SUCCESS || + buffer_append_bytes_with_offset(out, out_size, &offset, device_nonce, device_nonce_len) != PM3_SUCCESS || + buffer_append_bytes_with_offset(out, out_size, &offset, collector_id_be, sizeof(collector_id_be)) != PM3_SUCCESS || + buffer_append_bytes_with_offset(out, out_size, &offset, reader_ephemeral_public_key, 33) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + *out_len = offset; + return PM3_SUCCESS; +} + +static int gst_create_reader_signature(const uint8_t reader_private_key[32], + const uint8_t reader_public_key[65], + const uint8_t *signature_input, + size_t signature_input_len, + uint8_t *signature, + size_t *signature_len) { + if (reader_private_key == NULL || reader_public_key == NULL || signature_input == NULL || + signature == NULL || signature_len == NULL) { + return PM3_EINVARG; + } + + int res = ecdsa_signature_create(MBEDTLS_ECP_DP_SECP256R1, + (uint8_t *)reader_private_key, + (uint8_t *)reader_public_key, + (uint8_t *)signature_input, + (int)signature_input_len, + signature, signature_len, true); + return (res == 0) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int gst_build_session_payload(uint64_t session_id, uint8_t sequence_number, uint8_t status, + uint8_t out[10], size_t *out_len) { + if (out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + Uint8byteToMemBe(out, session_id); + out[8] = sequence_number; + out[9] = status; + *out_len = 10; + return PM3_SUCCESS; +} + +static int gst_build_negotiate_message(const uint8_t reader_nonce[32], + const uint8_t reader_ephemeral_public_key[33], + const uint8_t *signature, size_t signature_len, + uint32_t collector_id, + uint32_t key_version, + uint64_t session_id, + uint8_t sequence_number, + bool live_authentication, + uint8_t *out, size_t out_size, size_t *out_len) { + if (reader_nonce == NULL || reader_ephemeral_public_key == NULL || signature == NULL || + out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + + uint8_t session_payload[10] = {0}; + size_t session_payload_len = 0; + int res = gst_build_session_payload(session_id, sequence_number, 0x01, session_payload, &session_payload_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t collector_id_be[4]; + Uint4byteToMemBe(collector_id_be, collector_id); + + uint8_t signature_record_payload[GST_MAX_SIGNATURE_LEN + 1] = {0}; + size_t signature_record_payload_len = 0; + signature_record_payload[0] = 0x04; + signature_record_payload_len = 1; + if (buffer_append_bytes_with_offset(signature_record_payload, sizeof(signature_record_payload), &signature_record_payload_len, signature, signature_len) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + uint8_t collector_record_payload[5] = {0x04, 0, 0, 0, 0}; + memcpy(collector_record_payload + 1, collector_id_be, 4); + + uint8_t nested_signature_msg[GST_MAX_NDEF_BUFFER] = {0}; + size_t nested_signature_msg_len = 0; + gst_ndef_record_t nested_signature_records[] = { + {0x04, GST_TYPE_SIGNATURE, sizeof(GST_TYPE_SIGNATURE), NULL, 0, signature_record_payload, signature_record_payload_len}, + {0x04, GST_TYPE_COLLECTOR_ID, sizeof(GST_TYPE_COLLECTOR_ID), NULL, 0, collector_record_payload, sizeof(collector_record_payload)}, + }; + res = gst_ndef_encode_message(nested_signature_records, ARRAYLEN(nested_signature_records), + nested_signature_msg, sizeof(nested_signature_msg), &nested_signature_msg_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t cpr_payload[GST_MAX_NDEF_BUFFER] = {0}; + size_t cpr_payload_len = 0; + uint8_t key_version_be[4]; + Uint4byteToMemBe(key_version_be, key_version); + uint8_t live_auth = live_authentication ? 0x01 : 0x00; + + if (buffer_append_bytes_with_offset(cpr_payload, sizeof(cpr_payload), &cpr_payload_len, reader_nonce, 32) != PM3_SUCCESS || + buffer_append_bytes_with_offset(cpr_payload, sizeof(cpr_payload), &cpr_payload_len, &live_auth, 1) != PM3_SUCCESS || + buffer_append_bytes_with_offset(cpr_payload, sizeof(cpr_payload), &cpr_payload_len, reader_ephemeral_public_key, 33) != PM3_SUCCESS || + buffer_append_bytes_with_offset(cpr_payload, sizeof(cpr_payload), &cpr_payload_len, key_version_be, sizeof(key_version_be)) != PM3_SUCCESS || + buffer_append_bytes_with_offset(cpr_payload, sizeof(cpr_payload), &cpr_payload_len, nested_signature_msg, nested_signature_msg_len) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + uint8_t nested_negotiate_msg[GST_MAX_NDEF_BUFFER] = {0}; + size_t nested_negotiate_msg_len = 0; + gst_ndef_record_t nested_negotiate_records[] = { + {0x04, GST_TYPE_SESSION, sizeof(GST_TYPE_SESSION), NULL, 0, session_payload, session_payload_len}, + {0x04, GST_TYPE_CRYPTO_PARAMS, sizeof(GST_TYPE_CRYPTO_PARAMS), NULL, 0, cpr_payload, cpr_payload_len}, + }; + res = gst_ndef_encode_message(nested_negotiate_records, ARRAYLEN(nested_negotiate_records), + nested_negotiate_msg, sizeof(nested_negotiate_msg), &nested_negotiate_msg_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t outer_payload[GST_MAX_NDEF_BUFFER] = {0}; + size_t outer_payload_len = 0; + const uint8_t version[2] = {0x00, 0x01}; + if (buffer_append_bytes_with_offset(outer_payload, sizeof(outer_payload), &outer_payload_len, version, sizeof(version)) != PM3_SUCCESS || + buffer_append_bytes_with_offset(outer_payload, sizeof(outer_payload), &outer_payload_len, nested_negotiate_msg, nested_negotiate_msg_len) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + gst_ndef_record_t outer_record = { + .tnf = 0x04, + .type = GST_TYPE_NEGOTIATE_REQUEST, + .type_len = sizeof(GST_TYPE_NEGOTIATE_REQUEST), + .id = NULL, + .id_len = 0, + .payload = outer_payload, + .payload_len = outer_payload_len + }; + + return gst_ndef_encode_message(&outer_record, 1, out, out_size, out_len); +} + +static int gst_build_get_data_message(uint32_t collector_id, + uint64_t session_id, + uint8_t sequence_number, + gst_mode_t mode, + uint8_t system_flags, + uint8_t ui_flags, + uint8_t checkout_flags, + uint8_t cvm_flags, + uint8_t *out, size_t out_size, size_t *out_len) { + if (out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + + uint8_t session_payload[10] = {0}; + size_t session_payload_len = 0; + int res = gst_build_session_payload(session_id, sequence_number, 0x01, session_payload, &session_payload_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t collector_id_be[4]; + Uint4byteToMemBe(collector_id_be, collector_id); + uint8_t collector_record_payload[5] = {0x04, 0, 0, 0, 0}; + memcpy(collector_record_payload + 1, collector_id_be, 4); + + uint8_t merchant_payload[GST_MAX_NDEF_BUFFER] = {0}; + size_t merchant_payload_len = 0; + gst_ndef_record_t merchant_nested[] = { + {0x04, GST_TYPE_COLLECTOR_ID, sizeof(GST_TYPE_COLLECTOR_ID), NULL, 0, collector_record_payload, sizeof(collector_record_payload)}, + }; + res = gst_ndef_encode_message(merchant_nested, ARRAYLEN(merchant_nested), + merchant_payload, sizeof(merchant_payload), &merchant_payload_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t service_type_payload[] = {0x00}; + uint8_t service_list_payload[GST_MAX_NDEF_BUFFER] = {0}; + size_t service_list_payload_len = 0; + gst_ndef_record_t service_list_nested[] = { + {0x04, GST_TYPE_SERVICE_TYPE_REQUEST, sizeof(GST_TYPE_SERVICE_TYPE_REQUEST), NULL, 0, service_type_payload, sizeof(service_type_payload)}, + }; + res = gst_ndef_encode_message(service_list_nested, ARRAYLEN(service_list_nested), + service_list_payload, sizeof(service_list_payload), &service_list_payload_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t pos_capabilities_payload[5] = { + system_flags, + ui_flags, + checkout_flags, + cvm_flags, + (uint8_t)mode + }; + + uint8_t nested_request[GST_MAX_NDEF_BUFFER] = {0}; + size_t nested_request_len = 0; + gst_ndef_record_t request_records[] = { + {0x04, GST_TYPE_SESSION, sizeof(GST_TYPE_SESSION), NULL, 0, session_payload, session_payload_len}, + {0x04, GST_TYPE_MERCHANT, sizeof(GST_TYPE_MERCHANT), NULL, 0, merchant_payload, merchant_payload_len}, + {0x04, GST_TYPE_SERVICE_LIST, sizeof(GST_TYPE_SERVICE_LIST), NULL, 0, service_list_payload, service_list_payload_len}, + {0x04, GST_TYPE_POS_CAPABILITIES, sizeof(GST_TYPE_POS_CAPABILITIES), NULL, 0, pos_capabilities_payload, sizeof(pos_capabilities_payload)}, + }; + res = gst_ndef_encode_message(request_records, ARRAYLEN(request_records), + nested_request, sizeof(nested_request), &nested_request_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t outer_payload[GST_MAX_NDEF_BUFFER] = {0}; + size_t outer_payload_len = 0; + const uint8_t version[2] = {0x00, 0x01}; + if (buffer_append_bytes_with_offset(outer_payload, sizeof(outer_payload), &outer_payload_len, version, sizeof(version)) != PM3_SUCCESS || + buffer_append_bytes_with_offset(outer_payload, sizeof(outer_payload), &outer_payload_len, nested_request, nested_request_len) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + gst_ndef_record_t outer_record = { + .tnf = 0x04, + .type = GST_TYPE_SERVICE_REQUEST, + .type_len = sizeof(GST_TYPE_SERVICE_REQUEST), + .id = NULL, + .id_len = 0, + .payload = outer_payload, + .payload_len = outer_payload_len + }; + return gst_ndef_encode_message(&outer_record, 1, out, out_size, out_len); +} + +static int gst_perform_get_data(uint32_t collector_id, + uint64_t session_id, + uint8_t sequence_number, + gst_mode_t mode, + uint8_t *out, size_t out_max_len, + size_t *out_len, uint16_t *sw) { + if (out == NULL || out_len == NULL || sw == NULL) { + return PM3_EINVARG; + } + + uint8_t command_data[GST_MAX_NDEF_BUFFER] = {0}; + size_t command_data_len = 0; + uint8_t system_flags = 0x00; +#ifdef HAVE_ZLIB + system_flags |= GST_SYSTEM_ZLIB_SUPPORTED; +#endif + + int res = gst_build_get_data_message(collector_id, session_id, sequence_number, mode, + system_flags, 0x00, 0x00, 0x00, + command_data, sizeof(command_data), &command_data_len); + if (res != PM3_SUCCESS) { + return res; + } + + size_t total = 0; + uint16_t local_sw = 0; + uint8_t response_chunk[GST_MAX_BUFFER] = {0}; + size_t response_chunk_len = 0; + + res = gst_exchange_chained(false, true, 0x90, 0x50, 0x00, 0x00, + command_data, command_data_len, + response_chunk, sizeof(response_chunk), &response_chunk_len, &local_sw); + if (res != PM3_SUCCESS) { + return res; + } + if ((total + response_chunk_len) > out_max_len) { + return PM3_EOVFLOW; + } + memcpy(out + total, response_chunk, response_chunk_len); + total += response_chunk_len; + + for (uint8_t get_data_retry_count = 0; local_sw == 0x9100; get_data_retry_count++) { + if (get_data_retry_count >= GST_MAX_GET_DATA_CHAINED_RETRIES) { + PrintAndLogEx(ERR, "GET DATA returned chained response too many times (max=%" PRIu8 ")", GST_MAX_GET_DATA_CHAINED_RETRIES); + return PM3_ETIMEOUT; + } + response_chunk_len = 0; + res = gst_exchange_chained(false, true, 0x90, 0xC0, 0x00, 0x00, + NULL, 0, + response_chunk, sizeof(response_chunk), &response_chunk_len, &local_sw); + if (res != PM3_SUCCESS) { + return res; + } + if ((total + response_chunk_len) > out_max_len) { + return PM3_EOVFLOW; + } + memcpy(out + total, response_chunk, response_chunk_len); + total += response_chunk_len; + } + + *out_len = total; + *sw = local_sw; + return PM3_SUCCESS; +} + +static int gst_decrypt_payload(const uint8_t *encrypted_payload, size_t encrypted_payload_len, + uint32_t collector_id, + const uint8_t reader_nonce[32], + const uint8_t *device_nonce, size_t device_nonce_len, + const uint8_t *signature, size_t signature_len, + const mbedtls_ecp_keypair *reader_ephemeral_key, + const uint8_t reader_ephemeral_public_key[33], + const uint8_t *device_ephemeral_public_key, size_t device_ephemeral_public_key_len, + uint8_t *out, size_t out_max_len, size_t *out_len) { + if (encrypted_payload == NULL || reader_nonce == NULL || device_nonce == NULL || signature == NULL || + reader_ephemeral_key == NULL || reader_ephemeral_public_key == NULL || + device_ephemeral_public_key == NULL || out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + if (encrypted_payload_len < 44) { + return PM3_ESOFT; + } + + const uint8_t *iv = encrypted_payload; + const uint8_t *ciphertext = encrypted_payload + 12; + size_t ciphertext_len = encrypted_payload_len - 12 - 32; + const uint8_t *hmac = encrypted_payload + encrypted_payload_len - 32; + + mbedtls_ecp_keypair device_public_key; + mbedtls_ecp_keypair_init(&device_public_key); + int res = gst_load_public_key_any(device_ephemeral_public_key, device_ephemeral_public_key_len, &device_public_key); + if (res != PM3_SUCCESS) { + mbedtls_ecp_keypair_free(&device_public_key); + return res; + } + + mbedtls_mpi shared_secret; + mbedtls_mpi_init(&shared_secret); + int mres = mbedtls_ecdh_compute_shared((mbedtls_ecp_group *)&reader_ephemeral_key->grp, + &shared_secret, + &device_public_key.Q, + (mbedtls_mpi *)&reader_ephemeral_key->d, + NULL, NULL); + mbedtls_ecp_keypair_free(&device_public_key); + if (mres != 0) { + mbedtls_mpi_free(&shared_secret); + return PM3_ESOFT; + } + + uint8_t shared_secret_bytes[32] = {0}; + mres = mbedtls_mpi_write_binary(&shared_secret, shared_secret_bytes, sizeof(shared_secret_bytes)); + mbedtls_mpi_free(&shared_secret); + if (mres != 0) { + return PM3_ESOFT; + } + + uint8_t collector_id_be[4]; + Uint4byteToMemBe(collector_id_be, collector_id); + + uint8_t kdf_info[GST_MAX_BUFFER] = {0}; + size_t kdf_info_len = 0; + if (buffer_append_bytes_with_offset(kdf_info, sizeof(kdf_info), &kdf_info_len, reader_nonce, 32) != PM3_SUCCESS || + buffer_append_bytes_with_offset(kdf_info, sizeof(kdf_info), &kdf_info_len, device_nonce, device_nonce_len) != PM3_SUCCESS || + buffer_append_bytes_with_offset(kdf_info, sizeof(kdf_info), &kdf_info_len, collector_id_be, sizeof(collector_id_be)) != PM3_SUCCESS || + buffer_append_bytes_with_offset(kdf_info, sizeof(kdf_info), &kdf_info_len, reader_ephemeral_public_key, 33) != PM3_SUCCESS || + buffer_append_bytes_with_offset(kdf_info, sizeof(kdf_info), &kdf_info_len, signature, signature_len) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + uint8_t key_material[48] = {0}; + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + if (md_info == NULL) { + return PM3_ESOFT; + } + mres = mbedtls_hkdf(md_info, + device_ephemeral_public_key, device_ephemeral_public_key_len, + shared_secret_bytes, sizeof(shared_secret_bytes), + kdf_info, kdf_info_len, + key_material, sizeof(key_material)); + if (mres != 0) { + return PM3_ESOFT; + } + + const uint8_t *aes_key = key_material; + const uint8_t *hmac_key = key_material + 16; + + uint8_t derived_hmac[32] = {0}; + mbedtls_md_context_t md_ctx; + mbedtls_md_init(&md_ctx); + mres = mbedtls_md_setup(&md_ctx, md_info, 1); + if (mres == 0) { + mres = mbedtls_md_hmac_starts(&md_ctx, hmac_key, 32); + } + if (mres == 0) { + mres = mbedtls_md_hmac_update(&md_ctx, iv, 12); + } + if (mres == 0) { + mres = mbedtls_md_hmac_update(&md_ctx, ciphertext, ciphertext_len); + } + if (mres == 0) { + mres = mbedtls_md_hmac_finish(&md_ctx, derived_hmac); + } + mbedtls_md_free(&md_ctx); + if (mres != 0) { + return PM3_ESOFT; + } + if (memcmp(derived_hmac, hmac, sizeof(derived_hmac)) != 0) { + PrintAndLogEx(ERR, "HMAC verification failed"); + return PM3_ESOFT; + } + + if (ciphertext_len > out_max_len) { + return PM3_EOVFLOW; + } + + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + mres = mbedtls_aes_setkey_enc(&aes, aes_key, 128); + if (mres != 0) { + mbedtls_aes_free(&aes); + return PM3_ESOFT; + } + + uint8_t nonce_counter[16] = {0}; + memcpy(nonce_counter, iv, 12); + uint8_t stream_block[16] = {0}; + size_t nc_off = 0; + mres = mbedtls_aes_crypt_ctr(&aes, ciphertext_len, &nc_off, nonce_counter, stream_block, ciphertext, out); + mbedtls_aes_free(&aes); + if (mres != 0) { + return PM3_ESOFT; + } + + *out_len = ciphertext_len; + return PM3_SUCCESS; +} + +#ifdef HAVE_ZLIB +static int gst_decompress_payload_zlib(const uint8_t *compressed_payload, size_t compressed_payload_len, + uint8_t **out, size_t *out_len) { + if (compressed_payload == NULL || out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + + *out = NULL; + *out_len = 0; + + if (compressed_payload_len == 0) { + return PM3_SUCCESS; + } + + size_t capacity = compressed_payload_len * 2; + if (capacity < 1024) { + capacity = 1024; + } + if (capacity > GST_MAX_DECOMPRESSED_PAYLOAD) { + capacity = GST_MAX_DECOMPRESSED_PAYLOAD; + } + + uint8_t *buffer = calloc(capacity, sizeof(uint8_t)); + if (buffer == NULL) { + return PM3_EMALLOC; + } + + z_stream stream; + memset(&stream, 0, sizeof(stream)); + stream.next_in = (Bytef *)compressed_payload; + stream.avail_in = (uInt)compressed_payload_len; + stream.next_out = buffer; + stream.avail_out = (uInt)capacity; + + int zret = inflateInit(&stream); + if (zret != Z_OK) { + free(buffer); + return PM3_ESOFT; + } + + int status = PM3_SUCCESS; + while (true) { + zret = inflate(&stream, Z_NO_FLUSH); + if (zret == Z_STREAM_END) { + break; + } + if (!(zret == Z_OK || zret == Z_BUF_ERROR)) { + status = PM3_ESOFT; + break; + } + + if (stream.avail_out > 0) { + // We have spare output space but no progress/end marker: input stream is invalid/truncated. + if (stream.avail_in == 0 || zret == Z_BUF_ERROR) { + status = PM3_ESOFT; + break; + } + continue; + } + + size_t produced = (size_t)(stream.next_out - buffer); + if (capacity >= GST_MAX_DECOMPRESSED_PAYLOAD) { + status = PM3_EOVFLOW; + break; + } + + size_t new_capacity = capacity * 2; + if (new_capacity > GST_MAX_DECOMPRESSED_PAYLOAD) { + new_capacity = GST_MAX_DECOMPRESSED_PAYLOAD; + } + + uint8_t *new_buffer = realloc(buffer, new_capacity); + if (new_buffer == NULL) { + status = PM3_EMALLOC; + break; + } + + buffer = new_buffer; + capacity = new_capacity; + stream.next_out = buffer + produced; + stream.avail_out = (uInt)(capacity - produced); + } + + inflateEnd(&stream); + + if (status != PM3_SUCCESS) { + free(buffer); + return status; + } + + *out = buffer; + *out_len = (size_t)(stream.next_out - buffer); + return PM3_SUCCESS; +} +#endif + +static int gst_selftest_zlib(void) { +#ifdef HAVE_ZLIB + // Captured from a compressed RECORD_BUNDLE payload produced by Google's demo pass. + static const uint8_t compressed_payload[] = { + 0x78, 0xDA, 0x5D, 0xD4, 0xBD, 0x4A, 0x03, 0x41, 0x18, 0x85, 0xE1, 0xDD, + 0xEC, 0x22, 0x58, 0x6A, 0x67, 0x99, 0xD4, 0x0A, 0xF9, 0x7E, 0xA2, 0xC6, + 0xDA, 0x26, 0x76, 0xE2, 0x56, 0x82, 0x4A, 0x48, 0x02, 0x46, 0x82, 0x1A, + 0x93, 0x08, 0x41, 0xEC, 0x2C, 0x2D, 0x2C, 0x82, 0x8D, 0xE0, 0x05, 0x88, + 0xB5, 0xD7, 0xA0, 0x77, 0x21, 0xDE, 0x87, 0xE0, 0xA2, 0xCC, 0xD9, 0x39, + 0x4E, 0xB5, 0xF3, 0xEE, 0x34, 0x0F, 0xF3, 0x73, 0x97, 0x25, 0x49, 0xFE, + 0xDE, 0x9D, 0x5C, 0x2F, 0xD2, 0xA5, 0x61, 0x9E, 0x26, 0xE5, 0xD8, 0xAD, + 0x95, 0xE9, 0x6D, 0x34, 0x5F, 0x64, 0xCB, 0x17, 0xC3, 0x7E, 0x5E, 0xFF, + 0xD8, 0xDB, 0x4F, 0xBF, 0x8E, 0x5F, 0x3A, 0xE5, 0xCF, 0xFC, 0x39, 0x2D, + 0xCE, 0x93, 0x9B, 0x46, 0xBF, 0x3B, 0xED, 0x6E, 0x34, 0x1B, 0x3B, 0xBF, + 0x1F, 0x27, 0xA7, 0x83, 0xAB, 0x41, 0xB3, 0xB1, 0xFE, 0x57, 0x25, 0xAE, + 0x12, 0xAA, 0xC6, 0x55, 0x43, 0xB5, 0xB8, 0x5A, 0xA8, 0x1E, 0x57, 0x0F, + 0xB5, 0x15, 0xD7, 0x56, 0xA8, 0x9B, 0x71, 0xDD, 0x0C, 0x75, 0x2B, 0xAE, + 0x5B, 0xA1, 0x6E, 0xC7, 0x75, 0x3B, 0xD4, 0x76, 0x5C, 0xDB, 0x50, 0x10, + 0x4E, 0x2A, 0x1D, 0xF3, 0xE0, 0x13, 0x02, 0x0A, 0x84, 0x42, 0x44, 0x81, + 0x51, 0x08, 0x29, 0x50, 0x0A, 0x31, 0x05, 0x4E, 0x21, 0xA8, 0x40, 0x2A, + 0x44, 0x15, 0x58, 0x85, 0xB0, 0x02, 0xAD, 0x10, 0x57, 0xE0, 0x55, 0xF2, + 0x2A, 0xBC, 0x4A, 0x5E, 0xAD, 0xF6, 0x93, 0x37, 0x14, 0x5E, 0x25, 0xAF, + 0xC2, 0xAB, 0xE4, 0x55, 0x78, 0x95, 0xBC, 0x0A, 0xAF, 0x92, 0x57, 0xE1, + 0x55, 0xF2, 0x2A, 0xBC, 0x4A, 0x5E, 0x85, 0x57, 0xC9, 0xAB, 0xF0, 0x1A, + 0x79, 0x0D, 0x5E, 0x23, 0xAF, 0xC1, 0x6B, 0xE4, 0xB5, 0xEA, 0x04, 0xF3, + 0x11, 0x86, 0xD7, 0xC8, 0x6B, 0xF0, 0x1A, 0x79, 0x0D, 0x5E, 0x23, 0xAF, + 0xC1, 0x6B, 0xE4, 0x35, 0x78, 0x8D, 0xBC, 0x06, 0xAF, 0x91, 0xD7, 0xE0, + 0x75, 0xF2, 0x3A, 0xBC, 0x4E, 0x5E, 0x87, 0xD7, 0xC9, 0xEB, 0xF0, 0x3A, + 0x79, 0xBD, 0xBA, 0xB3, 0x7C, 0x69, 0xE1, 0x75, 0xF2, 0x3A, 0xBC, 0x4E, + 0x5E, 0x87, 0xD7, 0xC9, 0xEB, 0xF0, 0x3A, 0x79, 0x1D, 0x5E, 0x27, 0x6F, + 0x39, 0xBB, 0x2D, 0xB2, 0x4E, 0x78, 0xCD, 0x6A, 0xE3, 0xF9, 0x7C, 0x5C, + 0x64, 0xED, 0xDE, 0x6C, 0xB2, 0xC8, 0x56, 0x7A, 0xE5, 0x5B, 0x96, 0xFC, + 0x1B, 0x6B, 0x69, 0x96, 0x15, 0xBD, 0xCB, 0x51, 0x32, 0x38, 0x2F, 0xCA, + 0x15, 0xB3, 0x69, 0x7E, 0x7F, 0x74, 0xF8, 0x50, 0x7F, 0x3C, 0x58, 0xFD, + 0x1E, 0x7C, 0x8E, 0xC6, 0x67, 0x4F, 0xAF, 0x3F, 0xF7, 0x6A, 0x82, 0x30, + }; + static const uint8_t loyalty_object_id[] = {0x04, 0x21, 0xC9, 0x4A, 0x51, 0x01, 0xE2, 0x5E, 0xAA}; + static const uint8_t customer_id[] = {0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static const size_t sample_decompressed_len = 1312; + static const size_t service_value_record_1_len = 1224; + static const size_t loyalty_record_len = 1206; + static const size_t service_value_record_2_len = 73; + static const size_t customer_record_len = 57; + + uint8_t *decompressed_payload = NULL; + size_t decompressed_payload_len = 0; + int res = gst_decompress_payload_zlib(compressed_payload, sizeof(compressed_payload), + &decompressed_payload, &decompressed_payload_len); + bool inflate_ok = (res == PM3_SUCCESS) && (decompressed_payload_len == sample_decompressed_len); + PrintAndLogEx(INFO, "zlib sample inflate... ( %s )", inflate_ok ? _GREEN_("ok") : _RED_("fail")); + + bool parse_ok = false; + // Check that inflation worked properly by validating inner data + if (inflate_ok) { + size_t offset = 0; + gst_ndef_record_view_t first_record; + gst_ndef_record_view_t second_record; + gst_ndef_record_view_t loyalty_record; + gst_ndef_record_view_t object_id_record; + gst_ndef_record_view_t customer_record; + gst_ndef_record_view_t customer_id_record; + + res = gst_ndef_parse_record(decompressed_payload, decompressed_payload_len, &offset, &first_record); + if (res == PM3_SUCCESS && + bytes_equal_not_null(first_record.type, first_record.type_len, + GST_TYPE_SERVICE_VALUE, sizeof(GST_TYPE_SERVICE_VALUE)) && + first_record.payload_len == service_value_record_1_len) { + res = gst_ndef_find_by_type_or_id(first_record.payload, first_record.payload_len, + GST_OBJECT_TYPE_LOYALTY, sizeof(GST_OBJECT_TYPE_LOYALTY), + &loyalty_record); + } + if (res == PM3_SUCCESS && loyalty_record.payload_len == loyalty_record_len) { + res = gst_ndef_find_by_type_or_id(loyalty_record.payload, loyalty_record.payload_len, + GST_TYPE_OBJECT_ID, sizeof(GST_TYPE_OBJECT_ID), + &object_id_record); + } + if (res == PM3_SUCCESS && + bytes_equal_not_null(object_id_record.payload, object_id_record.payload_len, + loyalty_object_id, sizeof(loyalty_object_id))) { + res = gst_ndef_parse_record(decompressed_payload, decompressed_payload_len, &offset, &second_record); + } + if (res == PM3_SUCCESS && + bytes_equal_not_null(second_record.type, second_record.type_len, + GST_TYPE_SERVICE_VALUE, sizeof(GST_TYPE_SERVICE_VALUE)) && + second_record.payload_len == service_value_record_2_len) { + res = gst_ndef_find_by_type_or_id(second_record.payload, second_record.payload_len, + GST_TYPE_CUSTOMER, sizeof(GST_TYPE_CUSTOMER), + &customer_record); + } + if (res == PM3_SUCCESS && customer_record.payload_len == customer_record_len) { + res = gst_ndef_find_by_type_or_id(customer_record.payload, customer_record.payload_len, + GST_TYPE_CUSTOMER_ID, sizeof(GST_TYPE_CUSTOMER_ID), + &customer_id_record); + } + parse_ok = (res == PM3_SUCCESS) && + bytes_equal_not_null(customer_id_record.payload, customer_id_record.payload_len, + customer_id, sizeof(customer_id)); + } + PrintAndLogEx(INFO, "zlib sample parse.... ( %s )", parse_ok ? _GREEN_("ok") : _RED_("fail")); + + free(decompressed_payload); + + return (inflate_ok && parse_ok) ? PM3_SUCCESS : PM3_ESOFT; +#else + PrintAndLogEx(INFO, "zlib support......... ( %s )", _YELLOW_("unsupported")); + return PM3_ENOTIMPL; +#endif +} + +static void gst_print_formatted_payload(const char *label, const uint8_t *payload, size_t payload_len) { + if (payload == NULL || payload_len == 0) { + PrintAndLogEx(INFO, "%s ", label); + return; + } + + uint8_t format = payload[0]; + const uint8_t *value = payload + 1; + size_t value_len = payload_len - 1; + + if (is_printable_ascii(value, value_len)) { + PrintAndLogEx(INFO, "%s \"%.*s\" (fmt=0x%02X, ASCII)", label, (int)value_len, (const char *)value, format); + } else { + PrintAndLogEx(INFO, "%s %s (fmt=0x%02X)", label, sprint_hex_inrow(value, value_len), format); + } +} + +static const char *gst_get_object_name(const uint8_t *type, size_t type_len) { + for (size_t i = 0; i < ARRAYLEN(gst_object_types); i++) { + if (bytes_equal_not_null(type, type_len, gst_object_types[i].type, gst_object_types[i].type_len)) { + return gst_object_types[i].name; + } + } + return NULL; +} + +static void gst_print_service_objects(const uint8_t *message, size_t message_len) { + size_t offset = 0; + size_t object_count = 0; + + while (offset < message_len) { + gst_ndef_record_view_t top_record; + int res = gst_ndef_parse_record(message, message_len, &offset, &top_record); + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "Failed to parse top-level NDEF record (err=%d)", res); + break; + } + + if (!bytes_equal_not_null(top_record.type, top_record.type_len, GST_TYPE_SERVICE_VALUE, sizeof(GST_TYPE_SERVICE_VALUE))) { + if (top_record.me) { + break; + } + continue; + } + + gst_ndef_record_view_t issuer_record; + res = gst_ndef_find_by_type_or_id(top_record.payload, top_record.payload_len, + GST_TYPE_ISSUER, sizeof(GST_TYPE_ISSUER), + &issuer_record); + if (res != PM3_SUCCESS || issuer_record.payload_len < 2) { + PrintAndLogEx(WARNING, "Service value record without valid issuer field"); + if (top_record.me) { + break; + } + continue; + } + + uint8_t issuer_type = issuer_record.payload[1]; + const uint8_t *issuer_id = issuer_record.payload + 2; + size_t issuer_id_len = issuer_record.payload_len - 2; + + size_t nested_offset = 0; + while (nested_offset < top_record.payload_len) { + gst_ndef_record_view_t nested; + res = gst_ndef_parse_record(top_record.payload, top_record.payload_len, &nested_offset, &nested); + if (res != PM3_SUCCESS) { + break; + } + + bool is_customer = bytes_equal_not_null(nested.type, nested.type_len, GST_TYPE_CUSTOMER, sizeof(GST_TYPE_CUSTOMER)); + const char *object_name = gst_get_object_name(nested.type, nested.type_len); + if (!is_customer && object_name == NULL) { + if (nested.me) { + break; + } + continue; + } + + object_count++; + PrintAndLogEx(INFO, ""); + PrintAndLogEx(INFO, _CYAN_("Object #%" PRIuPTR), (uintptr_t)object_count); + PrintAndLogEx(INFO, " kind.................... %s", is_customer ? "customer" : object_name); + PrintAndLogEx(INFO, " issuer_type............. %s (0x%02X)", gst_issuer_type_name(issuer_type), issuer_type); + PrintAndLogEx(INFO, " issuer_id............... %s", sprint_hex_inrow(issuer_id, issuer_id_len)); + + if (is_customer) { + gst_ndef_record_view_t customer_id_record; + gst_ndef_record_view_t language_record; + if (gst_ndef_find_by_type_or_id(nested.payload, nested.payload_len, + GST_TYPE_CUSTOMER_ID, sizeof(GST_TYPE_CUSTOMER_ID), + &customer_id_record) == PM3_SUCCESS) { + PrintAndLogEx(INFO, " customer_id............. %s", + sprint_hex_inrow(customer_id_record.payload, customer_id_record.payload_len)); + } + if (gst_ndef_find_by_type_or_id(nested.payload, nested.payload_len, + GST_TYPE_CUSTOMER_LANGUAGE, sizeof(GST_TYPE_CUSTOMER_LANGUAGE), + &language_record) == PM3_SUCCESS) { + gst_print_formatted_payload(" language................", language_record.payload, language_record.payload_len); + } + } else { + gst_ndef_record_view_t object_id_record; + gst_ndef_record_view_t service_number_record; + if (gst_ndef_find_by_type_or_id(nested.payload, nested.payload_len, + GST_TYPE_OBJECT_ID, sizeof(GST_TYPE_OBJECT_ID), + &object_id_record) == PM3_SUCCESS) { + PrintAndLogEx(INFO, " object_id............... %s", + sprint_hex_inrow(object_id_record.payload, object_id_record.payload_len)); + } + if (gst_ndef_find_by_type_or_id(nested.payload, nested.payload_len, + GST_TYPE_SERVICE_NUMBER, sizeof(GST_TYPE_SERVICE_NUMBER), + &service_number_record) == PM3_SUCCESS) { + gst_print_formatted_payload(" service_number..........", service_number_record.payload, service_number_record.payload_len); + } + } + + if (nested.me) { + break; + } + } + + if (top_record.me) { + break; + } + } + + if (object_count == 0) { + PrintAndLogEx(INFO, "(no service objects found)"); + } +} + +static int gst_parse_negotiate_response(const uint8_t *response_data, size_t response_len, + uint8_t *negotiate_sequence, + uint8_t *device_ephemeral_public_key, size_t *device_ephemeral_public_key_len) { + if (response_data == NULL || negotiate_sequence == NULL || device_ephemeral_public_key == NULL || + device_ephemeral_public_key_len == NULL) { + return PM3_EINVARG; + } + + gst_ndef_record_view_t negotiate_record; + int res = gst_ndef_find_by_type_or_id(response_data, response_len, + GST_TYPE_NEGOTIATE_RESPONSE, sizeof(GST_TYPE_NEGOTIATE_RESPONSE), + &negotiate_record); + if (res != PM3_SUCCESS) { + return res; + } + + gst_ndef_record_view_t dpk_record; + res = gst_ndef_find_by_type_or_id(negotiate_record.payload, negotiate_record.payload_len, + GST_TYPE_HANDSET_EPH_PUBKEY, sizeof(GST_TYPE_HANDSET_EPH_PUBKEY), + &dpk_record); + if (res != PM3_SUCCESS) { + return res; + } + + if (dpk_record.payload_len != 33 && dpk_record.payload_len != 65) { + return PM3_ESOFT; + } + memcpy(device_ephemeral_public_key, dpk_record.payload, dpk_record.payload_len); + *device_ephemeral_public_key_len = dpk_record.payload_len; + + gst_ndef_record_view_t session_record; + res = gst_ndef_find_by_type_or_id(negotiate_record.payload, negotiate_record.payload_len, + GST_TYPE_SESSION, sizeof(GST_TYPE_SESSION), + &session_record); + if (res != PM3_SUCCESS) { + return res; + } + if (session_record.payload_len < 9) { + return PM3_ESOFT; + } + + *negotiate_sequence = session_record.payload[8]; + return PM3_SUCCESS; +} + +static int gst_extract_record_bundle(const uint8_t *response_data, size_t response_len, + uint8_t *flags, + const uint8_t **bundle_payload, size_t *bundle_payload_len) { + if (response_data == NULL || flags == NULL || bundle_payload == NULL || bundle_payload_len == NULL) { + return PM3_EINVARG; + } + + gst_ndef_record_view_t service_response_record; + int res = gst_ndef_find_by_type_or_id(response_data, response_len, + GST_TYPE_SERVICE_RESPONSE, sizeof(GST_TYPE_SERVICE_RESPONSE), + &service_response_record); + if (res != PM3_SUCCESS) { + return res; + } + + gst_ndef_record_view_t record_bundle_record; + res = gst_ndef_find_by_type_or_id(service_response_record.payload, service_response_record.payload_len, + GST_TYPE_RECORD_BUNDLE, sizeof(GST_TYPE_RECORD_BUNDLE), + &record_bundle_record); + if (res != PM3_SUCCESS) { + return res; + } + if (record_bundle_record.payload_len < 1) { + return PM3_ESOFT; + } + + *flags = record_bundle_record.payload[0]; + *bundle_payload = record_bundle_record.payload + 1; + *bundle_payload_len = record_bundle_record.payload_len - 1; + return PM3_SUCCESS; +} + +static int gst_info(gst_select_behavior_t select_behavior) { + gst_ose_info_t ose_info; + int res = gst_select_ose(&ose_info, true); + if (res != PM3_SUCCESS) { + DropField(); + return res; + } + + PrintAndLogEx(INFO, ""); + gst_print_ose_info(&ose_info); + + bool should_select = gst_should_select_smart_tap(select_behavior, &ose_info); + if (should_select) { + gst_select_info_t select_info; + res = gst_select_smart_tap_v2(&select_info, true); + if (res != PM3_SUCCESS) { + DropField(); + return res; + } + PrintAndLogEx(INFO, ""); + gst_print_select_info(&select_info); + } + + PrintAndLogEx(INFO, ""); + DropField(); + return PM3_SUCCESS; +} + +static int gst_read(const gst_read_config_t *cfg) { + if (cfg == NULL) { + return PM3_EINVARG; + } + + int status = PM3_ESOFT; + pcrypto_rng_t rng; + + mbedtls_ecp_keypair longterm_key; + mbedtls_ecp_keypair reader_ephemeral_key; + mbedtls_ecp_keypair_init(&longterm_key); + mbedtls_ecp_keypair_init(&reader_ephemeral_key); + + uint8_t longterm_public_key[65] = {0}; + uint8_t reader_ephemeral_public_key[33] = {0}; + uint8_t reader_nonce[32] = {0}; + uint8_t session_id_be[8] = {0}; + uint64_t session_id = 0; + uint8_t reader_signature[GST_MAX_SIGNATURE_LEN] = {0}; + size_t reader_signature_len = sizeof(reader_signature); + + uint8_t device_nonce[GST_MAX_NONCE_LEN] = {0}; + size_t device_nonce_len = 0; + + uint8_t device_ephemeral_public_key[65] = {0}; + size_t device_ephemeral_public_key_len = 0; + + uint8_t negotiate_response[GST_MAX_BUFFER] = {0}; + size_t negotiate_response_len = 0; + uint16_t negotiate_sw = 0; + + uint8_t get_data_response[GST_MAX_BUFFER] = {0}; + size_t get_data_response_len = 0; + uint16_t get_data_sw = 0; + + uint8_t decrypted_payload[GST_MAX_BUFFER] = {0}; + size_t decrypted_payload_len = 0; +#ifdef HAVE_ZLIB + uint8_t *decompressed_payload = NULL; + size_t decompressed_payload_len = 0; +#endif + + uint8_t negotiate_cmd[GST_MAX_NDEF_BUFFER] = {0}; + size_t negotiate_cmd_len = 0; + + status = pcrypto_rng_init(&rng, GST_RNG_PERSONALIZATION, sizeof(GST_RNG_PERSONALIZATION) - 1); + if (status != PM3_SUCCESS) { + goto out; + } + + status = gst_load_private_key_raw(cfg->reader_private_key, &longterm_key, &rng); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Invalid reader private key"); + goto out; + } + + status = gst_export_pubkey_uncompressed(&longterm_key, longterm_public_key); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to export reader public key"); + goto out; + } + + if (cfg->have_reader_ephemeral_private_key) { + status = gst_load_private_key_raw(cfg->reader_ephemeral_private_key, &reader_ephemeral_key, &rng); + } else { + status = gst_generate_ephemeral_keypair(&reader_ephemeral_key, &rng); + } + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to prepare reader ephemeral key"); + goto out; + } + + status = gst_export_pubkey_compressed(&reader_ephemeral_key, reader_ephemeral_public_key); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to export reader ephemeral public key"); + goto out; + } + + if (cfg->have_reader_nonce) { + memcpy(reader_nonce, cfg->reader_nonce, sizeof(reader_nonce)); + } else { + status = pcrypto_rng_fill(&rng, reader_nonce, sizeof(reader_nonce)); + if (status != PM3_SUCCESS) { + goto out; + } + } + + if (cfg->have_session_id) { + memcpy(session_id_be, cfg->session_id, sizeof(session_id_be)); + } else { + status = pcrypto_rng_fill(&rng, session_id_be, sizeof(session_id_be)); + if (status != PM3_SUCCESS) { + goto out; + } + } + session_id = MemBeToUint8byte(session_id_be); + + gst_ose_info_t ose_info; + status = gst_select_ose(&ose_info, true); + if (status != PM3_SUCCESS) { + goto out; + } + + PrintAndLogEx(INFO, ""); + gst_print_ose_info(&ose_info); + + bool should_select = gst_should_select_smart_tap(cfg->select_behavior, &ose_info); + if (should_select) { + gst_select_info_t select_info; + status = gst_select_smart_tap_v2(&select_info, true); + if (status != PM3_SUCCESS) { + goto out; + } + PrintAndLogEx(INFO, ""); + gst_print_select_info(&select_info); + if (select_info.have_handset_nonce) { + memcpy(device_nonce, select_info.handset_nonce, select_info.handset_nonce_len); + device_nonce_len = select_info.handset_nonce_len; + } + } + + if (device_nonce_len == 0) { + if (ose_info.have_directory_device_nonce) { + memcpy(device_nonce, ose_info.directory_device_nonce, ose_info.directory_device_nonce_len); + device_nonce_len = ose_info.directory_device_nonce_len; + } else if (ose_info.have_ose_device_nonce) { + memcpy(device_nonce, ose_info.ose_device_nonce, ose_info.ose_device_nonce_len); + device_nonce_len = ose_info.ose_device_nonce_len; + } + } + if (device_nonce_len == 0) { + PrintAndLogEx(ERR, "Could not obtain handset/device nonce"); + status = PM3_ESOFT; + goto out; + } + + uint8_t auth_device_nonce[GST_MAX_NONCE_LEN] = {0}; + memcpy(auth_device_nonce, device_nonce, device_nonce_len); + if (!cfg->live_authentication) { + memset(auth_device_nonce, 0, device_nonce_len); + } + + uint8_t signature_input[GST_MAX_BUFFER] = {0}; + size_t signature_input_len = 0; + status = gst_build_signature_input(reader_nonce, + auth_device_nonce, device_nonce_len, + cfg->collector_id, + reader_ephemeral_public_key, + signature_input, sizeof(signature_input), &signature_input_len); + if (status != PM3_SUCCESS) { + goto out; + } + + status = gst_create_reader_signature(cfg->reader_private_key, longterm_public_key, + signature_input, signature_input_len, + reader_signature, &reader_signature_len); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to create reader signature"); + goto out; + } + + PrintAndLogEx(INFO, ""); + PrintAndLogInfoHeader("Negotiate secure channel"); + PrintAndLogEx(INFO, "Collector ID.............. %" PRIu32 " (0x%08" PRIX32 ")", cfg->collector_id, cfg->collector_id); + PrintAndLogEx(INFO, "Long-term key version..... %" PRIu32 " (0x%08" PRIX32 ")", cfg->key_version, cfg->key_version); + PrintAndLogEx(INFO, "Session ID................ " _YELLOW_("%s"), sprint_hex_inrow(session_id_be, sizeof(session_id_be))); + PrintAndLogEx(INFO, "Reader nonce.............. " _YELLOW_("%s"), sprint_hex_inrow(reader_nonce, sizeof(reader_nonce))); + PrintAndLogEx(INFO, "Device nonce.............. " _YELLOW_("%s"), sprint_hex_inrow(device_nonce, device_nonce_len)); + PrintAndLogEx(INFO, "Reader ephemeral pubkey... " _YELLOW_("%s"), sprint_hex_inrow(reader_ephemeral_public_key, sizeof(reader_ephemeral_public_key))); + PrintAndLogEx(INFO, "Reader signature.......... " _YELLOW_("%s"), sprint_hex_inrow(reader_signature, reader_signature_len)); + PrintAndLogEx(INFO, "Live authentication....... %s", cfg->live_authentication ? _GREEN_("yes") : "no"); + + status = gst_build_negotiate_message(reader_nonce, reader_ephemeral_public_key, + reader_signature, reader_signature_len, + cfg->collector_id, cfg->key_version, + session_id, 1, cfg->live_authentication, + negotiate_cmd, sizeof(negotiate_cmd), &negotiate_cmd_len); + if (status != PM3_SUCCESS) { + goto out; + } + + status = gst_exchange_chained(false, true, 0x90, 0x53, 0x00, 0x00, + negotiate_cmd, negotiate_cmd_len, + negotiate_response, sizeof(negotiate_response), &negotiate_response_len, &negotiate_sw); + if (status != PM3_SUCCESS) { + goto out; + } + + if (gst_status_is_transient_failure(negotiate_sw)) { + PrintAndLogEx(WARNING, "Transient failure on NEGOTIATE (sequence=1), retrying with sequence=3..."); + status = gst_build_negotiate_message(reader_nonce, reader_ephemeral_public_key, + reader_signature, reader_signature_len, + cfg->collector_id, cfg->key_version, + session_id, 3, cfg->live_authentication, + negotiate_cmd, sizeof(negotiate_cmd), &negotiate_cmd_len); + if (status != PM3_SUCCESS) { + goto out; + } + status = gst_exchange_chained(false, true, 0x90, 0x53, 0x00, 0x00, + negotiate_cmd, negotiate_cmd_len, + negotiate_response, sizeof(negotiate_response), &negotiate_response_len, &negotiate_sw); + if (status != PM3_SUCCESS) { + goto out; + } + } + + gst_print_status_line("NEGOTIATE status..........", negotiate_sw); + if (!(negotiate_sw == 0x9000 || negotiate_sw == 0x9002)) { + status = PM3_ESOFT; + goto out; + } + + uint8_t negotiate_sequence = 0; + status = gst_parse_negotiate_response(negotiate_response, negotiate_response_len, + &negotiate_sequence, + device_ephemeral_public_key, &device_ephemeral_public_key_len); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to parse NEGOTIATE response"); + goto out; + } + + uint8_t get_data_sequence = (uint8_t)(negotiate_sequence + 1); + uint8_t get_data_retry_sequence = (uint8_t)(negotiate_sequence + 3); + + status = gst_perform_get_data(cfg->collector_id, session_id, get_data_sequence, cfg->mode, + get_data_response, sizeof(get_data_response), &get_data_response_len, &get_data_sw); + if (status != PM3_SUCCESS) { + goto out; + } + if (gst_status_is_transient_failure(get_data_sw)) { + PrintAndLogEx(WARNING, "Transient failure on GET DATA, retrying..."); + status = gst_perform_get_data(cfg->collector_id, session_id, get_data_retry_sequence, cfg->mode, + get_data_response, sizeof(get_data_response), &get_data_response_len, &get_data_sw); + if (status != PM3_SUCCESS) { + goto out; + } + } + + PrintAndLogEx(INFO, ""); + PrintAndLogInfoHeader("Get data"); + gst_print_status_line("GET DATA status...........", get_data_sw); + if (!gst_status_is_success(get_data_sw)) { + status = PM3_ESOFT; + goto out; + } + + if (get_data_response_len == 0) { + PrintAndLogEx(INFO, "No data returned by handset"); + status = PM3_SUCCESS; + goto out; + } + + uint8_t bundle_flags = 0; + const uint8_t *bundle_payload = NULL; + size_t bundle_payload_len = 0; + status = gst_extract_record_bundle(get_data_response, get_data_response_len, + &bundle_flags, &bundle_payload, &bundle_payload_len); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to parse SERVICE_RESPONSE / RECORD_BUNDLE"); + goto out; + } + + gst_print_bundle_flags(bundle_flags); + + const uint8_t *payload_to_parse = bundle_payload; + size_t payload_to_parse_len = bundle_payload_len; + + if (bundle_flags & 0x01) { + status = gst_decrypt_payload(bundle_payload, bundle_payload_len, + cfg->collector_id, + reader_nonce, + device_nonce, device_nonce_len, + reader_signature, reader_signature_len, + &reader_ephemeral_key, + reader_ephemeral_public_key, + device_ephemeral_public_key, device_ephemeral_public_key_len, + decrypted_payload, sizeof(decrypted_payload), &decrypted_payload_len); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to decrypt RECORD_BUNDLE payload"); + goto out; + } + payload_to_parse = decrypted_payload; + payload_to_parse_len = decrypted_payload_len; + } + + if (bundle_flags & 0x02) { +#ifdef HAVE_ZLIB + status = gst_decompress_payload_zlib(payload_to_parse, payload_to_parse_len, + &decompressed_payload, &decompressed_payload_len); + if (status != PM3_SUCCESS) { + if (status == PM3_EOVFLOW) { + PrintAndLogEx(ERR, "Compressed payload exceeds %d bytes limit", GST_MAX_DECOMPRESSED_PAYLOAD); + } else { + PrintAndLogEx(ERR, "Failed to decompress RECORD_BUNDLE payload (zlib)"); + } + goto out; + } + payload_to_parse = decompressed_payload; + payload_to_parse_len = decompressed_payload_len; +#else + PrintAndLogEx(WARNING, "RECORD_BUNDLE is compressed (0x02 flag set) but zlib is unavailable. Skipping decompression."); +#endif + } + + PrintAndLogEx(INFO, ""); + PrintAndLogInfoHeader("Service objects"); + gst_print_service_objects(payload_to_parse, payload_to_parse_len); + status = PM3_SUCCESS; + +out: +#ifdef HAVE_ZLIB + free(decompressed_payload); +#endif + DropField(); + mbedtls_ecp_keypair_free(&reader_ephemeral_key); + mbedtls_ecp_keypair_free(&longterm_key); + pcrypto_rng_free(&rng); + return status; +} + +static int CmdHFGSTTest(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf gst test", + "Perform self tests", + "hf gst test"); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + + PrintAndLogEx(INFO, "------ " _CYAN_("Google Smart Tap tests") " ------"); + int res = gst_selftest_zlib(); + if (res == PM3_ENOTIMPL) { + PrintAndLogEx(INFO, "Tests ( %s )", _YELLOW_("skipped")); + } else { + PrintAndLogEx(SUCCESS, "Tests ( %s )", (res == PM3_SUCCESS) ? _GREEN_("ok") : _RED_("fail")); + } + return res; +} + +static int CmdHFGSTInfo(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf gst info", + "Select OSE / Smart Tap applet and print capabilities.", + "hf gst info\n" + "hf gst info --select-smarttap2 yes -a"); + + void *argtable[] = { + arg_param_begin, + arg_str0(NULL, "select-smarttap2", "", "Whether to perform Smart Tap applet select (default: auto)"), + arg_lit0("a", "apdu", "Show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + gst_select_behavior_t select_behavior = GST_SELECT_AUTO; + CLIParserOption select_options[] = { + {GST_SELECT_AUTO, "auto"}, + {GST_SELECT_YES, "yes"}, + {GST_SELECT_NO, "no"}, + {0, NULL} + }; + if (CLIGetOptionList(arg_get_str(ctx, 1), select_options, (int *)&select_behavior) != PM3_SUCCESS) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + bool apdu_logging = arg_get_lit(ctx, 2); + CLIParserFree(ctx); + + bool restore_apdu_logging = GetAPDULogging(); + SetAPDULogging(apdu_logging); + int res = gst_info(select_behavior); + SetAPDULogging(restore_apdu_logging); + return res; +} + +static int CmdHFGSTRead(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf gst read", + "Execute Google Smart Tap read flow and print parsed pass objects.", + "hf gst read --cid 20180608 --rpk gst.google.der\n" + "hf gst read --cid 13380028 --rpk gst.passkit.der\n" + "hf gst read --cid 20180608 --rpk \"-----BEGIN EC PRIVATE KEY-----\\nMHcCAQEEIIJtF+UHZ7FlsOTZ4zL40dHiAiQoT7Ta8eUKAyRucHl9oAoGCCqGSM49\\nAwEHoUQDQgAEchyXj869zfmKhRi9xP7f2AK07kEo4lE7ZlWTN14jh4YBTny+hRGR\\nXcUzevV9zSSPJlPHpqqu5pEwlv1xyFvE1w==\\n-----END EC PRIVATE KEY-----\"\n" + "hf gst read --cid 20180608 --rpk MHcCAQEEIIJtF+UHZ7FlsOTZ4zL40dHiAiQoT7Ta8eUKAyRucHl9oAoGCCqGSM49AwEHoUQDQgAEchyXj869zfmKhRi9xP7f2AK07kEo4lE7ZlWTN14jh4YBTny+hRGRXcUzevV9zSSPJlPHpqqu5pEwlv1xyFvE1w==\n" + "hf gst read --collector-id 20180608 --reader-private-key gst.google.der\n" + "hf gst read --collector-id 20180608 --reader-private-key 826d17e50767b165b0e4d9e332f8d1d1e20224284fb4daf1e50a03246e70797d"); + + void *argtable[] = { + arg_param_begin, + arg_str1("c", "collector-id,collectorid,cid", "", "Collector identifier (32-bit value)"), + arg_str1("p", "reader-private-key,readerprivkey,rpk", "", "Reader private key in PEM, PEM base64 body, DER hex, 32-byte scalar hex, or file path"), + arg_str0(NULL, "key-version,keyversion,kv", "", "Long-term key version (default: 1)"), + arg_str0(NULL, "session-id,sid", "", "Session id (8 bytes, random if omitted)"), + arg_str0(NULL, "reader-nonce,nonce", "", "Reader nonce (32 bytes, random if omitted)"), + arg_str0(NULL, "reader-ephemeral-private-key,ephemeral-privkey,epk", "", "Reader ephemeral private key (32 bytes, random if omitted)"), + arg_str0(NULL, "mode", "", "Reader mode (default: pass-over-payment)"), + arg_str0(NULL, "select-smarttap2", "", "Whether to perform Smart Tap applet select (default: auto)"), + arg_lit0(NULL, "no-live-auth", "Use zeroed handset nonce for reader signature"), + arg_lit0("a", "apdu", "Show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + gst_read_config_t cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.key_version = 1; + cfg.mode = GST_MODE_PASS_OVER_PAYMENT; + cfg.select_behavior = GST_SELECT_AUTO; + cfg.live_authentication = true; + + char collector_id_text[64] = {0}; + int collector_id_text_len = 0; + if (CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)collector_id_text, sizeof(collector_id_text), &collector_id_text_len) != 0 || + collector_id_text_len == 0 || + parse_uint32_hex_or_dec(collector_id_text, &cfg.collector_id) != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Invalid collector-id"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + char reader_key_text[GST_MAX_PRIVATE_KEY_ARG] = {0}; + int reader_key_text_len = 0; + if (CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)reader_key_text, sizeof(reader_key_text), &reader_key_text_len) != 0 || + reader_key_text_len == 0 || + ensure_ec_private_key(reader_key_text, MBEDTLS_ECP_DP_SECP256R1, cfg.reader_private_key, sizeof(cfg.reader_private_key)) != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Invalid reader-private-key format"); + PrintAndLogEx(INFO, "Accepted formats:"); + PrintAndLogEx(INFO, " 1) PEM string (BEGIN/END EC PRIVATE KEY or PRIVATE KEY)"); + PrintAndLogEx(INFO, " 2) PEM base64 body only"); + PrintAndLogEx(INFO, " 3) DER key bytes as hex"); + PrintAndLogEx(INFO, " 4) Raw 32-byte private scalar as hex"); + PrintAndLogEx(INFO, " 5) File path to a key in one of the formats above"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + char key_version_text[64] = {0}; + int key_version_text_len = 0; + if (CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)key_version_text, sizeof(key_version_text), &key_version_text_len) == 0 && + key_version_text_len > 0) { + if (parse_uint32_hex_or_dec(key_version_text, &cfg.key_version) != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Invalid key-version"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + } + + int session_id_len = 0; + CLIGetHexWithReturn(ctx, 4, cfg.session_id, &session_id_len); + if (session_id_len == 8) { + cfg.have_session_id = true; + } else if (session_id_len != 0) { + PrintAndLogEx(ERR, "session-id must be 8 bytes when provided (got %d)", session_id_len); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + int reader_nonce_len = 0; + CLIGetHexWithReturn(ctx, 5, cfg.reader_nonce, &reader_nonce_len); + if (reader_nonce_len == 32) { + cfg.have_reader_nonce = true; + } else if (reader_nonce_len != 0) { + PrintAndLogEx(ERR, "reader-nonce must be 32 bytes when provided (got %d)", reader_nonce_len); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + int eph_priv_len = 0; + CLIGetHexWithReturn(ctx, 6, cfg.reader_ephemeral_private_key, &eph_priv_len); + if (eph_priv_len == 32) { + cfg.have_reader_ephemeral_private_key = true; + } else if (eph_priv_len != 0) { + PrintAndLogEx(ERR, "reader-ephemeral-private-key must be 32 bytes when provided (got %d)", eph_priv_len); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + CLIParserOption mode_options[] = { + {GST_MODE_PASS_ONLY, "pass-only"}, + {GST_MODE_PAYMENT_ONLY, "payment-only"}, + {GST_MODE_PASS_AND_PAYMENT, "pass-and-payment"}, + {GST_MODE_PASS_OVER_PAYMENT, "pass-over-payment"}, + {0, NULL} + }; + if (CLIGetOptionList(arg_get_str(ctx, 7), mode_options, (int *)&cfg.mode) != PM3_SUCCESS) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + CLIParserOption select_options[] = { + {GST_SELECT_AUTO, "auto"}, + {GST_SELECT_YES, "yes"}, + {GST_SELECT_NO, "no"}, + {0, NULL} + }; + if (CLIGetOptionList(arg_get_str(ctx, 8), select_options, (int *)&cfg.select_behavior) != PM3_SUCCESS) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + cfg.live_authentication = !arg_get_lit(ctx, 9); + cfg.apdu_logging = arg_get_lit(ctx, 10); + + CLIParserFree(ctx); + + bool restore_apdu_logging = GetAPDULogging(); + SetAPDULogging(cfg.apdu_logging); + int res = gst_read(&cfg); + SetAPDULogging(restore_apdu_logging); + return res; +} + +static int CmdHFGSTList(const char *Cmd) { + return CmdTraceListAlias(Cmd, "hf gst", "7816"); +} + +static command_t CommandTable[] = { + {"-----------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("General") " -----------------------"}, + {"help", CmdHelp, AlwaysAvailable, "This help"}, + {"list", CmdHFGSTList, AlwaysAvailable, "List ISO 14443A/7816 history"}, + {"test", CmdHFGSTTest, AlwaysAvailable, "Perform self tests"}, + {"-----------", CmdHelp, IfPm3Iso14443a, "--------------------- " _CYAN_("Operations") " ----------------------"}, + {"info", CmdHFGSTInfo, IfPm3Iso14443a, "Get Google Smart Tap applet information"}, + {"read", CmdHFGSTRead, IfPm3Iso14443a, "Read and decode Google Smart Tap pass objects"}, + {NULL, NULL, NULL, NULL} +}; + +int CmdHFGST(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(CommandTable, Cmd); +} + +static int CmdHelp(const char *Cmd) { + (void)Cmd; + CmdsHelp(CommandTable); + return PM3_SUCCESS; +} diff --git a/client/src/cmdhfgst.h b/client/src/cmdhfgst.h new file mode 100644 index 000000000..140689057 --- /dev/null +++ b/client/src/cmdhfgst.h @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +// High frequency Google Smart Tap commands +//----------------------------------------------------------------------------- + +#ifndef CMDHFGST_H__ +#define CMDHFGST_H__ + +#include "common.h" + +int CmdHFGST(const char *Cmd); + +#endif diff --git a/client/src/crypto/libpcrypto.c b/client/src/crypto/libpcrypto.c index 9d506d8a8..ca0826eec 100644 --- a/client/src/crypto/libpcrypto.c +++ b/client/src/crypto/libpcrypto.c @@ -22,11 +22,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -38,8 +40,382 @@ #include "libpcrypto.h" #include "util.h" #include "ui.h" +#include "fileutils.h" #include "math.h" +#define PCRYPTO_MAX_KEY_INPUT 8192 +#define PCRYPTO_MAX_KEY_FILE_BYTES (64 * 1024) + +int pcrypto_rng_init(pcrypto_rng_t *rng, const uint8_t *personalization, size_t personalization_len) { + if (rng == NULL || (personalization_len > 0 && personalization == NULL)) { + return PM3_EINVARG; + } + + memset(rng, 0, sizeof(*rng)); + mbedtls_entropy_init(&rng->entropy); + mbedtls_ctr_drbg_init(&rng->ctr_drbg); + + int ret = mbedtls_ctr_drbg_seed(&rng->ctr_drbg, mbedtls_entropy_func, &rng->entropy, + personalization, personalization_len); + if (ret != 0) { + PrintAndLogEx(ERR, "Failed to initialize random generator (mbedtls: %d)", ret); + mbedtls_ctr_drbg_free(&rng->ctr_drbg); + mbedtls_entropy_free(&rng->entropy); + return PM3_ESOFT; + } + + rng->seeded = true; + return PM3_SUCCESS; +} + +void pcrypto_rng_free(pcrypto_rng_t *rng) { + if (rng == NULL) { + return; + } + + mbedtls_ctr_drbg_free(&rng->ctr_drbg); + mbedtls_entropy_free(&rng->entropy); + rng->seeded = false; +} + +int pcrypto_rng_fill(pcrypto_rng_t *rng, uint8_t *out, size_t out_len) { + if (rng == NULL || out == NULL || rng->seeded == false) { + return PM3_EINVARG; + } + if (out_len == 0) { + return PM3_SUCCESS; + } + + return (mbedtls_ctr_drbg_random(&rng->ctr_drbg, out, out_len) == 0) ? PM3_SUCCESS : PM3_ESOFT; +} + +static void pcrypto_trim_ascii_inplace(char *text) { + if (text == NULL) { + return; + } + + size_t start = 0; + size_t len = strlen(text); + while (start < len && isspace((unsigned char)text[start])) { + start++; + } + while (len > start && isspace((unsigned char)text[len - 1])) { + len--; + } + + if (start > 0) { + memmove(text, text + start, len - start); + } + text[len - start] = '\0'; +} + +static void pcrypto_unescape_newlines_inplace(char *text) { + if (text == NULL) { + return; + } + + size_t read_pos = 0; + size_t write_pos = 0; + size_t len = strlen(text); + while (read_pos < len) { + if (text[read_pos] == '\\' && (read_pos + 1) < len) { + char esc = text[read_pos + 1]; + if (esc == 'n') { + text[write_pos++] = '\n'; + read_pos += 2; + continue; + } + if (esc == 'r') { + text[write_pos++] = '\r'; + read_pos += 2; + continue; + } + if (esc == 't') { + text[write_pos++] = '\t'; + read_pos += 2; + continue; + } + } + text[write_pos++] = text[read_pos++]; + } + text[write_pos] = '\0'; +} + +static int pcrypto_copy_without_whitespace(const char *src, char *dst, size_t dst_size, size_t *dst_len) { + if (src == NULL || dst == NULL || dst_len == NULL || dst_size == 0) { + return PM3_EINVARG; + } + + size_t out = 0; + for (size_t i = 0; src[i] != '\0'; i++) { + if (isspace((unsigned char)src[i])) { + continue; + } + if ((out + 1) >= dst_size) { + return PM3_EOVFLOW; + } + dst[out++] = src[i]; + } + dst[out] = '\0'; + *dst_len = out; + return PM3_SUCCESS; +} + +static int pcrypto_extract_priv_scalar_from_pk(const mbedtls_pk_context *pkctx, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len) { + if (pkctx == NULL || out_priv == NULL || out_priv_len == 0) { + return PM3_EINVARG; + } + + mbedtls_pk_type_t pk_type = mbedtls_pk_get_type(pkctx); + if (!(pk_type == MBEDTLS_PK_ECKEY || pk_type == MBEDTLS_PK_ECKEY_DH)) { + return PM3_EINVARG; + } + + mbedtls_ecp_keypair *ec = mbedtls_pk_ec(*pkctx); + if (ec == NULL || ec->grp.id != curveid) { + return PM3_EINVARG; + } + if (mbedtls_mpi_bitlen(&ec->d) > (out_priv_len * 8U)) { + return PM3_EINVARG; + } + if (mbedtls_ecp_check_privkey(&ec->grp, &ec->d) != 0) { + return PM3_EINVARG; + } + if (mbedtls_mpi_write_binary(&ec->d, out_priv, out_priv_len) != 0) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int pcrypto_parse_ec_private_blob(const uint8_t *blob, size_t blob_len, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len) { + if (blob == NULL || out_priv == NULL || blob_len == 0 || out_priv_len == 0) { + return PM3_EINVARG; + } + + mbedtls_pk_context pkctx; + mbedtls_pk_init(&pkctx); + + int ret = mbedtls_pk_parse_key(&pkctx, blob, blob_len, NULL, 0); + if (ret != 0) { + uint8_t *nul_terminated = calloc(blob_len + 1, sizeof(uint8_t)); + if (nul_terminated == NULL) { + mbedtls_pk_free(&pkctx); + return PM3_EMALLOC; + } + memcpy(nul_terminated, blob, blob_len); + ret = mbedtls_pk_parse_key(&pkctx, nul_terminated, blob_len + 1, NULL, 0); + free(nul_terminated); + } + + if (ret != 0) { + mbedtls_pk_free(&pkctx); + return PM3_EINVARG; + } + + int res = pcrypto_extract_priv_scalar_from_pk(&pkctx, curveid, out_priv, out_priv_len); + mbedtls_pk_free(&pkctx); + return res; +} + +static int pcrypto_parse_ec_private_base64(const char *input, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len) { + if (input == NULL || out_priv == NULL || out_priv_len == 0) { + return PM3_EINVARG; + } + + char compact[PCRYPTO_MAX_KEY_INPUT] = {0}; + size_t compact_len = 0; + int res = pcrypto_copy_without_whitespace(input, compact, sizeof(compact), &compact_len); + if (res != PM3_SUCCESS || compact_len == 0) { + return PM3_EINVARG; + } + + size_t decoded_capacity = ((compact_len * 3) / 4) + 4; + uint8_t *decoded = calloc(decoded_capacity, sizeof(uint8_t)); + if (decoded == NULL) { + return PM3_EMALLOC; + } + + size_t decoded_len = 0; + int b64_res = mbedtls_base64_decode(decoded, decoded_capacity, &decoded_len, + (const unsigned char *)compact, compact_len); + if (b64_res != 0 || decoded_len == 0) { + free(decoded); + return PM3_EINVARG; + } + + res = pcrypto_parse_ec_private_blob(decoded, decoded_len, curveid, out_priv, out_priv_len); + free(decoded); + return res; +} + +static int pcrypto_validate_raw_scalar(const uint8_t *scalar, size_t scalar_len, mbedtls_ecp_group_id curveid) { + if (scalar == NULL || scalar_len == 0) { + return PM3_EINVARG; + } + + mbedtls_ecp_group grp; + mbedtls_mpi d; + mbedtls_ecp_group_init(&grp); + mbedtls_mpi_init(&d); + + int status = PM3_ESOFT; + if (mbedtls_ecp_group_load(&grp, curveid) != 0) { + goto out; + } + if (mbedtls_mpi_read_binary(&d, scalar, scalar_len) != 0) { + status = PM3_EINVARG; + goto out; + } + if (mbedtls_ecp_check_privkey(&grp, &d) != 0) { + status = PM3_EINVARG; + goto out; + } + status = PM3_SUCCESS; + +out: + mbedtls_mpi_free(&d); + mbedtls_ecp_group_free(&grp); + return status; +} + +static int pcrypto_parse_ec_private_text(const char *input, bool allow_file_path, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len); + +static int pcrypto_parse_ec_private_file(const char *path, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len) { + if (path == NULL || out_priv == NULL || out_priv_len == 0) { + return PM3_EINVARG; + } + + FILE *f = fopen(path, "rb"); + if (f == NULL) { + return PM3_EFILE; + } + + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return PM3_EFILE; + } + long file_len_l = ftell(f); + if (file_len_l < 0) { + fclose(f); + return PM3_EFILE; + } + if ((size_t)file_len_l > PCRYPTO_MAX_KEY_FILE_BYTES) { + fclose(f); + return PM3_EOVFLOW; + } + if (fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return PM3_EFILE; + } + + size_t file_len = (size_t)file_len_l; + uint8_t *data = calloc(file_len + 1, sizeof(uint8_t)); + if (data == NULL) { + fclose(f); + return PM3_EMALLOC; + } + size_t read_len = fread(data, 1, file_len, f); + fclose(f); + if (read_len != file_len) { + free(data); + return PM3_EFILE; + } + + int res = pcrypto_parse_ec_private_blob(data, file_len, curveid, out_priv, out_priv_len); + if (res == PM3_SUCCESS) { + free(data); + return PM3_SUCCESS; + } + + char *text = calloc(file_len + 1, sizeof(char)); + if (text == NULL) { + free(data); + return PM3_EMALLOC; + } + memcpy(text, data, file_len); + text[file_len] = '\0'; + free(data); + + res = pcrypto_parse_ec_private_text(text, false, curveid, out_priv, out_priv_len); + free(text); + return res; +} + +static int pcrypto_parse_ec_private_text(const char *input, bool allow_file_path, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len) { + if (input == NULL || out_priv == NULL || out_priv_len == 0) { + return PM3_EINVARG; + } + + char normalized[PCRYPTO_MAX_KEY_INPUT] = {0}; + size_t input_len = strlen(input); + if (input_len >= sizeof(normalized)) { + return PM3_EOVFLOW; + } + memcpy(normalized, input, input_len + 1); + pcrypto_trim_ascii_inplace(normalized); + + if (normalized[0] == '\0') { + return PM3_EINVARG; + } + + if (allow_file_path) { + char *resolved_path = NULL; + if (searchFile(&resolved_path, RESOURCES_SUBDIR, normalized, "", true) == PM3_SUCCESS) { + int res = pcrypto_parse_ec_private_file(resolved_path, curveid, out_priv, out_priv_len); + free(resolved_path); + return res; + } + } + + // Only unescape after path resolution fails, to avoid mutating valid paths + // (for example Windows paths containing '\t', '\n' or '\r'). + pcrypto_unescape_newlines_inplace(normalized); + + uint8_t decoded[PCRYPTO_MAX_KEY_INPUT] = {0}; + int decoded_len = -1; + char compact[PCRYPTO_MAX_KEY_INPUT] = {0}; + size_t compact_len = 0; + if (pcrypto_copy_without_whitespace(normalized, compact, sizeof(compact), &compact_len) == PM3_SUCCESS && + compact_len > 0) { + decoded_len = hex_to_bytes(compact, decoded, sizeof(decoded)); + } + if (decoded_len > 0) { + if ((size_t)decoded_len == out_priv_len && + pcrypto_validate_raw_scalar(decoded, out_priv_len, curveid) == PM3_SUCCESS) { + memcpy(out_priv, decoded, out_priv_len); + return PM3_SUCCESS; + } + int res = pcrypto_parse_ec_private_blob(decoded, (size_t)decoded_len, curveid, out_priv, out_priv_len); + if (res == PM3_SUCCESS) { + return PM3_SUCCESS; + } + } + + int res = pcrypto_parse_ec_private_blob((const uint8_t *)normalized, strlen(normalized), + curveid, out_priv, out_priv_len); + if (res == PM3_SUCCESS) { + return PM3_SUCCESS; + } + + return pcrypto_parse_ec_private_base64(normalized, curveid, out_priv, out_priv_len); +} + +int ensure_ec_private_key(const char *input_or_path, mbedtls_ecp_group_id curveid, uint8_t *out_priv, size_t out_priv_len) { + return pcrypto_parse_ec_private_text(input_or_path, true, curveid, out_priv, out_priv_len); +} + void des_encrypt(void *out, const void *in, const void *key) { mbedtls_des_context ctx; mbedtls_des_setkey_enc(&ctx, key); diff --git a/client/src/crypto/libpcrypto.h b/client/src/crypto/libpcrypto.h index 7b086bf33..227cf7f5d 100644 --- a/client/src/crypto/libpcrypto.h +++ b/client/src/crypto/libpcrypto.h @@ -22,11 +22,23 @@ #include #include #include +#include +#include #include #define CRYPTO_AES_BLOCK_SIZE 16 #define CRYPTO_AES128_KEY_SIZE 16 +typedef struct { + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + bool seeded; +} pcrypto_rng_t; + +int pcrypto_rng_init(pcrypto_rng_t *rng, const uint8_t *personalization, size_t personalization_len); +void pcrypto_rng_free(pcrypto_rng_t *rng); +int pcrypto_rng_fill(pcrypto_rng_t *rng, uint8_t *out, size_t out_len); + void des_encrypt(void *out, const void *in, const void *key); void des_decrypt(void *out, const void *in, const void *key); void des_encrypt_ecb(void *out, const void *in, const int length, const void *key); @@ -54,6 +66,7 @@ int ecdsa_public_key_from_pk(mbedtls_pk_context *pk, mbedtls_ecp_group_id curvei int ecdsa_signature_create(mbedtls_ecp_group_id curveid, uint8_t *key_d, uint8_t *key_xy, uint8_t *input, int length, uint8_t *signature, size_t *signaturelen, bool hash); int ecdsa_signature_verify(mbedtls_ecp_group_id curveid, uint8_t *key_xy, uint8_t *input, int length, uint8_t *signature, size_t signaturelen, bool hash); int ecdsa_signature_r_s_verify(mbedtls_ecp_group_id curveid, uint8_t *key_xy, uint8_t *input, int length, uint8_t *r_s, size_t r_s_len, bool hash); +int ensure_ec_private_key(const char *input_or_path, mbedtls_ecp_group_id curveid, uint8_t *out_priv, size_t out_priv_len); char *ecdsa_get_error(int ret); diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 929eb0aa9..053e78a77 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -282,6 +282,11 @@ const static vocabulary_t vocabulary[] = { { 1, "hf gallagher diversifykey" }, { 1, "hf gallagher decode" }, { 1, "hf gallagher encode" }, + { 1, "hf gst help" }, + { 1, "hf gst list" }, + { 1, "hf gst test" }, + { 0, "hf gst info" }, + { 0, "hf gst read" }, { 1, "hf iclass help" }, { 1, "hf iclass list" }, { 0, "hf iclass dump" }, diff --git a/client/src/ui.c b/client/src/ui.c index c08f6808e..1a5f81a7b 100644 --- a/client/src/ui.c +++ b/client/src/ui.c @@ -232,6 +232,42 @@ void PrintAndLogOptions(const char *str[][2], size_t size, size_t space) { PrintAndLogEx(NORMAL, "%s", buff); } +void PrintAndLogInfoHeaderWithWidth(const char *title, size_t width) { + if (title == NULL) { + return; + } + + const size_t title_len = strlen(title); + if (width > (MAX_PRINT_BUFFER - 1)) { + width = MAX_PRINT_BUFFER - 1; + } + + char dashes[MAX_PRINT_BUFFER] = {0}; + memset(dashes, '-', sizeof(dashes) - 1); + + if (title_len + 2 >= width) { + PrintAndLogEx(INFO, _CYAN_("%s"), title); + return; + } + + size_t dash_count = width - (title_len + 2); + size_t left = dash_count / 2; + size_t right = dash_count - left; + if (left > (sizeof(dashes) - 1)) { + left = sizeof(dashes) - 1; + } + if (right > (sizeof(dashes) - 1)) { + right = sizeof(dashes) - 1; + } + + PrintAndLogEx(INFO, "%.*s " _CYAN_("%s") " %.*s", + (int)left, dashes, title, (int)right, dashes); +} + +void PrintAndLogInfoHeader(const char *title) { + PrintAndLogInfoHeaderWithWidth(title, 82); +} + static uint8_t PrintAndLogEx_spinidx = 0; void PrintAndLogEx(logLevel_t level, const char *fmt, ...) { diff --git a/client/src/ui.h b/client/src/ui.h index b79798862..eb8dc4a94 100644 --- a/client/src/ui.h +++ b/client/src/ui.h @@ -78,6 +78,8 @@ extern session_arg_t g_session; #define PROMPT_CLEARLINE PrintAndLogEx(INPLACE, " \r") void PrintAndLogOptions(const char *str[][2], size_t size, size_t space); void PrintAndLogEx(logLevel_t level, const char *fmt, ...); +void PrintAndLogInfoHeaderWithWidth(const char *title, size_t width); +void PrintAndLogInfoHeader(const char *title); void SetFlushAfterWrite(bool value); bool GetFlushAfterWrite(void); void memcpy_filter_ansi(void *dest, const void *src, size_t n, bool filter); diff --git a/client/src/util.c b/client/src/util.c index d9a8c49e4..0cb222326 100644 --- a/client/src/util.c +++ b/client/src/util.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include // Mingw @@ -672,6 +673,52 @@ int hex_to_bytes(const char *hexValue, uint8_t *bytesValue, size_t maxBytesValue return bytesValueLen; } +int parse_uint32_hex_or_dec(const char *text, uint32_t *out) { + if (text == NULL || out == NULL || text[0] == '\0') { + return PM3_EINVARG; + } + + int base = 10; + if (text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) { + base = 16; + } else { + for (const char *p = text; *p; p++) { + if (isalpha((unsigned char)*p)) { + base = 16; + break; + } + } + } + + errno = 0; + char *end = NULL; + unsigned long value = strtoul(text, &end, base); + if (errno == ERANGE || end == text || end == NULL || *end != '\0' || value > UINT32_MAX) { + return PM3_EINVARG; + } + + *out = (uint32_t)value; + return PM3_SUCCESS; +} + +bool bytes_equal_not_null(const void *a, size_t a_len, const void *b, size_t b_len) { + return a_len == b_len && a != NULL && b != NULL && memcmp(a, b, a_len) == 0; +} + +int buffer_append_bytes_with_offset(uint8_t *buf, size_t buf_len, size_t *offset, const void *data, size_t data_len) { + if (buf == NULL || offset == NULL || (data_len > 0 && data == NULL)) { + return PM3_EINVARG; + } + if (*offset > buf_len || data_len > (buf_len - *offset)) { + return PM3_EOVFLOW; + } + if (data_len > 0) { + memcpy(buf + *offset, data, data_len); + *offset += data_len; + } + return PM3_SUCCESS; +} + // takes a number (uint64_t) and creates a binarray in dest. void num_to_bytebits(uint64_t n, size_t len, uint8_t *dest) { while (len--) { @@ -1368,6 +1415,19 @@ void clean_ascii(unsigned char *buf, size_t len) { } } +bool is_printable_ascii(const uint8_t *data, size_t data_len) { + if (data == NULL || data_len == 0) { + return false; + } + + for (size_t i = 0; i < data_len; i++) { + if (data[i] < 0x20 || data[i] > 0x7E) { + return false; + } + } + return true; +} + bool decode_zero_padded_ascii(const uint8_t *data, size_t data_len, char *out, size_t out_len) { if (data == NULL || out == NULL || out_len == 0) { return false; diff --git a/client/src/util.h b/client/src/util.h index 2a5250489..cb3a1555c 100644 --- a/client/src/util.h +++ b/client/src/util.h @@ -103,6 +103,9 @@ void print_buffer(const uint8_t *data, const size_t len, int level); void print_blocks(uint32_t *data, size_t len); int hex_to_bytes(const char *hexValue, uint8_t *bytesValue, size_t maxBytesValueLen); +int parse_uint32_hex_or_dec(const char *text, uint32_t *out); +bool bytes_equal_not_null(const void *a, size_t a_len, const void *b, size_t b_len); +int buffer_append_bytes_with_offset(uint8_t *buf, size_t buf_len, size_t *offset, const void *data, size_t data_len); void num_to_bytebits(uint64_t n, size_t len, uint8_t *dest); void num_to_bytebitsLSBF(uint64_t n, size_t len, uint8_t *dest); void bytes_to_bytebits(const void *src, const size_t srclen, void *dest); @@ -166,6 +169,7 @@ void strn_upper(char *s, size_t n); bool str_startswith(const char *s, const char *pre); // check for prefix in string bool str_endswith(const char *s, const char *suffix); // check for suffix in string void clean_ascii(unsigned char *buf, size_t len); +bool is_printable_ascii(const uint8_t *data, size_t data_len); bool decode_zero_padded_ascii(const uint8_t *data, size_t data_len, char *out, size_t out_len); void str_cleanrn(char *buf, size_t len); void str_creplace(char *buf, size_t len, char from, char to); diff --git a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md index be1b4e136..2f1a198ca 100644 --- a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md @@ -60,7 +60,7 @@ Install the requirements ```sh sudo apt-get install --no-install-recommends git ca-certificates build-essential pkg-config \ libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev \ -libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev +libbz2-dev liblz4-dev zlib1g-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev ``` ### If you don't need... @@ -73,6 +73,9 @@ you can skip the installation of `qt6-base-dev`. 👉 If you don't need support for Python3 scripts in the Proxmark3 client, you can skip the installation of `libpython3-dev`. +👉 If you don't need support for decompressing compressed Google Smart Tap payloads in the Proxmark3 client, +you can skip the installation of `zlib1g-dev`. + 👉 If you don't need support for NFC ePaper devices, you can skip the installation of `libgd-dev`. diff --git a/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md b/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md index 0b4ea8383..021b70578 100644 --- a/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md @@ -175,9 +175,13 @@ These instructions will show how to setup the environment on OSX to the point wh 2. Install dependencies: ``` -brew install readline qt gd pkgconfig coreutils openssl +brew install readline qt gd pkgconfig coreutils openssl zlib brew install RfidResearchGroup/proxmark3/arm-none-eabi-gcc ``` + +If you don't need support for decompressing compressed Google Smart Tap payloads in the Proxmark3 client, +you can skip the installation of `zlib`. + 3. (optional) Install makefile dependencies: ``` brew install recode diff --git a/tools/pm3_tests.sh b/tools/pm3_tests.sh index e94d5096e..f0aa9cbe5 100755 --- a/tools/pm3_tests.sh +++ b/tools/pm3_tests.sh @@ -626,6 +626,7 @@ while true; do if ! CheckExecute "emv test" "$CLIENTBIN -c 'emv test'" "Tests \( ok"; then break; fi if ! CheckExecute "hf cipurse test" "$CLIENTBIN -c 'hf cipurse test'" "Tests \( ok"; then break; fi if ! CheckExecute "hf mfdes test" "$CLIENTBIN -c 'hf mfdes test'" "Tests \( ok"; then break; fi + if ! CheckExecute "hf gst test" "$CLIENTBIN -c 'hf gst test'" "Tests \( ok"; then break; fi if ! CheckExecute "hf waveshare load" "$CLIENTBIN -c 'hf waveshare load -m 6 -f tools/lena.bmp -s dither.bmp' && echo '34ff55fe7257876acf30dae00eb0e439 dither.bmp' | md5sum -c -" "dither.bmp: OK"; then break; fi fi echo -e "\n------------------------------------------------------------"