// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
/**
 * Encryption utilities for sensitive data at rest
 *
 * Uses AES-256-GCM for authenticated encryption with PBKDF2 key derivation.
 *
 * Security notes:
 * - Key derivation uses PBKDF2-SHA512 with 100,000+ iterations (OWASP recommended)
 * - NEVER falls back to JWT secret (would cause data loss on rotation)
 * - Supports versioned key derivation for future algorithm changes
 */
import { createCipheriv, createDecipheriv, randomBytes, createHash, pbkdf2Sync } from 'node:crypto';
import { getConfig } from './config.js';
import { EncryptionError } from './errors.js';
import { createLogger } from './logger.js';
import { devOnlyDefault } from './security-mode.js';
const logger = createLogger({ component: 'encryption' });
/**
 * Track whether we've already logged the SHA-256 deprecation warning
 * to avoid spamming logs on every decrypt operation
 */
let sha256DeprecationWarningLogged = false;
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;
const KEY_LENGTH = 32; // 256 bits for AES-256
/**
 * Development-only fallback values
 * These are only used when devOnlyDefault() permits (non-production modes)
 */
const DEV_FALLBACK_SALT = 'vorion-dev-salt-do-not-use-in-production';
const DEV_FALLBACK_KEY = 'vorion-dev-key-32-chars-minimum!!';
/**
 * Derive a 256-bit key using the specified KDF version
 *
 * @param kdfVersion - Which key derivation function to use (1=SHA-256 legacy, 2=PBKDF2)
 * @returns Derived 32-byte key suitable for AES-256
 * @throws Error if encryption key is not configured (no fallback to JWT secret)
 */
function deriveKey(kdfVersion = 2) {
    const config = getConfig();
    // Get encryption key - use devOnlyDefault for secure fallback handling
    let encryptionKey = config.encryption?.key;
    if (!encryptionKey) {
        // devOnlyDefault throws in production, returns fallback in development
        encryptionKey = devOnlyDefault('VORION_ENCRYPTION_KEY', DEV_FALLBACK_KEY);
    }
    if (kdfVersion === 1) {
        // Legacy SHA-256 derivation (insecure, for migration only)
        // WARNING: This should only be used to decrypt old data during migration
        if (!sha256DeprecationWarningLogged) {
            sha256DeprecationWarningLogged = true;
            logger.warn({
                kdfVersion: 1,
                recommendation: 'Migrate to KDF version 2 (PBKDF2-SHA512)',
                migrationGuide: 'Use migrateEnvelope() or migrateEncryptedField() to upgrade encrypted data',
            }, 'SECURITY DEPRECATION: Using legacy SHA-256 key derivation (KDF v1). ' +
                'This method is cryptographically weak and should only be used for decrypting legacy data. ' +
                'Please migrate all encrypted data to KDF v2 (PBKDF2-SHA512) using the migration utilities.');
        }
        return createHash('sha256').update(encryptionKey).digest();
    }
    // Version 2: PBKDF2-SHA512 (secure, current)
    let salt = config.encryption?.salt;
    if (!salt) {
        // devOnlyDefault throws in production, returns fallback in development
        salt = devOnlyDefault('VORION_ENCRYPTION_SALT', DEV_FALLBACK_SALT);
    }
    const iterations = config.encryption?.pbkdf2Iterations ?? 100000;
    return pbkdf2Sync(encryptionKey, salt, iterations, KEY_LENGTH, 'sha512');
}
/**
 * Get the current KDF version from config
 */
function getCurrentKdfVersion() {
    const config = getConfig();
    return (config.encryption?.kdfVersion ?? 2);
}
/**
 * Encrypt a value using AES-256-GCM with PBKDF2-derived key
 */
export function encrypt(plaintext) {
    const kdfVersion = getCurrentKdfVersion();
    const key = deriveKey(kdfVersion);
    const iv = randomBytes(IV_LENGTH);
    const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
    const encrypted = Buffer.concat([
        cipher.update(plaintext, 'utf8'),
        cipher.final(),
    ]);
    return {
        ciphertext: encrypted.toString('base64'),
        iv: iv.toString('base64'),
        authTag: cipher.getAuthTag().toString('base64'),
        version: 1,
        kdfVersion,
    };
}
/**
 * Decrypt a value using AES-256-GCM
 *
 * Automatically detects and uses the correct KDF version from the envelope.
 * This ensures backward compatibility with data encrypted using the legacy SHA-256 method.
 */
export function decrypt(envelope) {
    if (envelope.version !== 1) {
        throw new EncryptionError(`Unsupported encryption version: ${envelope.version}`, { version: envelope.version });
    }
    // Use the KDF version from the envelope, defaulting to v1 for backward compatibility
    // with data encrypted before kdfVersion was added to envelopes
    const kdfVersion = envelope.kdfVersion ?? 1;
    const key = deriveKey(kdfVersion);
    const iv = Buffer.from(envelope.iv, 'base64');
    const authTag = Buffer.from(envelope.authTag, 'base64');
    const ciphertext = Buffer.from(envelope.ciphertext, 'base64');
    const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
    decipher.setAuthTag(authTag);
    const decrypted = Buffer.concat([
        decipher.update(ciphertext),
        decipher.final(),
    ]);
    return decrypted.toString('utf8');
}
/**
 * Encrypt a JSON object, returning an encrypted field marker
 */
export function encryptObject(data) {
    const plaintext = JSON.stringify(data);
    return {
        __encrypted: true,
        envelope: encrypt(plaintext),
    };
}
/**
 * Decrypt an encrypted field marker back to a JSON object
 */
export function decryptObject(field) {
    if (!field.__encrypted || !field.envelope) {
        throw new EncryptionError('Invalid encrypted field format', { hasEncryptedFlag: !!field.__encrypted, hasEnvelope: !!field.envelope });
    }
    const plaintext = decrypt(field.envelope);
    return JSON.parse(plaintext);
}
/**
 * Check if a value is an encrypted field
 */
export function isEncryptedField(value) {
    return (typeof value === 'object' &&
        value !== null &&
        '__encrypted' in value &&
        value.__encrypted === true &&
        'envelope' in value);
}
/**
 * Compute SHA-256 hash for tamper detection
 */
export function computeHash(data) {
    return createHash('sha256').update(data).digest('hex');
}
/**
 * Compute chained hash (includes previous hash for chain integrity)
 */
export function computeChainedHash(data, previousHash) {
    return createHash('sha256')
        .update(previousHash)
        .update(data)
        .digest('hex');
}
// ============================================================================
// Migration Utilities
// ============================================================================
/**
 * Check if an envelope needs migration to the current KDF version
 *
 * @param envelope - The encrypted envelope to check
 * @returns true if the envelope uses an older KDF version than current config
 */
export function needsMigration(envelope) {
    const currentVersion = getCurrentKdfVersion();
    const envelopeVersion = envelope.kdfVersion ?? 1;
    return envelopeVersion < currentVersion;
}
/**
 * Migrate an encrypted envelope to the current KDF version
 *
 * This decrypts the data using its original KDF and re-encrypts with the current KDF.
 * Use this during a migration window to upgrade legacy encrypted data.
 *
 * @param envelope - The encrypted envelope to migrate
 * @returns New envelope encrypted with current KDF version
 * @throws EncryptionError if decryption or re-encryption fails
 */
export function migrateEnvelope(envelope) {
    if (!needsMigration(envelope)) {
        return envelope; // Already at current version
    }
    // Decrypt with original KDF version (handled automatically by decrypt)
    const plaintext = decrypt(envelope);
    // Re-encrypt with current KDF version
    return encrypt(plaintext);
}
/**
 * Migrate an encrypted field to the current KDF version
 *
 * @param field - The encrypted field to migrate
 * @returns New field with envelope encrypted using current KDF version
 */
export function migrateEncryptedField(field) {
    if (!needsMigration(field.envelope)) {
        return field;
    }
    return {
        __encrypted: true,
        envelope: migrateEnvelope(field.envelope),
    };
}
/**
 * Get the KDF version used in an envelope
 *
 * @param envelope - The envelope to check
 * @returns The KDF version (1 for legacy, 2 for PBKDF2)
 */
export function getEnvelopeKdfVersion(envelope) {
    return envelope.kdfVersion ?? 1;
}
/**
 * Create an encryptIfEnabled function with injected configuration.
 *
 * This factory function removes the service locator anti-pattern by
 * accepting configuration via parameter rather than calling getConfig().
 *
 * NOTE: This version does not include tracing to avoid circular dependencies.
 * Use the traceEncryptSync wrapper from intent/tracing.js when tracing is needed.
 *
 * @param config - Application configuration (for encryption settings)
 * @returns Function that encrypts data if encryption is enabled in config
 *
 * @example
 * // With dependency injection
 * const encryptIfEnabled = createEncryptIfEnabled(config);
 * const encrypted = encryptIfEnabled(sensitiveData);
 *
 * @example
 * // For testing
 * const mockConfig = { intent: { encryptContext: false } } as Config;
 * const encryptIfEnabled = createEncryptIfEnabled(mockConfig);
 */
export function createEncryptIfEnabled(config) {
    return (data) => {
        if (config.intent.encryptContext) {
            return encryptObject(data);
        }
        return data;
    };
}
//# sourceMappingURL=encryption.js.map