UNPKG

age-encryption

Version:

<p align="center"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://github.com/FiloSottile/age/blob/main/logo/logo_white.svg"> <source media="(prefers-color-scheme: light)" srcset="https://github.com/FiloSottile/a

153 lines (152 loc) 4.47 kB
import { base64nopad } from "@scure/base"; /** * A stanza is a section of an age header. This is part of the low-level * {@link Recipient} and {@link Identity} APIs. */ export class Stanza { /** * All space-separated arguments on the first line of the stanza. * Each argument is a string that does not contain spaces. * The first argument is often a recipient type, which should look like * `example.com/...` to avoid collisions. */ args; /** * The raw body of the stanza. This is automatically base64-encoded and * split into lines of 48 characters each. */ body; constructor(args, body) { this.args = args; this.body = body; } } class ByteReader { arr; constructor(arr) { this.arr = arr; } toString(bytes) { bytes.forEach((b) => { if (b < 32 || b > 136) { throw Error("invalid non-ASCII byte in header"); } }); return new TextDecoder().decode(bytes); } readString(n) { const out = this.arr.subarray(0, n); this.arr = this.arr.subarray(n); return this.toString(out); } readLine() { const i = this.arr.indexOf("\n".charCodeAt(0)); if (i >= 0) { const out = this.arr.subarray(0, i); this.arr = this.arr.subarray(i + 1); return this.toString(out); } return null; } rest() { return this.arr; } } function parseNextStanza(header) { const hdr = new ByteReader(header); if (hdr.readString(3) !== "-> ") { throw Error("invalid stanza"); } const argsLine = hdr.readLine(); if (argsLine === null) { throw Error("invalid stanza"); } const args = argsLine.split(" "); if (args.length < 1) { throw Error("invalid stanza"); } for (const arg of args) { if (arg.length === 0) { throw Error("invalid stanza"); } } const bodyLines = []; for (;;) { const nextLine = hdr.readLine(); if (nextLine === null) { throw Error("invalid stanza"); } const line = base64nopad.decode(nextLine); if (line.length > 48) { throw Error("invalid stanza"); } bodyLines.push(line); if (line.length < 48) { break; } } const body = flattenArray(bodyLines); return [new Stanza(args, body), hdr.rest()]; } function flattenArray(arr) { const len = arr.reduce(((sum, line) => sum + line.length), 0); const out = new Uint8Array(len); let n = 0; for (const a of arr) { out.set(a, n); n += a.length; } return out; } export function parseHeader(header) { const hdr = new ByteReader(header); const versionLine = hdr.readLine(); if (versionLine !== "age-encryption.org/v1") { throw Error("invalid version " + (versionLine ?? "line")); } let rest = hdr.rest(); const stanzas = []; for (;;) { let s; [s, rest] = parseNextStanza(rest); stanzas.push(s); const hdr = new ByteReader(rest); if (hdr.readString(4) === "--- ") { const headerNoMAC = header.subarray(0, header.length - hdr.rest().length - 1); const macLine = hdr.readLine(); if (macLine === null) { throw Error("invalid header"); } const mac = base64nopad.decode(macLine); return { stanzas: stanzas, headerNoMAC: headerNoMAC, MAC: mac, rest: hdr.rest(), }; } } } export function encodeHeaderNoMAC(recipients) { const lines = []; lines.push("age-encryption.org/v1\n"); for (const s of recipients) { lines.push("-> " + s.args.join(" ") + "\n"); for (let i = 0; i < s.body.length; i += 48) { let end = i + 48; if (end > s.body.length) end = s.body.length; lines.push(base64nopad.encode(s.body.subarray(i, end)) + "\n"); } if (s.body.length % 48 === 0) lines.push("\n"); } lines.push("---"); return new TextEncoder().encode(lines.join("")); } export function encodeHeader(recipients, MAC) { return flattenArray([ encodeHeaderNoMAC(recipients), new TextEncoder().encode(" " + base64nopad.encode(MAC) + "\n") ]); }