diff --git a/client/src/cmdhfvas.c b/client/src/cmdhfvas.c index dab9a5ba6..cc78f929f 100644 --- a/client/src/cmdhfvas.c +++ b/client/src/cmdhfvas.c @@ -28,8 +28,10 @@ #include "util.h" #include "util_posix.h" #include "iso7816/iso7816core.h" +#include "crc16.h" #include #include +#include #include "mifare.h" #include #include @@ -43,6 +45,10 @@ #include "mbedtls/entropy.h" #define VAS_MAX_KEY_INPUT 8192 +#define VAS_MAX_PID_ITEMS 8 +#define VAS_MAX_KEY_ITEMS 16 +#define VAS_MAX_MOBILE_TOKEN_LEN 32 +#define VAS_MAX_CRYPTOGRAM_LEN 256 static const iso14a_polling_frame_t WUPA_FRAME = { .frame = { 0x52 }, @@ -58,9 +64,58 @@ static const iso14a_polling_frame_t ECP_VAS_ONLY_FRAME = { .extra_delay = 0, }; -uint8_t aid[] = { 0x4f, 0x53, 0x45, 0x2e, 0x56, 0x41, 0x53, 0x2e, 0x30, 0x31 }; -uint8_t getVasUrlOnlyP2 = 0x00; -uint8_t getVasFullReqP2 = 0x01; + +enum { + VAS_MODE_VAS_OR_PAY = 0x00, + VAS_MODE_VAS_AND_PAY = 0x01, + VAS_MODE_VAS_ONLY = 0x02, +}; + +static int vas_parse_mode(const char *mode_text, uint8_t *mode_out) { + if (mode_out == NULL) { + return PM3_EINVARG; + } + + if (mode_text == NULL || *mode_text == '\0' || strcmp(mode_text, "vasonly") == 0 || strcmp(mode_text, "vas") == 0) { + *mode_out = VAS_MODE_VAS_ONLY; + return PM3_SUCCESS; + } + if (strcmp(mode_text, "vasandpay") == 0) { + *mode_out = VAS_MODE_VAS_AND_PAY; + return PM3_SUCCESS; + } + if (strcmp(mode_text, "vasorpay") == 0) { + *mode_out = VAS_MODE_VAS_OR_PAY; + return PM3_SUCCESS; + } + + PrintAndLogEx(FAILED, "Invalid mode '%s' (expected: vasorpay, vasandpay, vasonly)", mode_text); + return PM3_EINVARG; +} + +static void vas_build_ecp_frame(uint8_t vas_mode, iso14a_polling_frame_t *frame_out) { + if (frame_out == NULL) { + return; + } + + memset(frame_out, 0, sizeof(*frame_out)); + frame_out->frame[0] = 0x6a; + frame_out->frame[1] = 0x01; + frame_out->frame[2] = 0x00; + frame_out->frame[3] = 0x00; + frame_out->frame[4] = vas_mode; + compute_crc(CRC_14443_A, frame_out->frame, 5, &frame_out->frame[5], &frame_out->frame[6]); + frame_out->frame_length = 7; + frame_out->last_byte_bits = 8; + frame_out->extra_delay = 0; +} + +static const uint8_t aid[] = { 0x4f, 0x53, 0x45, 0x2e, 0x56, 0x41, 0x53, 0x2e, 0x30, 0x31 }; +static const uint8_t getVasUrlOnlyP2 = 0x00; +static const uint8_t getVasFullReqP2 = 0x01; +static const uint8_t kVasEncryptedDataLabel[27] = "ApplePay encrypted VAS data"; +static const uint8_t kVasAesGcmLabel[13] = "id-aes256-GCM"; + static bool VASWalletTypeIsApplePay(const uint8_t *walletType, size_t walletTypeLen) { static const uint8_t applePayWalletType[] = "ApplePay"; @@ -69,6 +124,8 @@ static bool VASWalletTypeIsApplePay(const uint8_t *walletType, size_t walletType && memcmp(walletType, applePayWalletType, sizeof(applePayWalletType) - 1) == 0; } +static const uint16_t VAS_STATUS_NOT_AVAILABLE = 0xFFFF; + 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; @@ -76,28 +133,13 @@ static void PrintVASFeatureBit(const char *bits, uint8_t mask, uint8_t bit, cons 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; +static void PrintVASCapabilitiesValue(const char *label, uint8_t mask) { + if (label == NULL || *label == '\0') { + label = "Capabilities"; } - 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); + PrintAndLogEx(INFO, "%s " _YELLOW_("%s") " (" _YELLOW_("0x%02X") ")", label, 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"); @@ -108,7 +150,76 @@ static void PrintVASCapabilitiesMeaning(const struct tlv *capabilities) { 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) { +static const char *vas_status_name(uint16_t sw) { + static char unknown_status_name[20] = {0}; + + switch (sw) { + case VAS_STATUS_NOT_AVAILABLE: + return "N/A"; + case 0x9000: + return "OK"; + case 0x6100: + return "WARNING_NO_DATA_RETURNED"; + case 0x6287: + return "DATA_NOT_ACTIVATED"; + case 0x6982: + return "SECURITY_STATUS_NOT_SATISFIED"; + case 0x6984: + return "USER_INTERVENTION_REQUIRED"; + case 0x6A81: + return "FUNCTION_NOT_SUPPORTED"; + case 0x6A82: + return "FILE_NOT_FOUND"; + case 0x6700: + return "WRONG_LC_FIELD"; + case 0x6A80: + return "INCORRECT_DATA"; + case 0x6A83: + return "DATA_NOT_FOUND"; + case 0x6B00: + return "WRONG_PARAMETERS"; + } + snprintf(unknown_status_name, sizeof(unknown_status_name), "UNKNOWN (0x%04X)", sw); + return unknown_status_name; +} + +static bool vas_status_is_success(uint16_t sw) { + const uint8_t sw1 = (uint8_t)(sw >> 8); + return (sw1 == 0x90) || (sw1 == 0x91); +} + +static void PrintVASStatusLine(const char *label, uint16_t sw) { + PrintAndLogEx(INFO, "%s " _YELLOW_("%04X") " (%s)", label, sw, vas_status_name(sw)); +} + +static const char *vas_status_meaning(uint16_t sw) { + switch (sw) { + case VAS_STATUS_NOT_AVAILABLE: + return "No ISO14443-A card in field"; + case 0x6287: + return "Data not activated (device locked or authentication required)"; + case 0x6984: + return "User intervention/selection required"; + case 0x6A83: + return "Data not found for this pass identifier"; + case 0x9000: + return "Success"; + default: + return "Request failed"; + } +} + +static void PrintVASFailureReason(uint16_t select_status, uint16_t get_data_status) { + if (select_status == VAS_STATUS_NOT_AVAILABLE && get_data_status == VAS_STATUS_NOT_AVAILABLE) { + PrintAndLogEx(WARNING, "%s: %s", vas_status_name(VAS_STATUS_NOT_AVAILABLE), vas_status_meaning(VAS_STATUS_NOT_AVAILABLE)); + } else if (select_status != 0x9000) { + PrintAndLogEx(FAILED, "%s: OSE/VAS applet not selected", vas_status_name(select_status)); + } else if (get_data_status != VAS_STATUS_NOT_AVAILABLE) { + PrintAndLogEx(FAILED, "%s: %s", vas_status_name(get_data_status), vas_status_meaning(get_data_status)); + } +} + +static int ParseSelectVASResponse(const uint8_t *response, size_t resLen, uint8_t *capabilitiesOut) { struct tlvdb *tlvRoot = tlvdb_parse_multi(response, resLen); const struct tlvdb *versionTlv = tlvdb_find_full(tlvRoot, 0x9F21); @@ -121,9 +232,6 @@ static int ParseSelectVASResponse(const uint8_t *response, size_t resLen, bool v tlvdb_free(tlvRoot); return PM3_ECARDEXCHANGE; } - if (verbose) { - PrintAndLogEx(INFO, "Mobile VAS application version: %d.%d", version->value[0], version->value[1]); - } if (version->value[0] != 0x01 || version->value[1] != 0x00) { tlvdb_free(tlvRoot); return PM3_ECARDEXCHANGE; @@ -143,47 +251,24 @@ static int ParseSelectVASResponse(const uint8_t *response, size_t resLen, bool v tlvdb_free(tlvRoot); return PM3_ECARDEXCHANGE; } + if (capabilitiesOut != NULL) { + *capabilitiesOut = capabilities->value[3]; + } tlvdb_free(tlvRoot); 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); +static int PrintVASSelectInfo(const uint8_t *response, size_t responseLen) { + struct tlvdb *tlvRoot = tlvdb_parse_multi(response, 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); + PrintAndLogEx(INFO, ""); + PrintAndLogInfoHeader("OSE Information"); bool skip_vas_details = false; + const struct tlvdb *walletTypeTlv = tlvdb_find_full(tlvRoot, 0x50); if (walletTypeTlv == NULL) { PrintAndLogEx(WARNING, "Wallet type.......... " _YELLOW_("not present")); } else { @@ -224,17 +309,19 @@ static int info_vas(void) { 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); + if (capabilities->len != 4) { + PrintAndLogEx(WARNING, "Mobile capabilities.. " _YELLOW_("invalid length (%zu)"), capabilities->len); + } else { + PrintVASCapabilitiesValue("Mobile capabilities..", capabilities->value[3]); + } } } 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) { +static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size_t urlLen, uint8_t vas_mode, bool isFinalRequest, uint8_t *out, int *outLen) { if (pidHash == NULL && url == NULL) { PrintAndLogEx(FAILED, "Must provide a Pass Type ID or a URL"); return PM3_EINVARG; @@ -245,7 +332,7 @@ static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size return PM3_EINVARG; } - uint8_t p2 = pidHash == NULL ? getVasUrlOnlyP2 : getVasFullReqP2; + const uint8_t p2 = pidHash == NULL ? getVasUrlOnlyP2 : getVasFullReqP2; size_t reqTlvLen = 19 + (pidHash != NULL ? 35 : 0) + (url != NULL ? 3 + urlLen : 0); uint8_t *reqTlv = calloc(reqTlvLen, sizeof(uint8_t)); @@ -257,14 +344,18 @@ static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size uint8_t version[] = {0x9F, 0x22, 0x02, 0x01, 0x00}; memcpy(reqTlv, version, sizeof(version)); - uint8_t unknown[] = {0x9F, 0x28, 0x04, 0x00, 0x00, 0x00, 0x00}; - memcpy(reqTlv + sizeof(version), unknown, sizeof(unknown)); + uint8_t terminalNonce[] = {0x9F, 0x28, 0x04, 0x00, 0x00, 0x00, 0x00}; + for (size_t i = 0; i < 4; i++) { + terminalNonce[3 + i] = (uint8_t)(rand() & 0xFF); + } + memcpy(reqTlv + sizeof(version), terminalNonce, sizeof(terminalNonce)); - uint8_t terminalCapabilities[] = {0x9F, 0x26, 0x04, 0x00, 0x00, 0x00, 0x02}; - memcpy(reqTlv + sizeof(version) + sizeof(unknown), terminalCapabilities, sizeof(terminalCapabilities)); + uint8_t capabilitiesMask = (isFinalRequest ? 0x00 : 0x80) | (vas_mode & 0x03); + uint8_t terminalCapabilities[] = {0x9F, 0x26, 0x04, 0x00, 0x80, 0x00, capabilitiesMask}; + memcpy(reqTlv + sizeof(version) + sizeof(terminalNonce), terminalCapabilities, sizeof(terminalCapabilities)); if (pidHash != NULL) { - size_t offset = sizeof(version) + sizeof(unknown) + sizeof(terminalCapabilities); + size_t offset = sizeof(version) + sizeof(terminalNonce) + sizeof(terminalCapabilities); reqTlv[offset] = 0x9F; reqTlv[offset + 1] = 0x25; reqTlv[offset + 2] = 32; @@ -272,7 +363,7 @@ static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size } if (url != NULL) { - size_t offset = sizeof(version) + sizeof(unknown) + sizeof(terminalCapabilities) + (pidHash != NULL ? 35 : 0); + size_t offset = sizeof(version) + sizeof(terminalNonce) + sizeof(terminalCapabilities) + (pidHash != NULL ? 35 : 0); reqTlv[offset] = 0x9F; reqTlv[offset + 1] = 0x29; reqTlv[offset + 2] = urlLen; @@ -293,23 +384,56 @@ static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size return PM3_SUCCESS; } -static int ParseGetVASDataResponse(const uint8_t *res, size_t resLen, uint8_t *cryptogram, size_t *cryptogramLen) { +static int ParseGetVASDataResponse(const uint8_t *res, size_t resLen, + bool requireCryptogram, + uint8_t *mobileToken, size_t *mobileTokenLen, + uint8_t *cryptogram, size_t *cryptogramLen) { struct tlvdb *tlvRoot = tlvdb_parse_multi(res, resLen); if (tlvRoot == NULL) { return PM3_ECARDEXCHANGE; } + bool has_payload = false; + + if (mobileTokenLen != NULL) { + *mobileTokenLen = 0; + } + const struct tlvdb *mobileTokenTlvdb = tlvdb_find_full(tlvRoot, 0x9F2A); + if (mobileTokenTlvdb != NULL && mobileToken != NULL && mobileTokenLen != NULL) { + const struct tlv *mobileTokenTlv = tlvdb_get_tlv(mobileTokenTlvdb); + if (mobileTokenTlv->len > VAS_MAX_MOBILE_TOKEN_LEN) { + tlvdb_free(tlvRoot); + return PM3_ECARDEXCHANGE; + } + memcpy(mobileToken, mobileTokenTlv->value, mobileTokenTlv->len); + *mobileTokenLen = mobileTokenTlv->len; + has_payload = true; + } + const struct tlvdb *cryptogramTlvdb = tlvdb_find_full(tlvRoot, 0x9F27); if (cryptogramTlvdb == NULL) { tlvdb_free(tlvRoot); - return PM3_ECARDEXCHANGE; + if (requireCryptogram || !has_payload) { + return PM3_ECARDEXCHANGE; + } + return PM3_SUCCESS; } - const struct tlv *cryptogramTlv = tlvdb_get_tlv(cryptogramTlvdb); - memcpy(cryptogram, cryptogramTlv->value, cryptogramTlv->len); - *cryptogramLen = cryptogramTlv->len; + if (cryptogram != NULL && cryptogramLen != NULL) { + const struct tlv *cryptogramTlv = tlvdb_get_tlv(cryptogramTlvdb); + if (cryptogramTlv->len > VAS_MAX_CRYPTOGRAM_LEN) { + tlvdb_free(tlvRoot); + return PM3_ECARDEXCHANGE; + } + memcpy(cryptogram, cryptogramTlv->value, cryptogramTlv->len); + *cryptogramLen = cryptogramTlv->len; + has_payload = true; + } tlvdb_free(tlvRoot); + if (!has_payload) { + return PM3_ECARDEXCHANGE; + } return PM3_SUCCESS; } @@ -362,7 +486,7 @@ static int LoadReaderPrivateKey(const char *input_or_path, mbedtls_ecp_keypair * return PM3_SUCCESS; } -static int GetPrivateKeyHint(mbedtls_ecp_keypair *privKey, uint8_t *keyHint) { +static int GetPrivateKeyId(mbedtls_ecp_keypair *privKey, uint8_t *keyId) { uint8_t xcoord[32] = {0}; if (mbedtls_mpi_write_binary(&privKey->Q.X, xcoord, sizeof(xcoord))) { return PM3_EINVARG; @@ -371,7 +495,7 @@ static int GetPrivateKeyHint(mbedtls_ecp_keypair *privKey, uint8_t *keyHint) { uint8_t hash[32] = {0}; sha256hash(xcoord, 32, hash); - memcpy(keyHint, hash, 4); + memcpy(keyId, hash, 4); return PM3_SUCCESS; } @@ -394,10 +518,10 @@ static int LoadMobileEphemeralKey(const uint8_t *xcoordBuf, mbedtls_ecp_keypair } static int internalVasDecrypt(uint8_t *cipherText, size_t cipherTextLen, uint8_t *sharedSecret, - uint8_t *ansiSharedInfo, size_t ansiSharedInfoLen, + const uint8_t *ansiSharedInfo, size_t ansiSharedInfoLen, const uint8_t *gcmAad, size_t gcmAadLen, uint8_t *out, size_t *outLen) { uint8_t key[32] = {0}; - if (ansi_x963_sha256(sharedSecret, 32, ansiSharedInfo, ansiSharedInfoLen, sizeof(key), key)) { + if (ansi_x963_sha256(sharedSecret, 32, (uint8_t *)ansiSharedInfo, ansiSharedInfoLen, sizeof(key), key)) { PrintAndLogEx(FAILED, "ANSI X9.63 key derivation failed"); return PM3_EINVARG; } @@ -424,14 +548,16 @@ static int internalVasDecrypt(uint8_t *cipherText, size_t cipherTextLen, uint8_t } static int DecryptVASCryptogram(uint8_t *pidHash, uint8_t *cryptogram, size_t cryptogramLen, mbedtls_ecp_keypair *privKey, uint8_t *out, size_t *outLen, uint32_t *timestamp) { - uint8_t keyHint[4] = {0}; - if (GetPrivateKeyHint(privKey, keyHint) != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "Unable to generate key hint"); + uint8_t keyId[4] = {0}; + if (GetPrivateKeyId(privKey, keyId) != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Unable to generate key id"); return PM3_EINVARG; } - if (memcmp(keyHint, cryptogram, 4) != 0) { + if (memcmp(keyId, cryptogram, 4) != 0) { PrintAndLogEx(FAILED, "Private key does not match cryptogram"); + PrintAndLogEx(INFO, "Key identifier........... " _YELLOW_("%s"), sprint_hex_inrow(keyId, sizeof(keyId))); + PrintAndLogEx(INFO, "Cryptogram data........ " _YELLOW_("%s"), sprint_hex_inrow(cryptogram, cryptogramLen)); return PM3_EINVARG; } @@ -468,19 +594,16 @@ static int DecryptVASCryptogram(uint8_t *pidHash, uint8_t *cryptogram, size_t cr } mbedtls_mpi_free(&sharedSecret); - uint8_t string1[27] = "ApplePay encrypted VAS data"; - uint8_t string2[13] = "id-aes256-GCM"; - - uint8_t method1SharedInfo[73] = {0}; - method1SharedInfo[0] = 13; - memcpy(method1SharedInfo + 1, string2, sizeof(string2)); - memcpy(method1SharedInfo + 1 + sizeof(string2), string1, sizeof(string1)); - memcpy(method1SharedInfo + 1 + sizeof(string2) + sizeof(string1), pidHash, 32); + uint8_t method1SharedInfo[1 + sizeof(kVasAesGcmLabel) + sizeof(kVasEncryptedDataLabel) + 32] = {0}; + method1SharedInfo[0] = sizeof(kVasAesGcmLabel); + memcpy(method1SharedInfo + 1, kVasAesGcmLabel, sizeof(kVasAesGcmLabel)); + memcpy(method1SharedInfo + 1 + sizeof(kVasAesGcmLabel), kVasEncryptedDataLabel, sizeof(kVasEncryptedDataLabel)); + memcpy(method1SharedInfo + 1 + sizeof(kVasAesGcmLabel) + sizeof(kVasEncryptedDataLabel), pidHash, 32); uint8_t decryptedData[68] = {0}; size_t decryptedDataLen = 0; if (internalVasDecrypt(cryptogram + 4 + 32, cryptogramLen - 4 - 32, sharedSecretBytes, method1SharedInfo, sizeof(method1SharedInfo), NULL, 0, decryptedData, &decryptedDataLen)) { - if (internalVasDecrypt(cryptogram + 4 + 32, cryptogramLen - 4 - 32, sharedSecretBytes, string1, sizeof(string1), pidHash, 32, decryptedData, &decryptedDataLen)) { + if (internalVasDecrypt(cryptogram + 4 + 32, cryptogramLen - 4 - 32, sharedSecretBytes, kVasEncryptedDataLabel, sizeof(kVasEncryptedDataLabel), pidHash, 32, decryptedData, &decryptedDataLen)) { return PM3_EINVARG; } } @@ -496,63 +619,214 @@ static int DecryptVASCryptogram(uint8_t *pidHash, uint8_t *cryptogram, size_t cr return PM3_SUCCESS; } -static int VASReader(uint8_t *pidHash, const char *url, size_t urlLen, uint8_t *cryptogram, size_t *cryptogramLen, bool verbose) { - clearCommandBuffer(); +typedef struct { + mbedtls_ecp_keypair key; + uint8_t key_id[4]; + const char *source; +} vas_reader_key_t; - iso14a_polling_parameters_t polling_parameters = { - .frames = { WUPA_FRAME, ECP_VAS_ONLY_FRAME }, - .frame_count = 2, - .extra_timeout = 250 - }; +static void VasReaderCleanup(vas_reader_key_t *keys, int keys_initialized, + char **key_values, int key_count, + char **pid_values, int pid_count) { + for (int i = 0; keys != NULL && i < keys_initialized; i++) { + mbedtls_ecp_keypair_free(&keys[i].key); + } + for (int i = 0; i < key_count; i++) { + free(key_values[i]); + } + for (int i = 0; i < pid_count; i++) { + free(pid_values[i]); + } +} - if (SelectCard14443A_4_WithParameters(false, false, NULL, &polling_parameters) != PM3_SUCCESS) { - PrintAndLogEx(WARNING, "No ISO14443-A Card in field"); - return PM3_ECARDEXCHANGE; +static void PrintVasCryptogramInfo(const char *label, const uint8_t *cryptogram, size_t clen) { + if (clen == 0) { + return; + } + PrintAndLogEx(INFO, "%s", label); + if (clen >= 4) { + PrintAndLogEx(INFO, " Key id...... " _YELLOW_("%s"), sprint_hex_inrow(cryptogram, 4)); + } + PrintAndLogEx(INFO, " Ciphertext.. " _YELLOW_("%s"), sprint_hex_inrow(cryptogram, clen)); +} + + +static int VASSelectOse(uint16_t *select_status_out) { + if (select_status_out != NULL) { + *select_status_out = VAS_STATUS_NOT_AVAILABLE; } uint16_t status = 0; size_t resLen = 0; uint8_t selectResponse[APDU_RES_LEN] = {0}; - Iso7816Select(CC_CONTACTLESS, false, true, aid, sizeof(aid), selectResponse, APDU_RES_LEN, &resLen, &status); + Iso7816Select(CC_CONTACTLESS, false, true, (uint8_t *)aid, sizeof(aid), selectResponse, APDU_RES_LEN, &resLen, &status); + + if (select_status_out != NULL) { + *select_status_out = status; + } if (status != 0x9000) { - PrintAndLogEx(FAILED, "Card doesn't support VAS"); return PM3_ECARDEXCHANGE; } - if (ParseSelectVASResponse(selectResponse, resLen, verbose) != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "Card doesn't support VAS"); + PrintVASSelectInfo(selectResponse, resLen); + if (ParseSelectVASResponse(selectResponse, resLen, NULL) != PM3_SUCCESS) { return PM3_ECARDEXCHANGE; } + return PM3_SUCCESS; +} + +static int VASGetData(const char *passIdentifier, const uint8_t *pidHash, bool hasPid, + const char *url, size_t urlLen, uint8_t vas_mode, bool isFinalRequest, + bool verbose, uint8_t *cryptogram, size_t *cryptogramLen, + uint16_t *get_data_status_out) { + if (get_data_status_out != NULL) { + *get_data_status_out = VAS_STATUS_NOT_AVAILABLE; + } + if (cryptogramLen != NULL) { + *cryptogramLen = 0; + } + + char pass_header[64] = {0}; + const char *displayedPassIdentifier = passIdentifier != NULL ? passIdentifier : "unknown-pass-identifier"; + snprintf(pass_header, sizeof(pass_header), "VAS Get Data %s", displayedPassIdentifier); + PrintAndLogInfoHeader(pass_header); + PrintAndLogEx(INFO, "Pass type id hash...... " _YELLOW_("%s"), hasPid ? sprint_hex_inrow(pidHash, 32) : "n/a"); + uint8_t getVasApdu[PM3_CMD_DATA_SIZE]; int getVasApduLen = 0; - - int s = CreateGetVASDataCommand(pidHash, url, urlLen, getVasApdu, &getVasApduLen); + int s = CreateGetVASDataCommand(pidHash, url, urlLen, vas_mode, isFinalRequest, getVasApdu, &getVasApduLen); if (s != PM3_SUCCESS) { return s; } uint8_t apduRes[APDU_RES_LEN] = {0}; - int apduResLen = 0; - - s = ExchangeAPDU14a(getVasApdu, getVasApduLen, false, false, apduRes, APDU_RES_LEN, &apduResLen); + size_t apduResLen = 0; + uint16_t getDataStatus = 0; + sAPDU_t getVasCmd = { + .CLA = getVasApdu[0], + .INS = getVasApdu[1], + .P1 = getVasApdu[2], + .P2 = getVasApdu[3], + .Lc = getVasApdu[4], + .data = getVasApdu + 5, + }; + s = Iso7816ExchangeEx(CC_CONTACTLESS, false, true, getVasCmd, false, 0x00, apduRes, APDU_RES_LEN, &apduResLen, &getDataStatus); if (s != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "Failed to send APDU"); return s; } - if (apduResLen == 2 && apduRes[0] == 0x62 && apduRes[1] == 0x87) { - PrintAndLogEx(WARNING, "Device returned error on GET VAS DATA. Either doesn't have pass with matching id, or requires user authentication."); - return PM3_ECARDEXCHANGE; + if (get_data_status_out != NULL) { + *get_data_status_out = getDataStatus; + } + + if (!vas_status_is_success(getDataStatus)) { + PrintVASStatusLine("GET VAS DATA status....", getDataStatus); + return PM3_SUCCESS; } if (apduResLen == 0 || apduRes[0] != 0x70) { - PrintAndLogEx(FAILED, "Invalid response from peer"); return PM3_ECARDEXCHANGE; } - return ParseGetVASDataResponse(apduRes, apduResLen, cryptogram, cryptogramLen); + uint8_t mobileToken[VAS_MAX_MOBILE_TOKEN_LEN] = {0}; + size_t mobileTokenLen = 0; + s = ParseGetVASDataResponse(apduRes, apduResLen, hasPid, mobileToken, &mobileTokenLen, cryptogram, cryptogramLen); + if (s != PM3_SUCCESS) { + return s; + } + + if (mobileTokenLen > 0) { + PrintAndLogEx(INFO, "Device token (9F2A)... " _YELLOW_("%s"), sprint_hex_inrow(mobileToken, mobileTokenLen)); + } + + PrintVASStatusLine("GET VAS DATA status....", getDataStatus); + + if (verbose && cryptogramLen != NULL && *cryptogramLen > 0) { + PrintAndLogEx(INFO, "Cryptogram data........ " _YELLOW_("%s"), sprint_hex_inrow(cryptogram, *cryptogramLen)); + } + return PM3_SUCCESS; +} + +static int VASRead(bool has_pid, size_t request_count, + char *const *pid_values, const char *url, int urllen, + uint8_t vas_mode, bool verbose, + vas_reader_key_t *keys, int key_count) { + uint16_t select_status = VAS_STATUS_NOT_AVAILABLE; + if (VASSelectOse(&select_status) != PM3_SUCCESS) { + PrintVASFailureReason(select_status, VAS_STATUS_NOT_AVAILABLE); + return PM3_ECARDEXCHANGE; + } + + for (size_t request_idx = 0; request_idx < request_count; request_idx++) { + uint8_t pidhash[32] = {0}; + const char *passIdentifier = "unknown-pass-identifier"; + if (has_pid) { + passIdentifier = pid_values[request_idx]; + sha256hash((uint8_t *)passIdentifier, strlen(passIdentifier), pidhash); + } + + PrintAndLogEx(INFO, ""); + + uint8_t cryptogram[VAS_MAX_CRYPTOGRAM_LEN] = {0}; + size_t clen = 0; + uint16_t get_data_status = VAS_STATUS_NOT_AVAILABLE; + bool isFinalRequest = (request_idx + 1 == request_count); + int res = VASGetData(passIdentifier, has_pid ? pidhash : NULL, has_pid, + url, urllen, vas_mode, isFinalRequest, verbose, + cryptogram, &clen, &get_data_status); + if (res != PM3_SUCCESS) { + PrintVASFailureReason(select_status, get_data_status); + break; + } + + if (!has_pid) { + PrintAndLogEx(SUCCESS, "Request completed"); + continue; + } + + if (!vas_status_is_success(get_data_status)) { + continue; + } + + int matched_key_idx = -1; + if (clen >= 4) { + for (int i = 0; i < key_count; i++) { + if (memcmp(keys[i].key_id, cryptogram, sizeof(keys[i].key_id)) == 0) { + matched_key_idx = i; + break; + } + } + } + + uint8_t msg[64] = {0}; + size_t mlen = 0; + uint32_t timestamp = 0; + + if (key_count == 0) { + PrintVasCryptogramInfo("Cryptogram (no key provided to decrypt)", cryptogram, clen); + continue; + } + + if (matched_key_idx < 0) { + PrintAndLogEx(FAILED, "No matching key identifier found, cannot decrypt VAS data"); + PrintVasCryptogramInfo("Cryptogram", cryptogram, clen); + continue; + } + + res = DecryptVASCryptogram(pidhash, cryptogram, clen, &keys[matched_key_idx].key, msg, &mlen, ×tamp); + if (res != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Cannot decrypt VAS data"); + PrintVasCryptogramInfo("Cryptogram", cryptogram, clen); + continue; + } + + PrintAndLogEx(SUCCESS, "Pass data"); + PrintAndLogEx(SUCCESS, " Timestamp... " _YELLOW_("%d") " (secs since Jan 1, 2001)", timestamp); + PrintAndLogEx(SUCCESS, " Message..... " _YELLOW_("%s"), sprint_ascii(msg, mlen)); + } + return PM3_SUCCESS; } static int CmdVASReader(const char *Cmd) { @@ -563,92 +837,155 @@ static int CmdVASReader(const char *Cmd) { "hf vas reader --pid pass.com.passkit.pksamples.nfcdemo -k vas.passkit.der -@\n" "hf vas reader --pid pass.com.pronto.zebra-wallet-pass.demo -k vas.zebra.der -@\n" "hf vas reader --pid pass.com.springcard.springblue.generic -k vas.springcard.der -@\n" + "hf vas reader --pid pass.id.one --pid pass.id.two -k key.one.der -k key.two.der\n" + "hf vas reader --mode vasandpay --pid pass.id -k key.der\n" ); void *argtable[] = { arg_param_begin, - arg_str0(NULL, "pid", "", "PID, pass type id"), - arg_str0("k", "key,file,reader-private-key,readerprivkey,rpk", "", "Terminal private key: PEM, DER hex, scalar hex/base64, or file path"), + arg_strx0(NULL, "pid", "", "PID, pass type id (repeat --pid for multiple values)"), + arg_strx0("k", "key,file,reader-private-key,readerprivkey,rpk", "", "Terminal private key (repeat --key for multiple values)"), arg_str0(NULL, "url", "", "a URL to provide to the mobile device"), + arg_str0(NULL, "mode", "", "VAS mode used in ECP and GET DATA capabilities"), arg_lit0("@", NULL, "continuous mode"), arg_lit0("v", "verbose", "Verbose output"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, false); - int pidlen = 0; - char pid[512] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)pid, 512, &pidlen); + struct arg_str *pid_args = arg_get_str(ctx, 1); + struct arg_str *key_args = arg_get_str(ctx, 2); + const int pid_count = pid_args->count; + const int key_count = key_args->count; - int key_input_len = 0; - char key_input[VAS_MAX_KEY_INPUT] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)key_input, sizeof(key_input), &key_input_len); - - if (key_input_len == 0 && pidlen > 0) { - PrintAndLogEx(FAILED, "Must provide terminal private key if a pass type id is provided"); + if (pid_count == 0 && key_count > 0) { + PrintAndLogEx(FAILED, "--key requires at least one --pid"); CLIParserFree(ctx); return PM3_EINVARG; } + if (pid_count > VAS_MAX_PID_ITEMS) { + PrintAndLogEx(FAILED, "Too many --pid values (max %d)", VAS_MAX_PID_ITEMS); + CLIParserFree(ctx); + return PM3_EINVARG; + } + if (key_count > VAS_MAX_KEY_ITEMS) { + PrintAndLogEx(FAILED, "Too many --key values (max %d)", VAS_MAX_KEY_ITEMS); + CLIParserFree(ctx); + return PM3_EINVARG; + } + char *pid_values[VAS_MAX_PID_ITEMS] = {0}; + char *key_values[VAS_MAX_KEY_ITEMS] = {0}; + for (int i = 0; i < pid_count; i++) { + pid_values[i] = strdup(pid_args->sval[i]); + if (pid_values[i] == NULL) { + VasReaderCleanup(NULL, 0, key_values, 0, pid_values, i); + CLIParserFree(ctx); + return PM3_EMALLOC; + } + } + + for (int i = 0; i < key_count; i++) { + key_values[i] = strdup(key_args->sval[i]); + if (key_values[i] == NULL) { + VasReaderCleanup(NULL, 0, key_values, i, pid_values, pid_count); + CLIParserFree(ctx); + return PM3_EMALLOC; + } + } int urllen = 0; char url[512] = {0}; CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)url, 512, &urllen); - bool continuous = arg_get_lit(ctx, 4); - bool verbose = arg_get_lit(ctx, 5); - CLIParserFree(ctx); - - const bool has_pid = pidlen > 0; - mbedtls_ecp_keypair privKey; - mbedtls_ecp_keypair_init(&privKey); - - if (has_pid && LoadReaderPrivateKey(key_input, &privKey) != PM3_SUCCESS) { - mbedtls_ecp_keypair_free(&privKey); - return PM3_ESOFT; + char mode_text[32] = {0}; + int mode_len = 0; + CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)mode_text, sizeof(mode_text), &mode_len); + (void)mode_len; + str_lower(mode_text); + uint8_t vas_mode = VAS_MODE_VAS_ONLY; + if (vas_parse_mode(mode_text, &vas_mode) != PM3_SUCCESS) { + VasReaderCleanup(NULL, 0, key_values, key_count, pid_values, pid_count); + CLIParserFree(ctx); + return PM3_EINVARG; } - PrintAndLogEx(INFO, "Requesting pass type id... " _GREEN_("%s"), sprint_ascii((uint8_t *) pid, pidlen)); + bool continuous = arg_get_lit(ctx, 5); + bool verbose = arg_get_lit(ctx, 6); + + const bool has_pid = pid_count > 0; + const size_t request_count = has_pid ? (size_t)pid_count : 1; + + vas_reader_key_t keys[VAS_MAX_KEY_ITEMS] = {0}; + for (int i = 0; i < key_count; i++) { + mbedtls_ecp_keypair_init(&keys[i].key); + keys[i].source = key_values[i]; + if (LoadReaderPrivateKey(keys[i].source, &keys[i].key) != PM3_SUCCESS) { + VasReaderCleanup(keys, i + 1, key_values, key_count, pid_values, pid_count); + CLIParserFree(ctx); + return PM3_ESOFT; + } + if (GetPrivateKeyId(&keys[i].key, keys[i].key_id) != PM3_SUCCESS) { + VasReaderCleanup(keys, i + 1, key_values, key_count, pid_values, pid_count); + CLIParserFree(ctx); + return PM3_ESOFT; + } + } + + CLIParserFree(ctx); + + if (has_pid) { + PrintAndLogEx(INFO, "Requesting pass type id entries... " _GREEN_("%d"), pid_count); + } else { + PrintAndLogEx(INFO, "Requesting VAS URL........ " _GREEN_("%s"), sprint_ascii((uint8_t *) url, urllen)); + } if (continuous) { PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); } - uint8_t pidhash[32] = {0}; - sha256hash((uint8_t *) pid, pidlen, pidhash); + int final_res = PM3_SUCCESS; - size_t clen = 0; - size_t mlen = 0; - uint8_t cryptogram[120] = {0}; - uint8_t msg[64] = {0}; - uint32_t timestamp = 0; - int res = PM3_SUCCESS; + iso14a_polling_frame_t ecp_frame; + vas_build_ecp_frame(vas_mode, &ecp_frame); + iso14a_polling_parameters_t polling_parameters = { + .frames = { WUPA_FRAME, ecp_frame }, + .frame_count = 2, + .extra_timeout = 250 + }; do { if (continuous && kbd_enter_pressed()) { break; } + clearCommandBuffer(); - res = VASReader(has_pid ? pidhash : NULL, url, urllen, cryptogram, &clen, verbose); - if (res == PM3_SUCCESS) { - if (has_pid) { - res = DecryptVASCryptogram(pidhash, cryptogram, clen, &privKey, msg, &mlen, ×tamp); - if (res == PM3_SUCCESS) { - PrintAndLogEx(SUCCESS, "Timestamp... " _YELLOW_("%d") " (secs since Jan 1, 2001)", timestamp); - PrintAndLogEx(SUCCESS, "Message..... " _YELLOW_("%s"), sprint_ascii(msg, mlen)); - // extra sleep after successfull read - if (continuous) { - msleep(3000); - } - } - } else { - PrintAndLogEx(SUCCESS, "URL-only request completed"); + if (SelectCard14443A_4_WithParameters(false, false, NULL, &polling_parameters) != PM3_SUCCESS) { + PrintVASFailureReason(VAS_STATUS_NOT_AVAILABLE, VAS_STATUS_NOT_AVAILABLE); + if (final_res == PM3_SUCCESS) { + final_res = PM3_ECARDEXCHANGE; } + msleep(1000); + continue; } + int iter_res = VASRead(has_pid, request_count, pid_values, + url, urllen, vas_mode, verbose, + keys, key_count); + if (iter_res != PM3_SUCCESS && final_res == PM3_SUCCESS) { + final_res = iter_res; + } + + if (continuous) { + // Drop field so that iPhone displays the checkmark or a pass + DropField(); + msleep(3000); + } + PrintAndLogEx(NORMAL, ""); msleep(300); } while (continuous); - mbedtls_ecp_keypair_free(&privKey); - return res; + VasReaderCleanup(keys, key_count, key_values, key_count, pid_values, pid_count); + DropField(); + return final_res; } static int CmdVASInfo(const char *Cmd) { @@ -670,8 +1007,22 @@ static int CmdVASInfo(const char *Cmd) { bool restore_apdu_logging = GetAPDULogging(); SetAPDULogging(apdu_logging); - int res = info_vas(); + + clearCommandBuffer(); + + iso14a_polling_parameters_t polling_parameters = { + .frames = { WUPA_FRAME, ECP_VAS_ONLY_FRAME }, + .frame_count = 2, + .extra_timeout = 250 + }; + + int res = PM3_ECARDEXCHANGE; + if (SelectCard14443A_4_WithParameters(false, false, NULL, &polling_parameters) == PM3_SUCCESS) { + res = VASSelectOse(NULL); + } + SetAPDULogging(restore_apdu_logging); + DropField(); return res; }