From 0ab4cc161bb4c4695e044d131b666071243b8491 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:44:49 +0200 Subject: [PATCH] Implement 'hf vas info' command --- CHANGELOG.md | 1 + client/src/cmdhfvas.c | 157 ++++++++++++++++++++++++++++++++ client/src/pm3line_vocabulary.h | 1 + 3 files changed, 159 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03bbed800..1ca171090 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] +- Added `hf vas info` command (@kormax) - Changed `wiegand encode` / `wiegand decode` - added support for encoding and decoding the new 96-bit ASN.1 encoded format, `--bin` encoding, verbose PACS encoding output, and explicit rejection of raw/binary decodes above 96 bits (@cindersocket) - Added Mifare Classic support to `hf gallagher` command (@pingu2211) - Added `hf felica discnodes` command (@kormax) diff --git a/client/src/cmdhfvas.c b/client/src/cmdhfvas.c index 9ce6e70e4..f6664b7c5 100644 --- a/client/src/cmdhfvas.c +++ b/client/src/cmdhfvas.c @@ -59,6 +59,52 @@ uint8_t aid[] = { 0x4f, 0x53, 0x45, 0x2e, 0x56, 0x41, 0x53, 0x2e, 0x30, 0x31 }; uint8_t getVasUrlOnlyP2 = 0x00; uint8_t getVasFullReqP2 = 0x01; +static bool VASWalletTypeIsApplePay(const uint8_t *walletType, size_t walletTypeLen) { + static const uint8_t applePayWalletType[] = "ApplePay"; + return walletType != NULL + && walletTypeLen == (sizeof(applePayWalletType) - 1) + && memcmp(walletType, applePayWalletType, sizeof(applePayWalletType) - 1) == 0; +} + +static void PrintVASFeatureBit(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 PrintVASCapabilitiesMeaning(const struct tlv *capabilities) { + if (capabilities == NULL) { + return; + } + + if (capabilities->len != 4) { + PrintAndLogEx(WARNING, "Capabilities: expected 4 bytes, got %zu", capabilities->len); + return; + } + + const uint8_t leading0 = capabilities->value[0]; + const uint8_t leading1 = capabilities->value[1]; + const uint8_t leading2 = capabilities->value[2]; + const uint8_t mask = capabilities->value[3]; + const char *bits = sprint_bin(&mask, 1); + + if (leading0 != 0x00 || leading1 != 0x00 || leading2 != 0x00) { + PrintAndLogEx(WARNING, " Mobile caps.... leading bytes non-zero (%02X %02X %02X); only last byte is interpreted", + leading0, leading1, leading2); + } + + PrintAndLogEx(INFO, " Capabilities.. " _YELLOW_("%s") " (" _YELLOW_("0x%02X") ")", bits, mask); + PrintVASFeatureBit(bits, mask, 7, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + PrintVASFeatureBit(bits, mask, 6, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + PrintVASFeatureBit(bits, mask, 5, "Payment may be performed", "Payment may not be performed"); + PrintVASFeatureBit(bits, mask, 4, "Payment may be skipped", "Payment may not be skipped"); + PrintVASFeatureBit(bits, mask, 3, "VAS may be performed", "VAS may not be performed"); + PrintVASFeatureBit(bits, mask, 2, "VAS may be skipped", "VAS may not be skipped"); + PrintVASFeatureBit(bits, mask, 1, "Encrypted VAS data supported", "Encrypted VAS data not supported"); + PrintVASFeatureBit(bits, mask, 0, "Plaintext VAS data supported", "Plaintext VAS data not supported"); +} + static int ParseSelectVASResponse(const uint8_t *response, size_t resLen, bool verbose) { struct tlvdb *tlvRoot = tlvdb_parse_multi(response, resLen); @@ -99,6 +145,92 @@ static int ParseSelectVASResponse(const uint8_t *response, size_t resLen, bool v return PM3_SUCCESS; } +static int info_vas(void) { + clearCommandBuffer(); + + iso14a_polling_parameters_t polling_parameters = { + .frames = { WUPA_FRAME, ECP_VAS_ONLY_FRAME }, + .frame_count = 2, + .extra_timeout = 250 + }; + + if (SelectCard14443A_4_WithParameters(false, false, NULL, &polling_parameters) != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "No ISO14443-A Card in field"); + return PM3_ECARDEXCHANGE; + } + + uint16_t status = 0; + size_t responseLen = 0; + uint8_t selectResponse[APDU_RES_LEN] = {0}; + Iso7816Select(CC_CONTACTLESS, false, true, aid, sizeof(aid), selectResponse, APDU_RES_LEN, &responseLen, &status); + DropField(); + + if (status != 0x9000) { + PrintAndLogEx(FAILED, "Card doesn't support VAS"); + return PM3_ECARDEXCHANGE; + } + + struct tlvdb *tlvRoot = tlvdb_parse_multi(selectResponse, responseLen); + if (tlvRoot == NULL) { + PrintAndLogEx(FAILED, "Unable to parse VAS select response"); + return PM3_ECARDEXCHANGE; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("VAS Applet Information") " ------------------------"); + + const struct tlvdb *walletTypeTlv = tlvdb_find_full(tlvRoot, 0x50); + bool skip_vas_details = false; + if (walletTypeTlv == NULL) { + PrintAndLogEx(WARNING, "Wallet type.......... " _YELLOW_("not present")); + } else { + const struct tlv *walletType = tlvdb_get_tlv(walletTypeTlv); + PrintAndLogEx(INFO, "Wallet type.......... " _YELLOW_("%s"), sprint_ascii(walletType->value, walletType->len)); + if (VASWalletTypeIsApplePay(walletType->value, walletType->len) == false) { + PrintAndLogEx(WARNING, "Wallet type is not ApplePay. This likely isn't Apple VAS."); + skip_vas_details = true; + } + } + + if (skip_vas_details == false) { + const struct tlvdb *versionTlv = tlvdb_find_full(tlvRoot, 0x9F21); + if (versionTlv == NULL) { + PrintAndLogEx(WARNING, "VAS version.......... " _YELLOW_("not present")); + } else { + const struct tlv *version = tlvdb_get_tlv(versionTlv); + if (version->len == 2) { + PrintAndLogEx(INFO, "VAS version.......... " _YELLOW_("%d.%d"), version->value[0], version->value[1]); + } else { + PrintAndLogEx(WARNING, "VAS version.......... " _YELLOW_("invalid length (%zu)"), version->len); + } + } + + const struct tlvdb *nonceTlv = tlvdb_find_full(tlvRoot, 0x9F24); + if (nonceTlv == NULL) { + PrintAndLogEx(WARNING, "Device nonce......... " _YELLOW_("not present")); + } else { + const struct tlv *nonce = tlvdb_get_tlv(nonceTlv); + PrintAndLogEx(INFO, "Device nonce......... " _YELLOW_("%s"), sprint_hex_inrow(nonce->value, nonce->len)); + if (nonce->len != 4) { + PrintAndLogEx(WARNING, "Device nonce......... " _YELLOW_("unexpected length (%zu)"), nonce->len); + } + } + + const struct tlvdb *capabilitiesTlv = tlvdb_find_full(tlvRoot, 0x9F23); + if (capabilitiesTlv == NULL) { + PrintAndLogEx(WARNING, "Mobile capabilities.. " _YELLOW_("not present")); + } else { + const struct tlv *capabilities = tlvdb_get_tlv(capabilitiesTlv); + PrintAndLogEx(INFO, "Mobile capabilities.. " _YELLOW_("%s"), sprint_hex_inrow(capabilities->value, capabilities->len)); + PrintVASCapabilitiesMeaning(capabilities); + } + } + + tlvdb_free(tlvRoot); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; +} + static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size_t urlLen, uint8_t *out, int *outLen) { if (pidHash == NULL && url == NULL) { PrintAndLogEx(FAILED, "Must provide a Pass Type ID or a URL"); @@ -507,6 +639,30 @@ static int CmdVASReader(const char *Cmd) { return res; } +static int CmdVASInfo(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf vas info", + "Select VAS applet and print capabilities.", + "hf vas info\n" + "hf vas info -a"); + + void *argtable[] = { + arg_param_begin, + arg_lit0("a", "apdu", "Show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + bool apdu_logging = arg_get_lit(ctx, 1); + CLIParserFree(ctx); + + bool restore_apdu_logging = GetAPDULogging(); + SetAPDULogging(apdu_logging); + int res = info_vas(); + SetAPDULogging(restore_apdu_logging); + return res; +} + static int CmdVASDecrypt(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf vas decrypt", @@ -582,6 +738,7 @@ static command_t CommandTable[] = { {"--------", CmdHelp, AlwaysAvailable, "----------- " _CYAN_("Value Added Service") " -----------"}, {"help", CmdHelp, AlwaysAvailable, "This help"}, {"--------", CmdHelp, AlwaysAvailable, "----------------- " _CYAN_("General") " -----------------"}, + {"info", CmdVASInfo, IfPm3Iso14443a, "Get VAS applet information"}, {"reader", CmdVASReader, IfPm3Iso14443a, "Read and decrypt VAS message"}, {"decrypt", CmdVASDecrypt, AlwaysAvailable, "Decrypt a previously captured VAS cryptogram"}, {NULL, NULL, NULL, NULL} diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 211b3efc8..763a82935 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -556,6 +556,7 @@ const static vocabulary_t vocabulary[] = { { 1, "hf topaz view" }, { 0, "hf topaz wrbl" }, { 1, "hf vas help" }, + { 0, "hf vas info" }, { 0, "hf vas reader" }, { 1, "hf vas decrypt" }, { 1, "hf waveshare help" },