← back to Wine Finder Next
lib/encryption.ts
252 lines
// Advanced Encryption and Security
import crypto from 'crypto';
// Encryption configuration
const ENCRYPTION_CONFIG = {
algorithm: 'aes-256-gcm' as const,
keyLength: 32,
ivLength: 16,
saltLength: 64,
tagLength: 16,
iterations: 100000,
digest: 'sha512' as const
};
// Get encryption key from environment or generate
function getEncryptionKey(): Buffer {
const key = process.env.ENCRYPTION_KEY;
if (!key) {
console.warn('[SECURITY WARNING] No ENCRYPTION_KEY set. Using default (NOT SECURE FOR PRODUCTION)');
return crypto.scryptSync('default-key-change-me', 'salt', ENCRYPTION_CONFIG.keyLength);
}
return crypto.scryptSync(key, 'salt', ENCRYPTION_CONFIG.keyLength);
}
// Encrypt sensitive data
export function encrypt(plaintext: string): string {
try {
const iv = crypto.randomBytes(ENCRYPTION_CONFIG.ivLength);
const key = getEncryptionKey();
const cipher = crypto.createCipheriv(ENCRYPTION_CONFIG.algorithm, key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
// Combine: iv + encrypted + tag
return iv.toString('hex') + ':' + encrypted + ':' + tag.toString('hex');
} catch (error) {
console.error('[ENCRYPTION ERROR]', error);
throw new Error('Encryption failed');
}
}
// Decrypt sensitive data
export function decrypt(ciphertext: string): string {
try {
const parts = ciphertext.split(':');
if (parts.length !== 3) {
throw new Error('Invalid ciphertext format');
}
const iv = Buffer.from(parts[0], 'hex');
const encrypted = parts[1];
const tag = Buffer.from(parts[2], 'hex');
const key = getEncryptionKey();
const decipher = crypto.createDecipheriv(ENCRYPTION_CONFIG.algorithm, key, iv);
decipher.setAuthTag(tag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
console.error('[DECRYPTION ERROR]', error);
throw new Error('Decryption failed');
}
}
// Hash sensitive data (one-way)
export function secureHash(data: string, salt?: string): string {
const actualSalt = salt || crypto.randomBytes(ENCRYPTION_CONFIG.saltLength).toString('hex');
const hash = crypto.pbkdf2Sync(
data,
actualSalt,
ENCRYPTION_CONFIG.iterations,
64,
ENCRYPTION_CONFIG.digest
).toString('hex');
return `${actualSalt}:${hash}`;
}
// Verify hash
export function verifyHash(data: string, hashedData: string): boolean {
try {
const [salt, originalHash] = hashedData.split(':');
const hash = crypto.pbkdf2Sync(
data,
salt,
ENCRYPTION_CONFIG.iterations,
64,
ENCRYPTION_CONFIG.digest
).toString('hex');
return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(originalHash));
} catch {
return false;
}
}
// Generate cryptographically secure random tokens
export function generateSecureToken(length: number = 32): string {
return crypto.randomBytes(length).toString('base64url');
}
// Generate API key
export function generateAPIKey(): string {
const prefix = 'wdao'; // Wine DAO
const key = crypto.randomBytes(32).toString('base64url');
return `${prefix}_${key}`;
}
// Hash API key for storage
export function hashAPIKey(apiKey: string): string {
return crypto.createHash('sha256').update(apiKey).digest('hex');
}
// Sign data with HMAC
export function signData(data: string, secret?: string): string {
const actualSecret = secret || process.env.HMAC_SECRET || 'change-me-in-production';
const hmac = crypto.createHmac('sha256', actualSecret);
hmac.update(data);
return hmac.digest('hex');
}
// Verify HMAC signature
export function verifySignature(data: string, signature: string, secret?: string): boolean {
const expectedSignature = signData(data, secret);
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Encrypt wallet private key (for storage)
export function encryptPrivateKey(privateKey: string, userPassword: string): string {
const salt = crypto.randomBytes(ENCRYPTION_CONFIG.saltLength);
const key = crypto.pbkdf2Sync(
userPassword,
salt,
ENCRYPTION_CONFIG.iterations,
ENCRYPTION_CONFIG.keyLength,
ENCRYPTION_CONFIG.digest
);
const iv = crypto.randomBytes(ENCRYPTION_CONFIG.ivLength);
const cipher = crypto.createCipheriv(ENCRYPTION_CONFIG.algorithm, key, iv);
let encrypted = cipher.update(privateKey, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
return salt.toString('hex') + ':' + iv.toString('hex') + ':' + encrypted + ':' + tag.toString('hex');
}
// Decrypt wallet private key
export function decryptPrivateKey(encryptedKey: string, userPassword: string): string {
const parts = encryptedKey.split(':');
if (parts.length !== 4) {
throw new Error('Invalid encrypted key format');
}
const salt = Buffer.from(parts[0], 'hex');
const iv = Buffer.from(parts[1], 'hex');
const encrypted = parts[2];
const tag = Buffer.from(parts[3], 'hex');
const key = crypto.pbkdf2Sync(
userPassword,
salt,
ENCRYPTION_CONFIG.iterations,
ENCRYPTION_CONFIG.keyLength,
ENCRYPTION_CONFIG.digest
);
const decipher = crypto.createDecipheriv(ENCRYPTION_CONFIG.algorithm, key, iv);
decipher.setAuthTag(tag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Generate cryptographic proof
export function generateProof(data: any): { data: any; proof: string; timestamp: number } {
const timestamp = Date.now();
const payload = JSON.stringify({ ...data, timestamp });
const proof = signData(payload);
return { data, proof, timestamp };
}
// Verify cryptographic proof
export function verifyProof(payload: { data: any; proof: string; timestamp: number }): boolean {
const expectedPayload = JSON.stringify({ ...payload.data, timestamp: payload.timestamp });
return verifySignature(expectedPayload, payload.proof);
}
// Constant-time string comparison (prevent timing attacks)
export function secureCompare(a: string, b: string): boolean {
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
// Generate secure random string
export function generateSecureRandomString(length: number = 32, charset: string = 'alphanumeric'): string {
const charsets = {
alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
hex: '0123456789abcdef',
numeric: '0123456789',
alpha: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
};
const chars = charsets[charset as keyof typeof charsets] || charsets.alphanumeric;
const bytes = crypto.randomBytes(length);
let result = '';
for (let i = 0; i < length; i++) {
result += chars[bytes[i] % chars.length];
}
return result;
}
// Data masking (for logs/display)
export function maskSensitiveData(data: string, visibleChars: number = 4): string {
if (data.length <= visibleChars * 2) {
return '*'.repeat(data.length);
}
const start = data.slice(0, visibleChars);
const end = data.slice(-visibleChars);
const middle = '*'.repeat(data.length - (visibleChars * 2));
return `${start}${middle}${end}`;
}
// Redact sensitive fields from objects
export function redactSensitiveFields(obj: any, fields: string[] = ['password', 'privateKey', 'secret', 'token']): any {
const redacted = { ...obj };
for (const field of fields) {
if (redacted[field]) {
redacted[field] = '[REDACTED]';
}
}
return redacted;
}