#!/usr/bin/env node // Node built-ins only. Pinned to docs/contract.md, "Proof and source session". import { createPrivateKey, createPublicKey, sign } from "node:crypto" import { readFileSync } from "node:fs" const base64 = (value) => Buffer.from(value, "utf8").toString("base64") try { const [requestFile, challengeFile, keyOption, keyName, ...extra] = process.argv.slice(2) if ( !requestFile || !challengeFile || !keyName || extra.length || !["--key-file", "--key-env"].includes(keyOption) ) throw new Error("usage") const request = JSON.parse(readFileSync(requestFile, "utf8")) const envelope = JSON.parse(readFileSync(challengeFile, "utf8")) const challenge = envelope.data const hex = /^[0-9a-f]{64}$/ if ( envelope.code !== null || !challenge || Object.keys(request).some( (key) => !["public_key", "username", "legal_acceptance_version"].includes(key), ) || !hex.test(request.public_key) || !hex.test(challenge.challenge) || typeof request.legal_acceptance_version !== "string" || !request.legal_acceptance_version || (request.username !== undefined && typeof request.username !== "string") || challenge.issuer !== "https://api.uno.fun" || challenge.audience !== "https://mpp.uno.fun" || challenge.kit_audience !== "https://kit.uno.fun" || !(typeof challenge.expires_at === "string" ? /^(0|[1-9][0-9]*)$/.test(challenge.expires_at) : Number.isSafeInteger(challenge.expires_at) && challenge.expires_at >= 0) || typeof challenge.signing_bytes !== "string" ) throw new Error("inputs") const bytes = Buffer.from(challenge.signing_bytes, "base64") if (bytes.toString("base64") !== challenge.signing_bytes) throw new Error("encoding") const expected = [ "uno-account-key-kit-session", "https://api.uno.fun", "https://mpp.uno.fun", "https://kit.uno.fun", challenge.challenge, request.public_key, base64(request.username ?? ""), base64(request.legal_acceptance_version), String(challenge.expires_at), ] const lines = new TextDecoder("utf-8", { fatal: true }) .decode(bytes) .split("\n") if ( lines.length !== 9 || lines.some((line, index) => line !== expected[index]) || !bytes.equals(Buffer.from(expected.join("\n"), "utf8")) ) throw new Error("statement") // Read the caller-named seed only after checking every statement line. const seed = ( keyOption === "--key-file" ? readFileSync(keyName, "utf8") : process.env[keyName] )?.trim() if (!seed || !hex.test(seed)) throw new Error("key") const key = createPrivateKey({ key: Buffer.concat([ Buffer.from("302e020100300506032b657004220420", "hex"), Buffer.from(seed, "hex"), ]), format: "der", type: "pkcs8", }) const publicKey = createPublicKey(key).export({ format: "der", type: "spki" }) if (publicKey.subarray(-32).toString("hex") !== request.public_key) throw new Error("key mismatch") process.stdout.write( `${JSON.stringify({ ...request, challenge: challenge.challenge, expires_at: challenge.expires_at, signature: sign(null, bytes, key).toString("base64"), })}\n`, ) } catch { // Never echo input, paths, environment values, or key parsing errors. process.stderr.write( "Refused to sign. Check the request, challenge, pinned contract, and key source.\n" + "Usage: node sign-session.mjs request.json challenge.json --key-file PATH | --key-env NAME\n", ) process.exitCode = 1 }