Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

34 lines · 1.8 KBRaw History
1import { b64, fromB64 } from "./util.js";
2
3/**
4 * Envelope encryption for secrets. Each scope (repo or org) gets its own AES-GCM
5 * key derived from MASTER_KEY with HKDF, so a leaked ciphertext from one repo
6 * cannot be replayed into another.
7 */
8async function scopeKey(masterKeyB64: string, scope: string): Promise<CryptoKey> {
9 const raw = await crypto.subtle.importKey("raw", fromB64(masterKeyB64) as BufferSource, "HKDF", false, ["deriveKey"]);
10 return crypto.subtle.deriveKey(
11 { name: "HKDF", hash: "SHA-256", salt: new TextEncoder().encode("diffinery-secrets-v1"), info: new TextEncoder().encode(scope) },
12 raw,
13 { name: "AES-GCM", length: 256 },
14 false,
15 ["encrypt", "decrypt"],
16 );
17}
18
19export async function encryptSecret(masterKey: string | undefined, scope: string, plaintext: string): Promise<string> {
20 if (!masterKey) throw new Error("MASTER_KEY is not configured");
21 const iv = crypto.getRandomValues(new Uint8Array(12));
22 const key = await scopeKey(masterKey, scope);
23 const ct = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: new TextEncoder().encode(scope) }, key, new TextEncoder().encode(plaintext)));
24 return `v1.${b64(iv)}.${b64(ct)}`;
25}
26
27export async function decryptSecret(masterKey: string | undefined, scope: string, envelope: string): Promise<string> {
28 if (!masterKey) throw new Error("MASTER_KEY is not configured");
29 const [v, ivB, ctB] = envelope.split(".");
30 if (v !== "v1" || !ivB || !ctB) throw new Error("bad secret envelope");
31 const key = await scopeKey(masterKey, scope);
32 const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: fromB64(ivB) as BufferSource, additionalData: new TextEncoder().encode(scope) }, key, fromB64(ctB) as BufferSource);
33 return new TextDecoder().decode(pt);
34}