pithos/hosted/theseus-id/index.js
Local Dev 1673e8f018 pithos-hosted: link a Theseus ID; a console session that cannot read data
A hosted account is its Ed25519 storage key, and that stays so: the key is
what owns and decrypts the data. A Theseus ID is added as a way to look at
the account (usage, sign-up, sign-out) from a browser that does not hold the
key, which is the account-level login DESIGN-theseus-id.md §11.3 asks for.

The owner links an ID with a normal session plus a Theseus ID sign-in for
project "pithos" (POST /hosted/v1/theseus-id). The ID then signs in to
POST /hosted/v1/console/session and gets a "console" token, which reaches
/hosted/v1/me, its own sign-out and the link status, and is refused
everything else: files, rules, moves and the forwarded owner API. Sessions
stored before scopes existed read as owner sessions. A move to a new ID in
Theseus carries the account along.

The verifier is TheseusID/lib's message.mjs and verify.mjs, copied verbatim
into hosted/theseus-id/ with libauth doing the curve maths, so the gateway
adds no dependency; a test fails if the copy drifts.
2026-10-06 20:34:52 +02:00

58 lines
2.6 KiB
JavaScript

// Theseus ID for the gateway: checks sign-ins for project "pithos"
// (DESIGN-theseus-id.md §4.3, §11.3).
//
// message.mjs and verify.mjs are verbatim copies of TheseusID/lib, so the
// gateway agrees with Theseus and the login service byte for byte; a test
// fails if they drift. The curve maths is libauth's, which the gateway
// already carries for the sign-up gate, instead of the noble packages the
// reference library is usually given.
import { sha256, hash160, secp256k1, utf8ToBin, base64ToBin, decodeCashAddress, binToHex } from '@bitauth/libauth';
import { createVerifier } from './verify.mjs';
export const PROJECT = 'pithos';
// The registry entry's origins (TheseusID/registry/projects.json) plus the
// login service. Theseus fills Origin from what it saw, so a sign-in made
// anywhere else does not verify here.
export const ORIGINS = ['https://pithos.sia', 'https://s3.silentmode.st', 'theseus-addon://pithos'];
export const PROVIDER_ORIGIN = 'https://id.theseus.x';
function varInt(n) {
if (n < 0xfd) return [n];
if (n <= 0xffff) return [0xfd, n & 0xff, n >> 8];
return [0xfe, n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >>> 24) & 0xff];
}
// BIP-137 over the real varint (sign-in messages run past 252 bytes), then
// the recovered key's hash compared with the claimed address. Recovery alone
// proves nothing: any signature recovers to some key. Theseus ID keys are
// compressed, so only headers 31-34 are accepted.
export function verifyAddress(message, signatureB64, address) {
let sig;
try { sig = base64ToBin(String(signatureB64 || '')); } catch { return false; }
if (!(sig instanceof Uint8Array) || sig.length !== 65 || sig[0] < 31 || sig[0] > 34) return false;
const magic = utf8ToBin('Bitcoin Signed Message:\n');
const msg = utf8ToBin(String(message));
const digest = sha256.hash(sha256.hash(Uint8Array.from([...varInt(magic.length), ...magic, ...varInt(msg.length), ...msg])));
const pub = secp256k1.recoverPublicKeyCompressed(sig.slice(1), (sig[0] - 31) & 3, digest);
if (typeof pub === 'string') return false;
const want = decodeCashAddress(String(address || ''));
if (typeof want === 'string' || want.type !== 'p2pkh') return false;
return binToHex(want.payload) === binToHex(hash160(pub));
}
/**
* @param {(nonce: string) => boolean} consumeNonce true once for a nonce this
* gateway issued; called last, so a bad request cannot burn a good nonce.
*/
export function createPithosVerifier(consumeNonce, now = () => Date.now()) {
return createVerifier({
crypto: { verifyAddress },
projectId: PROJECT,
origins: ORIGINS,
providerOrigin: PROVIDER_ORIGIN,
consumeNonce,
now,
});
}