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.
58 lines
2.6 KiB
JavaScript
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,
|
|
});
|
|
}
|