paste-free.openapps.xyz

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.
Owned app · rev 01fdb2b1f66e
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.
← All files
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
01fdb2b1f6 · 4 hours ago
Bounded source sync
fe5788c2d2 · 1 day ago
Bounded source sync
ac6f7dbc95 · 1 day ago
Bounded source sync
0a45926293 · 1 day ago