import * as crypto from 'crypto'; import { config } from '../config'; const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 16; const AUTH_TAG_LENGTH = 16; /** * Derive a 32-byte key from the encryption key string. * Uses SHA-256 so any length passphrase works. */ function deriveKey(): Buffer { return crypto.createHash('sha256').update(config.credentialEncryptionKey).digest(); } /** * Encrypt a plaintext password. * Returns base64 string: iv(16) + authTag(16) + ciphertext */ export function encryptPassword(plaintext: string): string { const key = deriveKey(); const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv(ALGORITHM, key, iv); const encrypted = Buffer.concat([ cipher.update(plaintext, 'utf8'), cipher.final(), ]); const authTag = cipher.getAuthTag(); // Format: iv + authTag + ciphertext const combined = Buffer.concat([iv, authTag, encrypted]); return combined.toString('base64'); } /** * Decrypt an encrypted password string. * Input is base64 string: iv(16) + authTag(16) + ciphertext */ export function decryptPassword(encrypted: string): string { const key = deriveKey(); const combined = Buffer.from(encrypted, 'base64'); const iv = combined.subarray(0, IV_LENGTH); const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH); const ciphertext = combined.subarray(IV_LENGTH + AUTH_TAG_LENGTH); const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(authTag); const decrypted = Buffer.concat([ decipher.update(ciphertext), decipher.final(), ]); return decrypted.toString('utf8'); } /** * Get the Turnitin password for an account. * If encryption key is set, tries to decrypt. Otherwise uses shared password. */ export function getAccountPassword(encryptedPassword?: string | null): string { if (encryptedPassword && config.credentialEncryptionKey) { try { return decryptPassword(encryptedPassword); } catch { // Fallback to shared password if decryption fails } } return config.turnitinSharedPassword; }