@hiprax/crypto
Version:
High-security encryption/decryption library using AES-256-GCM and Argon2id
722 lines • 34.9 kB
JavaScript
import crypto from 'node:crypto';
import argon2 from 'argon2';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { dirname } from 'node:path';
import { CryptoError, CryptoErrorType, SecurityLevel, EncryptionAlgorithm, } from './types.js';
/**
* High-security encryption manager using AES-256-GCM and Argon2id
* Implements industry-standard cryptographic practices with improved security
*/
export class CryptoManager {
algorithm;
keyLength;
ivLength;
saltLength;
tagLength;
argon2Options;
aad;
defaultPassphrase;
constructor(options = {}) {
this.algorithm = EncryptionAlgorithm.AES_256_GCM;
this.keyLength = 32; // 256 bits
this.ivLength = 12; // 96 bits for GCM
this.saltLength = 32; // 256 bits
this.tagLength = 16; // 128 bits for GCM
// Store default passphrase if provided and not empty
if (options.defaultPassphrase !== undefined &&
options.defaultPassphrase !== '') {
this.defaultPassphrase = options.defaultPassphrase;
}
// Argon2id parameters (high security)
this.argon2Options = {
type: argon2.argon2id,
memoryCost: options.memoryCost ?? 2 ** 16, // 64MB
timeCost: options.timeCost ?? 3,
parallelism: options.parallelism ?? 1,
hashLength: this.keyLength,
saltLength: this.saltLength,
};
// Use custom AAD or default
const aadString = options.aad ?? 'secure-crypto-tool-v2';
this.aad = Buffer.from(aadString, 'utf8');
}
/**
* Generate cryptographically secure random bytes
* @param length - Number of bytes to generate
* @returns Random bytes
* @throws CryptoError if length is invalid
*/
generateSecureRandom(length) {
if (!Number.isInteger(length) || length <= 0 || length > 1024) {
throw new CryptoError('Invalid length for random generation. Must be between 1 and 1024 bytes.', CryptoErrorType.INVALID_INPUT, 'INVALID_RANDOM_LENGTH');
}
return crypto.randomBytes(length);
}
/**
* Derive encryption key from password using Argon2id
* @param password - User password
* @param salt - Random salt
* @returns Derived key
* @throws CryptoError if derivation fails
*/
async deriveKey(password, salt) {
if (!password || typeof password !== 'string') {
throw new CryptoError('Password must be a non-empty string', CryptoErrorType.INVALID_INPUT, 'INVALID_PASSWORD');
}
if (!Buffer.isBuffer(salt) || salt.length !== this.saltLength) {
throw new CryptoError(`Invalid salt provided. Expected ${this.saltLength} bytes.`, CryptoErrorType.INVALID_INPUT, 'INVALID_SALT');
}
try {
const key = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: this.argon2Options.memoryCost,
timeCost: this.argon2Options.timeCost,
parallelism: this.argon2Options.parallelism,
hashLength: this.argon2Options.hashLength,
salt,
raw: true,
});
// Ensure we get exactly the key length we need
return Buffer.from(key).subarray(0, this.keyLength);
}
catch (error) {
throw new CryptoError(`Key derivation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, CryptoErrorType.ENCRYPTION_FAILED, 'KEY_DERIVATION_FAILED');
}
}
/**
* Derive encryption key from password using PBKDF2 (synchronous alternative to Argon2id)
* @param password - User password
* @param salt - Random salt
* @returns Derived key
* @throws CryptoError if derivation fails
*/
deriveKeySync(password, salt) {
if (!password || typeof password !== 'string') {
throw new CryptoError('Password must be a non-empty string', CryptoErrorType.INVALID_INPUT, 'INVALID_PASSWORD');
}
if (!Buffer.isBuffer(salt) || salt.length !== this.saltLength) {
throw new CryptoError(`Invalid salt provided. Expected ${this.saltLength} bytes.`, CryptoErrorType.INVALID_INPUT, 'INVALID_SALT');
}
try {
// Use PBKDF2 as a synchronous alternative to Argon2id
// Note: PBKDF2 is less secure than Argon2id but provides synchronous operation
const iterations = 100000; // High iteration count for security
const key = crypto.pbkdf2Sync(password, salt, iterations, this.keyLength, 'sha256');
return key;
}
catch (error) {
throw new CryptoError(`Synchronous key derivation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, CryptoErrorType.ENCRYPTION_FAILED, 'SYNC_KEY_DERIVATION_FAILED');
}
}
/**
* Encrypt data using AES-256-GCM
* @param data - Data to encrypt
* @param key - Encryption key
* @param iv - Initialization vector
* @returns Encrypted data with auth tag
* @throws CryptoError if encryption fails
*/
encryptData(data, key, iv) {
if (!Buffer.isBuffer(data)) {
throw new CryptoError('Data must be a Buffer', CryptoErrorType.INVALID_INPUT, 'INVALID_DATA');
}
if (!Buffer.isBuffer(key) || key.length !== this.keyLength) {
throw new CryptoError(`Invalid key provided. Expected ${this.keyLength} bytes.`, CryptoErrorType.INVALID_INPUT, 'INVALID_KEY');
}
if (!Buffer.isBuffer(iv) || iv.length !== this.ivLength) {
throw new CryptoError(`Invalid IV provided. Expected ${this.ivLength} bytes.`, CryptoErrorType.INVALID_INPUT, 'INVALID_IV');
}
try {
const cipher = crypto.createCipheriv(this.algorithm, key, iv);
cipher.setAAD(this.aad);
let encrypted = cipher.update(data);
encrypted = Buffer.concat([encrypted, cipher.final()]);
const tag = cipher.getAuthTag();
return { encrypted, tag };
}
catch (error) {
throw new CryptoError(`Encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`, CryptoErrorType.ENCRYPTION_FAILED, 'ENCRYPTION_FAILED');
}
}
/**
* Decrypt data using AES-256-GCM
* @param encryptedData - Encrypted data
* @param key - Decryption key
* @param iv - Initialization vector
* @param tag - Authentication tag
* @returns Decrypted data
* @throws CryptoError if decryption fails
*/
decryptData(encryptedData, key, iv, tag) {
if (!Buffer.isBuffer(encryptedData)) {
throw new CryptoError('Encrypted data must be a Buffer', CryptoErrorType.INVALID_INPUT, 'INVALID_ENCRYPTED_DATA');
}
if (!Buffer.isBuffer(key) || key.length !== this.keyLength) {
throw new CryptoError(`Invalid key provided. Expected ${this.keyLength} bytes.`, CryptoErrorType.INVALID_INPUT, 'INVALID_KEY');
}
if (!Buffer.isBuffer(iv) || iv.length !== this.ivLength) {
throw new CryptoError(`Invalid IV provided. Expected ${this.ivLength} bytes.`, CryptoErrorType.INVALID_INPUT, 'INVALID_IV');
}
if (!Buffer.isBuffer(tag) || tag.length !== this.tagLength) {
throw new CryptoError(`Invalid authentication tag provided. Expected ${this.tagLength} bytes.`, CryptoErrorType.INVALID_INPUT, 'INVALID_TAG');
}
try {
const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
decipher.setAAD(this.aad);
decipher.setAuthTag(tag);
let decrypted = decipher.update(encryptedData);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted;
}
catch (error) {
throw new CryptoError(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`, CryptoErrorType.DECRYPTION_FAILED, 'DECRYPTION_FAILED');
}
}
/**
* Encrypt text with password
* @param text - Text to encrypt
* @param password - Encryption password (optional if default passphrase is set)
* @returns Base64 encoded encrypted data
* @throws CryptoError if encryption fails
*/
async encryptText(text, password) {
if (!text || typeof text !== 'string') {
throw new CryptoError('Text must be a non-empty string', CryptoErrorType.INVALID_INPUT, 'INVALID_TEXT');
}
// Use provided password or default passphrase
const finalPassword = password || this.defaultPassphrase;
if (!finalPassword || typeof finalPassword !== 'string') {
throw new CryptoError('Password is required. Either provide a password parameter or set a default passphrase in the constructor.', CryptoErrorType.INVALID_INPUT, 'INVALID_PASSWORD');
}
// Validate password strength
if (!this.validatePassword(finalPassword)) {
throw new CryptoError('Password does not meet security requirements', CryptoErrorType.INVALID_PASSWORD, 'WEAK_PASSWORD');
}
try {
// Generate salt and IV
const salt = this.generateSecureRandom(this.saltLength);
const iv = this.generateSecureRandom(this.ivLength);
// Derive key from password
const key = await this.deriveKey(finalPassword, salt);
// Encrypt the text
const textBuffer = Buffer.from(text, 'utf8');
const { encrypted, tag } = this.encryptData(textBuffer, key, iv);
// Combine all components: salt + iv + tag + encrypted data
const combined = Buffer.concat([salt, iv, tag, encrypted]);
// Clear sensitive data from memory
this.secureClear(key);
this.secureClear(textBuffer);
return combined.toString('base64');
}
catch (error) {
if (error instanceof CryptoError) {
throw error;
}
throw new CryptoError(`Text encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`, CryptoErrorType.ENCRYPTION_FAILED, 'TEXT_ENCRYPTION_FAILED');
}
}
/**
* Decrypt text with password
* @param encryptedText - Base64 encoded encrypted text
* @param password - Decryption password (optional if default passphrase is set)
* @returns Decrypted text
* @throws CryptoError if decryption fails
*/
async decryptText(encryptedText, password) {
if (!encryptedText || typeof encryptedText !== 'string') {
throw new CryptoError('Encrypted text must be a non-empty string', CryptoErrorType.INVALID_INPUT, 'INVALID_ENCRYPTED_TEXT');
}
// Use provided password or default passphrase
const finalPassword = password || this.defaultPassphrase;
if (!finalPassword || typeof finalPassword !== 'string') {
throw new CryptoError('Password is required. Either provide a password parameter or set a default passphrase in the constructor.', CryptoErrorType.INVALID_INPUT, 'INVALID_PASSWORD');
}
try {
// Decode base64
const combined = Buffer.from(encryptedText, 'base64');
// Validate minimum size
const minSize = this.saltLength + this.ivLength + this.tagLength;
if (combined.length < minSize) {
throw new CryptoError('Encrypted data is too small to be valid', CryptoErrorType.INVALID_INPUT, 'INVALID_ENCRYPTED_DATA_SIZE');
}
// Extract components
const salt = combined.subarray(0, this.saltLength);
const iv = combined.subarray(this.saltLength, this.saltLength + this.ivLength);
const tag = combined.subarray(this.saltLength + this.ivLength, this.saltLength + this.ivLength + this.tagLength);
const encrypted = combined.subarray(this.saltLength + this.ivLength + this.tagLength);
// Derive key from password
const key = await this.deriveKey(finalPassword, salt);
// Decrypt the data
const decrypted = this.decryptData(encrypted, key, iv, tag);
// Clear sensitive data from memory
this.secureClear(key);
return decrypted.toString('utf8');
}
catch (error) {
if (error instanceof CryptoError) {
throw error;
}
throw new CryptoError(`Text decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`, CryptoErrorType.DECRYPTION_FAILED, 'TEXT_DECRYPTION_FAILED');
}
}
/**
* Encrypt text with password (synchronous version)
* @param text - Text to encrypt
* @param password - Encryption password (optional if default passphrase is set)
* @returns Base64 encoded encrypted data
* @throws CryptoError if encryption fails
*/
encryptTextSync(text, password) {
if (!text || typeof text !== 'string') {
throw new CryptoError('Text must be a non-empty string', CryptoErrorType.INVALID_INPUT, 'INVALID_TEXT');
}
// Use provided password or default passphrase
const finalPassword = password || this.defaultPassphrase;
if (!finalPassword || typeof finalPassword !== 'string') {
throw new CryptoError('Password is required. Either provide a password parameter or set a default passphrase in the constructor.', CryptoErrorType.INVALID_INPUT, 'INVALID_PASSWORD');
}
// Validate password strength
if (!this.validatePassword(finalPassword)) {
throw new CryptoError('Password does not meet security requirements', CryptoErrorType.INVALID_PASSWORD, 'WEAK_PASSWORD');
}
try {
// Generate salt and IV
const salt = this.generateSecureRandom(this.saltLength);
const iv = this.generateSecureRandom(this.ivLength);
// Derive key from password (synchronous)
const key = this.deriveKeySync(finalPassword, salt);
// Encrypt the text
const textBuffer = Buffer.from(text, 'utf8');
const { encrypted, tag } = this.encryptData(textBuffer, key, iv);
// Combine all components: salt + iv + tag + encrypted data
const combined = Buffer.concat([salt, iv, tag, encrypted]);
// Clear sensitive data from memory
this.secureClear(key);
this.secureClear(textBuffer);
return combined.toString('base64');
}
catch (error) {
if (error instanceof CryptoError) {
throw error;
}
throw new CryptoError(`Synchronous text encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`, CryptoErrorType.ENCRYPTION_FAILED, 'SYNC_TEXT_ENCRYPTION_FAILED');
}
}
/**
* Decrypt text with password (synchronous version)
* @param encryptedText - Base64 encoded encrypted text
* @param password - Decryption password (optional if default passphrase is set)
* @returns Decrypted text
* @throws CryptoError if decryption fails
*/
decryptTextSync(encryptedText, password) {
if (!encryptedText || typeof encryptedText !== 'string') {
throw new CryptoError('Encrypted text must be a non-empty string', CryptoErrorType.INVALID_INPUT, 'INVALID_ENCRYPTED_TEXT');
}
// Use provided password or default passphrase
const finalPassword = password || this.defaultPassphrase;
if (!finalPassword || typeof finalPassword !== 'string') {
throw new CryptoError('Password is required. Either provide a password parameter or set a default passphrase in the constructor.', CryptoErrorType.INVALID_INPUT, 'INVALID_PASSWORD');
}
try {
// Decode base64
const combined = Buffer.from(encryptedText, 'base64');
// Validate minimum size
const minSize = this.saltLength + this.ivLength + this.tagLength;
if (combined.length < minSize) {
throw new CryptoError('Encrypted data is too small to be valid', CryptoErrorType.INVALID_INPUT, 'INVALID_ENCRYPTED_DATA_SIZE');
}
// Extract components
const salt = combined.subarray(0, this.saltLength);
const iv = combined.subarray(this.saltLength, this.saltLength + this.ivLength);
const tag = combined.subarray(this.saltLength + this.ivLength, this.saltLength + this.ivLength + this.tagLength);
const encrypted = combined.subarray(this.saltLength + this.ivLength + this.tagLength);
// Derive key from password (synchronous)
const key = this.deriveKeySync(finalPassword, salt);
// Decrypt the data
const decrypted = this.decryptData(encrypted, key, iv, tag);
// Clear sensitive data from memory
this.secureClear(key);
return decrypted.toString('utf8');
}
catch (error) {
if (error instanceof CryptoError) {
throw error;
}
throw new CryptoError(`Synchronous text decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`, CryptoErrorType.DECRYPTION_FAILED, 'SYNC_TEXT_DECRYPTION_FAILED');
}
}
/**
* Encrypt file with password (streaming for large files)
* @param inputPath - Input file path
* @param outputPath - Output file path
* @param password - Encryption password (optional if default passphrase is set)
* @throws CryptoError if encryption fails
*/
async encryptFile(inputPath, outputPath, password) {
if (!inputPath || !outputPath) {
throw new CryptoError('Input path and output path are required', CryptoErrorType.INVALID_INPUT, 'MISSING_REQUIRED_PARAMS');
}
// Use provided password or default passphrase
const finalPassword = password || this.defaultPassphrase;
if (!finalPassword || typeof finalPassword !== 'string') {
throw new CryptoError('Password is required. Either provide a password parameter or set a default passphrase in the constructor.', CryptoErrorType.INVALID_INPUT, 'INVALID_PASSWORD');
}
// Validate password strength
if (!this.validatePassword(finalPassword)) {
throw new CryptoError('Password does not meet security requirements', CryptoErrorType.INVALID_PASSWORD, 'WEAK_PASSWORD');
}
try {
// Check if input file exists
if (!existsSync(inputPath)) {
throw new CryptoError(`Input file does not exist: ${inputPath}`, CryptoErrorType.FILE_ERROR, 'INPUT_FILE_NOT_FOUND');
}
// Ensure output directory exists
const outputDir = dirname(outputPath);
if (!existsSync(outputDir)) {
try {
await mkdir(outputDir, { recursive: true });
}
catch (dirError) {
throw new CryptoError(`Cannot create output directory: ${dirError instanceof Error ? dirError.message : 'Unknown error'}`, CryptoErrorType.FILE_ERROR, 'OUTPUT_DIR_CREATION_FAILED');
}
}
// Generate salt and IV
const salt = this.generateSecureRandom(this.saltLength);
const iv = this.generateSecureRandom(this.ivLength);
// Derive key from password
const key = await this.deriveKey(finalPassword, salt);
// Write header: salt + iv
const header = Buffer.concat([salt, iv]);
await writeFile(outputPath, header);
// Create encryption transform stream
const cipher = crypto.createCipheriv(this.algorithm, key, iv);
cipher.setAAD(this.aad);
// Create streams
const inputStream = createReadStream(inputPath);
const outputStream = createWriteStream(outputPath, { flags: 'a' });
// Pipe through encryption
await pipeline(inputStream, cipher, outputStream);
// Write authentication tag
const tag = cipher.getAuthTag();
await writeFile(outputPath, tag, { flag: 'a' });
// Clear sensitive data
this.secureClear(key);
}
catch (error) {
// Clean up partial output file if it exists
try {
if (existsSync(outputPath)) {
await writeFile(outputPath, ''); // Clear the file
}
}
catch {
// Ignore cleanup errors
}
if (error instanceof CryptoError) {
throw error;
}
throw new CryptoError(`File encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`, CryptoErrorType.ENCRYPTION_FAILED, 'FILE_ENCRYPTION_FAILED');
}
}
/**
* Decrypt file with password (streaming for large files)
* @param inputPath - Input file path
* @param outputPath - Output file path
* @param password - Decryption password (optional if default passphrase is set)
* @throws CryptoError if decryption fails
*/
async decryptFile(inputPath, outputPath, password) {
if (!inputPath || !outputPath) {
throw new CryptoError('Input path and output path are required', CryptoErrorType.INVALID_INPUT, 'MISSING_REQUIRED_PARAMS');
}
// Use provided password or default passphrase
const finalPassword = password || this.defaultPassphrase;
if (!finalPassword || typeof finalPassword !== 'string') {
throw new CryptoError('Password is required. Either provide a password parameter or set a default passphrase in the constructor.', CryptoErrorType.INVALID_INPUT, 'INVALID_PASSWORD');
}
try {
// Check if input file exists
if (!existsSync(inputPath)) {
throw new CryptoError(`Input file does not exist: ${inputPath}`, CryptoErrorType.FILE_ERROR, 'INPUT_FILE_NOT_FOUND');
}
// Ensure output directory exists
const outputDir = dirname(outputPath);
if (!existsSync(outputDir)) {
try {
await mkdir(outputDir, { recursive: true });
}
catch (dirError) {
throw new CryptoError(`Cannot create output directory: ${dirError instanceof Error ? dirError.message : 'Unknown error'}`, CryptoErrorType.FILE_ERROR, 'OUTPUT_DIR_CREATION_FAILED');
}
}
// Read the entire file to get its size
const fileBuffer = await readFile(inputPath);
// Calculate positions
const headerSize = this.saltLength + this.ivLength;
const tagStart = fileBuffer.length - this.tagLength;
// Validate file size
if (fileBuffer.length < headerSize + this.tagLength) {
throw new CryptoError('File is too small to be a valid encrypted file', CryptoErrorType.INVALID_INPUT, 'INVALID_ENCRYPTED_FILE_SIZE');
}
// Extract components
const salt = fileBuffer.slice(0, this.saltLength);
const iv = fileBuffer.slice(this.saltLength, headerSize);
const tag = fileBuffer.slice(tagStart);
const encryptedData = fileBuffer.slice(headerSize, tagStart);
// Derive key from password
const key = await this.deriveKey(finalPassword, salt);
// Create decryption transform stream
const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
decipher.setAAD(this.aad);
decipher.setAuthTag(tag);
// Decrypt the data
let decrypted = decipher.update(encryptedData);
decrypted = Buffer.concat([decrypted, decipher.final()]);
// Write decrypted data
await writeFile(outputPath, decrypted);
// Clear sensitive data
this.secureClear(key);
}
catch (error) {
// Clean up partial output file if it exists
try {
if (existsSync(outputPath)) {
await writeFile(outputPath, ''); // Clear the file
}
}
catch {
// Ignore cleanup errors
}
if (error instanceof CryptoError) {
throw error;
}
throw new CryptoError(`File decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`, CryptoErrorType.DECRYPTION_FAILED, 'FILE_DECRYPTION_FAILED');
}
}
/**
* Encrypt file with password (synchronous version)
* @param inputPath - Input file path
* @param outputPath - Output file path
* @param password - Encryption password (optional if default passphrase is set)
* @throws CryptoError if encryption fails
*/
encryptFileSync(inputPath, outputPath, password) {
if (!inputPath || !outputPath) {
throw new CryptoError('Input path and output path are required', CryptoErrorType.INVALID_INPUT, 'MISSING_REQUIRED_PARAMS');
}
// Use provided password or default passphrase
const finalPassword = password || this.defaultPassphrase;
if (!finalPassword || typeof finalPassword !== 'string') {
throw new CryptoError('Password is required. Either provide a password parameter or set a default passphrase in the constructor.', CryptoErrorType.INVALID_INPUT, 'INVALID_PASSWORD');
}
// Validate password strength
if (!this.validatePassword(finalPassword)) {
throw new CryptoError('Password does not meet security requirements', CryptoErrorType.INVALID_PASSWORD, 'WEAK_PASSWORD');
}
try {
// Check if input file exists
if (!existsSync(inputPath)) {
throw new CryptoError(`Input file does not exist: ${inputPath}`, CryptoErrorType.FILE_ERROR, 'INPUT_FILE_NOT_FOUND');
}
// Ensure output directory exists
const outputDir = dirname(outputPath);
if (!existsSync(outputDir)) {
try {
mkdirSync(outputDir, { recursive: true });
}
catch (dirError) {
throw new CryptoError(`Cannot create output directory: ${dirError instanceof Error ? dirError.message : 'Unknown error'}`, CryptoErrorType.FILE_ERROR, 'OUTPUT_DIR_CREATION_FAILED');
}
}
// Generate salt and IV
const salt = this.generateSecureRandom(this.saltLength);
const iv = this.generateSecureRandom(this.ivLength);
// Derive key from password (synchronous)
const key = this.deriveKeySync(finalPassword, salt);
// Read input file
const inputData = readFileSync(inputPath);
// Create encryption transform
const cipher = crypto.createCipheriv(this.algorithm, key, iv);
cipher.setAAD(this.aad);
// Encrypt the data
let encrypted = cipher.update(inputData);
encrypted = Buffer.concat([encrypted, cipher.final()]);
// Get authentication tag
const tag = cipher.getAuthTag();
// Write header: salt + iv
const header = Buffer.concat([salt, iv]);
writeFileSync(outputPath, header);
// Write encrypted data
writeFileSync(outputPath, encrypted, { flag: 'a' });
// Write authentication tag
writeFileSync(outputPath, tag, { flag: 'a' });
// Clear sensitive data
this.secureClear(key);
}
catch (error) {
// Clean up partial output file if it exists
try {
if (existsSync(outputPath)) {
writeFileSync(outputPath, ''); // Clear the file
}
}
catch {
// Ignore cleanup errors
}
if (error instanceof CryptoError) {
throw error;
}
throw new CryptoError(`Synchronous file encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`, CryptoErrorType.ENCRYPTION_FAILED, 'SYNC_FILE_ENCRYPTION_FAILED');
}
}
/**
* Decrypt file with password (synchronous version)
* @param inputPath - Input file path
* @param outputPath - Output file path
* @param password - Decryption password (optional if default passphrase is set)
* @throws CryptoError if decryption fails
*/
decryptFileSync(inputPath, outputPath, password) {
if (!inputPath || !outputPath) {
throw new CryptoError('Input path and output path are required', CryptoErrorType.INVALID_INPUT, 'MISSING_REQUIRED_PARAMS');
}
// Use provided password or default passphrase
const finalPassword = password || this.defaultPassphrase;
if (!finalPassword || typeof finalPassword !== 'string') {
throw new CryptoError('Password is required. Either provide a password parameter or set a default passphrase in the constructor.', CryptoErrorType.INVALID_INPUT, 'INVALID_PASSWORD');
}
try {
// Check if input file exists
if (!existsSync(inputPath)) {
throw new CryptoError(`Input file does not exist: ${inputPath}`, CryptoErrorType.FILE_ERROR, 'INPUT_FILE_NOT_FOUND');
}
// Ensure output directory exists
const outputDir = dirname(outputPath);
if (!existsSync(outputDir)) {
try {
mkdirSync(outputDir, { recursive: true });
}
catch (dirError) {
throw new CryptoError(`Cannot create output directory: ${dirError instanceof Error ? dirError.message : 'Unknown error'}`, CryptoErrorType.FILE_ERROR, 'OUTPUT_DIR_CREATION_FAILED');
}
}
// Read the entire file
const fileBuffer = readFileSync(inputPath);
// Calculate positions
const headerSize = this.saltLength + this.ivLength;
const tagStart = fileBuffer.length - this.tagLength;
// Validate file size
if (fileBuffer.length < headerSize + this.tagLength) {
throw new CryptoError('File is too small to be a valid encrypted file', CryptoErrorType.INVALID_INPUT, 'INVALID_ENCRYPTED_FILE_SIZE');
}
// Extract components
const salt = fileBuffer.slice(0, this.saltLength);
const iv = fileBuffer.slice(this.saltLength, headerSize);
const tag = fileBuffer.slice(tagStart);
const encryptedData = fileBuffer.slice(headerSize, tagStart);
// Derive key from password (synchronous)
const key = this.deriveKeySync(finalPassword, salt);
// Create decryption transform
const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
decipher.setAAD(this.aad);
decipher.setAuthTag(tag);
// Decrypt the data
let decrypted = decipher.update(encryptedData);
decrypted = Buffer.concat([decrypted, decipher.final()]);
// Write decrypted data
writeFileSync(outputPath, decrypted);
// Clear sensitive data
this.secureClear(key);
}
catch (error) {
// Clean up partial output file if it exists
try {
if (existsSync(outputPath)) {
writeFileSync(outputPath, ''); // Clear the file
}
}
catch {
// Ignore cleanup errors
}
if (error instanceof CryptoError) {
throw error;
}
throw new CryptoError(`Synchronous file decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`, CryptoErrorType.DECRYPTION_FAILED, 'SYNC_FILE_DECRYPTION_FAILED');
}
}
/**
* Securely clear sensitive data from memory
* @param buffer - Buffer to clear
*/
secureClear(buffer) {
if (buffer && Buffer.isBuffer(buffer)) {
buffer.fill(0);
}
}
/**
* Validate password strength
* @param password - Password to validate
* @returns True if password meets requirements
*/
validatePassword(password) {
if (!password || typeof password !== 'string') {
return false;
}
// Minimum 8 characters, at least one uppercase, one lowercase, one number, one special character
const minLength = 8;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
return (password.length >= minLength &&
hasUpperCase &&
hasLowerCase &&
hasNumbers &&
hasSpecialChar);
}
/**
* Get encryption parameters for debugging/info
* @returns Current encryption parameters
*/
getParameters() {
return {
algorithm: this.algorithm,
keyLength: this.keyLength,
ivLength: this.ivLength,
saltLength: this.saltLength,
tagLength: this.tagLength,
argon2Options: { ...this.argon2Options },
};
}
/**
* Get security level based on current configuration
* @returns Security level
*/
getSecurityLevel() {
const { memoryCost, timeCost } = this.argon2Options;
if (memoryCost >= 2 ** 18 && timeCost >= 4) {
return SecurityLevel.ULTRA;
}
else if (memoryCost >= 2 ** 16 && timeCost >= 3) {
return SecurityLevel.HIGH;
}
else if (memoryCost >= 2 ** 14 && timeCost >= 2) {
return SecurityLevel.MEDIUM;
}
else {
return SecurityLevel.LOW;
}
}
/**
* Check if a default passphrase is set
* @returns True if default passphrase is configured
*/
hasDefaultPassphrase() {
return (this.defaultPassphrase !== undefined && this.defaultPassphrase !== '');
}
}
//# sourceMappingURL=crypto-manager.js.map