/** * @fileOverview Utility functions for creating and verifying paycodes */ 'use strict'; /* eslint id-length: [1, {exceptions: ["x","y"], max: 50, min: 2}] */ const crypto = require('crypto'); const debug = require('debug')('utils:paycodes'); const BView = require('bit-buffer').BitView; const BStream = require('bit-buffer').BitStream; const {createError, paycodeString} = require('../ComServe/utils.js'); const PAYCODE_METHODS = ['Bridge', 'Credorax', 'WorldPay', 'RBS']; const pCSAsciiToBin = [ // 0 1 2 3 4 5 6 7 8 9 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // : ; < = > ? @ -1, -1, -1, -1, -1, -1, -1, // A B C D E F G H J K L M N P R S T U V W X Y Z 10, 11, 12, 13, 14, 15, 16, 17, -1, 18, 19, 20, 21, 22, -1, 23, -1, 24, 25, 26, 27, 28, 29, 30, 31, -1 ]; const DEFAULT_LENGTH = 5; const DEFAULT_METHOD = 'Bridge'; module.exports = { simplePayCode, payCodeGeneration, payCodeValidate, PAYCODE_METHODS }; /** * A function to simplify generating paycode using reasonable defaults * * @returns {String|number} - Paycode string or -1 on error */ function simplePayCode() { return payCodeGeneration(paycodeString, DEFAULT_LENGTH, DEFAULT_METHOD); } /** * Generates a combined Bank routing and random code. This function is strong * enough for cryptographic use unless system entropy is below a certain level. * * @param {String} list - The string to use e.g. 'utils.numeric' for decimal. From options in utils.js * @param {!int} length - The length of the resulting output string. * @param {!string} method - The string to use to describe the payment method. All shown at the top of this file. * @return {string} payCodeStr - A generated paycode string based on the PayCode Wiki definition, * or -1 if an illegal method, or length requested */ // eslint-disable-next-line complexity function payCodeGeneration(list, length, method) { /** * Local variables. */ let x; let y; /** * Now generate the dimensions and bitstream array positions. */ const size = payCodeDimensions(length); // used for all the bit string indexes /** * Check for paycode dimensions failure. */ if (size === -1) { debug('Paycode Generation: Error-PayCodeDimensions failed (length:', length, ')'); return -1; } /** * Check that the provided list is long enough. */ if (list.length < Math.pow(2, size.bitChar)) { debug('Paycode Generation: Error-PayCode String List to short (length:', list.length, ')'); return -1; } /** * Generate the Bit array */ const array = new ArrayBuffer(size.bitLength); const bv = new BView(array); const bs = new BStream(bv); /** * Initialise the bitString, should be zero, and is overwritten, but be paranoid. */ for (x = 0; x < size.bitLength; x++) { bs.writeBits(0, 1); } /** * BankID is the index into the Method array. */ const bankID = PAYCODE_METHODS.indexOf(method); if (bankID === -1) { debug('Paycode Generation: Error Bank method not found:', method, bankID); return -1; } /** * Check BankID size. */ if (bankID > (Math.pow(2, size.bankBits))) { debug('Paycode Generation: Error BankID length wrong'); return -1; } /** * Add in the bank ID. */ bv.setBits(size.bankPosition, bankID, size.bankBits); /** * Generate random stream of bytes for the unique code of the paycode * Note: No psuedo random stream back up if call fails ... Possible TBD ? * If the call fails and throw's an error */ const bytes = crypto.randomBytes(Math.ceil((size.bitLength / 8))); // used for a random number string /** * todo Blocking behaviour may have changed. * The crypto.randomBytes() method will block until there is sufficient entropy. * This should normally never take longer than a few milliseconds. * The only time when generating the random bytes may conceivably block for a * longer period of time is right after boot, when the whole system is still low on entropy. */ /** * Shift random values into the paycode bit stream. */ for (x = 0; x < (Math.ceil(size.uniqueCode / 8)); x++) { /** * Work through each byte and then add in the remainder, until random string of bits in the unique code position and length. */ if (x < Math.floor(size.uniqueCode / 8)) { bv.setUint8(((x * 8) + size.uniqueCodePosition), bytes[x]); } else { /** * Does not need a mask as only uses the required bits. */ bv.setBits(((x * 8) + size.uniqueCodePosition), bytes[x], (size.uniqueCode % 8)); } } /** * XOR the bankid, with the last bankid length of bits before the checksum. */ const bankVal = bv.getBits(size.bankPosition, size.bankBits, false); const lastCodeVal = bv.getBits(size.uniqueCodePosition, size.bankBits, false); /** * Be aware that there may be a 32 bit limit on the Xor that is not checked for here * as the paycode construction precludes this, as the bankBits are always less than 32 * however should we go for much larger paycodes with corresponding increases in bankbits * there could be a problem here. */ /* jshint -W016 */ const xorResult = bankVal ^ lastCodeVal; /* jshint +W016 */ /** * Then put it back in. */ bv.setBits(size.bankPosition, xorResult, size.bankBits); /** * Finally the generate the Checksum. */ let checkSum = 0; for (x = size.uniqueCodePosition; x < size.bitLength; x += size.checkSumLength) { if ((size.bitLength - x) > size.checkSumLength) { /* jshint -W016 */ checkSum ^= bv.getBits(x, size.checkSumLength, false); /* jshint +W016 */ } else { /* jshint -W016 */ checkSum ^= bv.getBits(x, (size.bitLength - x), false); /* jshint +W016 */ } } /** * Put the checksum into the code. */ bv.setBits(size.checkSumPosition, checkSum, size.checkSumLength); /** * Generate the paycode string from the bitStream. */ let payCodeStr = ''; for (x = 0, y = size.bitLength - size.bitChar; x < length; x++, y -= size.bitChar) { payCodeStr += list[bv.getBits(y, size.bitChar, false)]; } /* * Return the string. */ return payCodeStr; } /** * Check the PayCode for validity with checks for length, checksum, bankid/method. * * @type {Function} payCodeValidation * @param {!string} payCodeStr - The payCode to validate. * @returns {Object} error - A detailed error response, include a success message (if achieved). */ // eslint-disable-next-line complexity function payCodeValidate(payCodeStr) { /** * Local variables. */ let error; let x; let y; /** * Error method that requires codes (note the user may want to know the paycode is the wrong length if typed). */ error = createError(10000, 'Success'); /** * check the string length. */ if ((payCodeStr.length < 5) || (payCodeStr.length > 10) || (payCodeStr.length === 9)) { error = createError(330, 'Error-Incorrect PayCode Length'); return error; } /** * Now we have confirmed a valid paycode length we need to setup the position array. */ const size = payCodeDimensions(payCodeStr.length); /** * Check for paycodedimensions failure */ if (size === -1) { error = createError(330, 'Error-PayCodeDimensions failed'); return error; } /** * Create the binary stream and view. */ const array = new ArrayBuffer(size.bitLength); const bv = new BView(array); /** * Generate the bitStream from the paycode. * Trying to save on searches etc, do a lookup. */ const str = '0'; const lim0 = str.charCodeAt(); for (x = 0, y = size.bitLength - size.bitChar; x < payCodeStr.length; x++, y -= size.bitChar) { const code = payCodeStr[x].charCodeAt(); const index = code - lim0; if ((index < pCSAsciiToBin.length) && (pCSAsciiToBin[index] !== -1)) { bv.setBits(y, pCSAsciiToBin[index], size.bitChar); } else { /** * Error Condition */ error = createError(330, 'Error-Incorrect PayCode formatting'); return error; } } /** * Check the checksum. */ let checkSum = 0; /** * Re-generate the Checksum. */ for (x = size.uniqueCodePosition; x < (size.bitLength); x += size.checkSumLength) { if ((size.bitLength - x) > size.checkSumLength) { /* jshint -W016 */ checkSum ^= bv.getBits(x, size.checkSumLength, false); /* jshint +W016 */ } else { /* jshint -W016 */ checkSum ^= bv.getBits(x, (size.bitLength - x), false); /* jshint +W016 */ } } /** * Get the checksum from the paycode code. */ const payCodeCheckSum = bv.getBits(size.checkSumPosition, size.checkSumLength, false); if (checkSum !== payCodeCheckSum) { error = createError(330, 'Error-Incorrect PayCode checksum'); return error; } /** * Now extract the bank ID. * XOR the bankid, with the last bankid length of bits before the checksum. */ const bankVal = bv.getBits(size.bankPosition, size.bankBits, false); const lastCodeVal = bv.getBits(size.uniqueCodePosition, size.bankBits, false); /* jshint -W016 */ const bankID = bankVal ^ lastCodeVal; /* jshint +W016 */ if (bankID > PAYCODE_METHODS.length) { error = createError(330, 'Error-BankID not valid method'); return error; } /* * Return the Error. */ return error; } /* * Return the paycode bit dimensions based on the number of characters. * * @type {function} payCodeDimensions * @param {!int} length - The payCode length. * @return {object} errorReturn - returns -1 or the size array containing the bit details of the code. */ function payCodeDimensions(length) { /** * Local variables. */ const size = {}; /* * Paycode formats as follows * Chars 5 6 7 8 10 * Length(Bits) 25 30 35 40 50 * Bank bits 4 6 9 13 21 * Unique(RND) 19 21 22 23 24 * ChkSum 2 3 4 4 5 */ /** * Size of paycode Character in bits. */ size.bitChar = 5; /** * Size of paycode. */ size.bitLength = length * size.bitChar; /** * Sizing of paycode internals, based on Phabricator Wiki. */ if (length === 5) { // IS/DK/SCO size.bankBits = 4; size.uniqueCode = 19; size.checkSumLength = 2; } else if (length === 6) { // UK/ES/ER size.bankBits = 6; size.uniqueCode = 21; size.checkSumLength = 3; } else if (length === 7) { // DE/JPN/RU size.bankBits = 9; size.uniqueCode = 22; size.checkSumLength = 4; } else if (length === 8) { // USA/CN/IN size.bankBits = 13; size.uniqueCode = 23; size.checkSumLength = 4; } else if (length === 10) { // International size.bankBits = 21; size.uniqueCode = 24; size.checkSumLength = 5; } else { /* * Return error, not handled code length. */ debug('PayCodeDimensions: Error PayCode length unsupported'); return -1; } /* * Some positional pointers for bit handling, note LSB/MSB for position, due to bitstream/view model */ size.bankPosition = size.checkSumLength + size.uniqueCode; size.uniqueCodePosition = size.checkSumLength; size.checkSumPosition = 0; return size; }