import crypto from 'crypto';
import zlib from 'zlib';
import {promisify} from 'util';
import _ from 'lodash';
import baseConvert from './base_convert';
import Str from '../Str';
const gzinflate = promisify(zlib.inflateRaw);
const gzdeflate = promisify(zlib.deflateRaw);
/**
* Cryptographic utilities
* @namespace Crypt
*/
/**
* different charsets available
* @memberof Crypt
* @type {object}
*/
const chars = {};
chars.NUMERIC = '0123456789';
chars.LOWER = 'abcdefghijklmnopqrstuvwxyz';
chars.UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
// missing ', ", space, \ (as these might cause problems when copying strings)
chars.SYMBOLS = '-_!#$%&()*+,./:;<=>?@[]^`{|}~';
chars.HEX = chars.NUMERIC + 'abcdef';
chars.BASE_36 = chars.NUMERIC + chars.LOWER;
chars.ALPHA = chars.LOWER + chars.UPPER;
chars.ALPHA_NUMERIC = chars.NUMERIC + chars.ALPHA;
chars.BASE_62 = chars.ALPHA_NUMERIC;
chars.BASE_64 = chars.ALPHA_NUMERIC + '-_';
chars.PRINTABLE = chars.ALPHA_NUMERIC + chars.SYMBOLS;
// private data for this module
const data = {};
/**
* Return a random number between 0 and 1
* @memberof Crypt
* @return {number} A random number (float) between 0 and 1
*/
function random() {
return Math.random();
}
/**
* Return a random integer between min and max (both inclusive)
*
* @memberof Crypt
* @param {number} num If max is not passed, num is max and min is 0
* @param {number} [max] max value of int, num will be min
* @return {number} A random integer between min and max
*/
function randomInt(num, max) {
if (max === undefined) {
max = num;
num = 0;
}
return Math.floor((Math.random() * ((max - num) + 1)) + num);
}
/**
* Count the minimum number of bits to represent the provided number
*
* This is basically floor(log($number, 2))
* But avoids float precision issues
*
* @param number
* @return {number} The number of bits
* @ignore
*/
function countBits(number) {
let log2 = 0;
while ((number >>= 1)) {
log2++;
}
return log2;
}
/**
* @typedef {object} randomOpts
* @property {number} length integer
* @property {boolean} base36 if true will use BASE_36 charset
* @property {string} charset provide a charset string
* @property {function(number):Buffer} [randomBytesFunc] only considered for some fns
*/
/**
* Generate a random string based on the options passed.
* It can be treated as a Random UUID.
*
* You can give length and charset in options.
* If options is an integer it will treated as length.
* By default, length is 20 and charset is ALPHA_NUMERIC
*
* @memberof Crypt
* @param {number | randomOpts} options
* length of the id or options object
* @return {string} id
*/
function randomString(options = {}) {
let length;
let charset;
let result = '';
if (options === 0) {
return '';
}
if (typeof options === 'object') {
length = options.length || 20;
if (options.base36) {
charset = chars.BASE_36;
}
else {
charset = options.charset || chars.ALPHA_NUMERIC;
}
}
else if (typeof options === 'number') {
length = options;
charset = chars.ALPHA_NUMERIC;
}
else {
length = 20;
charset = chars.ALPHA_NUMERIC;
}
// determine mask for valid characters
const mask = 256 - (256 % charset.length);
let randomFunc;
if (options.randomBytesFunc) {
randomFunc = options.randomBytesFunc;
}
else {
randomFunc = crypto.randomBytes.bind(crypto);
}
// Generate the string
while (result.length < length) {
// determine number of bytes to generate
const bytes = (length - result.length) * Math.ceil(countBits(charset.length) / 8);
let randomBuffer;
try {
randomBuffer = randomFunc(bytes);
}
catch (e) {
continue;
}
for (let i = 0; i < randomBuffer.length; i++) {
if (randomBuffer[i] > mask) continue;
result += charset[randomBuffer[i] % charset.length];
}
}
return result;
}
/**
* Converts String to int key if not already number.
* Returns same number if key is already number
* @param {string | number} key
*/
function getIntegerKey(key) {
if (typeof key === 'number') return key;
if (typeof key === 'string') {
let value = 0;
for (let i = 0; i < key.length; ++i) {
value = ((value << 5) - value) + key.charCodeAt(i); // value * 31 + c
value |= 0; // to 32bit integer
}
return value;
}
return key;
}
/**
* Shuffle an array or a string.
*
* @memberof Crypt
* @template T
* @param {Array<T> | string} itemToShuffle item which you want to shuffle
* @param {object} [options = {}] object of {seed: number}
* @param {function():number} options.randomFunc Use this random function instead of default
* @param {number | string} options.seed optionally give a seed to do a constant shuffle
* @return {Array<T> | string} shuffled item
*/
function shuffle(itemToShuffle, options = {}) {
let array;
if (typeof itemToShuffle === 'string') {
array = itemToShuffle.split();
}
else {
array = itemToShuffle.slice();
}
if (!Array.isArray(array)) {
throw new Error('Array expected');
}
let randomFunc;
if (options.randomFunc) {
randomFunc = options.randomFunc;
}
else if (options.seed) {
let seed = getIntegerKey(options.seed);
randomFunc = function () {
const x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
};
}
else {
randomFunc = Math.random;
}
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(randomFunc() * (i + 1));
const temp = array[i];
array[i] = array[j];
array[j] = temp;
}
if (typeof itemToShuffle === 'string') {
return array.join();
}
return array;
}
/**
* @typedef {object} randomFunctions
* @property {number} seed
* @property {number} index
* @property {function():number} random number b/w 0 and 1
* @property {function(number, (number | undefined)):number} int int less than num or b/w num & max
* @property {function(number):Buffer} bytes
* @property {function((number | randomOpts)):string} string
* @property {function((any[] | string)):(any[] | string)} shuffle
*/
/**
*
* @memberof Crypt
* @param {number} seed integer
* @return {randomFunctions} Object of all random functions
*/
function seededRandom(seed) {
seed = getIntegerKey(seed);
return {
seed,
index: 0,
random() {
const x = Math.sin(seed + this.index++) * 10000;
return x - Math.floor(x);
},
int(num, max = undefined) {
if (max === undefined) {
max = num;
num = 0;
}
return Math.floor((this.random() * ((max - num) + 1)) + num);
},
bytes(numBytes) {
const result = [];
while (numBytes--) {
result.push(this.int(0, 255));
}
return Buffer.from(result);
},
string(options = {}) {
options.randomBytesFunc = this.bytes.bind(this);
return randomString(options);
},
shuffle(itemToShuffle) {
const randomFunc = this.random.bind(this);
return shuffle(itemToShuffle, {randomFunc});
},
};
}
/**
* Get nanoseconds in base62 or base36 format.
*
* @memberof Crypt
* @param {boolean} base36 use base36 format or not
* @return {string} nanoseconds
*/
function nanoSecondsAlpha(base36 = false) {
const hrtime = process.hrtime();
// largest base62 7 chars string is 3521_614_606_207
// largest base36 8 chars string is 2821_109_907_455
// last 9 digits are for nanoseconds
// hence take mod 3521 from seconds so that overall string length = 7
if (base36) {
const seconds = String(hrtime[0] % 2821) + ('000000000' + String(hrtime[1])).slice(-9);
return ('00000000' + baseConvert(seconds, 10, 36)).slice(-8);
}
const seconds = String(hrtime[0] % 3521) + ('000000000' + String(hrtime[1])).slice(-9);
return ('0000000' + baseConvert(seconds, 10, 62)).slice(-7);
}
/**
* Get sequential number that resets every millisecond.
*
* NOTE: Multiple call within the same millisecond will return 1, 2, 3 so on..
* The counter will reset on next millisecond
*
* @param {number} currentTime
* @return {number} sequential number
* @ignore
*/
function msCounter(currentTime) {
currentTime = currentTime || Date.now();
if (currentTime !== data.currentMillis) {
data.currentMillis = currentTime;
data.counter = 0;
}
data.counter = (data.counter || 0) + 1;
return data.counter;
}
/**
* Generate a sequential id based on current time in millisecond and some randomness.
* It can be treated as a Sequential UUID. Ideal for use as a DB primary key.
*
* NOTE: For best results use atleast 15 characters in base62 and 18 characters in base36 encoding
*
* @memberof Crypt
* @param {number|object} options length of the id or object of {length: int, base36: bool}
* @return {string} id
*/
function sequentialID(options) {
let length = 20;
let base36 = false;
if (options === 0) {
return '';
}
if (typeof options === 'object') {
length = options.length || length;
base36 = options.base36 || options.lowercase || base36;
}
else if (typeof options === 'number') {
length = options;
}
const currentTime = Date.now();
const counter = msCounter(currentTime);
let result;
if (base36) {
// convert current time in milliseconds to base36
// This will always return 8 characters till 2058
result = baseConvert(currentTime, 10, 36);
result += _.padStart(baseConvert(counter, 10, 36), 4, '0');
}
else {
// convert current time in milliseconds to base62
// This will always return 7 characters till 2080
result = baseConvert(currentTime, 10, 62);
result += _.padStart(baseConvert(counter, 10, 62), 3, '0');
}
if (length < result.length) {
return result.substring(0, length);
}
let randomStr;
if (base36) {
randomStr = randomString({length: length - result.length, charset: chars.BASE_36});
}
else {
randomStr = randomString(length - result.length);
}
return result + randomStr;
}
/**
* Add dashes to a hex string to make it like v4 UUID.
* v4 UUID = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
* where x is any hexadecimal digit and y is one of 8, 9, a, or b
* eg. f47ac10b-58cc-4372-a567-0e02b2c3d479
*
* @param {string} str the hex string
* @return {string} the modified string
* @private
*/
function addDashesForUUID(str) {
return str.substring(0, 8) + '-' +
str.substring(8, 12) + '-' +
'4' +
str.substring(12, 15) + '-' +
'a' +
str.substring(15, 18) + '-' +
str.substring(18, 30);
}
/**
* Get sequential ID in v4 UUID format.
* @memberof Crypt
*/
function sequentialUUID() {
return addDashesForUUID(baseConvert(sequentialID(21), 62, 16));
}
/**
* Get random ID in v4 UUID format.
* @memberof Crypt
*/
function randomUUID() {
return addDashesForUUID(randomString({length: 30, charset: chars.HEX}));
}
/**
* Supported Encodings:
* 'hex', 'binary' ('latin1'), 'ascii', 'base64', 'base64url',
* 'utf8', 'buffer', 'utf16le' ('ucs2')
* @typedef {object} encodingConversion
* @property {string} [toEncoding='base64url'] default 'base64url'
* @property {string} [fromEncoding='binary'] default 'binary'
*/
/**
* Encode the string/buffer using a given encoding.
* Supported Encodings:
* 'hex', 'binary' ('latin1'), 'ascii', 'base64', 'base64url',
* 'utf8', 'buffer', 'utf16le' ('ucs2')
*
* @memberof Crypt
* @param {string|Buffer} string item to be encoded
* @param {encodingConversion|string} opts object or string specifying the encoding(s)
* @return {string|Buffer} encoded item
*
* NOTE: If opts is a string, it is considered as the toEncoding.
* The fromEncoding defaults to binary, while the toEncoding defaults to base64url.
*/
function baseEncode(string, opts) {
if (_.isString(opts)) {
opts = {
toEncoding: opts,
fromEncoding: 'binary',
};
}
let fromEncoding = opts.fromEncoding || 'binary';
let toEncoding = opts.toEncoding || 'base64url';
if (fromEncoding === 'base64url') fromEncoding = 'base64';
const buffer = (string instanceof Buffer) ? string : Buffer.from(string, fromEncoding);
toEncoding = toEncoding.toLowerCase();
if (toEncoding === 'buffer') {
return buffer;
}
if (['ascii', 'utf8', 'utf16le', 'ucs2', 'base64', 'binary', 'hex'].indexOf(toEncoding) > -1) {
return buffer.toString(toEncoding);
}
if (toEncoding === 'base64url') {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
return string;
}
/**
* Decode a string encoded using a given encoding.
*
* @memberof Crypt
* @param {string|Buffer} string item to be decoded
* @param {encodingConversion|string} opts object or string specifying the encoding(s)
* @return {string} decoded item
*
* NOTE: If opts is a string, it is considered as the fromEncoding
* (i.e that was used to encode the string).
*/
function baseDecode(string, opts) {
if (_.isString(opts)) {
opts = {
fromEncoding: opts,
toEncoding: 'binary',
};
}
return baseEncode(string, opts);
}
/**
* Decode a string encoded using a given encoding to a buffer.
*
* @memberof Crypt
* @param {string|Buffer} string item to be decoded
* @param {string} fromEncoding encoding used to encode the string
* @return {Buffer} decoded item
*/
function baseDecodeToBuffer(string, fromEncoding) {
return baseEncode(string, {
fromEncoding,
toEncoding: 'buffer',
});
}
/**
* Supported Encodings:
* 'hex', 'binary' ('latin1'), 'ascii', 'base64', 'base64url', 'utf8', 'buffer'
* @typedef {object} encodingOpts
* @property {string} [encoding='hex'] default 'hex'
*/
/**
* Compute hash of a string using given algorithm
* encoding can be 'hex', 'binary' ('latin1'), 'ascii', 'base64', 'base64url', 'utf8', 'buffer'
*
* @memberof Crypt
* @param {string} algo algo to be used for hashing
* @param {string} string string to be hashed
* @param {encodingOpts} [opts={}]
* @return {string} encoded hash value
*/
function hash(algo, string, {encoding = 'hex'} = {}) {
const hashed = crypto.createHash(algo).update(string);
if (encoding === 'binary') encoding = 'latin1';
if (['latin1', 'base64', 'hex'].indexOf(encoding) > -1) {
return hashed.digest(encoding);
}
return baseEncode(hashed.digest(), encoding);
}
/**
* Compute hash of a string using md5
*
* @memberof Crypt
* @param {string} string string to be hashed
* @param {encodingOpts} [options={}] object of {encoding}
* @return {string} encoded hash value
*/
function md5(string, {encoding = 'hex'} = {}) {
return hash('md5', string, {encoding});
}
/**
* Compute hash of a string using sha1
*
* @memberof Crypt
* @param {string} string string to be hashed
* @param {encodingOpts} [options={}] object of {encoding}
* @return {string} encoded hash value
*/
function sha1(string, {encoding = 'hex'} = {}) {
return hash('sha1', string, {encoding});
}
/**
* Compute hash of a string using sha256
*
* @memberof Crypt
* @param {string} string string to be hashed
* @param {encodingOpts} [options={}] object of {encoding}
* @return {string} encoded hash value
*/
function sha256(string, {encoding = 'hex'} = {}) {
return hash('sha256', string, {encoding});
}
/**
* Compute hash of a string using sha384
*
* @memberof Crypt
* @param {string} string string to be hashed
* @param {encodingOpts} [options={}] object of {encoding}
* @return {string} encoded hash value
*/
function sha384(string, {encoding = 'hex'} = {}) {
return hash('sha384', string, {encoding});
}
/**
* Compute hash of a string using sha512
*
* @memberof Crypt
* @param {string} string string to be hashed
* @param {encodingOpts} [options={}] object of {encoding}
* @return {string} encoded hash value
*/
function sha512(string, {encoding = 'hex'} = {}) {
return hash('sha512', string, {encoding});
}
/**
* Create cryptographic HMAC digests using given algo
*
* @memberof Crypt
* @param {string} algo algo to be used for hashing
* @param {string} string string to be hashed
* @param {encodingOpts} [options={}] object of {encoding}
* @return {string} encoded HMAC digest
*/
function hmac(algo, string, key, {encoding = 'hex'} = {}) {
if (typeof key === 'string') key = Buffer.from(key, 'binary');
const hashed = crypto.createHmac(algo, key).update(string);
if (encoding === 'binary') encoding = 'latin1';
if (['latin1', 'base64', 'hex'].indexOf(encoding) > -1) {
return hashed.digest(encoding);
}
return baseEncode(hashed.digest(), encoding);
}
/**
* Create cryptographic HMAC digests using sha1
*
* @memberof Crypt
* @param {string} string string to be hashed
* @param {encodingOpts} [options={}] object of {encoding}
* @return {string} encoded HMAC digest
*/
function sha1Hmac(string, key, {encoding = 'hex'} = {}) {
return hmac('sha1', string, key, {encoding});
}
/**
* Create cryptographic HMAC digests using sha256
*
* @memberof Crypt
* @param {string} string string to be hashed
* @param {encodingOpts} [options={}] object of {encoding}
* @return {string} encoded HMAC digest
*/
function sha256Hmac(string, key, {encoding = 'hex'} = {}) {
return hmac('sha256', string, key, {encoding});
}
/**
* Sign a message using a private key.
*
* NOTE: Generate a key pair using:
* ```sh
* openssl ecparam -genkey -name secp256k1 | openssl ec -aes128 -out private.pem
* openssl ec -in private.pem -pubout -out public.pem
* ```
*
* @memberof Crypt
* @param {string} message the message to be signed
* @param {string|object} privateKey
* @param {encodingOpts} opts opts can have {encoding (default 'hex')
* @return {string} signature
*
* NOTE: If privateKey is a string, it is treated as a raw key with no passphrase.
* If privateKey is an object, it must contain the following property:
* key: <string> - PEM encoded private key (required)
*/
function sign(message, privateKey, opts = {}) {
let encoding = opts.encoding || 'hex';
const signed = crypto.createSign('SHA256').update(message);
if (encoding === 'binary') encoding = 'latin1';
if (['latin1', 'base64', 'hex'].indexOf(encoding) > -1) {
return signed.sign(privateKey, encoding);
}
return baseEncode(signed.sign(privateKey), encoding);
}
/**
* Verify a message using a public key
* opts can have {encoding (default 'hex')}
*
* NOTE: Generate a key pair using:
* ```sh
* openssl ecparam -genkey -name secp256k1 | openssl ec -aes128 -out private.pem
* openssl ec -in private.pem -pubout -out public.pem
* ```
*
* @memberof Crypt
* @param {string} message message to be verified
* @param {string} signature
* @param {string|object} publicKey
* @param {encodingOpts} [opts={}] opts can have {encoding (default 'hex')}
* @return {boolean} true or false, depending on the validity of
* the signature for the data and public key
*/
function verify(message, signature, publicKey, opts = {}) {
let encoding = opts.encoding || 'hex';
const verified = crypto.createVerify('SHA256').update(message);
if (encoding === 'binary') encoding = 'latin1';
if (['latin1', 'base64', 'hex'].indexOf(encoding) > -1) {
return verified.verify(publicKey, signature, encoding);
}
if (encoding === 'buffer') {
return verified.verify(publicKey, signature);
}
const signBuffer = baseDecodeToBuffer(signature, encoding);
return verified.verify(publicKey, signBuffer);
}
/**
* Encrypt the given string with the given key using AES 256
* Calling encrypt on the same string multiple times will return different encrypted strings
* Optionally specify encoding in which you want to get the output
*
* @memberof Crypt
* @param {string} string string to be encrypted
* @param {string} key key to be used
* @param {encodingOpts} [options={}] object of {encoding} (default: 'base64url')
* @return {string} encrypted string
*/
function encrypt(string, key, {encoding = 'base64url'} = {}) {
if (string.length < 6) {
string = _.padEnd(string, 6, '\v');
}
if (key.length !== 32) {
key = sha256(key, {encoding: 'buffer'});
}
const ivLength = 16;
const iv = crypto.randomBytes(ivLength);
const cipher = crypto.createCipheriv('AES-256-CFB', key, iv);
const crypted = Buffer.concat([iv, cipher.update(string), cipher.final()]);
return '1' + baseEncode(crypted, encoding);
}
/**
* Decrypt the given string with the given key encrypted using encrypt
*
* @memberof Crypt
* @param {string} string string to be decrypted
* @param {string} key key to be used
* @param {encodingOpts} [options={}] object of {encoding} (default: 'base64url')
* @return {string} decrypted string
*/
function decrypt(string, key, {encoding = 'base64url'} = {}) {
if (key.length !== 32) {
key = sha256(key, {encoding: 'buffer'});
}
const version = string.substring(0, 1); // eslint-disable-line
const decoded = baseDecodeToBuffer(string.substring(1), encoding);
const ivLength = 16;
const decipher = crypto.createDecipheriv('AES-256-CFB', key, decoded.slice(0, ivLength));
const decrypted = Buffer.concat([decipher.update(decoded.slice(ivLength)), decipher.final()]);
return _.trimEnd(decrypted, '\v');
}
/**
* Encrypt the given string with the given key using AES 256
* Calling EncryptStatic on the same string multiple times will return same encrypted strings
* this encryption is weaker than Encrypt but has the benefit of returing same encrypted string
* for same string and key.
*
* @memberof Crypt
* @param {string} string string to be encrypted
* @param {string} key key to be used
* @param {encodingOpts} [options={}] object of {encoding} (default: 'base64url')
* @return {string} encrypted string
*/
function encryptStatic(string, key, {encoding = 'base64url'} = {}) {
if (string.length < 6) {
string = _.padEnd(string, 6, '\v');
}
const cipher = crypto.createCipher('AES-256-CFB', key);
const crypted = Buffer.concat([cipher.update(string), cipher.final()]);
return '1' + baseEncode(crypted, encoding);
}
/**
* Decrypt the given string with the given key encrypted using encryptStatic
*
* @memberof Crypt
* @param {string} string string to be decrypted
* @param {string} key key to be used
* @param {encodingOpts} [options={}] object of {encoding} (default: 'base64url')
* @return {string} decrypted string
*/
function decryptStatic(string, key, {encoding = 'base64url'} = {}) {
const version = string.substring(0, 1); // eslint-disable-line
const decoded = baseDecodeToBuffer(string.substring(1), encoding);
const decipher = crypto.createDecipher('AES-256-CFB', key);
const decrypted = Buffer.concat([decipher.update(decoded), decipher.final()]);
return _.trimEnd(decrypted, '\v');
}
/**
* Convert a message into an encrypted token by:
* signing it with privateKey + encrypting it with publicKey
* you only need a publicKey to verify and decrypt this token
* @memberof Crypt
* @param {any} message will be JSON.stringified
* @param {string|object} privateKey
* @param {string} publicKey
* @return {string}
*/
function signAndEncrypt(message, privateKey, publicKey) {
const version = '1';
const stringified = JSON.stringify(message);
const signature = sign(stringified, privateKey, {encoding: 'base64url'});
const encrypted = encrypt(stringified, publicKey, {encoding: 'base64url'});
return version + encrypted + '.' + signature;
}
/**
* Convert an encrypted token (generated by signAndEncrypt) into a message
* @memberof Crypt
* @param {string} token generated by signAndEncrypt
* @param {string} publicKey
*/
function verifyAndDecrypt(token, publicKey) {
const [messageEncrypted, signature] = token.split('.');
if (!messageEncrypted || !signature) {
throw new Error('Malformed Token');
}
const version = messageEncrypted[0]; // eslint-disable-line
const message = decrypt(messageEncrypted.substring(1), publicKey, {encoding: 'base64url'});
if (!message) throw new Error('Malformed Token');
const verified = verify(message, signature, publicKey, {encoding: 'base64url'});
if (!verified) {
throw new Error('Incorrect Signature');
}
return JSON.parse(message);
}
/**
* Hash a given password using cryptographically strong hash function
* @memberof Crypt
* @param {string} password
* @param {object} [opts={}]
* @param {string|Buffer} opts.salt
* @return {string} 50 character long hash
*/
function hashPassword(password, opts = {}) {
const salt = opts.salt || crypto.randomBytes(12);
const hashed = crypto.pbkdf2Sync(password, salt, 1000, 25, 'sha256');
const passHash = '1' + baseEncode(Buffer.concat([salt, hashed]), 'base64url');
return passHash.substring(0, 50);
}
/**
* Verify that given password and hashed password are same or not
* @memberof Crypt
* @param {string} password
* @param {string} hashed
* @return {boolean}
*/
function verifyPassword(password, hashed) {
const version = hashed.substring(0, 1); // eslint-disable-line
const salt = baseDecodeToBuffer(hashed.substring(1), 'base64url').slice(0, 12);
if (hashed === hashPassword(password, {salt})) return true;
if (hashed === hashPassword(password.trim(), {salt})) return true;
if (hashed === hashPassword(Str.invertCase(password), {salt})) return true;
return false;
}
/**
* Base64 Encode
* @memberof Crypt
* @param {string} string
* @param {string} [fromEncoding='binary']
*/
function base64Encode(string, fromEncoding = 'binary') {
return baseEncode(string, {
fromEncoding,
toEncoding: 'base64',
});
}
/**
* URL Safe Base64 Encode
* @memberof Crypt
* @param {string} string
* @param {string} [fromEncoding='binary']
*/
function base64UrlEncode(string, fromEncoding = 'binary') {
return baseEncode(string, {
fromEncoding,
toEncoding: 'base64url',
});
}
/**
* Base64 Decode
* @memberof Crypt
* @param {string} string
* @param {string} [toEncoding='binary']
*/
function base64Decode(string, toEncoding = 'binary') {
return baseEncode(string, {
fromEncoding: 'base64',
toEncoding,
});
}
/**
* URL Safe Base64 Decode
* @memberof Crypt
* @param {string} string
* @param {string} [toEncoding='binary']
*/
function base64UrlDecode(string, toEncoding = 'binary') {
return baseEncode(string, {
fromEncoding: 'base64url',
toEncoding,
});
}
/**
* Pack many numbers into a single string
*
* @memberof Crypt
* @param {Array} numbers array of numbers to be packed
* @return {string} packed string
*/
function packNumbers(numbers) {
return baseConvert(
numbers.join('a').replace(/-/g, 'b').replace(/\./g, 'c'),
13, 62
);
}
/**
* Unpack a string packed with packNumbers
*
* @memberof Crypt
* @param {string} str string to be unpacked
* @return {Array} array of unpacked numbers
*/
function unpackNumbers(str) {
return baseConvert(str, 62, 13)
.replace(/b/g, '-')
.replace(/c/g, '.')
.split('a')
.map(Number);
}
/**
* Generate a random encrypted string that contains a timestamp.
*
* @memberof Crypt
* @param {number|object} options length of the id or object of {length: int}
* @param {number} options.length
* @param {number} [options.time] epoch time / 1000
* @return {string} id
*/
function encryptedTimestampedId(options, key) {
const length = options.length || options;
if (length < 10) {
throw new Error('Timestamped ID should be of minimum 15 length');
}
let time = options.time || Math.floor(Date.now() / 1000);
time = baseConvert(time, 10, 62);
const encrypted = encryptStatic(randomString(3) + time, key);
const remaining = length - encrypted.length - 1;
let randomStr = '';
if (remaining) {
randomStr = randomString(remaining);
}
return encrypted + '.' + randomStr;
}
/**
* Legacy obfuscation method
*
* @memberof Crypt
* @param {string} str the string to be obfuscted
* @return {string} obfuscated string
* @ignore
*/
async function javaObfuscate(str) {
if (!str) return '';
try {
return base64Encode(await gzdeflate(Buffer.from(Str.rot47(str), 'latin1')));
}
catch (e) {
return '';
}
}
/**
* Legacy unobfuscation method (obfuscated by javaObfuscate)
*
* @memberof Crypt
* @param {string} str the obfuscted string
* @return {string} unobfuscated string
* @ignore
*/
async function javaUnobfuscate(str) {
str = str.trim();
if (!str) return '';
try {
return Str.rot47((await gzinflate(base64Decode(str, 'buffer'))).toString('latin1'));
}
catch (e) {
return '';
}
}
/**
* @type {Str.rot13}
* @memberof Crypt
*/
const rot13 = Str.rot13;
/**
* @type {Str.rot47}
* @memberof Crypt
*/
const rot47 = Str.rot47;
module.exports = {
baseConvert,
chars,
random,
randomInt,
randomString,
randomID: randomString,
randomId: randomString,
sequentialID,
sequentialId: sequentialID,
sequentialUUID,
randomUUID,
uuid: randomUUID,
shuffle,
seededRandom,
nanoSecondsAlpha,
hash,
md5,
sha1,
sha256,
sha384,
sha512,
hmac,
sha1Hmac,
sha256Hmac,
sign,
verify,
encrypt,
decrypt,
encryptStatic,
decryptStatic,
signAndEncrypt,
verifyAndDecrypt,
hashPassword,
verifyPassword,
baseEncode,
baseDecode,
baseDecodeToBuffer,
base64Encode,
base64UrlEncode,
base64Decode,
base64UrlDecode,
packNumbers,
unpackNumbers,
encryptedTimestampedId,
rot13,
rot47,
javaObfuscate,
javaUnobfuscate,
};