UNPKG

@lanonasis/cli

Version:

Professional CLI for LanOnasis Memory as a Service (MaaS) with MCP support, seamless inline editing, and enterprise-grade security

172 lines (171 loc) 6.05 kB
/** * Cryptographic utilities for secure credential storage * * Security Model: * - API Keys: Hashed with SHA-256 (one-way, server compares hashes) * - Vendor Keys: Encrypted with AES-256-GCM (reversible, needed for API headers) * - Encryption key derived from machine-specific identifier + user password (optional) */ import crypto from 'crypto'; import { homedir, platform, hostname } from 'os'; import { hashApiKey, isSha256Hash } from '@lanonasis/security-sdk/hash-utils'; // Encryption algorithm configuration const ALGORITHM = 'aes-256-gcm'; const KEY_LENGTH = 32; // 256 bits const IV_LENGTH = 16; // 128 bits for GCM const AUTH_TAG_LENGTH = 16; // 128 bits const SALT_LENGTH = 32; // 256 bits // Default salt component for key derivation when no passphrase is provided // This is a known constant, not a secret - it's part of the encryption scheme const DEFAULT_SALT_COMPONENT = 'lanonasis-cli-default-salt'; /** * Get a machine-specific identifier for key derivation * Uses hostname + platform + homedir to create a stable machine fingerprint */ function getMachineId() { const machineFingerprint = `${hostname()}-${platform()}-${homedir()}`; return crypto .createHash('sha256') .update(machineFingerprint) .digest('hex'); } /** * Derive an encryption key from machine ID and optional user passphrase * Uses PBKDF2 with 100,000 iterations */ function deriveEncryptionKey(salt, passphrase) { const baseSecret = getMachineId() + (passphrase || DEFAULT_SALT_COMPONENT); return crypto.pbkdf2Sync(baseSecret, salt, 100000, KEY_LENGTH, 'sha256'); } /** * Encrypt sensitive data (like vendor keys) * * @param data - The sensitive data to encrypt * @param passphrase - Optional user passphrase for additional security * @returns Encrypted data structure */ export function encryptCredential(data, passphrase) { if (!data || typeof data !== 'string') { throw new Error('Data must be a non-empty string'); } // Generate random salt and IV const salt = crypto.randomBytes(SALT_LENGTH); const iv = crypto.randomBytes(IV_LENGTH); // Derive encryption key const key = deriveEncryptionKey(salt, passphrase); // Create cipher and encrypt const cipher = crypto.createCipheriv(ALGORITHM, key, iv); let encrypted = cipher.update(data, 'utf8', 'base64'); encrypted += cipher.final('base64'); // Get authentication tag const authTag = cipher.getAuthTag(); return { encrypted, iv: iv.toString('base64'), authTag: authTag.toString('base64'), salt: salt.toString('base64'), version: '1.0' }; } /** * Decrypt sensitive data * * @param encryptedData - The encrypted data structure * @param passphrase - Optional user passphrase (must match encryption) * @returns Decrypted data */ export function decryptCredential(encryptedData, passphrase) { if (!encryptedData || typeof encryptedData !== 'object') { throw new Error('Encrypted data must be an object'); } try { // Parse encrypted data components const salt = Buffer.from(encryptedData.salt, 'base64'); const iv = Buffer.from(encryptedData.iv, 'base64'); const authTag = Buffer.from(encryptedData.authTag, 'base64'); // Derive the same encryption key const key = deriveEncryptionKey(salt, passphrase); // Create decipher and decrypt const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(encryptedData.encrypted, 'base64', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { throw new Error('Decryption failed - credential may be corrupted or wrong passphrase'); } } /** * Securely store a vendor key * * @param vendorKey - The raw vendor key * @param passphrase - Optional user passphrase for additional security * @returns Secure storage structure */ export function secureStoreVendorKey(vendorKey, passphrase) { if (!vendorKey || typeof vendorKey !== 'string') { throw new Error('Vendor key must be a non-empty string'); } return { keyHash: hashApiKey(vendorKey), encryptedKey: encryptCredential(vendorKey, passphrase), createdAt: new Date().toISOString(), encrypted: true }; } /** * Retrieve a vendor key from secure storage * * @param secureKey - The secure storage structure * @param passphrase - Optional user passphrase (must match storage) * @returns Decrypted vendor key */ export function retrieveVendorKey(secureKey, passphrase) { if (!secureKey || !secureKey.encryptedKey) { throw new Error('Invalid secure key structure'); } return decryptCredential(secureKey.encryptedKey, passphrase); } /** * Validate a vendor key against stored hash * * @param vendorKey - The key to validate * @param storedHash - The stored hash to compare against * @returns True if key matches hash */ export function validateVendorKeyHash(vendorKey, storedHash) { if (!vendorKey || !storedHash) { return false; } try { const keyHash = hashApiKey(vendorKey); return keyHash === storedHash; } catch { return false; } } /** * Check if a value is encrypted vendor key data */ export function isEncryptedVendorKey(value) { return (value && typeof value === 'object' && value.encrypted === true && typeof value.keyHash === 'string' && isSha256Hash(value.keyHash) && value.encryptedKey && typeof value.encryptedKey.encrypted === 'string'); } /** * Migration helper: Check if vendor key needs encryption upgrade */ export function needsEncryptionMigration(vendorKey) { // If it's a plain string, it's not encrypted if (typeof vendorKey === 'string' && !isSha256Hash(vendorKey)) { return true; } // If it's not an encrypted structure, needs migration return !isEncryptedVendorKey(vendorKey); }