2026-02-14 00:59:57 +01:00
|
|
|
/**
|
|
|
|
|
* Token encoding/decoding for asset IDs.
|
|
|
|
|
* Obfuscates Immich UUIDs in public-facing URLs using AES encryption.
|
|
|
|
|
*
|
|
|
|
|
* Uses deterministic encryption (IV derived from asset ID) so the same
|
|
|
|
|
* asset always produces the same token — essential for browser caching.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import crypto from 'crypto';
|
|
|
|
|
import { getConfig } from './config';
|
|
|
|
|
|
|
|
|
|
let _key: Buffer | null = null;
|
|
|
|
|
|
|
|
|
|
function getKey(): Buffer {
|
2026-02-14 11:47:26 +01:00
|
|
|
if (!_key) {
|
2026-03-07 08:18:23 +01:00
|
|
|
const authSecret = getConfig().authSecret;
|
|
|
|
|
_key = crypto.createHash('sha256').update(authSecret).digest();
|
2026-02-14 11:47:26 +01:00
|
|
|
}
|
|
|
|
|
return _key;
|
2026-02-14 00:59:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Encode an asset ID into an opaque URL-safe token.
|
2026-03-15 07:32:42 +01:00
|
|
|
* Note: The AES-CBC deterministic IV causes the identical input asset ID
|
|
|
|
|
* to encrypt to the exact same cipher token. This provides URL obfuscation
|
|
|
|
|
* and caching consistency, but it does NOT provide k-anonymous cryptographic
|
|
|
|
|
* security guarantees against recognizing identical items if intercepted.
|
2026-02-14 00:59:57 +01:00
|
|
|
*/
|
|
|
|
|
export function encodeAssetId(assetId: string): string {
|
2026-02-14 11:47:26 +01:00
|
|
|
const key = getKey();
|
2026-03-08 16:26:05 +01:00
|
|
|
// Deterministic IV from asset ID (same input → same token).
|
|
|
|
|
// SHA-256 slice → first 16 bytes. MD5 was deprecated for cryptographic use.
|
|
|
|
|
const iv = crypto.createHash('sha256').update(assetId).digest().subarray(0, 16);
|
2026-02-14 11:47:26 +01:00
|
|
|
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
|
|
|
const encrypted = Buffer.concat([cipher.update(assetId, 'utf8'), cipher.final()]);
|
|
|
|
|
// Compact: base64url(iv + ciphertext)
|
|
|
|
|
return Buffer.concat([iv, encrypted]).toString('base64url');
|
2026-02-14 00:59:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Decode a token back into an asset ID.
|
|
|
|
|
* Returns null if the token is invalid.
|
|
|
|
|
*/
|
|
|
|
|
export function decodeAssetId(token: string): string | null {
|
2026-02-14 11:47:26 +01:00
|
|
|
try {
|
|
|
|
|
const key = getKey();
|
|
|
|
|
const data = Buffer.from(token, 'base64url');
|
|
|
|
|
if (data.length < 17) return null; // iv(16) + at least 1 byte
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
const iv = data.subarray(0, 16);
|
|
|
|
|
const encrypted = data.subarray(16);
|
|
|
|
|
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
|
|
|
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString(
|
|
|
|
|
'utf8',
|
|
|
|
|
);
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
// Validate it looks like a UUID
|
|
|
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
|
return uuidRegex.test(decrypted) ? decrypted : null;
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-02-14 00:59:57 +01:00
|
|
|
}
|