// oncepad-send.mjs — create a OncePad secret from a script or CI job. Node 20+, no dependencies. // // curl -O https://www.eff.org/files/2016/09/08/eff_short_wordlist_2_0.txt // printf '%s' "$DB_PASSWORD" | node oncepad-send.mjs // // Prints the link on stdout and the passphrase on stderr: send them on two different channels. import { readFileSync } from "node:fs"; const BASE = "https://oncepad.com"; const WORDS = readFileSync("eff_short_wordlist_2_0.txt", "utf8") .trim() .split("\n") .map((line) => line.split("\t")[1]); // A uniform index in [0, n): rejection sampling, so no word is more likely than another. function randIndex(n) { const limit = 2 ** 32 - (2 ** 32 % n); const buf = new Uint32Array(1); do crypto.getRandomValues(buf); while (buf[0] >= limit); return buf[0] % n; } // Seven DISTINCT list words, lowercase, single spaces (~72 bits). The reveal page only accepts list // words, so a phrase built any other way could not be typed in. function passphrase(n = 7) { const words = new Set(); while (words.size < n) words.add(WORDS[randIndex(WORDS.length)]); return [...words].join(" "); } // Suite 0x02: blob = 0x02 | salt(16) | iv(12) | AES-256-GCM(ciphertext + tag), // key = PBKDF2-HMAC-SHA256(passphrase, salt, 600000), AAD = 0x02 0x02 | salt | iv. async function seal(plaintext, phrase) { const salt = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(12)); const material = await crypto.subtle.importKey("raw", new TextEncoder().encode(phrase), "PBKDF2", false, ["deriveKey"]); const key = await crypto.subtle.deriveKey( { name: "PBKDF2", hash: "SHA-256", salt, iterations: 600000 }, material, { name: "AES-GCM", length: 256 }, false, ["encrypt"], ); const aad = new Uint8Array([0x02, 0x02, ...salt, ...iv]); const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad }, key, plaintext); return new Uint8Array([0x02, ...salt, ...iv, ...new Uint8Array(ct)]); } const phrase = passphrase(); const blob = await seal(readFileSync(0), phrase); const res = await fetch(`${BASE}/api/secrets`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ blob: Buffer.from(blob).toString("base64url") }), }); if (!res.ok) throw new Error(`OncePad answered ${res.status}`); const { id } = await res.json(); console.log(`${BASE}/s/${id}`); console.error(`passphrase: ${phrase}`);