UNPKG

@e280/authlocal

Version:

User-sovereign login system for everybody

74 lines (62 loc) 2.39 kB
import {Bytename, Hex, Thumbprint} from "@e280/stz" import {Identity} from "./types.js" import {deriveId, unpackKey} from "../crypto/crypto.js" import {validLabel} from "../../../common/utils/validation.js" /** serialize identities as seed text */ export async function seedPack(...identities: Identity[]) { const texts = await Promise.all(identities.map( async identity => JSON.stringify(identity.label) + (await dehydrate(identity.secret)) .split(" ") .map(s => `\n ${s}`) .join("") )) return texts.join("\n\n") } /** deserialize identities from seed text. returns an array of promises, one for each seed in the text. */ export function seedRecover(seedtext: string) { seedtext = seedtext.trim() const regex = /("[^"]*")([^"]+)/gm const matches = [...seedtext.matchAll(regex)] return matches.map( async([, labelstring, bytename]) => { const label = labelstring ? JSON.parse(labelstring) : "" const secret = await hydrate(bytename) const id = await deriveId(secret) return <Identity>{ id, secret, label: (label && validLabel(label)) ? label : Thumbprint.sigil.fromHex(id), } } ) } export class SeedError extends Error { name = this.constructor.name } export class SeedIncompleteError extends SeedError {} export class SeedChecksumError extends SeedError {} /** convert hex key to seedling (with a 2-byte checksum) */ async function dehydrate(secret: string) { const secretBytes = unpackKey(secret) const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", secretBytes)) const checksumBytes = hash.slice(0, 2) const seedBytes = new Uint8Array([...secretBytes, ...checksumBytes]) if (seedBytes.length !== 34) throw new SeedIncompleteError("seed must be 34 bytes") return Bytename.fromBytes(seedBytes) } /** convert seed to hex key (with checksum validation) */ async function hydrate(seedling: string) { const bytes = Bytename.toBytes(seedling) if (bytes.length !== 34) throw new SeedIncompleteError("seed must be 34 bytes") const secretBytes = bytes.slice(0, 32) const checksumBytes = bytes.slice(32, 34) const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", secretBytes)) const invalidChecksum = Hex.string(hash.slice(0, 2)) !== Hex.string(checksumBytes) if (invalidChecksum) throw new SeedChecksumError("invalid seed checksum") return Hex.string(secretBytes) }