Source: index.js

"use strict";
/**
 * @file A Unique Identifier Generator for Node
 * @author Adam Mill <hismajesty@theroyalwhee.com>
 * @copyright Copyright 2021-2022 Adam Mill
 * @license Apache-2.0
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validationBothFactory = exports.validationVerifyFactory = exports.validationSignFactory = exports.validationFactory = exports.identGenerator = void 0;
/**
 * Imports.
 * @private
 */
const node_crypto_1 = __importDefault(require("node:crypto"));
const istype_1 = require("@theroyalwhee0/istype");
const snowman_1 = require("@theroyalwhee0/snowman");
const base32h_1 = require("@base32h/base32h");
const constants_1 = require("./constants");
/**
 * Default Options.
 * @private
 */
const defaultOptions = {
    getRandomBytes: (size) => node_crypto_1.default.randomBytes(size),
};
/**
 * Build options from options and defaults.
 * @private
 * @param {object} options The options.
 * @returns {object} The merged/modified options.
 */
function buildOptions(options) {
    const built = Object.assign({}, defaultOptions, options);
    const idOptions = Object.assign({}, built.idOptions);
    built.idOptions = idOptions;
    if ('node' in built) {
        idOptions.node = built.node;
    }
    return built;
}
/**
 * identGenerator
 * @generator
 * @function identGenerator
 * @param {object} options Options.
 * @param {number} options.node The numeric ID of the node (0-1023).
 * @param {string} options.signKey The key to use for signing check.
 * @param {string} options.verifyKey The key to use for verify check.
 * @param {string} options.getRandomBytes Function to provide random bytes.
 * @param {object} options.idOptions Options passed to snowman.
 * @yields {string} The created ident.
 */
function* identGenerator(options) {
    options = buildOptions(options);
    const { verifyKey, signKey, getRandomBytes } = options;
    const ids = (0, snowman_1.idSequence)(options.idOptions);
    while (1) {
        // Create the buffer.
        const buffer = Buffer.alloc(constants_1.ALL_SIZE, 0);
        // Add the id.
        const { value: id, done } = ids.next();
        if (done) {
            throw new Error(`id sequence should never be done.`);
        }
        buffer.writeBigUInt64BE(id, 0);
        // Add the random bytes.
        const rnd = getRandomBytes(constants_1.RND_SIZE);
        rnd.copy(buffer, constants_1.ID_SIZE, 0, constants_1.RND_SIZE);
        // Add verification hmac.
        const verifyBuffer = buffer.slice(0, constants_1.ID_SIZE + constants_1.RND_SIZE);
        const hmacVerify = node_crypto_1.default.createHmac(constants_1.HMAC_ALGO, verifyKey);
        const verify = hmacVerify.update(verifyBuffer).digest();
        verify.copy(buffer, constants_1.ID_SIZE + constants_1.RND_SIZE, 0, constants_1.VERIFY_SIZE);
        // Add signature hmac.
        const signBuffer = buffer.slice(0, constants_1.ID_SIZE + constants_1.RND_SIZE + constants_1.VERIFY_SIZE);
        const hmacSign = node_crypto_1.default.createHmac(constants_1.HMAC_ALGO, signKey);
        const sign = hmacSign.update(signBuffer).digest();
        sign.copy(buffer, constants_1.ID_SIZE + constants_1.RND_SIZE + constants_1.VERIFY_SIZE, 0, constants_1.SIGN_SIZE);
        // Encode buffer and strip leading zeros.
        const ident = (0, base32h_1.encodeBin)(buffer).replace(/^0+/, '');
        yield ident;
    }
}
exports.identGenerator = identGenerator;
/**
 * Left trim buffer.
 * @private
 * @param {Buffer} buffer A buffer to left trim.
 * @param {number} byte The byte value to trim. Defaults to zero.
 * @returns {Buffer} The trimmed buffer.
 */
function leftTrimBuffer(buffer, byte = 0) {
    let idx;
    for (idx = 0; idx < buffer.length; idx++) {
        if (buffer[idx] !== byte) {
            break;
        }
    }
    return idx === 0 ? buffer : buffer.slice(idx);
}
/**
 * Low-level validation factory.
 * Use validationVerifyFactory, validationSignFactory, or
 * validationBothFactory instead.
 * @param {object} options Options.
 * @param {string} options.signKey The key to use for signing check.
 * @param {string} options.verifyKey The key to use for verify check.
 * @returns True if valid, false if not.
 */
function validationFactory(options) {
    options = buildOptions(options);
    const { verifyKey, signKey } = options;
    return function validation(value) {
        if (!(0, istype_1.isString)(value) || !constants_1.re_lax.test(value)) {
            return false;
        }
        const decoded = leftTrimBuffer(Buffer.from((0, base32h_1.decodeBin)(value)));
        const buffer = decoded.length < constants_1.ALL_SIZE ?
            Buffer.concat([Buffer.alloc(constants_1.ALL_SIZE - decoded.length, 0), decoded])
            : decoded;
        if (buffer.length !== constants_1.ALL_SIZE) {
            return false;
        }
        let start = 0, end = constants_1.ID_SIZE;
        const id = buffer.readBigUInt64BE();
        // Check ID.
        const [, , , idValid] = (0, snowman_1.explodeId)(id);
        if (!idValid) {
            return false;
        }
        start = end;
        end += constants_1.RND_SIZE;
        start = end;
        end += constants_1.VERIFY_SIZE;
        const verify = buffer.slice(start, end);
        start = end;
        end += constants_1.SIGN_SIZE;
        const sign = buffer.slice(start, end);
        if (verifyKey) {
            // Check verify hmac if given verify key...
            const hmacVerify = node_crypto_1.default.createHmac(constants_1.HMAC_ALGO, verifyKey);
            const verifyBuffer = buffer.slice(0, constants_1.ID_SIZE + constants_1.RND_SIZE);
            const verifyCheck = hmacVerify.update(verifyBuffer).digest().slice(0, constants_1.VERIFY_SIZE);
            if (!node_crypto_1.default.timingSafeEqual(verify, verifyCheck)) {
                return false;
            }
        }
        if (signKey) {
            // Check sign hmac if given sign key...
            const hmacSign = node_crypto_1.default.createHmac(constants_1.HMAC_ALGO, signKey);
            const signBuffer = buffer.slice(0, constants_1.ID_SIZE + constants_1.RND_SIZE + constants_1.VERIFY_SIZE);
            const signCheck = hmacSign.update(signBuffer).digest().slice(0, constants_1.SIGN_SIZE);
            if (!node_crypto_1.default.timingSafeEqual(sign, signCheck)) {
                return false;
            }
        }
        return true;
    };
}
exports.validationFactory = validationFactory;
/**
 * Validation factory requring a signKey.
 * @param {object} options Options.
 * @param {string} options.signKey The key to use for signing check.
 * @returns {boolean} True if valid, false if not.
 */
function validationSignFactory(options) {
    if (!options?.signKey) {
        throw new Error('signKey is required.');
    }
    return validationFactory(options);
}
exports.validationSignFactory = validationSignFactory;
/**
 * Validation factory requring a verifyKey.
 * @param {object} options Options.
 * @param {string} options.verifyKey The key to use for verify check.
 * @returns {boolean} True if valid, false if not.
 */
function validationVerifyFactory(options) {
    if (!options?.verifyKey) {
        throw new Error('verifyKey is required.');
    }
    return validationFactory(options);
}
exports.validationVerifyFactory = validationVerifyFactory;
/**
 * Validation factory requring both keys.
 * @param {object} options Options.
 * @param {string} options.signKey The key to use for signing check.
 * @param {string} options.verifyKey The key to use for verify check.
 * @returns {boolean} True if valid, false if not.
 */
function validationBothFactory(options) {
    if (!options?.signKey) {
        throw new Error('signKey is required.');
    }
    if (!options?.verifyKey) {
        throw new Error('verifyKey is required.');
    }
    return validationFactory(options);
}
exports.validationBothFactory = validationBothFactory;
//# sourceMappingURL=index.js.map