UNPKG

smartcardx

Version:

Backend library for communication with smartcards using system native PCSC interface. Plain Iso7816 + EMV + GlobalPlatform functionality.

407 lines (406 loc) 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isHexString = isHexString; exports.strHasHexPrefix = strHasHexPrefix; exports.normalizeHexString = normalizeHexString; exports.getMinWordNum = getMinWordNum; exports.hexDecode = hexDecode; exports.hexEncode = hexEncode; exports.isBinData = isBinData; exports.importBinData = importBinData; exports.decodeAtr = decodeAtr; const hexValidationRegex = /^(0[xX])?[0-9A-Fa-f]+$/g; // '0x' prefix allowed /** Checks if string is a valid hex string. Both with or without `0x` prefix. Case insensitive. Empty string is a valid hex string. */ function isHexString(str) { if (str.length < 1) return true; if (str.match(hexValidationRegex)) return true; return false; } /** Returns true if string begins with `0x`. Case insensitive. */ function strHasHexPrefix(str) { if (str.length < 2) return false; if (str[0] === '0' && (str[1] === 'x' || str[1] === 'X')) return true; return false; } /** Removes the initial `0x` (if any) from a string and adds a leading zero if length is odd. Case insensitive. */ function normalizeHexString(str) { return `${str.length % 2 ? '0' : ''}${strHasHexPrefix(str) ? str.substring(2) : str}`; } /** Given a numeric value and a word length in bits, returns the minimum number of words needed to represent that value * @param value - numeric value * @param wordBitLen - length of word in bits. Default: `8` */ function getMinWordNum(value, wordBitLen = 8) { return Math.max(1, Math.ceil(Math.log2(value + 1) / wordBitLen)); } /** Decodes a hex string. Case insensitive. Strings can have `0x` prefix. Throws if `outBuffer` is defined, but does not have enough space (considering `outOffset`, if any). * @param str - hex string to decode * @param outBuffer - if defined, the result of the decoding will be written to this/underlying ArrayBuffer. If defined, the returned Uint8Array will refecence the memory region to which data were written. * @param outOffset - This has effect ONLY IF `outBuffer` is defined. If specified, the result of the decoding will be written starting from this offset. In case `outBuffer` is an ArrayBufferView, this value is relative to the byteOffset of the view itself, not to the start of the underlying ArrayBuffer */ function hexDecode(str, outBuffer, outOffset = 0) { if (typeof str !== 'string') throw new TypeError('Not a string'); if (!isHexString(str)) throw new Error(`Not a hex string: [${str}]`); const _str = normalizeHexString(str); const requiredByteLength = _str.length / 2; let res; if (typeof outBuffer === 'undefined') { res = new Uint8Array(requiredByteLength); } else { if (outBuffer instanceof ArrayBuffer) { res = new Uint8Array(outBuffer); } else if (ArrayBuffer.isView(outBuffer)) { res = new Uint8Array(outBuffer.buffer).subarray(outBuffer.byteOffset, outBuffer.byteOffset + outBuffer.byteLength); } else { throw new TypeError('outBuffer must be an ArrayBuffer or ArrayBufferView'); } if (outOffset < 0 || outOffset >= outBuffer.byteLength) throw new Error(`outOffset value out of bounds; value: ${outOffset}`); res = res.subarray(outOffset); if (requiredByteLength > res.byteLength) throw new Error('Not enough space in the provided outBuffer'); } for (let byteIdx = 0; byteIdx < requiredByteLength; byteIdx++) { const strIdx = byteIdx * 2; res[byteIdx] = parseInt(`${_str[strIdx]}${_str[strIdx + 1]}`, 16); } return res.subarray(0, requiredByteLength); } function hexEncode(data) { let result = ''; let byteArray; try { byteArray = importBinData(data); } catch (error) { throw new Error(`Error hexencoding value: ${error.message}`); } for (let i = 0; i < byteArray.byteLength; i++) { const byteHex = byteArray[i].toString(16); result += `${byteHex.length % 2 ? '0' : ''}${byteHex}`; } return result; } function isBinData(input) { if (typeof input === 'string' && isHexString(input)) { return true; } else if (input instanceof ArrayBuffer) { return true; } else if (ArrayBuffer.isView(input)) { return true; } else if (Array.isArray(input)) { for (let i = 0; i < input.length; i++) { if (typeof input[i] !== 'number') return false; } return true; } return false; } /** Converts various binary data representations to an Uint8Array. Where possible, the returned Uint8Array will reference the same memory region as input data. In case of hex strings and number arrays a new memory region will be allocated. If a copy of data is needed in any case, see the `outBuffer` parameter below. Throws if `outBuffer` is defined, but does not have enough space (considering `outOffset`, if any). * @param inData - input binary data. Strings must contain a valid hex value. If type is a numeric array, all values should be in the range 0-255, otherwise they will be wrapped around. For example -1 = 255 and 256/512 = 0 * @param outBuffer - if defined, the result of the import will be copied to this/underlying ArrayBuffer. If defined, the returned Uint8Array will refecence the memory region to which data were written. * @param outOffset - This has effect ONLY IF `outBuffer` is defined. If specified, the result of the import will be written starting from this offset. In case `outBuffer` is an ArrayBufferView, this value is relative to the byteOffset of the view itself, not to the start of the underlying ArrayBuffer */ function importBinData(inData, outBuffer, outOffset = 0) { if (typeof inData === 'string') { try { return hexDecode(inData, outBuffer, outOffset); } catch (error) { throw new Error(`Error decoding hex string: ${error.message}`); } } let inByteArray = new Uint8Array(0); let requiredByteLength; let dataIsNumArray = false; if (inData instanceof ArrayBuffer) { inByteArray = new Uint8Array(inData); requiredByteLength = inByteArray.byteLength; } else if (ArrayBuffer.isView(inData)) { inByteArray = new Uint8Array(inData.buffer).subarray(inData.byteOffset, inData.byteOffset + inData.byteLength); requiredByteLength = inByteArray.byteLength; } else if (Array.isArray(inData)) { dataIsNumArray = true; requiredByteLength = inData.length; } else { throw new TypeError('Accepted binary data types: hex string, number[], ArrayBuffer, ArrayBufferView'); } let outByteArray = new Uint8Array(0); const copyRequired = typeof outBuffer !== 'undefined'; if (!copyRequired) { if (dataIsNumArray) { outByteArray = new Uint8Array(requiredByteLength); } else { outByteArray = inByteArray; // is undefined only if data is a number[] } } else { if (outBuffer instanceof ArrayBuffer) { outByteArray = new Uint8Array(outBuffer); } else if (ArrayBuffer.isView(outBuffer)) { outByteArray = new Uint8Array(outBuffer.buffer).subarray(outBuffer.byteOffset, outBuffer.byteOffset + outBuffer.byteLength); } else { throw new TypeError('outBuffer must be an ArrayBuffer or ArrayBufferView'); } if (outOffset < 0 || outOffset >= outBuffer.byteLength) throw new Error(`outOffset value out of bounds; value: ${outOffset}`); outByteArray = outByteArray.subarray(outOffset); if (requiredByteLength > outByteArray.byteLength) throw new Error('Not enough space in the provided outBuffer'); } if (dataIsNumArray) { for (let i = 0; i < inData.length; i++) { if (typeof inData[i] !== 'number') throw new TypeError('Data is not a numeric array'); outByteArray[i] = inData[i]; } } else if (copyRequired) { outByteArray.set(inByteArray); } return outByteArray.subarray(0, requiredByteLength); } function decodeTA(byte, i) { let result = byte; if (i === 1) { const low = byte & 0x0f; const high = (byte >> 4) & 0x0f; result = { fMax: -1, Fi: -1, Di: -1, cyclesPerETU: -1 }; switch (low) { case 1: result.Di = 1; break; case 2: result.Di = 2; break; case 3: result.Di = 4; break; case 4: result.Di = 8; break; case 5: result.Di = 16; break; case 6: result.Di = 32; break; case 7: result.Di = 64; break; case 8: result.Di = 12; break; case 9: result.Di = 20; break; default: break; } switch (high) { case 0: result.Fi = 372; result.fMax = 4; break; case 1: result.Fi = 372; result.fMax = 5; break; case 2: result.Fi = 558; result.fMax = 6; break; case 3: result.Fi = 744; result.fMax = 8; break; case 4: result.Fi = 1116; result.fMax = 12; break; case 5: result.Fi = 1488; result.fMax = 16; break; case 6: result.Fi = 1860; result.fMax = 20; break; case 9: result.Fi = 512; result.fMax = 5; break; case 10: result.Fi = 768; result.fMax = 7.5; break; case 11: result.Fi = 1024; result.fMax = 10; break; case 12: result.Fi = 1536; result.fMax = 15; break; case 13: result.Fi = 2048; result.fMax = 20; break; default: break; } if (result.Fi > 0 && result.Di > 0) result.cyclesPerETU = result.Fi / result.Di; } else if (i === 2) { result = { T: byte & 0x0f, canChange: ((byte >> 7) & 0x01) === 0, implicitETUDuration: ((byte >> 4) & 0x01) > 0, }; } return result; } function decodeTB(byte, i) { let result = byte; if (i === 1) { result = { connected: false, PI1: -1, I: -1 }; const PI1 = byte & 0x1f; const I = (byte >> 5) & 0x03; switch (true) { case PI1 > 0: result.connected = true; result.PI1 = PI1; switch (I) { case 0: result.I = 25; break; case 1: result.I = 50; break; default: break; } break; default: break; } } else if (i === 3) { result = { BWI: (byte >> 4) & 0x0f, CWI: byte & 0x0f, }; } return result; } function decodeTC(byte, _i) { return byte; } function isValidOffset(offset, length) { if (offset < length) return true; return false; } function decodeAtr(atr) { let inBuffer; try { inBuffer = importBinData(atr); } catch (error) { throw new Error(`Error decoding ATR: Error importing ATR binary: ${error.message}`); } if (inBuffer.byteLength < 2) { throw new Error(`Error decoding ATR: ATR length expected to be at least 2 bytes. Received: ${inBuffer.byteLength}`); } const result = { TS: 'direct', T0: { K: 0, Y: '0b0000' }, TA: {}, TB: {}, TC: {}, TD: {}, historicalBytes: new Uint8Array(0), }; switch (inBuffer[0]) { case 0x3b: result.TS = 'direct'; break; case 0x3f: result.TS = 'inverse'; break; default: throw new Error('Error decoding ATR: invalid TS byte value'); } result.T0.K = inBuffer[1] & 0x0f; result.T0.Y = `0b${((inBuffer[1] >> 4) & 0x0f).toString(2).padStart(4, '0')}`; let currI = 0; let currOffset = 1; let lastStructuralByte = inBuffer[1]; while (true) { currI++; currOffset++; if ((lastStructuralByte & 0x10) > 0) { if (!isValidOffset(currOffset, inBuffer.byteLength)) throw new Error(`Error decoding ATR: Error decodinng TA(${currI}): unexpected end of data`); result.TA[currI] = decodeTA(inBuffer[currOffset], currI); currOffset += 1; } if ((lastStructuralByte & 0x20) > 0) { if (!isValidOffset(currOffset, inBuffer.byteLength)) throw new Error(`Error decoding ATR: Error decodinng TB(${currI}): unexpected end of data`); result.TB[currI] = decodeTB(inBuffer[currOffset], currI); currOffset += 1; } if ((lastStructuralByte & 0x40) > 0) { if (!isValidOffset(currOffset, inBuffer.byteLength)) throw new Error(`Error decoding ATR: Error decodinng TC(${currI}): unexpected end of data`); result.TC[currI] = decodeTC(inBuffer[currOffset], currI); currOffset += 1; } if ((lastStructuralByte & 0x80) > 0) { if (!isValidOffset(currOffset, inBuffer.byteLength)) throw new Error(`Error decoding ATR: Error decodinng TD(${currI}): unexpected end of data`); result.TD[currI] = { Y: `0b${((inBuffer[currOffset] >> 4) & 0x0f).toString(2).padStart(4, '0')}`, T: inBuffer[currOffset] & 0x0f, }; lastStructuralByte = inBuffer[currOffset]; } else { // no structural byte means no more interface bytes break; } } if (inBuffer.byteLength < currOffset + result.T0.K) { throw new Error('Error decoding ATR: error reading historical bytes: unexpected end of data'); } if (result.T0.K > 0) { result.historicalBytes = new Uint8Array(result.T0.K); importBinData(inBuffer.subarray(currOffset, currOffset + result.T0.K), result.historicalBytes); currOffset += result.T0.K; } if (currOffset < inBuffer.byteLength) result.TCK = inBuffer[currOffset]; return result; }