fix: support PHP encryption format for TOTP secrets

PHP uses base64(nonce+ciphertext+tag), TS was using hex:hex:hex.
decrypt() now auto-detects the format. encrypt() now outputs
PHP-compatible base64 format for cross-compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-03-23 11:42:32 +01:00
parent f40f9d2a4b
commit 1a62b31cd2

View File

@@ -10,18 +10,20 @@ export function encrypt(plaintext: string): string {
const iv = crypto.randomBytes(IV_LENGTH); const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv); const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex'); const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag(); const tag = cipher.getAuthTag();
return iv.toString('hex') + ':' + encrypted + ':' + tag.toString('hex'); // Use PHP-compatible format: base64(nonce + ciphertext + tag)
return Buffer.concat([iv, encrypted, tag]).toString('base64');
} }
export function decrypt(ciphertext: string): string { export function decrypt(ciphertext: string): string {
const key = Buffer.from(config.totp.encryptionKey, 'hex'); const key = Buffer.from(config.totp.encryptionKey, 'hex');
const parts = ciphertext.split(':');
if (parts.length !== 3) throw new Error('Invalid ciphertext format');
// Detect format: PHP uses base64(nonce+ciphertext+tag), TS uses hex:hex:hex
const parts = ciphertext.split(':');
if (parts.length === 3) {
// TS format: iv:encrypted:tag (hex)
const iv = Buffer.from(parts[0], 'hex'); const iv = Buffer.from(parts[0], 'hex');
const encrypted = parts[1]; const encrypted = parts[1];
const tag = Buffer.from(parts[2], 'hex'); const tag = Buffer.from(parts[2], 'hex');
@@ -32,4 +34,22 @@ export function decrypt(ciphertext: string): string {
let decrypted = decipher.update(encrypted, 'hex', 'utf8'); let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8'); decrypted += decipher.final('utf8');
return decrypted; return decrypted;
}
// PHP format: base64(nonce + ciphertext + tag)
const raw = Buffer.from(ciphertext, 'base64');
if (raw.length < IV_LENGTH + TAG_LENGTH + 1) {
throw new Error('Invalid ciphertext format');
}
const iv = raw.subarray(0, IV_LENGTH);
const tag = raw.subarray(raw.length - TAG_LENGTH);
const encrypted = raw.subarray(IV_LENGTH, raw.length - TAG_LENGTH);
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(tag);
let decrypted = decipher.update(encrypted);
const final = decipher.final();
return Buffer.concat([decrypted, final]).toString('utf8');
} }