alotta good shit

This commit is contained in:
2026-09-19 06:20:37 +02:00
parent 74f7884525
commit 1477d56658
45 changed files with 4363 additions and 2069 deletions
+30 -108
View File
@@ -3,89 +3,6 @@ import db from './sql.mjs';
import lib from './lib.mjs';
import cfg from './config.mjs';
const SPKI_ED25519_HEADER = Buffer.from('302a300506032b6570032100', 'hex');
/**
* Parse an OpenSSH formatted Ed25519 public key.
* Format: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... [comment]"
* @param {string} sshKey
* @returns {{ keyObject: crypto.KeyObject, rawPub: Buffer, wirePub: Buffer, fingerprint: string, shortFingerprint: string }}
*/
export function parseOpenSshPubkey(sshKey) {
if (!sshKey || typeof sshKey !== 'string') {
throw new Error('Missing or invalid SSH public key');
}
const parts = sshKey.trim().split(/\s+/);
if (parts.length < 2 || parts[0] !== 'ssh-ed25519') {
throw new Error('Only ssh-ed25519 keys are supported');
}
const wirePub = Buffer.from(parts[1], 'base64');
if (wirePub.length < 19) {
throw new Error('Invalid OpenSSH public key wire payload');
}
const typeLen = wirePub.readUInt32BE(0);
if (typeLen !== 11) {
throw new Error('Invalid key type length in OpenSSH wire format');
}
const type = wirePub.subarray(4, 4 + typeLen).toString('utf8');
if (type !== 'ssh-ed25519') {
throw new Error(`Expected ssh-ed25519, got ${type}`);
}
const keyLenOffset = 4 + typeLen;
const keyLen = wirePub.readUInt32BE(keyLenOffset);
if (keyLen !== 32) {
throw new Error(`Invalid Ed25519 key length: expected 32, got ${keyLen}`);
}
const rawPub = wirePub.subarray(keyLenOffset + 4, keyLenOffset + 4 + keyLen);
if (rawPub.length !== 32) {
throw new Error('Malformed Ed25519 raw public key');
}
// Construct standard SPKI DER for crypto.createPublicKey
const der = Buffer.concat([SPKI_ED25519_HEADER, rawPub]);
const keyObject = crypto.createPublicKey({ key: der, format: 'der', type: 'spki' });
// Standard OpenSSH SHA256 fingerprint: SHA256:<base64-without-padding>
const fingerprint = 'SHA256:' + crypto.createHash('sha256').update(wirePub).digest('base64').replace(/=+$/, '');
const shortFingerprint = fingerprint.slice(7, 15);
return { keyObject, rawPub, wirePub, fingerprint, shortFingerprint };
}
/**
* Verify an Ed25519 signature against an OpenSSH public key.
* @param {string} sshPubkey
* @param {string|Buffer} message
* @param {string} signature (hex or base64)
* @returns {boolean}
*/
export function verifySignature(sshPubkey, message, signature) {
try {
const { keyObject } = parseOpenSshPubkey(sshPubkey);
const msgBuf = Buffer.isBuffer(message) ? message : Buffer.from(message, 'utf8');
let sigBuf;
if (typeof signature === 'string') {
const isHex = /^[0-9a-fA-F]{128}$/.test(signature);
sigBuf = isHex ? Buffer.from(signature, 'hex') : Buffer.from(signature, 'base64');
} else if (Buffer.isBuffer(signature)) {
sigBuf = signature;
} else {
return false;
}
return crypto.verify(null, msgBuf, keyObject, sigBuf);
} catch (err) {
return false;
}
}
import security from './security.mjs';
import { getHashUserIps } from './settings.mjs';
@@ -127,35 +44,40 @@ export async function logAnonActivity(req, { action, targetId = null, details =
}
/**
* Find or create a shadow user in the database for an anonymous SSH identity.
* @param {string} pubkey
* @param {string} fingerprint
* Find or create a shadow user in the database for a passkey-authenticated anonymous identity.
*
* @param {string} credentialId - base64url WebAuthn credential ID
* @param {object} [req]
* @param {string} [hwFingerprint]
* @returns {Promise<{ userId: number, isNew: boolean }>}
* @returns {Promise<{ userId: number, isNew: boolean, fingerprint: string }>}
*/
export async function getOrCreateAnonUser(pubkey, fingerprint, req = null, hwFingerprint = null) {
const normPubkey = pubkey.trim();
export async function getOrCreateAnonUserByCredential(credentialId, req = null, hwFingerprint = null) {
const auditIp = req ? resolveAuditIP(req) : null;
// Derive a stable "fingerprint" from the credential ID (for ban checks / display)
const fpBytes = crypto.createHash('sha256').update(Buffer.from(credentialId)).digest();
const fingerprint = 'SHA256:' + fpBytes.toString('base64').replace(/=+$/, '');
// Check if we already have a row for this credential
const existing = await db`
SELECT user_id FROM anon_identities
WHERE pubkey = ${normPubkey}
SELECT user_id FROM anon_identities
WHERE credential_id = ${credentialId}
LIMIT 1
`;
if (existing.length > 0) {
await db`
UPDATE anon_identities
UPDATE anon_identities
SET last_seen = NOW()
${auditIp ? db`, last_ip = ${auditIp}` : db``}
${hwFingerprint ? db`, hw_fingerprint = ${hwFingerprint}` : db``}
WHERE pubkey = ${normPubkey}
`;
return { userId: existing[0].user_id, isNew: false };
WHERE credential_id = ${credentialId}
`.catch(() => {});
return { userId: existing[0].user_id, isNew: false, fingerprint };
}
// Generate unique shadow username
const shortHash = crypto.createHash('sha256').update(fingerprint).digest('hex').slice(0, 8);
// Generate unique shadow username based on fingerprint short hash
const shortHash = fpBytes.toString('hex').slice(0, 8);
let baseLogin = `anon_${shortHash}`;
let finalLogin = baseLogin;
let counter = 1;
@@ -180,28 +102,28 @@ export async function getOrCreateAnonUser(pubkey, fingerprint, req = null, hwFin
`;
await db`
INSERT INTO anon_identities (user_id, pubkey, fingerprint, created_ip, last_ip, hw_fingerprint)
VALUES (${userId}, ${normPubkey}, ${fingerprint}, ${auditIp}, ${auditIp}, ${hwFingerprint})
ON CONFLICT (pubkey) DO UPDATE
INSERT INTO anon_identities (user_id, credential_id, fingerprint, created_ip, last_ip, hw_fingerprint)
VALUES (${userId}, ${credentialId}, ${fingerprint}, ${auditIp}, ${auditIp}, ${hwFingerprint})
ON CONFLICT (credential_id) DO UPDATE
SET last_seen = NOW()
${auditIp ? db`, last_ip = ${auditIp}` : db``}
${hwFingerprint ? db`, hw_fingerprint = ${hwFingerprint}` : db``}
`;
return { userId, isNew: true };
return { userId, isNew: true, fingerprint };
}
/**
* Create a valid session in user_sessions for this anonymous user.
* @param {number} userId
* @param {object} req
* Create a valid session in user_sessions for an anonymous user.
* @param {number} userId
* @param {object} req
* @param {string} [hwFingerprint]
* @returns {Promise<{ session: string, csrf_token: string }>}
*/
export async function createAnonSession(userId, req, hwFingerprint = null) {
const auditIp = resolveAuditIP(req);
// Update anon_identities last_ip, created_ip, and hw_fingerprint
// Update anon_identities last_ip and hw_fingerprint
await db`
UPDATE anon_identities
SET last_ip = ${auditIp},
@@ -210,13 +132,13 @@ export async function createAnonSession(userId, req, hwFingerprint = null) {
WHERE user_id = ${userId}
`.catch(() => {});
// 1. If req.session is already active for this exact userId, reuse it!
// If req.session is already active for this exact userId, reuse it
if (req?.session && req.session.id === userId && req.session.csrf_token && req.cookies?.session) {
await logAnonActivity(req, { action: 'handshake', hwFingerprint });
return { session: req.cookies.session, csrf_token: req.session.csrf_token };
}
// 2. If client has a session cookie that maps to this userId in DB, reuse it!
// If client has a session cookie that maps to this userId in DB, reuse it
if (req?.cookies?.session) {
const existingHash = lib.sha256(req.cookies.session);
const existing = await db`
@@ -245,7 +167,7 @@ export async function createAnonSession(userId, req, hwFingerprint = null) {
browser: ua,
created_at: stamp,
last_used: stamp,
last_action: '/anon/session',
last_action: '/anon/passkey/auth',
kmsi: 1,
ip: ip
};