pithos/hosted/auth.js
Local Dev d00bd369b7 Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.

Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00

65 lines
2.5 KiB
JavaScript

// Sign-in: a person is an Ed25519 key (from their Theseus vault, or a key file
// on hosts without one). The gateway hands out a nonce, the client signs it,
// and gets a session token back. Only token hashes are stored.
import crypto from 'node:crypto';
import path from 'node:path';
import { readJson, writeJson } from './store.js';
const SPKI_ED25519 = Buffer.from('302a300506032b6570032100', 'hex');
const SESSION_DAYS = 90;
export const accountId = (pubkeyHex) => crypto.createHash('sha256').update(Buffer.from(pubkeyHex, 'hex')).digest('hex').slice(0, 32);
export function challengeText(publicUrl, nonce) {
return `pithos-hosted-v1\n${publicUrl}\n${nonce}`;
}
export function verifyEd25519(pubkeyHex, message, signatureB64) {
if (!/^[0-9a-f]{64}$/i.test(pubkeyHex || '')) return false;
try {
const key = crypto.createPublicKey({ key: Buffer.concat([SPKI_ED25519, Buffer.from(pubkeyHex, 'hex')]), format: 'der', type: 'spki' });
return crypto.verify(null, Buffer.from(message, 'utf8'), key, Buffer.from(String(signatureB64 || ''), 'base64'));
} catch { return false; }
}
export function createAuth(root) {
const file = path.join(root, 'sessions.json');
const nonces = new Map(); // nonce -> expiry
const sessions = readJson(file, {});
const hash = (t) => crypto.createHash('sha256').update(t).digest('hex');
return {
challenge() {
const now = Date.now();
for (const [n, exp] of nonces) if (exp < now) nonces.delete(n);
if (nonces.size > 10_000) throw Object.assign(new Error('too many sign-ins in progress, try again shortly'), { status: 503 });
const nonce = crypto.randomBytes(24).toString('base64url');
nonces.set(nonce, now + 5 * 60_000);
return nonce;
},
// Consumes the nonce whether or not the signature holds.
takeNonce(nonce) {
const exp = nonces.get(nonce);
nonces.delete(nonce);
return !!exp && exp > Date.now();
},
issue(id) {
const token = crypto.randomBytes(32).toString('base64url');
const now = Date.now();
for (const [h, s] of Object.entries(sessions)) if (s.exp < now) delete sessions[h];
sessions[hash(token)] = { id, exp: now + SESSION_DAYS * 86_400_000 };
writeJson(file, sessions);
return token;
},
lookup(token) {
const s = token && sessions[hash(token)];
return s && s.exp > Date.now() ? s.id : null;
},
revoke(token) {
delete sessions[hash(token)];
writeJson(file, sessions);
},
};
}