'use strict'; const crypto = require('node:crypto'); const DEFAULT_SECRET_BYTES = 32; const DEFAULT_MAX_BODY_BYTES = 64 * 1024; const DEFAULT_BODY_DEADLINE_MS = 2_000; const DEFAULT_CHALLENGE_TTL_MS = 5_000; const DEFAULT_MAX_CHALLENGES = 64; const APT_EXTENSION_ID = 'onbkfpbggekakjddomjjnboippimlmch'; const APT_EXTENSION_ORIGIN = `chrome-extension://${APT_EXTENSION_ID}`; const SECRET_PATTERN = /^[A-Za-z0-9_-]{43}$/; const NONCE_PATTERN = SECRET_PATTERN; const PROOF_PATTERN = SECRET_PATTERN; const EXTENSION_ORIGIN_PATTERN = /^chrome-extension:\/\/[a-p]{32}$/; const SERVER_PROOF_DOMAIN = 'apt-server-challenge-v1'; const COOKIE_PROOF_DOMAIN = 'apt-cookie-request-v1'; class BridgeAuthError extends Error { constructor(code, message, statusCode = 400) { super(message); this.name = 'BridgeAuthError'; this.code = code; this.statusCode = statusCode; } } function dependencies(overrides = {}) { return { randomBytes: overrides.randomBytes || crypto.randomBytes, timingSafeEqual: overrides.timingSafeEqual || crypto.timingSafeEqual, createHmac: overrides.createHmac || crypto.createHmac, createHash: overrides.createHash || crypto.createHash, setTimeout: overrides.setTimeout || setTimeout, clearTimeout: overrides.clearTimeout || clearTimeout, now: overrides.now || Date.now }; } function generatePairingSecret(options = {}) { const deps = dependencies(options); const byteCount = options.byteCount || DEFAULT_SECRET_BYTES; if (!Number.isSafeInteger(byteCount) || byteCount < DEFAULT_SECRET_BYTES) { throw new BridgeAuthError('INVALID_SECRET_SIZE', 'Pairing secrets must contain at least 32 random bytes.'); } return deps.randomBytes(byteCount).toString('base64url'); } function assertPairingSecret(secret) { if (typeof secret !== 'string' || !SECRET_PATTERN.test(secret)) { throw new BridgeAuthError('INVALID_PAIRING_SECRET', 'The pairing secret has an invalid format.'); } } function canonicalServerProof(clientNonce, serverNonce) { return JSON.stringify([SERVER_PROOF_DOMAIN, clientNonce, serverNonce]); } function canonicalCookieProof({ clientNonce, serverNonce, deploymentUrl, cookieValue } = {}) { return JSON.stringify([COOKIE_PROOF_DOMAIN, clientNonce, serverNonce, deploymentUrl, cookieValue]); } function computeHmac(secret, message, options = {}) { assertPairingSecret(secret); return dependencies(options).createHmac('sha256', secret).update(message, 'utf8').digest('base64url'); } function computeServerProof(secret, clientNonce, serverNonce, options = {}) { return computeHmac(secret, canonicalServerProof(clientNonce, serverNonce), options); } function computeCookieProof(secret, request, options = {}) { return computeHmac(secret, canonicalCookieProof(request), options); } function isValidEnvelope(envelope) { return Boolean( envelope && envelope.version === 2 && envelope.algorithm === 'electron-safe-storage' && typeof envelope.ciphertext === 'string' && /^[A-Za-z0-9+/]+={0,2}$/.test(envelope.ciphertext) && envelope.ciphertext.length >= 16 && envelope.ciphertext.length <= 8192 && typeof envelope.digest === 'string' && PROOF_PATTERN.test(envelope.digest) ); } function assertExtensionOrigin(origin) { if (typeof origin !== 'string' || !EXTENSION_ORIGIN_PATTERN.test(origin) || origin !== APT_EXTENSION_ORIGIN) { throw new BridgeAuthError('INVALID_EXTENSION_ORIGIN', 'The bridge only accepts the committed Alta Proxy Tool extension origin.'); } } function safeEqualText(left, right, deps) { if (typeof left !== 'string' || typeof right !== 'string') return false; const leftBytes = Buffer.from(left, 'utf8'); const rightBytes = Buffer.from(right, 'utf8'); return leftBytes.length === rightBytes.length && deps.timingSafeEqual(leftBytes, rightBytes); } class BridgeAuth { constructor({ expectedOrigin = APT_EXTENSION_ORIGIN, envelope = null, protect, unprotect, maxChallenges = DEFAULT_MAX_CHALLENGES, challengeTtlMs = DEFAULT_CHALLENGE_TTL_MS, ...injected } = {}) { assertExtensionOrigin(expectedOrigin); if (typeof protect !== 'function' || typeof unprotect !== 'function') { throw new BridgeAuthError('SECURE_STORAGE_UNAVAILABLE', 'Secure pairing storage is unavailable.', 503); } if (!Number.isSafeInteger(maxChallenges) || maxChallenges < 1 || maxChallenges > 1024) { throw new BridgeAuthError('INVALID_CHALLENGE_LIMIT', 'Challenge limit is invalid.'); } if (!Number.isSafeInteger(challengeTtlMs) || challengeTtlMs < 1 || challengeTtlMs > 60_000) { throw new BridgeAuthError('INVALID_CHALLENGE_TTL', 'Challenge lifetime is invalid.'); } if (envelope !== null && !isValidEnvelope(envelope)) { throw new BridgeAuthError('INVALID_SECRET_ENVELOPE', 'The persisted pairing envelope is invalid.'); } this.expectedOrigin = expectedOrigin; this._protect = protect; this._unprotect = unprotect; this._maxChallenges = maxChallenges; this._challengeTtlMs = challengeTtlMs; this._dependencies = dependencies(injected); this._challenges = new Map(); this._secret = null; this._envelope = envelope ? Object.freeze({ ...envelope }) : null; if (envelope) { try { const secret = unprotect(Buffer.from(envelope.ciphertext, 'base64')); assertPairingSecret(secret); const digest = this._dependencies.createHash('sha256').update(secret, 'utf8').digest('base64url'); if (!safeEqualText(digest, envelope.digest, this._dependencies)) throw new Error('digest mismatch'); this._secret = secret; } catch { throw new BridgeAuthError('INVALID_SECRET_ENVELOPE', 'The persisted pairing envelope could not be decrypted.'); } } } get envelope() { return this._envelope ? { ...this._envelope } : null; } rotate() { const secret = generatePairingSecret(this._dependencies); let protectedBytes; try { protectedBytes = this._protect(secret); } catch { throw new BridgeAuthError('SECURE_STORAGE_UNAVAILABLE', 'Secure pairing storage is unavailable.', 503); } if (!Buffer.isBuffer(protectedBytes) || protectedBytes.length === 0 || protectedBytes.length > 6144) { throw new BridgeAuthError('SECURE_STORAGE_UNAVAILABLE', 'Secure pairing storage returned invalid ciphertext.', 503); } this._secret = secret; this._challenges.clear(); this._envelope = Object.freeze({ version: 2, algorithm: 'electron-safe-storage', ciphertext: protectedBytes.toString('base64'), digest: this._dependencies.createHash('sha256').update(secret, 'utf8').digest('base64url') }); return { secret, envelope: this.envelope }; } revoke() { this._secret = null; this._envelope = null; this._challenges.clear(); } _purgeExpired(now) { for (const [nonce, challenge] of this._challenges) { if (challenge.expiresAt <= now) this._challenges.delete(nonce); } } issueChallenge(clientNonce) { if (!this._secret) throw new BridgeAuthError('PAIRING_UNAVAILABLE', 'Bridge pairing is unavailable.', 503); if (typeof clientNonce !== 'string' || !NONCE_PATTERN.test(clientNonce)) { throw new BridgeAuthError('INVALID_NONCE', 'Client nonce is invalid.'); } const now = this._dependencies.now(); this._purgeExpired(now); if (this._challenges.size >= this._maxChallenges) { throw new BridgeAuthError('CHALLENGE_CAPACITY', 'Too many bridge challenges are outstanding.', 429); } let serverNonce; do { serverNonce = this._dependencies.randomBytes(32).toString('base64url'); } while (this._challenges.has(serverNonce)); const expiresAt = now + this._challengeTtlMs; this._challenges.set(serverNonce, { clientNonce, expiresAt }); return { serverNonce, serverProof: computeServerProof(this._secret, clientNonce, serverNonce, this._dependencies) }; } authenticateCookie(request = {}) { const { clientNonce, serverNonce, deploymentUrl, cookieValue, proof } = request; if (!this._secret || !NONCE_PATTERN.test(typeof clientNonce === 'string' ? clientNonce : '') || !NONCE_PATTERN.test(typeof serverNonce === 'string' ? serverNonce : '') || !PROOF_PATTERN.test(typeof proof === 'string' ? proof : '') || typeof deploymentUrl !== 'string' || deploymentUrl.length < 1 || deploymentUrl.length > 2048 || typeof cookieValue !== 'string' || cookieValue.length < 1 || cookieValue.length > 4096) { return false; } const challenge = this._challenges.get(serverNonce); if (!challenge) return false; this._challenges.delete(serverNonce); if (challenge.expiresAt <= this._dependencies.now() || challenge.clientNonce !== clientNonce) return false; const expected = computeCookieProof(this._secret, request, this._dependencies); return safeEqualText(proof, expected, this._dependencies); } } function readJsonBody(stream, options = {}) { const maxBytes = options.maxBytes ?? DEFAULT_MAX_BODY_BYTES; const deadlineMs = options.deadlineMs ?? DEFAULT_BODY_DEADLINE_MS; const deps = dependencies(options); if (!stream || typeof stream.on !== 'function') return Promise.reject(new BridgeAuthError('INVALID_BODY_STREAM', 'A readable request body is required.')); if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) return Promise.reject(new BridgeAuthError('INVALID_BODY_LIMIT', 'Body limit must be a positive integer.')); if (!Number.isSafeInteger(deadlineMs) || deadlineMs < 1) return Promise.reject(new BridgeAuthError('INVALID_BODY_DEADLINE', 'Body deadline must be a positive integer.')); return new Promise((resolve, reject) => { const chunks = []; let total = 0; let settled = false; const cleanup = () => { deps.clearTimeout(timer); stream.removeListener('data', onData); stream.removeListener('end', onEnd); stream.removeListener('error', onError); }; const fail = (error) => { if (settled) return; settled = true; cleanup(); if (typeof stream.pause === 'function') stream.pause(); reject(error); }; const onData = (chunk) => { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); total += buffer.length; if (total > maxBytes) return fail(new BridgeAuthError('BODY_TOO_LARGE', 'Request body exceeds the configured limit.', 413)); chunks.push(buffer); }; const onError = () => fail(new BridgeAuthError('BODY_READ_ERROR', 'Could not read the request body.')); const onEnd = () => { if (settled) return; settled = true; cleanup(); let parsed; try { parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { reject(new BridgeAuthError('MALFORMED_JSON', 'Request body must be valid JSON.')); return; } if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { reject(new BridgeAuthError('INVALID_JSON_BODY', 'Request body must be a JSON object.')); return; } resolve(parsed); }; const timer = deps.setTimeout(() => fail(new BridgeAuthError('BODY_DEADLINE_EXCEEDED', 'Request body deadline exceeded.', 408)), deadlineMs); stream.on('data', onData); stream.on('end', onEnd); stream.on('error', onError); }); } function createRequestLimiter({ maxConcurrent = 4 } = {}) { if (!Number.isSafeInteger(maxConcurrent) || maxConcurrent < 1) throw new BridgeAuthError('INVALID_CONCURRENCY_LIMIT', 'Concurrency limit must be a positive integer.'); let active = 0; return { get active() { return active; }, async run(work) { if (typeof work !== 'function') throw new BridgeAuthError('INVALID_REQUEST_WORK', 'Request work must be a function.'); if (active >= maxConcurrent) throw new BridgeAuthError('TOO_MANY_REQUESTS', 'Too many bridge requests are active.', 429); active += 1; try { return await work(); } finally { active -= 1; } } }; } module.exports = { APT_EXTENSION_ID, APT_EXTENSION_ORIGIN, BridgeAuth, BridgeAuthError, COOKIE_PROOF_DOMAIN, DEFAULT_BODY_DEADLINE_MS, DEFAULT_CHALLENGE_TTL_MS, DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_CHALLENGES, EXTENSION_ORIGIN_PATTERN, NONCE_PATTERN, SECRET_PATTERN, SERVER_PROOF_DOMAIN, canonicalCookieProof, canonicalServerProof, computeCookieProof, computeServerProof, createRequestLimiter, generatePairingSecret, isValidEnvelope, readJsonBody };