72 lines
2.0 KiB
TypeScript
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"));
|
|
}
|