UNPKG

tiny-crypto-suite

Version:

Tiny tools, big crypto — seamless encryption and certificate handling for modern web and Node apps.

487 lines (451 loc) 20.2 kB
'use strict'; var crypto = require('crypto'); var fs = require('fs'); var buffer = require('buffer'); var os = require('./lib/os.cjs'); var TinyCryptoParser = require('./lib/TinyCryptoParser.cjs'); /** * TinyCrypto is a utility class that provides methods for secure key generation, * encryption, and decryption of data. It also allows for serialization * and deserialization of complex data types, and offers methods to save and load encryption * configurations and keys from files. * * @class */ class TinyCrypto { /** * Important instance used to validate values. * @type {TinyCryptoParser} */ #parser = new TinyCryptoParser(); /** * Indicates whether the serialization or deserialization should be performed deeply. * @type {boolean} */ isDeep = true; /** * Sets the deep serialization and deserialization mode. * If the argument is a boolean, updates the deep mode accordingly. * Throws an error if the value is not a boolean. * * @param {boolean} value - A boolean indicating whether deep mode should be enabled. * @throws {Error} Throws if the provided value is not a boolean. */ setDeepMode(value) { if (typeof value !== 'boolean') throw new Error('The value provided to setDeepMode must be a boolean.'); this.isDeep = value; } /** * Add a new value type and its converter function. * @param {string} typeName * @param {(data: any) => any} getFunction * @param {(data: any) => { __type: string, value?: any }} convertFunction */ addValueType(typeName, getFunction, convertFunction) { return this.#parser.addValueType(typeName, getFunction, convertFunction); } /** * Creates a new instance of the TinyCrypto class with configurable options. * * @param {Object} [options={}] - Configuration options for encryption and decryption. * @param {string} [options.algorithm='aes-256-gcm'] - The encryption algorithm to use. Recommended: 'aes-256-gcm' for authenticated encryption. * @param {BufferEncoding} [options.outputEncoding='hex'] - The encoding used when returning encrypted data (e.g., 'hex', 'base64'). * @param {BufferEncoding} [options.inputEncoding='utf8'] - The encoding used for plaintext inputs (e.g., 'utf8'). * @param {number} [options.authTagLength=16] - The length of the authentication tag used in GCM mode. Usually 16 for AES-256-GCM. * @param {Buffer} [options.key] - Optional 32-byte cryptographic key. If not provided, a random key is generated. * * @throws {Error} Throws if the provided key is not 32 bytes long. * * @example * const crypto = new CryptoManager({ * algorithm: 'aes-256-gcm', * outputEncoding: 'base64', * key: randomBytes(32), * }); */ constructor(options = {}) { this.algorithm = options.algorithm || 'aes-256-gcm'; this.authTagLength = options.authTagLength || 16; this.key = options.key || this.generateKey(); /** @type {BufferEncoding} */ this.outputEncoding = options.outputEncoding || 'hex'; /** @type {BufferEncoding} */ this.inputEncoding = options.inputEncoding || 'utf8'; } /** * Exports the current cryptographic key. * * @returns {string} The exported key as a hex string. */ getKey() { return this.key.toString('hex'); } /** * Sets the cryptographic key. * * This method allows setting a cryptographic key directly. The key should be provided as a string * (in hex format) and will be converted to a Buffer for internal use. If the key format is incorrect, * an error will be thrown. * * @param {string} keyHex - The cryptographic key in hex format to be set. * @throws {Error} If the provided key is not a valid hex string. */ setKey(keyHex) { if (typeof keyHex !== 'string' || !/^[a-fA-F0-9]+$/.test(keyHex)) throw new Error('Invalid key format. The key must be a valid hex string.'); this.key = buffer.Buffer.from(keyHex, 'hex'); } /** * Generates a secure random cryptographic key. * * @param {number} [value=32] - The number of bytes to generate. Default is 32 bytes (256 bits), suitable for AES-256. * @returns {Buffer} A securely generated random key as a Buffer. * * @example * const key = cryptoManager.generateKey(); // Generates a 32-byte key * const customKey = cryptoManager.generateKey(16); // Generates a 16-byte key (e.g. for AES-128) */ generateKey(value = 32) { return crypto.randomBytes(value); // 256-bit } /** * Generates a secure random Initialization Vector (IV). * * @param {number} [value=12] - The number of bytes to generate. Default is 12 bytes (96 bits), the recommended size for AES-GCM. * @returns {Buffer} A securely generated IV as a Buffer. * * @example * const iv = cryptoManager.generateIV(); // Generates a 12-byte IV * const customIV = cryptoManager.generateIV(16); // Generates a 16-byte IV if needed for other algorithms */ generateIV(value = 12) { return crypto.randomBytes(value); // 96-bit padrão para GCM } /** * @typedef {Object} EncryptedDataParams * @property {string} iv - The Initialization Vector (IV) used in encryption, encoded with the output encoding. * @property {string} encrypted - The encrypted data to decrypt, encoded with the output encoding. * @property {string} authTag - The authentication tag used to verify the integrity of the encrypted data. */ /** * Encrypts a given value (string, number, object, etc.) * * The value is first serialized de forma segura (preservando o tipo) antes da criptografia. * * @param {*} data - The data to encrypt. Can be of any supported type (string, number, boolean, Date, JSON, etc.). * @param {Buffer} [iv=this.generateIV()] - Optional Initialization Vector (IV). If not provided, a secure random IV is generated. * @returns {EncryptedDataParams} An object containing the encrypted data. * * @example * const result = cryptoManager.encrypt('Hello, world!'); * // { * // iv: 'b32a...', * // encrypted: 'c1d5...', * // authTag: 'aa93...' * // } */ encrypt(data, iv = this.generateIV()) { const plainText = this.isDeep ? this.#parser.serializeDeep(data) : this.#parser.serialize(data); const cipher = crypto.createCipheriv(this.algorithm, this.key, iv, { // @ts-ignore authTagLength: this.authTagLength, }); let encrypted = cipher.update(plainText, this.inputEncoding); encrypted = buffer.Buffer.concat([encrypted, cipher.final()]); const authTag = cipher.getAuthTag(); return { iv: iv.toString(this.outputEncoding), encrypted: encrypted.toString(this.outputEncoding), authTag: authTag.toString(this.outputEncoding), }; } /** * Decrypts a previously encrypted value. * * The method checks the integrity of the data using the authentication tag (`authTag`) and ensures the data is properly decrypted. * After decryption, it automatically deserializes the data back to its original type. * * @param {EncryptedDataParams} params - An object containing the encrypted data. * @param {string|null} [expectedType=null] - Optionally specify the expected type of the decrypted data. If provided, the method will validate the type of the deserialized value. * @returns {*} The decrypted value, which will be the original type of the data before encryption. * @throws {Error} Throws if the authentication tag doesn't match or the data has been tampered with. * @throws {Error} Throws if the deserialized value doesn't match the `expectedType`. * * @example * const encryptedData = { * iv: 'b32a...', * encrypted: 'c1d5...', * authTag: 'aa93...' * }; * const decrypted = cryptoManager.decrypt(encryptedData, 'string'); * console.log(decrypted); // Outputs: 'Hello, world!' */ decrypt({ iv, encrypted, authTag }, expectedType = null) { const ivBuffer = buffer.Buffer.from(iv, this.outputEncoding); const encryptedBuffer = buffer.Buffer.from(encrypted, this.outputEncoding); const authTagBuffer = buffer.Buffer.from(authTag, this.outputEncoding); const decipher = crypto.createDecipheriv(this.algorithm, this.key, ivBuffer, { // @ts-ignore authTagLength: this.authTagLength, }); decipher.setAuthTag(authTagBuffer); /** @type {string} */ let decrypted = decipher.update(encryptedBuffer, undefined, this.inputEncoding); decrypted += decipher.final(this.inputEncoding); const { value } = this.isDeep ? this.#parser.deserializeDeep(decrypted, expectedType) : this.#parser.deserialize(decrypted, expectedType); return value; } /** * Retrieves the type of the original data from an encrypted object. * * This method decrypts the encrypted data and extracts its type information without fully deserializing the value. * It is useful when you need to verify the type of the encrypted data before fully decrypting it. * * @param {EncryptedDataParams} params - An object containing the encrypted data. * @returns {string} The type of the original data (e.g., 'string', 'number', 'date', etc.). * * @example * const encryptedData = { * iv: 'b32a...', * encrypted: 'c1d5...', * authTag: 'aa93...' * }; * const dataType = cryptoManager.getTypeFromEncrypted(encryptedData); * console.log(dataType); // Outputs: 'string' */ getTypeFromEncrypted({ iv, encrypted, authTag }) { const ivBuffer = buffer.Buffer.from(iv, this.outputEncoding); const encryptedBuffer = buffer.Buffer.from(encrypted, this.outputEncoding); const authTagBuffer = buffer.Buffer.from(authTag, this.outputEncoding); const decipher = crypto.createDecipheriv(this.algorithm, this.key, ivBuffer, { // @ts-ignore authTagLength: this.authTagLength, }); decipher.setAuthTag(authTagBuffer); let decrypted = decipher.update(encryptedBuffer, undefined, this.inputEncoding); decrypted += decipher.final(this.inputEncoding); const { type } = this.#parser.deserialize(decrypted); return typeof type === 'string' ? type : 'unknown'; } /** * Saves the cryptographic key to a file. * * If running in a browser, the method generates a download link for the key as a text file. * If running in Node.js, the method saves the key to the specified file path. * * @param {string} [filename='secret.key'] - The name of the file to save the key. Defaults to 'secret.key'. * @throws {Error} Throws an error if the file cannot be written in Node.js. * * @example * // In a browser, triggers a download of the key * cryptoManager.saveKeyToFile('myKey.key'); * * // In Node.js, saves the key to 'myKey.key' * cryptoManager.saveKeyToFile('myKey.key'); */ saveKeyToFile(filename = 'secret.key') { const data = this.key.toString('hex'); if (os.isBrowser()) { const blob = new Blob([data], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } else fs.writeFileSync(filename, data); } /** * Saves the current cryptographic configuration to a JSON file. * * If running in a browser, the method generates a download link for the configuration as a JSON file. * If running in Node.js, the method saves the configuration to the specified file path. * * @param {string} [filename='crypto-config.json'] - The name of the file to save the configuration. Defaults to 'crypto-config.json'. * @throws {Error} Throws an error if the file cannot be written in Node.js. * * @example * // In a browser, triggers a download of the configuration * cryptoManager.saveConfigToFile('myConfig.json'); * * // In Node.js, saves the configuration to 'myConfig.json' * cryptoManager.saveConfigToFile('myConfig.json'); */ saveConfigToFile(filename = 'crypto-config.json') { const configData = JSON.stringify(this.exportConfig(), null, 2); if (os.isBrowser()) { const blob = new Blob([configData], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } else fs.writeFileSync(filename, configData); } /** * Loads and imports cryptographic configuration from a JSON file. * * If running in a browser, the method allows the user to select a file, reads the file as text, * parses the JSON, and imports the configuration. * If running in Node.js, the method reads the file synchronously and imports the configuration. * * @param {File|string} file - The file to load the configuration from. In the browser, this is a `File` object, and in Node.js, it's a file path. * @returns {Promise<void>} A promise that resolves when the configuration is successfully loaded and imported. * @throws {Error} Throws an error if the JSON file is invalid or the file cannot be read. * * @example * // In a browser, prompt user to select a file and load the configuration * cryptoManager.loadConfigFromFile(file) * .then(() => console.log('Config loaded successfully')) * .catch(err => console.error('Error loading config:', err)); * * // In Node.js, load the configuration from a file path * cryptoManager.loadConfigFromFile('myConfig.json') * .then(() => console.log('Config loaded successfully')) * .catch(err => console.error('Error loading config:', err)); */ async loadConfigFromFile(file) { if (os.isBrowser()) { return new Promise((resolve, reject) => { if (!(file instanceof File)) return reject(new Error('In browser, the file must be a File object')); const reader = new FileReader(); reader.onload = () => { try { const config = typeof reader.result === 'string' ? JSON.parse(reader.result) : {}; resolve(this.importConfig(config)); } catch (err) { reject(new Error('Invalid config JSON file')); } }; reader.onerror = () => reject(reader.error); reader.readAsText(file); }); } else { const raw = fs.readFileSync(/** @type {string} */ (file), 'utf8'); const config = JSON.parse(raw); return this.importConfig(config); } } /** * Loads a cryptographic key from a file and sets it for encryption/decryption. * * If running in a browser, the method allows the user to select a file, reads the file as text, * and loads the key (in hexadecimal format) into the current instance. * If running in Node.js, the method reads the file synchronously, parses the hexadecimal key, * and loads it into the current instance. * * @param {File|string} file - The file to load the key from. In the browser, this is a `File` object, and in Node.js, it's a file path. * @returns {Promise<Buffer>} A promise that resolves with the key as a `Buffer` when the file is successfully loaded. * @throws {Error} Throws an error if the file cannot be read or if the key is invalid. * * @example * // In a browser, prompt user to select a file and load the key * cryptoManager.loadKeyFromFile(file) * .then(key => console.log('Key loaded successfully:', key)) * .catch(err => console.error('Error loading key:', err)); * * // In Node.js, load the key from a file path * cryptoManager.loadKeyFromFile('myKey.key') * .then(key => console.log('Key loaded successfully:', key)) * .catch(err => console.error('Error loading key:', err)); */ async loadKeyFromFile(file) { if (os.isBrowser()) { return new Promise((resolve, reject) => { if (!(file instanceof File)) return reject(new Error('In browser, the file must be a File object')); const reader = new FileReader(); reader.onload = () => { const hexKey = typeof reader.result === 'string' ? reader.result.trim() : ''; const keyBuffer = buffer.Buffer.from(hexKey, 'hex'); this.key = keyBuffer; resolve(keyBuffer); }; reader.onerror = () => reject(reader.error); reader.readAsText(file); }); } else { const hexKey = fs.readFileSync(/** @type {string} */ (file), 'utf8'); const keyBuffer = buffer.Buffer.from(hexKey, 'hex'); this.key = keyBuffer; return keyBuffer; } } /** * Exports the current cryptographic configuration as a JSON object. * * The exported configuration includes the encryption algorithm, output encoding format, * input encoding format, the cryptographic key (in hexadecimal format), and the authentication tag length. * This method does not include any sensitive data like the raw key, only its hexadecimal representation. * * @returns {{algorithm: string, outputEncoding: BufferEncoding, inputEncoding: BufferEncoding, key: string, authTagLength: number }} The exported configuration as a plain JavaScript object. * @example * const config = cryptoManager.exportConfig(); * console.log(config); * // Example output: * // { * // algorithm: 'aes-256-gcm', * // outputEncoding: 'hex', * // inputEncoding: 'utf8', * // key: 'abcdef1234567890...', * // authTagLength: 16 * // } */ exportConfig() { return { algorithm: this.algorithm, outputEncoding: this.outputEncoding, inputEncoding: this.inputEncoding, key: this.key.toString('hex'), authTagLength: this.authTagLength, }; } /** * Imports a cryptographic configuration from a JSON object. * * This method sets the configuration for the encryption process, including the algorithm, encoding formats, * authentication tag length, and the cryptographic key (in hexadecimal string format). * If any of the expected properties are missing or invalid, an error will be thrown. * * @param {Object} config - The configuration object to import. * @param {string} config.algorithm - The encryption algorithm (e.g., 'aes-256-gcm'). * @param {BufferEncoding} config.outputEncoding - The output encoding format (e.g., 'hex'). * @param {BufferEncoding} config.inputEncoding - The input encoding format (e.g., 'utf8'). * @param {number} config.authTagLength - The authentication tag length (e.g., 16). * @param {string} config.key - The cryptographic key in hexadecimal string format. * * @throws {Error} If any required property is missing or has an invalid type. * @example * const config = { * algorithm: 'aes-256-gcm', * outputEncoding: 'hex', * inputEncoding: 'utf8', * authTagLength: 16, * key: 'abcdef1234567890abcdef1234567890', * }; * cryptoManager.importConfig(config); */ importConfig(config) { if (typeof config.algorithm === 'string') this.algorithm = config.algorithm; else if (typeof config.algorithm !== 'undefined') throw new Error('Invalid or missing "algorithm" property. Expected a string.'); if (typeof config.outputEncoding === 'string') this.outputEncoding = config.outputEncoding; else if (typeof config.outputEncoding !== 'undefined') throw new Error('Invalid or missing "outputEncoding" property. Expected a string.'); if (typeof config.inputEncoding === 'string') this.inputEncoding = config.inputEncoding; else if (typeof config.inputEncoding !== 'undefined') throw new Error('Invalid or missing "inputEncoding" property. Expected a string.'); if (typeof config.authTagLength === 'number') this.authTagLength = config.authTagLength; else if (typeof config.authTagLength !== 'undefined') throw new Error('Invalid or missing "authTagLength" property. Expected a number.'); if (typeof config.key === 'string') this.key = buffer.Buffer.from(config.key, 'hex'); else if (typeof config.key !== 'undefined') throw new Error('Invalid or missing "key" property. Expected a hexadecimal string.'); } } module.exports = TinyCrypto;