Files
pieced-portal/src/lib/crypto.ts
2026-04-11 17:21:52 +02:00

72 lines
2.0 KiB
TypeScript

/**
* AES-256-GCM encryption for tenant package credentials.
*
* Credentials are encrypted before storage in tenant_requests.encrypted_secrets
* and decrypted only during admin approval to write to OpenBao tenant paths.
*
* Format: [12-byte IV][ciphertext][16-byte auth tag] as a single Buffer.
*
* Provision the key:
* bao kv put pieced/portal/encryption-key key="$(openssl rand -hex 32)"
*/
import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
const ALGORITHM = "aes-256-gcm";
const IV_LENGTH = 12;
const TAG_LENGTH = 16;
let cachedKey: Buffer | null = null;
async function getEncryptionKey(): Promise<Buffer> {
if (cachedKey) return cachedKey;
const { readSecret } = await import("./openbao");
const data = await readSecret("pieced/portal/encryption-key");
const hex = data?.key;
if (!hex || typeof hex !== "string" || hex.length !== 64) {
throw new Error(
"Invalid encryption key at secret/data/pieced/portal/encryption-key"
);
}
cachedKey = Buffer.from(hex, "hex");
return cachedKey;
}
export async function encryptSecrets(
secrets: Record<string, Record<string, string>>
): Promise<Buffer> {
const key = await getEncryptionKey();
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);
const plaintext = JSON.stringify(secrets);
const encrypted = Buffer.concat([
cipher.update(plaintext, "utf8"),
cipher.final(),
]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, encrypted, tag]);
}
export async function decryptSecrets(
blob: Buffer
): Promise<Record<string, Record<string, string>>> {
const key = await getEncryptionKey();
const iv = blob.subarray(0, IV_LENGTH);
const tag = blob.subarray(blob.length - TAG_LENGTH);
const ciphertext = blob.subarray(IV_LENGTH, blob.length - TAG_LENGTH);
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(tag);
const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
return JSON.parse(decrypted.toString("utf8"));
}