import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync, } from 'crypto'; /** * Checks if an object is an instance of Crypto. * @param {any} object - The object to check. * @returns {boolean} Returns true if the object is an instance of Crypto, false otherwise. */ export function isCrypto(object: any): object is Crypto { return ( typeof object === 'object' && object !== null && typeof object['encrypt'] === 'function' && typeof object['decrypt'] === 'function' ); } /** * Interface representing the set of parameters to create an instance of the Crypto class. * @interface CryptoOptions * @property {string} password - The password used for key derivation. * @property {string} salt - The salt used for key derivation. * @property {string} [algorithm='sha512'] - The hash algorithm to use for key derivation. * @property {number} [iterations=1000] - The number of iterations to use for key derivation. * @property {number} [keyLength=32] - The length of the derived key in bytes. * @property {number|Buffer} [iv] - The initialization vector for the AES encryption. */ export interface CryptoOptions { password: string; salt: string; algorithm?: string; iterations?: number; keyLength?: number; iv?: number | Buffer; } /** * Class representing a cryptographic utility for encrypting and decrypting text. * @class * @param {string|CryptoOptions} passwordOrOptions - The password used for key derivation or an options object. * @param {string} salt - The salt used for key derivation. * @param {string} [algorithm='sha512'] - The hash algorithm to use for key derivation. * @param {number} [iterations=1000] - The number of iterations to use for key derivation. * @param {number} [keyLength=32] - The length of the derived key in bytes. * @param {number|Buffer} [iv] - The initialization vector for the AES encryption. */ export class Crypto { private iv: Buffer; private key: Buffer; constructor( password: string, salt: string, algorithm?: string, iterations?: number, keyLength?: number, iv?: number | Buffer ); constructor(options: CryptoOptions); constructor( passwordOrOptions: string | CryptoOptions, salt?: string, algorithm: string = 'sha512', iterations: number = 1000, keyLength: number = 32, iv: number | Buffer = randomBytes(16) ) { let password: string; if (typeof passwordOrOptions === 'string') { password = passwordOrOptions; this.iv = typeof iv === 'number' ? this.convertNumberToIV(iv) : iv; } else { ({ password, salt, algorithm = 'sha512', iterations = 1000, keyLength = 32, iv = randomBytes(16), } = passwordOrOptions); this.iv = typeof passwordOrOptions.iv === 'number' ? this.convertNumberToIV(passwordOrOptions.iv) : passwordOrOptions.iv ?? randomBytes(16); } this.key = pbkdf2Sync(password, salt!, iterations, keyLength, algorithm); } private convertNumberToIV(iv: number): Buffer { // Создаем Buffer размером 16 байт, заполняя его значением iv const buffer = Buffer.alloc(16); buffer.writeUInt32BE(iv, 12); // Записываем iv в последний 4 байта Buffer'а return buffer; } /** * Encrypt a text string. * Uses the IV supplied at construction time (fixed or random-at-construct). * The IV is **not** embedded in the output — both sides must use the same IV. * For persistent storage use {@link encryptStorable} instead. * * @param {string} text - The plain text to encrypt. * @returns {string} The encrypted text as a hex string. */ public encrypt(text: string): string { const cipher = createCipheriv('aes-256-ctr', this.key, this.iv); const encrypted = Buffer.concat([ cipher.update(text, 'utf8'), cipher.final(), ]); return encrypted.toString('hex'); } /** * Decrypt an encrypted text string produced by {@link encrypt}. * Uses the IV supplied at construction time. * * @param {string} text - The encrypted text as a hex string. * @returns {string} The decrypted plain text. */ public decrypt(text: string): string { const encryptedText = Buffer.from(text, 'hex'); const decipher = createDecipheriv('aes-256-ctr', this.key, this.iv); const decrypted = Buffer.concat([ decipher.update(encryptedText), decipher.final(), ]); return decrypted.toString('utf8'); } /** * Encrypts text and prepends a fresh random IV to the output. * * Use this method when the ciphertext will be stored (database, file, etc.) * and retrieved later in a different process instance. A new random IV is * generated for every call, so repeated encryptions of the same plaintext * produce different ciphertexts — no IV reuse across records. * * Output format: `hex( IV[16 bytes] || ciphertext[N bytes] )` * * The construction-time IV is **not** used by this method. * * @param {string} text - The plain text to encrypt. * @returns {string} Hex string containing the prepended IV and ciphertext. * @see {@link decryptStorable} */ public encryptStorable(text: string): string { const iv = randomBytes(16); const cipher = createCipheriv('aes-256-ctr', this.key, iv); const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]); return Buffer.concat([iv, encrypted]).toString('hex'); } /** * Decrypts a value produced by {@link encryptStorable}. * * Extracts the first 16 bytes as the IV, then decrypts the remainder. * The construction-time IV is **not** used by this method. * * @param {string} text - Hex string `IV[16 bytes] || ciphertext` from {@link encryptStorable}. * @returns {string} The decrypted plain text. * @throws {RangeError} When the input is shorter than 16 bytes (32 hex chars). * @see {@link encryptStorable} */ public decryptStorable(text: string): string { const buf = Buffer.from(text, 'hex'); if (buf.length < 16) { throw new RangeError( `decryptStorable: input too short (${buf.length} bytes); ` + 'expected at least 16 bytes for the IV prefix.', ); } const iv = buf.subarray(0, 16); const ciphertext = buf.subarray(16); const decipher = createDecipheriv('aes-256-ctr', this.key, iv); return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8'); } /** * Returns a UUID ver.4 string. * @returns {string} UUID ver.4 string. */ public static getUUID(): string { return crypto.randomUUID(); } } export default Crypto;