//-----------------------------------------------------------------------------
// 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 <ctype.h>
#include <inttypes.h>
#include <mbedtls/aes.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/ecdh.h>
#include <mbedtls/ecp.h>
#include <mbedtls/entropy.h>
#include <mbedtls/hkdf.h>
#include <mbedtls/md.h>
#include <mbedtls/ecc_point_compression.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_ZLIB
#include <zlib.h>
#endif
#include "cliparser.h"
#include "cmdhf14a.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"
#include "util_posix.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;
    bool verbose;
} 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 bool gst_wallet_type_is_apple_pay(const uint8_t *wallet_type, size_t wallet_type_len) {
    static const uint8_t apple_pay_wallet_type[] = "ApplePay";
    return wallet_type != NULL
           && wallet_type_len == (sizeof(apple_pay_wallet_type) - 1)
           && memcmp(wallet_type, apple_pay_wallet_type, sizeof(apple_pay_wallet_type) - 1) == 0;
}

static void gst_print_wallet_type_hint(const uint8_t *wallet_type, size_t wallet_type_len) {
    if (gst_wallet_type_is_apple_pay(wallet_type, wallet_type_len)) {
        PrintAndLogEx(INFO, "Hint: detected Apple VAS flavor of OSE.VAS.01. Try " _YELLOW_("hf vas") " commands.");
    }
}

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.");
            gst_print_wallet_type_hint(info->wallet_type, info->wallet_type_len);
            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 activate_field, 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(activate_field, 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 = {0};
        gst_ndef_record_view_t second_record = {0};
        gst_ndef_record_view_t loyalty_record = {0};
        gst_ndef_record_view_t object_id_record = {0};
        gst_ndef_record_view_t customer_record = {0};
        gst_ndef_record_view_t customer_id_record = {0};

        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 <empty>", 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++;
            if (object_count > 1) {
                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, 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, false, true);
    if (status != PM3_SUCCESS) {
        goto out;
    }

    const bool wallet_type_is_android_pay = ose_info.have_wallet_type &&
                                            gst_wallet_type_is_android_pay(ose_info.wallet_type, ose_info.wallet_type_len);
    const bool wallet_type_is_known_non_android = ose_info.have_wallet_type && !wallet_type_is_android_pay;

    if (cfg->verbose) {
        PrintAndLogEx(INFO, "");
        gst_print_ose_info(&ose_info);
    } else if (!wallet_type_is_android_pay) {
        const char *wallet_type = ose_info.have_wallet_type
                                  ? sprint_ascii(ose_info.wallet_type, ose_info.wallet_type_len)
                                  : "not present";
        PrintAndLogEx(WARNING, "Wallet type............... " _YELLOW_("%s"), wallet_type);
        PrintAndLogEx(ERR, "Wallet type is not AndroidPay. This likely isn't Google Smart Tap.");
        gst_print_wallet_type_hint(ose_info.wallet_type, ose_info.wallet_type_len);
    }

    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) {
        if (wallet_type_is_known_non_android) {
            status = PM3_ESOFT;
            goto out;
        }
        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;
    }

    if (cfg->verbose) {
        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;
        }
    }

    if (cfg->verbose || negotiate_sw != 0x9000) {
        if (!cfg->verbose) {
            PrintAndLogEx(INFO, "");
        }
        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;
        }
    }

    if (cfg->verbose) {
        PrintAndLogEx(INFO, "");
        PrintAndLogInfoHeader("Get data");
    }
    if (cfg->verbose || get_data_sw != 0x9000) {
        if (!cfg->verbose) {
            PrintAndLogEx(INFO, "");
        }
        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;
    }

    if (cfg->verbose) {
        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", "<auto|yes|no>", "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\n"
                  "hf gst read --cid 20180608 --rpk gst.google.der -@");

    void *argtable[] = {
        arg_param_begin,
        arg_str1("c", "collector-id,collectorid,cid", "<hex|dec>", "Collector identifier (32-bit value)"),
        arg_str1("p", "reader-private-key,readerprivkey,rpk", "<pem|der-b64|der-hex|scalar-b64|scalar-hex|path>", "Reader private key: PEM, DER hex, scalar hex/base64, or file path"),
        arg_str0(NULL, "key-version,keyversion,kv", "<hex|dec>", "Long-term key version (default: 1)"),
        arg_str0(NULL, "session-id,sid", "<hex>", "Session id (8 bytes, random if omitted)"),
        arg_str0(NULL, "reader-nonce,nonce", "<hex>", "Reader nonce (32 bytes, random if omitted)"),
        arg_str0(NULL, "reader-ephemeral-private-key,ephemeral-privkey,epk", "<hex>", "Reader ephemeral private key (32 bytes, random if omitted)"),
        arg_str0(NULL, "mode", "<pass-only|payment-only|pass-and-payment|pass-over-payment>", "Reader mode (default: pass-over-payment)"),
        arg_str0(NULL, "select-smarttap2", "<auto|yes|no>", "Whether to perform Smart Tap applet select (default: auto)"),
        arg_lit0(NULL, "no-live-auth", "Use zeroed handset nonce for reader signature"),
        arg_lit0("@", NULL, "continuous mode"),
        arg_lit0("a", "apdu", "Show APDU requests and responses"),
        arg_lit0("v", "verbose", "Verbose output"),
        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 with headers (BEGIN PRIVATE KEY)");
        PrintAndLogEx(INFO, "  2) DER bytes as hex or base64");
        PrintAndLogEx(INFO, "  3) Scalar as hex or base64");
        PrintAndLogEx(INFO, "  4) File path to a key in any 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);
    bool continuous = arg_get_lit(ctx, 10);
    cfg.apdu_logging = arg_get_lit(ctx, 11);
    cfg.verbose = arg_get_lit(ctx, 12);

    CLIParserFree(ctx);

    if (continuous) {
        PrintAndLogEx(INFO, "Press " _GREEN_("<Enter>") " to exit");
    }

    bool restore_apdu_logging = GetAPDULogging();
    SetAPDULogging(cfg.apdu_logging);

    int res = PM3_SUCCESS;
    do {
        if (continuous && kbd_enter_pressed()) {
            break;
        }
        clearCommandBuffer();

        int discovery_res = SelectCard14443A_4(false, false, NULL);
        if (discovery_res != PM3_SUCCESS) {
            if (!continuous) {
                PrintAndLogEx(WARNING, "No ISO14443-A card in field");
            }
            if (res == PM3_SUCCESS) {
                res = discovery_res;
            }
            msleep(300);
            continue;
        }

        int iter_res = gst_read(&cfg);
        if (iter_res != PM3_SUCCESS && res == PM3_SUCCESS) {
            res = iter_res;
        }

        if (continuous) {
            msleep(3000);
        }
        PrintAndLogEx(NORMAL, "");
        msleep(300);
    } while (continuous);

    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;
}
