UNPKG

emv

Version:

EMV / Chip and PIN CLI and library for PC/SC card readers

690 lines 22.7 kB
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