402 lines
12 KiB
JavaScript
402 lines
12 KiB
JavaScript
/**
|
|
* @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;
|
|
}
|