keyvault-cli
Version:
Secure API key management CLI tool
101 lines (100 loc) • 4.02 kB
JavaScript
import * as crypto from 'crypto';
import { ed25519 } from '@noble/curves/ed25519';
export class KeyVaultCrypto {
/**
* Generate deterministic key pair from passphrase
* Same passphrase will always generate same keys
*/
static generateKeyPair(passphrase) {
// Create deterministic seed from passphrase + salt
const salt = 'keyvault-seed-salt-v1';
const seed = crypto.pbkdf2Sync(passphrase, salt, 100000, 32, 'sha256');
// Generate Ed25519 key pair from seed
const privateKeyBytes = ed25519.utils.randomPrivateKey(); // We'll override this
// Use seed as private key (32 bytes)
const privateKey = seed;
const publicKey = ed25519.getPublicKey(privateKey);
// Create address from public key hash
const address = this.createAddress(publicKey);
return {
publicKey: Buffer.from(publicKey).toString('hex'),
privateKey: Buffer.from(privateKey).toString('hex'),
address
};
}
/**
* Create a unique address from public key
*/
static createAddress(publicKey) {
const hash = crypto.createHash('sha256').update(publicKey).digest();
const address = hash.slice(0, 20).toString('hex');
return '0x' + address;
}
/**
* Encrypt data with public key (for server storage)
*/
static encrypt(data, publicKeyHex) {
try {
// For now, use symmetric encryption with derived key from public key
// In production, you'd use proper asymmetric encryption
const key = crypto.createHash('sha256').update(Buffer.from(publicKeyHex, 'hex')).digest();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Combine IV + encrypted data
return iv.toString('hex') + ':' + encrypted;
}
catch (error) {
throw new Error('Encryption failed: ' + error.message);
}
}
/**
* Decrypt data with private key (client-side only)
*/
static decrypt(encryptedData, privateKeyHex) {
try {
const [ivHex, encrypted] = encryptedData.split(':');
if (!ivHex || !encrypted) {
throw new Error('Invalid encrypted data format');
}
// Derive same key from private key
const publicKey = ed25519.getPublicKey(Buffer.from(privateKeyHex, 'hex'));
const key = crypto.createHash('sha256').update(publicKey).digest();
const iv = Buffer.from(ivHex, 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
catch (error) {
throw new Error('Decryption failed: ' + error.message);
}
}
/**
* Validate passphrase strength
*/
static validatePassphrase(passphrase) {
if (passphrase.length < 12) {
return { valid: false, message: 'Passphrase must be at least 12 characters long' };
}
if (passphrase.length > 100) {
return { valid: false, message: 'Passphrase too long (max 100 characters)' };
}
// Check for basic complexity
const hasLetter = /[a-zA-Z]/.test(passphrase);
const hasNumber = /[0-9]/.test(passphrase);
if (!hasLetter || !hasNumber) {
return { valid: false, message: 'Passphrase should contain both letters and numbers' };
}
return { valid: true };
}
/**
* Mask a key for display
*/
static maskKey(key) {
if (key.length <= 8)
return '*'.repeat(key.length);
return key.substring(0, 4) + '*'.repeat(key.length - 8) + key.substring(key.length - 4);
}
}