Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 6eca480d6447d873e2e5eed9
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
src/lib/crypto.ts
// Encryption in the browser, AES-GCM, two ways. With a random 256-bit key that lives only in the link's #fragment,
// the server stores base64(iv + ciphertext). With a password, the key comes from PBKDF2 and the server stores
// PASSWORD_PREFIX + base64(salt + iv + ciphertext). The plaintext is {t: title, l: language, c: content}.
import { PASSWORD_PREFIX } from "../../functions/lib/paste";
export interface Sealed { title: string; language: string; content: string }
function toBase64(bytes: Uint8Array): string {
let bin = "";
for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
return btoa(bin);
}
const fromBase64 = (s: string) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
const toUrl = (s: string) => s.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const fromUrl = (s: string) => s.replace(/-/g, "+").replace(/_/g, "/");
const pack = (data: Sealed) => new TextEncoder().encode(JSON.stringify({ t: data.title, l: data.language, c: data.content }));
function unpack(plain: ArrayBuffer): Sealed {
const env = JSON.parse(new TextDecoder().decode(plain));
return { title: String(env.t ?? ""), language: String(env.l ?? "text"), content: String(env.c ?? "") };
}
async function encrypt(key: CryptoKey, data: Sealed, head: Uint8Array): Promise<string> {
const iv = crypto.getRandomValues(new Uint8Array(12));
const cipher = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, pack(data)));
const out = new Uint8Array(head.length + iv.length + cipher.length);
out.set(head);
out.set(iv, head.length);
out.set(cipher, head.length + iv.length);
return toBase64(out);
}
async function importKey(secret: string) {
return crypto.subtle.importKey("raw", fromBase64(fromUrl(secret)), "AES-GCM", false, ["encrypt", "decrypt"]);
}
export function newSecret(): string {
return toUrl(toBase64(crypto.getRandomValues(new Uint8Array(32))));
}
export async function seal(data: Sealed, secret: string): Promise<string> {
return encrypt(await importKey(secret), data, new Uint8Array(0));
}
/** Throws when the key is wrong or the text was changed. */
export async function open(sealed: string, secret: string): Promise<Sealed> {
const bytes = fromBase64(sealed);
return unpack(await crypto.subtle.decrypt({ name: "AES-GCM", iv: bytes.subarray(0, 12) }, await importKey(secret), bytes.subarray(12)));
}
const PBKDF2_ROUNDS = 600000;
async function passwordKey(password: string, salt: Uint8Array) {
const base = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveKey"]);
return crypto.subtle.deriveKey({ name: "PBKDF2", salt, iterations: PBKDF2_ROUNDS, hash: "SHA-256" }, base, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
}
export async function sealWithPassword(data: Sealed, password: string): Promise<string> {
const salt = crypto.getRandomValues(new Uint8Array(16));
return PASSWORD_PREFIX + (await encrypt(await passwordKey(password, salt), data, salt));
}
/** Throws when the password is wrong. */
export async function openWithPassword(sealed: string, password: string): Promise<Sealed> {
const bytes = fromBase64(sealed.slice(PASSWORD_PREFIX.length));
const key = await passwordKey(password, bytes.subarray(0, 16));
return unpack(await crypto.subtle.decrypt({ name: "AES-GCM", iv: bytes.subarray(16, 28) }, key, bytes.subarray(28)));
}
Change history
Quality pass: password lock, year expiry, line links, text/plain API
Bounded source sync
Bounded source sync
Bounded source sync