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

102 lines (101 loc) 3.31 kB
import { base64nopad } from "@scure/base"; import { LineReader, flatten, prepend } from "./io.js"; /** * 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; } } async function parseNextStanza(hdr) { const argsLine = await hdr.readLine(); if (argsLine === null) { throw Error("invalid stanza"); } const args = argsLine.split(" "); if (args.length < 2 || args.shift() !== "->") { return { next: argsLine }; } for (const arg of args) { if (arg.length === 0) { throw Error("invalid stanza"); } } const bodyLines = []; for (;;) { const nextLine = await 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 = flatten(bodyLines); return { s: new Stanza(args, body) }; } export async function parseHeader(header) { const hdr = new LineReader(header); const versionLine = await hdr.readLine(); if (versionLine !== "age-encryption.org/v1") { throw Error("invalid version " + (versionLine ?? "line")); } const stanzas = []; for (;;) { const { s, next: macLine } = await parseNextStanza(hdr); if (s !== undefined) { stanzas.push(s); continue; } if (!macLine.startsWith("--- ")) { throw Error("invalid header"); } const MAC = base64nopad.decode(macLine.slice(4)); const { rest, transcript } = hdr.close(); const headerNoMAC = transcript.slice(0, transcript.length - 1 - macLine.length + 3); return { stanzas, headerNoMAC, MAC, headerSize: transcript.length, rest: prepend(header, 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 flatten([ encodeHeaderNoMAC(recipients), new TextEncoder().encode(" " + base64nopad.encode(MAC) + "\n") ]); }