bridge-node-server/node_server/utils/paycodes.js
Martin Donnelly 57bd6c8e6a init
2018-06-24 21:15:03 +01:00

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;
}