import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;
const SALT_LENGTH = 32;
const KEY_LENGTH = 32;
/**
* Derives a 256-bit key from the encryption key using scrypt.
*/
function deriveKey(encryptionKey: string, salt: Buffer): Buffer {
return scryptSync(encryptionKey, salt, KEY_LENGTH);
}
/**
* Encrypts a plaintext string using AES-256-GCM.
* Returns a base64-encoded string containing: salt + iv + authTag + ciphertext
*/
export function encrypt(plaintext: string, encryptionKey: string): string {
const salt = randomBytes(SALT_LENGTH);
const key = deriveKey(encryptionKey, salt);
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
// Combine: salt + iv + authTag + ciphertext
const combined = Buffer.concat([salt, iv, authTag, encrypted]);
return combined.toString('base64');
}
/**
* Decrypts a base64-encoded ciphertext that was encrypted with encrypt().
*/
export function decrypt(ciphertext: string, encryptionKey: string): string {
const combined = Buffer.from(ciphertext, 'base64');
// Extract components
const salt = combined.subarray(0, SALT_LENGTH);
const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
const authTag = combined.subarray(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
const encrypted = combined.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
const key = deriveKey(encryptionKey, salt);
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]);
return decrypted.toString('utf8');
}
/**
* Generates a cryptographically secure random string for OAuth state.
* Returns a 256-bit random value as a base64url-encoded string.
*/
export function generateSecureState(): string {
const bytes = randomBytes(32);
return bytes.toString('base64url');
}
/**
* Generates a random encryption key suitable for TOKEN_ENCRYPTION_KEY.
* Returns a 32-character hex string (256 bits).
*/
export function generateEncryptionKey(): string {
return randomBytes(32).toString('hex');
}