/*
* Author: Vlad Seryakov vseryakov@gmail.com
* backendjs 2018
*/
const crypto = require('node:crypto');
const logger = require(__dirname + '/../logger');
const lib = require(__dirname + '/../lib');
/**
* Generates cryptographically strong pseudorandom data. The size argument is a number indicating the number of bytes to generate.
* By default encode is "hex", can be any encoding supported by Buffer, "binary" returns Buffer itself.
* @param {int} [size=8]
* @param {string} [encode=hex]
* @return {string|Buffer}
* @memberof module:lib
* @method randomBytes
*/
lib.randomBytes = function(size, encode)
{
var b = crypto.randomBytes(this.toNumber(size, { min: 0, dflt: 8 }));
return encode === "binary" ? b : b.toString(encode || "hex");
}
/**
* Encrypt data with the given key, GCM mode is default
* @param {string|Buffer} key
* @param {string|Buffer} data
* @param {object} [options]
* @return {string|Buffer}
* @memberof module:lib
* @method encrypt
*/
lib.encrypt = function(key, data, options)
{
if (!key || !data) return '';
try {
options = options || this.empty;
const encode = options.encode === "binary" ? undefined : options.encode || "base64";
key = Buffer.isBuffer(key) ? key : typeof key == "string" ? key : String(key);
data = Buffer.isBuffer(data) ? data : Buffer.from(typeof data == "string" ? data : String(data));
const iv = crypto.randomBytes(options.iv_length || 16);
const password = crypto.pbkdf2Sync(key, iv.toString(), options.key_iterations || 10000, options.key_length || 32, options.key_hash || 'sha256');
const cipher = crypto.createCipheriv(options.algorithm || 'aes-256-gcm', password, iv, { authTagLength: options.tag_length || 16 });
var msg = Buffer.concat([cipher.update(data), cipher.final()]);
msg = Buffer.concat([iv, cipher.getAuthTag(), msg]);
if (encode) msg = msg.toString(encode);
} catch (e) {
msg = '';
logger.debug('encrypt:', options, e.stack);
}
return msg;
}
/**
* Decrypt data with the given key, GCM mode is default
* @param {string|Buffer} key
* @param {string|Buffer} data
* @param {object} [options]
* @return {string}
* @memberof module:lib
* @method decrypt
*/
lib.decrypt = function(key, data, options)
{
if (!key || !data) return '';
try {
options = options || this.empty;
const encode = options.encode === "binary" ? undefined : options.encode || "base64";
key = Buffer.isBuffer(key) ? key : typeof key == "string" ? key : String(key);
data = Buffer.isBuffer(data) ? data : Buffer.from(typeof data == "string" ? data : String(data), encode);
const iv = data.slice(0, options.iv_length || 16);
const password = crypto.pbkdf2Sync(key, iv.toString(), options.key_iterations || 10000, options.key_length || 32, options.key_hash || 'sha256');
const decipher = crypto.createDecipheriv(options.algorithm || 'aes-256-gcm', password, iv, { authTagLength: options.tag_length || 16 });
const tag = data.slice(iv.length, iv.length + (options.tag_length || 16));
decipher.setAuthTag(tag);
var msg = Buffer.concat([decipher.update(data.slice(iv.length + tag.length)), decipher.final()]).toString("utf8");
} catch (e) {
msg = '';
logger.debug('decrypt:', options, e.stack);
}
return msg;
}
/**
* HMAC signing and base64 encoded, default algorithm is sha1
* @param {string} key
* @param {string|Buffer} data
* @param {toString} [algorithm=sha1]
* @param {string} [encode=base64]
* @return {string}
* @memberof module:lib
* @method sign
*/
lib.sign = function (key, data, algorithm, encode)
{
encode = encode === "binary" ? undefined : encode || "base64";
try {
return crypto.createHmac(algorithm || "sha1", key || "").update(data || "").digest(encode);
} catch (e) {
logger.error('sign:', algorithm, encode, e.stack);
return "";
}
}
/**
* Generate random key, size if specified defines how many random bits to generate
* @param {int} [size=256]
* @return {string}
* @memberof module:lib
* @method random
*/
lib.random = function(size)
{
return this.sign(crypto.randomBytes(64), crypto.randomBytes(size || 256), 'sha256').replace(/[=+%]/g, '');
}
/**
* Return random number between 0 and USHORT_MAX
* @return {number}
* @memberof module:lib
* @method randomUShort
*/
lib.randomUShort = function()
{
return crypto.randomBytes(2).readUInt16LE(0);
}
/**
* Return random number between 0 and SHORT_MAX
* @return {number}
* @memberof module:lib
* @method randomShort
*/
lib.randomShort = function()
{
return Math.abs(crypto.randomBytes(2).readInt16LE(0));
}
/**
* Return random number between 0 and ULONG_MAX
* @return {number}
* @memberof module:lib
* @method randomUInt
*/
lib.randomUInt = function()
{
return crypto.randomBytes(6).readUIntLE(0, 6);
}
/**
* Returns random number between 0 and 1, 32 bits
* @return {number}
* @memberof module:lib
* @method randomFloat
*/
lib.randomFloat = function()
{
return parseFloat("0." + crypto.randomBytes(4).readUInt32LE(0));
}
/**
* Return random integer between min and max inclusive using crypto generator, based on
* https://github.com/joepie91/node-random-number-csprng
* @param {int} min
* @param {int} max
* @return {number}
* @memberof module:lib
* @method randomInt
*/
lib.randomInt = function(min, max)
{
min = this.toNumber(min, { min: 0, max: 429497294 });
max = this.toNumber(max, { min: 0, max: 429497295 });
if (max <= min) max = 429497295;
var bits = Math.ceil(Math.log2(max - min));
var bytes = Math.ceil(bits / 8);
var mask = Math.pow(2, bits) - 1, n;
for (var t = 0; t < 3; t++) {
var d = crypto.randomBytes(bytes);
n = 0;
for (var i = 0; i < bytes; i++) n |= d[i] << 8 * i;
n = n & mask;
if (n <= max - min) break;
}
return min + n;
}
/**
* Generates a random number between given min and max (required)
* Optional third parameter indicates the number of decimal points to return:
* - If it is not given or is NaN, random number is unmodified
* - If >0, then that many decimal points are returned (e.g., "2" -> 12.52
* @param {int} min
* @param {int} max
* @param {int} [decs]
* @return {number}
* @memberof module:lib
* @method randomNum
*/
lib.randomNum = function(min, max, decs)
{
var num = min + (this.randomFloat() * (max - min));
return (typeof decs !== 'number' || decs <= 0) ? num : parseFloat(num.toFixed(decs));
}
/**
* Timing safe string compare using double HMAC, from suryagh/tsscmp
* @param {string|Buffer} a
* @param {string|Buffer} b
* @return {boolean}
* @memberof module:lib
* @method timingSafeEqual
*/
lib.timingSafeEqual = function(a, b)
{
if (typeof a == "string" && typeof b == "string") {
return a.length === b.length && crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
} else
if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) {
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
return false;
}
/**
* Create a Timed One-Time Password, RFC6328
* @param {string} key
* @param {object} [options]
* @return {number}
* @memberof module:lib
* @method totp
*/
lib.totp = function(key, options)
{
if (typeof key != "string") return 0;
const time = Buffer.from(Math.floor((options?.time || Date.now()) / 1000 / (options?.interval || 30)).toString(16).padStart(16, "0"), "hex");
const hmac = lib.sign(this.fromBase32(key.toUpperCase()), time, options?.algorithm || "sha1", "binary");
const offset = hmac[hmac.length - 1] & 0xf;
const code = (hmac[offset] & 0x7f) << 24 | (hmac[offset + 1] & 0xff) << 16 | (hmac[offset + 2] & 0xff) << 8 | (hmac[offset + 3] & 0xff);
return (code % Math.pow(10, options?.digits || 6)).toString().padStart(options?.digits || 6, '0');
}
const _skip32table = [
0xa3,0xd7,0x09,0x83,0xf8,0x48,0xf6,0xf4,0xb3,0x21,0x15,0x78,0x99,0xb1,0xaf,0xf9,
0xe7,0x2d,0x4d,0x8a,0xce,0x4c,0xca,0x2e,0x52,0x95,0xd9,0x1e,0x4e,0x38,0x44,0x28,
0x0a,0xdf,0x02,0xa0,0x17,0xf1,0x60,0x68,0x12,0xb7,0x7a,0xc3,0xe9,0xfa,0x3d,0x53,
0x96,0x84,0x6b,0xba,0xf2,0x63,0x9a,0x19,0x7c,0xae,0xe5,0xf5,0xf7,0x16,0x6a,0xa2,
0x39,0xb6,0x7b,0x0f,0xc1,0x93,0x81,0x1b,0xee,0xb4,0x1a,0xea,0xd0,0x91,0x2f,0xb8,
0x55,0xb9,0xda,0x85,0x3f,0x41,0xbf,0xe0,0x5a,0x58,0x80,0x5f,0x66,0x0b,0xd8,0x90,
0x35,0xd5,0xc0,0xa7,0x33,0x06,0x65,0x69,0x45,0x00,0x94,0x56,0x6d,0x98,0x9b,0x76,
0x97,0xfc,0xb2,0xc2,0xb0,0xfe,0xdb,0x20,0xe1,0xeb,0xd6,0xe4,0xdd,0x47,0x4a,0x1d,
0x42,0xed,0x9e,0x6e,0x49,0x3c,0xcd,0x43,0x27,0xd2,0x07,0xd4,0xde,0xc7,0x67,0x18,
0x89,0xcb,0x30,0x1f,0x8d,0xc6,0x8f,0xaa,0xc8,0x74,0xdc,0xc9,0x5d,0x5c,0x31,0xa4,
0x70,0x88,0x61,0x2c,0x9f,0x0d,0x2b,0x87,0x50,0x82,0x54,0x64,0x26,0x7d,0x03,0x40,
0x34,0x4b,0x1c,0x73,0xd1,0xc4,0xfd,0x3b,0xcc,0xfb,0x7f,0xab,0xe6,0x3e,0x5b,0xa5,
0xad,0x04,0x23,0x9c,0x14,0x51,0x22,0xf0,0x29,0x79,0x71,0x7e,0xff,0x8c,0x0e,0xe2,
0x0c,0xef,0xbc,0x72,0x75,0x6f,0x37,0xa1,0xec,0xd3,0x8e,0x62,0x8b,0x86,0x10,0xe8,
0x08,0x77,0x11,0xbe,0x92,0x4f,0x24,0xc5,0x32,0x36,0x9d,0xcf,0xf3,0xa6,0xbb,0xac,
0x5e,0x6c,0xa9,0x13,0x57,0x25,0xb5,0xe3,0xbd,0xa8,0x3a,0x01,0x05,0x59,0x2a,0x46,
];
function _round16(key, k, n)
{
var g1 = (n >> 8) & 0xff;
var g2 = (n >> 0) & 0xff;
var g3 = _skip32table[g2 ^ key[(4 * k + 0) % 10]] ^ g1;
var g4 = _skip32table[g3 ^ key[(4 * k + 1) % 10]] ^ g2;
var g5 = _skip32table[g4 ^ key[(4 * k + 2) % 10]] ^ g3;
var g6 = _skip32table[g5 ^ key[(4 * k + 3) % 10]] ^ g4;
return (g5 << 8) + g6;
}
/**
* Encrypt/decrypt a number using a 10 byte `key` array, `op` == `d` for decrypt, other is encrypt
*
* based on public domain javascript implementation of:
*
* SKIP32 -- 32 bit block cipher based on SKIPJACK.
* Written by Greg Rose, QUALCOMM Australia, 1999/04/27.
* In common: F-table, G-permutation, key schedule.
* Different: 24 round feistel structure.
* Based on: Unoptimized test implementation of SKIPJACK algorithm Panu Rissanen <bande@lut.fi>
* SKIPJACK and KEA Algorithm Specifications
* Version 2.0
* 29 May 1998
* @param {string} op
* @param {string} key
* @param {number} num
* @return {number}
* @memberof module:lib
* @method toSkip32
*/
lib.toSkip32 = function(op, key, num)
{
var k = 0, d = 1;
if (op == "d") k = 23, d = -1;
var wl = (((num >> 24) & 0xff) << 8) + (((num >> 16) & 0xff) << 0);
var wr = (((num >> 8) & 0xff) << 8) + (((num >> 0) & 0xff) << 0);
for (let i = 0; i < 24/2; i++) {
wr ^= _round16(key, k, wl) ^ k;
k += d;
wl ^= _round16(key, k, wr) ^ k;
k += d;
}
return ((wr << 16) | wl) >>> 0;
}
/**
* Hash given plain text password with node.scrypt to be saved in users database, returns base64 encoded encrypted and salt
* as `encrypted:salt`
* @param {string} text
* @return {function} callback - (err, secret)
*/
lib.prepareSecret = function(text, callback)
{
var secret, salt = crypto.randomBytes(16).toString("base64");
crypto.scrypt(lib.isString(text) || String(text), salt, 64, (err, key) => {
if (!err) secret = key.toString("base64") + ":" + salt;
callback(err, secret);
});
}
/**
* Verify an encrypted secret with given plain text password
* @param {string} secret - a hashed secret by `prepareSecret`
* @param {string} password - plain text password to be verified
* @return {function} callback - (err, true) if verified otherwise error or not equal
*/
lib.checkSecret = function(secret, password, callback)
{
if (!secret || !password) return callback();
// Exact
if (secret === password) return callback(null, true);
const [, salt] = lib.split(secret, ":");
if (!secret || !salt) return callback();
crypto.scrypt(password, salt, 64, (err, key) => {
if (!err && lib.timingSafeEqual(key, Buffer.from(secret, "base64"))) return callback(null, true);
callback(err);
});
}