← 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;
}