emv
Version:
EMV / Chip and PIN CLI and library for PC/SC card readers
690 lines • 22.7 kB
JavaScript
import { parse } from '@tomkp/ber-tlv';
// ANSI color codes for terminal output
const DIM = '\x1b[2m';
const RESET = '\x1b[0m';
const CYAN = '\x1b[36m'; // Identifiers (AID, PAN, names)
const YELLOW = '\x1b[33m'; // Dates
const MAGENTA = '\x1b[35m'; // Cryptographic (certs, keys, cryptograms)
const GREEN = '\x1b[32m'; // Verification (CVM, PIN)
const BLUE = '\x1b[34m'; // Transaction data (amounts, counters)
/**
* Tag categories for color coding
*/
var TagCategory;
(function (TagCategory) {
TagCategory[TagCategory["IDENTIFIER"] = 0] = "IDENTIFIER";
TagCategory[TagCategory["DATE"] = 1] = "DATE";
TagCategory[TagCategory["CRYPTO"] = 2] = "CRYPTO";
TagCategory[TagCategory["VERIFICATION"] = 3] = "VERIFICATION";
TagCategory[TagCategory["TRANSACTION"] = 4] = "TRANSACTION";
TagCategory[TagCategory["DEFAULT"] = 5] = "DEFAULT";
})(TagCategory || (TagCategory = {}));
/**
* Get the category for a tag to determine its color
*/
function getTagCategory(tagNum) {
switch (tagNum) {
// Identifiers
case 0x4f: // APP_IDENTIFIER
case 0x50: // APP_LABEL
case 0x5a: // PAN
case 0x5f20: // CARDHOLDER_NAME
case 0x5f34: // PAN_SEQUENCE_NUMBER
case 0x84: // DEDICATED_FILE_NAME
case 0x9f06: // AID_TERMINAL
case 0x9f12: // APP_PREFERRED_NAME
return TagCategory.IDENTIFIER;
// Dates
case 0x5f24: // APP_EXPIRY
case 0x5f25: // APP_EFFECTIVE
case 0x9a: // TRANSACTION_DATE
case 0x9f21: // TRANSACTION_TIME
return TagCategory.DATE;
// Cryptographic
case 0x90: // ISSUER_PK_CERTIFICATE
case 0x92: // ISSUER_PK_REMAINDER
case 0x93: // SIGNED_STATIC_APPLICATION_DATA
case 0x9f26: // APPLICATION_CRYPTOGRAM
case 0x9f27: // CRYPTOGRAM_INFORMATION_DATA
case 0x9f32: // ISSUER_PK_EXPONENT
case 0x9f46: // ICC_PK_CERTIFICATE
case 0x9f47: // ICC_PK_EXPONENT
case 0x9f48: // ICC_PK_REMAINDER
case 0x9f4c: // ICC_DYNAMIC_NUMBER
case 0x8f: // CA_PK_INDEX
return TagCategory.CRYPTO;
// Verification
case 0x8e: // CVM_LIST
case 0x9f34: // CVM_RESULTS
case 0x9f17: // PIN_TRY_COUNT
case 0x9f0d: // IAC_DEFAULT
case 0x9f0e: // IAC_DENIAL
case 0x9f0f: // IAC_ONLINE
case 0x95: // TVR
return TagCategory.VERIFICATION;
// Transaction
case 0x9f02: // AUTH_AMOUNT_NUM
case 0x9f03: // OTHER_AMOUNT_NUM
case 0x9f36: // APP_TRANSACTION_COUNTER
case 0x9f41: // TRANSACTION_SEQUENCE_COUNTER
case 0x9c: // TRANSACTION_TYPE
case 0x5f2a: // TRANSACTION_CURRENCY_CODE
case 0x9f42: // APP_CURRENCY_CODE
return TagCategory.TRANSACTION;
default:
return TagCategory.DEFAULT;
}
}
/**
* Get the color code for a tag category
*/
function getTagColor(tagNum) {
switch (getTagCategory(tagNum)) {
case TagCategory.IDENTIFIER:
return CYAN;
case TagCategory.DATE:
return YELLOW;
case TagCategory.CRYPTO:
return MAGENTA;
case TagCategory.VERIFICATION:
return GREEN;
case TagCategory.TRANSACTION:
return BLUE;
default:
return '';
}
}
/**
* Convert tag bytes to a single number for comparison
*/
function tagBytesToNumber(bytes) {
let result = 0;
for (const byte of bytes) {
result = (result << 8) | byte;
}
return result;
}
/**
* Tags that contain binary data where ASCII display is not useful
*/
const BINARY_TAGS = new Set([
0x90, // ISSUER_PK_CERTIFICATE
0x92, // ISSUER_PK_REMAINDER
0x93, // SIGNED_STATIC_APPLICATION_DATA
0x9f46, // ICC_PK_CERTIFICATE
0x9f48, // ICC_PK_REMAINDER
0x9f2d, // ICC_PIN_ENCIPHERMENT_PK_CERT
0x9f4c, // ICC_DYNAMIC_NUMBER
]);
/**
* Format a PAN with spaces for readability
*/
function formatPan(hex) {
// Remove any trailing 'F' padding
const pan = hex.replace(/F+$/i, '');
// Add spaces every 4 digits
return pan.replace(/(.{4})/g, '$1 ').trim();
}
/**
* Parse Track 2 equivalent data (tag 57)
*/
function formatTrack2(buffer) {
const hex = buffer.toString('hex').toUpperCase();
const separatorIndex = hex.indexOf('D');
if (separatorIndex === -1) {
return hex;
}
const pan = hex.substring(0, separatorIndex);
const rest = hex.substring(separatorIndex + 1);
const expiry = rest.substring(0, 4);
const serviceCode = rest.substring(4, 7);
const discretionary = rest.substring(7).replace(/F+$/i, '');
const expiryFormatted = expiry.length === 4 ? `20${expiry.substring(0, 2)}-${expiry.substring(2, 4)}` : expiry;
let result = `${hex}\n ${DIM}PAN: ${formatPan(pan)}`;
result += `, Exp: ${expiryFormatted}`;
result += `, Svc: ${serviceCode}`;
if (discretionary) {
result += `, DD: ${discretionary}`;
}
result += RESET;
return result;
}
/**
* Format EMV date (YYMMDD) to readable format
*/
function formatDate(buffer) {
const hex = buffer.toString('hex').toUpperCase();
if (hex.length !== 6)
return hex;
const year = `20${hex.substring(0, 2)}`;
const month = hex.substring(2, 4);
const day = hex.substring(4, 6);
return `${hex} ${DIM}(${year}-${month}-${day})${RESET}`;
}
/**
* Service code digit meanings
*/
const SERVICE_CODE_D1 = {
'1': 'International, chip',
'2': 'International, chip, w/PIN',
'5': 'National, chip',
'6': 'National, chip, w/PIN',
'7': 'Private',
};
const SERVICE_CODE_D2 = {
'0': 'Normal',
'2': 'Positive auth (contact issuer)',
'4': 'Positive auth (contact issuer, exceptions)',
};
const SERVICE_CODE_D3 = {
'0': 'No restrictions, PIN required',
'1': 'No restrictions',
'2': 'Goods/services only',
'3': 'ATM only, PIN required',
'4': 'Cash only',
'5': 'Goods/services only, PIN required',
'6': 'No restrictions, PIN prompt',
'7': 'Goods/services only, PIN prompt',
};
/**
* Format service code with meaning
*/
function formatServiceCode(buffer) {
const hex = buffer.toString('hex').toUpperCase();
if (hex.length < 3)
return hex;
const d1 = hex[0];
const d2 = hex[1];
const d3 = hex[2];
if (d1 === undefined || d2 === undefined || d3 === undefined)
return hex;
const meanings = [];
const m1 = SERVICE_CODE_D1[d1];
const m2 = SERVICE_CODE_D2[d2];
const m3 = SERVICE_CODE_D3[d3];
if (m1)
meanings.push(m1);
if (m2)
meanings.push(m2);
if (m3)
meanings.push(m3);
if (meanings.length > 0) {
return `${hex} ${DIM}(${meanings.join(', ')})${RESET}`;
}
return hex;
}
/**
* CVM (Cardholder Verification Method) codes
*/
const CVM_CODES = {
0x00: 'Fail CVM',
0x01: 'Plaintext PIN by ICC',
0x02: 'Enciphered PIN online',
0x03: 'Plaintext PIN by ICC + signature',
0x04: 'Enciphered PIN by ICC',
0x05: 'Enciphered PIN by ICC + signature',
0x1e: 'Signature',
0x1f: 'No CVM required',
0x3f: 'No CVM required (amount)',
};
const CVM_CONDITIONS = {
0x00: 'Always',
0x01: 'If unattended cash',
0x02: 'If not unattended cash/manual/PIN',
0x03: 'If terminal supports CVM',
0x04: 'If manual cash',
0x05: 'If purchase with cashback',
0x06: 'If transaction in app currency & under X',
0x07: 'If transaction in app currency & over X',
0x08: 'If transaction in app currency & under Y',
0x09: 'If transaction in app currency & over Y',
};
/**
* Format CVM list
*/
function formatCvmList(buffer) {
const hex = buffer.toString('hex').toUpperCase();
if (buffer.length < 8) {
return hex;
}
const amountX = buffer.readUInt32BE(0);
const amountY = buffer.readUInt32BE(4);
const rules = [];
for (let i = 8; i + 1 < buffer.length; i += 2) {
const cvmByte = buffer[i];
const condByte = buffer[i + 1];
if (cvmByte === undefined || condByte === undefined)
continue;
const cvmCode = cvmByte & 0x3f;
const failIfUnsuccessful = (cvmByte & 0x40) === 0;
const cvmName = CVM_CODES[cvmCode] ?? `Unknown(${cvmCode.toString(16)})`;
const condName = CVM_CONDITIONS[condByte] ?? `Cond(${condByte.toString(16)})`;
const failStr = failIfUnsuccessful ? '' : ' [continue if fails]';
const ruleHex = cvmByte.toString(16).padStart(2, '0').toUpperCase() +
condByte.toString(16).padStart(2, '0').toUpperCase();
rules.push(`${ruleHex} ${DIM}${cvmName} ${condName}${failStr}${RESET}`);
}
let result = hex;
if (rules.length > 0) {
const amountInfo = [];
if (amountX > 0)
amountInfo.push(`X=${String(amountX)}`);
if (amountY > 0)
amountInfo.push(`Y=${String(amountY)}`);
if (amountInfo.length > 0) {
result += ` ${DIM}(${amountInfo.join(', ')})${RESET}`;
}
result += '\n ' + rules.join('\n ');
}
return result;
}
/**
* Application Usage Control (AUC) bit meanings
*/
const AUC_BITS = [
[0, 0x80, 'Domestic cash'],
[0, 0x40, 'International cash'],
[0, 0x20, 'Domestic goods'],
[0, 0x10, 'International goods'],
[0, 0x08, 'Domestic services'],
[0, 0x04, 'International services'],
[0, 0x02, 'ATMs'],
[0, 0x01, 'Non-ATM terminals'],
[1, 0x80, 'Domestic cashback'],
[1, 0x40, 'International cashback'],
];
/**
* Format AUC flags
*/
function formatAuc(buffer) {
const enabled = [];
for (const [byteIdx, mask, name] of AUC_BITS) {
const byte = buffer[byteIdx];
if (byte !== undefined && byte & mask) {
enabled.push(name);
}
}
const hex = buffer.toString('hex').toUpperCase();
if (enabled.length === 0)
return hex;
return `${hex} ${DIM}(${enabled.join(', ')})${RESET}`;
}
/**
* Issuer Action Code (IAC) / Terminal Verification Results (TVR) bit meanings
*/
const TVR_BITS = [
[0, 0x80, 'Offline data auth not performed'],
[0, 0x40, 'SDA failed'],
[0, 0x20, 'ICC data missing'],
[0, 0x10, 'Card on exception file'],
[0, 0x08, 'DDA failed'],
[0, 0x04, 'CDA failed'],
[1, 0x80, 'ICC & terminal versions differ'],
[1, 0x40, 'Expired app'],
[1, 0x20, 'App not yet effective'],
[1, 0x10, 'Service not allowed'],
[1, 0x08, 'New card'],
[2, 0x80, 'CVM not successful'],
[2, 0x40, 'Unrecognised CVM'],
[2, 0x20, 'PIN try limit exceeded'],
[2, 0x10, 'PIN required, pad not present'],
[2, 0x08, 'PIN required, pad present but PIN not entered'],
[2, 0x04, 'Online PIN entered'],
[3, 0x80, 'Transaction exceeds floor limit'],
[3, 0x40, 'Lower consecutive offline limit exceeded'],
[3, 0x20, 'Upper consecutive offline limit exceeded'],
[3, 0x10, 'Random selected for online'],
[3, 0x08, 'Merchant forced online'],
[4, 0x80, 'Default TDOL used'],
[4, 0x40, 'Issuer auth failed'],
[4, 0x20, 'Script failed before final GAC'],
[4, 0x10, 'Script failed after final GAC'],
];
/**
* Format IAC/TVR flags - shows each byte with its set bits
* For IAC tags, these are masks (conditions to check), not events that occurred
*/
function formatIacTvr(buffer) {
const hex = buffer.toString('hex').toUpperCase();
const setBits = [];
for (let byteIdx = 0; byteIdx < buffer.length && byteIdx < 5; byteIdx++) {
const byte = buffer[byteIdx];
if (byte === undefined)
continue;
for (const [idx, mask, name] of TVR_BITS) {
if (idx === byteIdx && byte & mask) {
setBits.push(name);
}
}
}
if (setBits.length === 0) {
return `${hex} ${DIM}(none set)${RESET}`;
}
return `${hex}\n ${DIM}${setBits.join('\n ')}${RESET}`;
}
/**
* Format Track 1 Discretionary Data
* Shows hex with decoded ASCII below
*/
function formatTrack1Dd(buffer) {
const hex = buffer.toString('hex').toUpperCase();
const ascii = buffer.toString().replace(/[^\x20-\x7E]/g, '.');
return `${hex}\n ${DIM}${ascii}${RESET}`;
}
/**
* AIP (Application Interchange Profile) bit meanings
*/
const AIP_BYTE1_BITS = [
[0x40, 'SDA supported'],
[0x20, 'DDA supported'],
[0x10, 'Cardholder verification supported'],
[0x08, 'Terminal risk management to be performed'],
[0x04, 'Issuer authentication supported'],
[0x01, 'CDA supported'],
];
const AIP_BYTE2_BITS = [
[0x80, 'MSD supported'],
[0x40, 'EMV mode supported'],
];
/**
* Format GPO response (Response Message Template Format 1 or 2)
* Decodes AIP and AFL
*/
export function formatGpoResponse(buffer) {
const hex = buffer.toString('hex').toUpperCase();
// Check for Format 1 (tag 80) or Format 2 (tag 77)
if (buffer.length < 2) {
return hex;
}
const tag = buffer[0];
const len = buffer[1];
if (tag === 0x80 && len !== undefined && buffer.length >= len + 2) {
// Format 1: 80 len AIP(2) AFL(var)
const aip = buffer.subarray(2, 4);
const afl = buffer.subarray(4, 2 + len);
return formatGpoFormat1(hex, aip, afl);
}
else if (tag === 0x77) {
// Format 2: 77 len [94 len AFL] [82 len AIP] ...
// Parse as TLV - for now just show hex
return `${hex}\n ${DIM}(Response Template Format 2)${RESET}`;
}
return hex;
}
function formatGpoFormat1(hex, aip, afl) {
const lines = [hex];
// Decode AIP
const aipHex = aip.toString('hex').toUpperCase();
const aipByte1 = aip[0] ?? 0;
const aipByte2 = aip[1] ?? 0;
const aipFeatures = [];
for (const [mask, name] of AIP_BYTE1_BITS) {
if (aipByte1 & mask) {
aipFeatures.push(name);
}
}
for (const [mask, name] of AIP_BYTE2_BITS) {
if (aipByte2 & mask) {
aipFeatures.push(name);
}
}
lines.push(`${DIM}AIP: ${aipHex}${aipFeatures.length > 0 ? ` (${aipFeatures.join(', ')})` : ''}${RESET}`);
// Decode AFL (4 bytes per entry)
if (afl.length >= 4) {
const aflEntries = [];
for (let i = 0; i + 3 < afl.length; i += 4) {
const sfi = (afl[i] ?? 0) >> 3;
const firstRec = afl[i + 1] ?? 0;
const lastRec = afl[i + 2] ?? 0;
const sdaRecs = afl[i + 3] ?? 0;
aflEntries.push(`SFI ${String(sfi)}: records ${String(firstRec)}-${String(lastRec)}${sdaRecs > 0 ? ` (${String(sdaRecs)} for SDA)` : ''}`);
}
lines.push(`${DIM}AFL: ${aflEntries.join(', ')}${RESET}`);
}
return lines.join('\n ');
}
/**
* Get custom formatter for a specific tag
*/
function getTagFormatter(tagNum) {
switch (tagNum) {
case 0x57: // TRACK_2
return formatTrack2;
case 0x5a: // PAN
return (buf) => {
const hex = buf.toString('hex').toUpperCase();
return `${hex} ${DIM}(${formatPan(hex)})${RESET}`;
};
case 0x5f24: // APP_EXPIRY
case 0x5f25: // APP_EFFECTIVE
return formatDate;
case 0x5f30: // SERVICE_CODE
return formatServiceCode;
case 0x8e: // CVM_LIST
return formatCvmList;
case 0x9f07: // APP_USAGE_CONTROL
return formatAuc;
case 0x9f0d: // IAC_DEFAULT
case 0x9f0e: // IAC_DENIAL
case 0x9f0f: // IAC_ONLINE
case 0x95: // TVR
return formatIacTvr;
case 0x9f1f: // TRACK_1_DD
return formatTrack1Dd;
default:
return undefined;
}
}
/**
* EMV tag dictionary mapping hex codes to human-readable names.
* Based on EMV Book 3 specification.
*/
export const EMV_TAGS = {
'4F': 'APP_IDENTIFIER',
'50': 'APP_LABEL',
'57': 'TRACK_2',
'5A': 'PAN',
'5F20': 'CARDHOLDER_NAME',
'5F24': 'APP_EXPIRY',
'5F25': 'APP_EFFECTIVE',
'5F28': 'ISSUER_COUNTRY_CODE',
'5F2A': 'TRANSACTION_CURRENCY_CODE',
'5F2D': 'LANGUAGE_PREFERENCE',
'5F30': 'SERVICE_CODE',
'5F34': 'PAN_SEQUENCE_NUMBER',
'5F36': 'TRANSACTION_CURRENCY_EXPONENT',
'5F50': 'ISSUER_URL',
'61': 'APPLICATION_TEMPLATE',
'6F': 'FILE_CONTROL_INFO',
'70': 'EMV_APP_ELEMENTARY_FILE',
'71': 'ISSUER_SCRIPT_TEMPLATE_1',
'72': 'ISSUER_SCRIPT_TEMPLATE_2',
'77': 'RESPONSE_TEMPLATE_2',
'80': 'RESPONSE_TEMPLATE_1',
'81': 'AUTH_AMOUNT_BIN',
'82': 'APP_INTERCHANGE_PROFILE',
'83': 'COMMAND_TEMPLATE',
'84': 'DEDICATED_FILE_NAME',
'86': 'ISSUER_SCRIPT_CMD',
'87': 'APP_PRIORITY',
'88': 'SFI',
'89': 'AUTH_IDENTIFICATION_RESPONSE',
'8A': 'AUTH_RESPONSE_CODE',
'8C': 'CDOL_1',
'8D': 'CDOL_2',
'8E': 'CVM_LIST',
'8F': 'CA_PK_INDEX',
'90': 'ISSUER_PK_CERTIFICATE',
'91': 'ISSUER_AUTH_DATA',
'92': 'ISSUER_PK_REMAINDER',
'93': 'SIGNED_STATIC_APPLICATION_DATA',
'94': 'APP_FILE_LOCATOR',
'95': 'TERMINAL_VERIFICATION_RESULTS',
'98': 'TC_HASH_VALUE',
'99': 'TRANSACTION_PIN_DATA',
'9A': 'TRANSACTION_DATE',
'9B': 'TRANSACTION_STATUS_INFORMATION',
'9C': 'TRANSACTION_TYPE',
'9D': 'DIRECTORY_DEFINITION_FILE',
'9F01': 'ACQUIRER_ID',
'9F02': 'AUTH_AMOUNT_NUM',
'9F03': 'OTHER_AMOUNT_NUM',
'9F04': 'OTHER_AMOUNT_BIN',
'9F05': 'APP_DISCRETIONARY_DATA',
'9F06': 'AID_TERMINAL',
'9F07': 'APP_USAGE_CONTROL',
'9F08': 'APP_VERSION_NUMBER',
'9F09': 'APP_VERSION_NUMBER_TERMINAL',
'9F0D': 'IAC_DEFAULT',
'9F0E': 'IAC_DENIAL',
'9F0F': 'IAC_ONLINE',
'9F10': 'ISSUER_APPLICATION_DATA',
'9F11': 'ISSUER_CODE_TABLE_IDX',
'9F12': 'APP_PREFERRED_NAME',
'9F13': 'LAST_ONLINE_ATC',
'9F14': 'LOWER_OFFLINE_LIMIT',
'9F15': 'MERCHANT_CATEGORY_CODE',
'9F16': 'MERCHANT_ID',
'9F17': 'PIN_TRY_COUNT',
'9F18': 'ISSUER_SCRIPT_ID',
'9F1A': 'TERMINAL_COUNTRY_CODE',
'9F1B': 'TERMINAL_FLOOR_LIMIT',
'9F1C': 'TERMINAL_ID',
'9F1D': 'TRM_DATA',
'9F1E': 'IFD_SERIAL_NUM',
'9F1F': 'TRACK_1_DD',
'9F21': 'TRANSACTION_TIME',
'9F22': 'CA_PK_INDEX_TERM',
'9F23': 'UPPER_OFFLINE_LIMIT',
'9F26': 'APPLICATION_CRYPTOGRAM',
'9F27': 'CRYPTOGRAM_INFORMATION_DATA',
'9F2D': 'ICC_PIN_ENCIPHERMENT_PK_CERT',
'9F32': 'ISSUER_PK_EXPONENT',
'9F33': 'TERMINAL_CAPABILITIES',
'9F34': 'CVM_RESULTS',
'9F35': 'APP_TERMINAL_TYPE',
'9F36': 'APP_TRANSACTION_COUNTER',
'9F37': 'APP_UNPREDICTABLE_NUMBER',
'9F38': 'ICC_PDOL',
'9F39': 'POS_ENTRY_MODE',
'9F3A': 'AMOUNT_REF_CURRENCY',
'9F3B': 'APP_REF_CURRENCY',
'9F3C': 'TRANSACTION_REF_CURRENCY_CODE',
'9F3D': 'TRANSACTION_REF_CURRENCY_EXPONENT',
'9F40': 'ADDITIONAL_TERMINAL_CAPABILITIES',
'9F41': 'TRANSACTION_SEQUENCE_COUNTER',
'9F42': 'APP_CURRENCY_CODE',
'9F43': 'APP_REF_CURRENCY_EXPONENT',
'9F44': 'APP_CURRENCY_EXPONENT',
'9F45': 'DATA_AUTH_CODE',
'9F46': 'ICC_PK_CERTIFICATE',
'9F47': 'ICC_PK_EXPONENT',
'9F48': 'ICC_PK_REMAINDER',
'9F49': 'DDOL',
'9F4A': 'STATIC_DATA_AUTHENTICATION_TAG_LIST',
'9F4C': 'ICC_DYNAMIC_NUMBER',
A5: 'FCI_TEMPLATE',
BF0C: 'FCI_ISSUER_DD',
};
/**
* Get the human-readable name for an EMV tag
*/
export function getTagName(tag) {
const tagHex = tag.toString(16).toUpperCase();
if (tagHex in EMV_TAGS) {
return EMV_TAGS[tagHex];
}
return `UNKNOWN_${tagHex}`;
}
function formatTlvData(data, indent = 0) {
const tagNum = data.tag.bytes ? tagBytesToNumber(data.tag.bytes) : data.tag.number;
const tagHex = tagNum.toString(16).toUpperCase();
const tagName = getTagName(tagNum);
const prefix = ' '.repeat(indent);
const color = getTagColor(tagNum);
const colorReset = color ? RESET : '';
let result = `${prefix}${color}${tagHex} (${tagName})${colorReset}`;
if (data.children && data.children.length > 0) {
result += ':\n';
for (const child of data.children) {
result += formatTlvData(child, indent + 1);
}
}
else {
const buffer = Buffer.from(data.value);
const formatter = getTagFormatter(tagNum);
if (formatter) {
// Use custom formatter for known tags
const formatted = formatter(buffer);
result += `: ${formatted}\n`;
}
else if (BINARY_TAGS.has(tagNum)) {
// For binary data (certificates, etc.), only show hex with truncation
const hex = buffer.toString('hex').toUpperCase();
if (hex.length > 64) {
result += `: ${hex.substring(0, 64)}... (${String(buffer.length)} bytes)\n`;
}
else {
result += `: ${hex}\n`;
}
}
else {
// Default: show hex and ASCII
const hex = buffer.toString('hex').toUpperCase();
const ascii = buffer.toString().replace(/[^\x20-\x7E]/g, '.');
result += `: ${hex} [${ascii}]\n`;
}
}
return result;
}
function findInTlv(data, tag) {
const tagNum = data.tag.bytes ? tagBytesToNumber(data.tag.bytes) : data.tag.number;
if (tagNum === tag) {
return Buffer.from(data.value);
}
if (data.children) {
for (const child of data.children) {
const result = findInTlv(child, tag);
if (result !== undefined) {
return result;
}
}
}
return undefined;
}
/**
* Format a card response as a human-readable string
*/
export function format(response) {
const parsed = parse(response.buffer);
if (parsed.length === 0) {
return '';
}
return parsed.map((tlv) => formatTlvData(tlv)).join('');
}
/**
* Find a specific tag in a Buffer containing TLV data
* @param buffer - The buffer to search
* @param tag - The tag number to find (e.g., 0x4F for APP_IDENTIFIER)
* @returns The tag value as a Buffer, or undefined if not found
*/
export function findTagInBuffer(buffer, tag) {
const parsed = parse(buffer);
for (const tlv of parsed) {
const result = findInTlv(tlv, tag);
if (result !== undefined) {
return result;
}
}
return undefined;
}
/**
* Find a specific tag in a card response
* @param response - The card response to search
* @param tag - The tag number to find (e.g., 0x4F for APP_IDENTIFIER)
* @returns The tag value as a Buffer, or undefined if not found
*/
export function findTag(response, tag) {
return findTagInBuffer(response.buffer, tag);
}
//# sourceMappingURL=emv-tags.js.map