/**
 * src/utils/crypto.ts
 * أدوات التشفير — Encryption utilities for tenant data security
 * Uses Node.js built-in crypto module — AES-256-GCM + SHA-256
 */
import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync, timingSafeEqual, } from 'node:crypto';
// ─── Constants ───────────────────────────────────────────────────────
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16; // 128 bits
const TAG_LENGTH = 16; // 128 bits
const KEY_LENGTH = 32; // 256 bits
const SALT_LENGTH = 16;
const ENCODING = 'base64';
const SEPARATOR = ':';
// ─── AES-256-GCM Encryption ─────────────────────────────────────────
/**
 * تشفير النص — Encrypt text using AES-256-GCM
 *
 * Output format: iv:authTag:ciphertext (all base64)
 *
 * @param text - Plain text to encrypt
 * @param key - Encryption key (32 bytes hex or raw string that gets derived)
 * @returns Encrypted string in format iv:tag:ciphertext
 */
export function encrypt(text, key) {
    const derivedKey = deriveKey(key);
    const iv = randomBytes(IV_LENGTH);
    const cipher = createCipheriv(ALGORITHM, derivedKey, iv, {
        authTagLength: TAG_LENGTH,
    });
    let encrypted = cipher.update(text, 'utf8', ENCODING);
    encrypted += cipher.final(ENCODING);
    const authTag = cipher.getAuthTag();
    return [
        iv.toString(ENCODING),
        authTag.toString(ENCODING),
        encrypted,
    ].join(SEPARATOR);
}
/**
 * فك التشفير — Decrypt AES-256-GCM encrypted text
 *
 * @param ciphertext - Encrypted string in format iv:tag:ciphertext
 * @param key - Same key used for encryption
 * @returns Decrypted plain text
 * @throws Error if decryption fails (wrong key or tampered data)
 */
export function decrypt(ciphertext, key) {
    const parts = ciphertext.split(SEPARATOR);
    if (parts.length !== 3) {
        throw new Error('Invalid ciphertext format — expected iv:tag:encrypted');
    }
    const [ivStr, tagStr, encryptedStr] = parts;
    const derivedKey = deriveKey(key);
    const iv = Buffer.from(ivStr, ENCODING);
    const authTag = Buffer.from(tagStr, ENCODING);
    const encrypted = Buffer.from(encryptedStr, ENCODING);
    const decipher = createDecipheriv(ALGORITHM, derivedKey, iv, {
        authTagLength: TAG_LENGTH,
    });
    decipher.setAuthTag(authTag);
    let decrypted = decipher.update(encrypted);
    decrypted = Buffer.concat([decrypted, decipher.final()]);
    return decrypted.toString('utf8');
}
// ─── Phone Hashing ───────────────────────────────────────────────────
/**
 * هاش رقم الهاتف — SHA-256 hash for phone number indexing
 * Normalizes phone number before hashing for consistency
 *
 * @param phone - Phone number (any format)
 * @returns Hex-encoded SHA-256 hash
 */
export function hashPhone(phone) {
    // Normalize: remove spaces, dashes, and ensure + prefix
    const normalized = phone.replace(/[\s\-()]/g, '').replace(/^00/, '+');
    return createHash('sha256').update(normalized).digest('hex');
}
// ─── Key Generation ──────────────────────────────────────────────────
/**
 * توليد مفتاح مستأجر — Generate a cryptographically secure per-tenant key
 * Returns a 64-character hex string (256 bits of entropy)
 */
export function generateTenantKey() {
    return randomBytes(KEY_LENGTH).toString('hex');
}
// ─── Key Derivation ──────────────────────────────────────────────────
/**
 * Derive a 256-bit key from a password/passphrase using scrypt.
 * If key is already 64 hex chars (32 bytes), use directly.
 */
function deriveKey(key) {
    // If the key is already 64 hex chars (32 bytes), use directly
    if (/^[0-9a-f]{64}$/i.test(key)) {
        return Buffer.from(key, 'hex');
    }
    // Use a per-installation random salt stored under <dataDir>/crypto.salt.
    // The previous deterministic-salt design (sha256 of a hardcoded string)
    // was a foot-gun: every install shared the same salt, eliminating its
    // anti-rainbow-table purpose. We still need salt determinism within an
    // install for encrypt/decrypt pairing — generating once at first use
    // and persisting gives us both. (Security audit MEDIUM-4.)
    const salt = getOrCreateSalt();
    return scryptSync(key, salt, KEY_LENGTH);
}
let _cachedSalt = null;
function getOrCreateSalt() {
    if (_cachedSalt)
        return _cachedSalt;
    // Lazy import to avoid circular imports at module load.
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    const { existsSync, readFileSync, writeFileSync, mkdirSync } = require('node:fs');
    const { join } = require('node:path');
    const { homedir, platform } = require('node:os');
    const { randomBytes } = require('node:crypto');
    const dataDir = process.env.MUASSIS_DATA_DIR ?? (platform() === 'darwin' ? join(homedir(), 'Library', 'Application Support', 'Muassis')
        : platform() === 'win32' ? join(process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'), 'Muassis')
            : join(process.env.XDG_DATA_HOME ?? join(homedir(), '.local', 'share'), 'muassis'));
    mkdirSync(dataDir, { recursive: true });
    const saltFile = join(dataDir, 'crypto.salt');
    if (existsSync(saltFile)) {
        _cachedSalt = readFileSync(saltFile);
    }
    else {
        _cachedSalt = randomBytes(SALT_LENGTH);
        writeFileSync(saltFile, _cachedSalt, { mode: 0o600 });
    }
    return _cachedSalt;
}
// ─── Utility ─────────────────────────────────────────────────────────
/**
 * Constant-time string comparison to prevent timing attacks
 */
export function secureCompare(a, b) {
    const bufA = Buffer.from(a);
    const bufB = Buffer.from(b);
    if (bufA.length !== bufB.length)
        return false;
    return timingSafeEqual(bufA, bufB);
}
//# sourceMappingURL=crypto.js.map