// Isolated S00-019 copy of the reviewed S00-017 ordering guard. import {registerSha512} from './sha512-mechanism.mjs'; // No production transport, credential persistence, or channel binding is provided. const SUPPORTED = new Set(['SCRAM-SHA-512']); const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; function text(value) { // SASL's parsed additional-data is a Buffer, not raw XML/base64 text. if (!value || typeof value.toString !== 'function' || value.length > 4096) { throw new Error('Invalid SASL data'); } const result = value.toString('utf8'); if (!/^[\x21-\x7e]*$/.test(result)) throw new Error('Invalid SASL data'); return result; } function firstMessage(value, nonce) { // Deliberately narrow, no extensions. Bound work before calling library crypto. const match = /^r=([\x21-\x2b\x2d-\x7e]+),s=([A-Za-z0-9+/=]+),i=([1-9][0-9]*)$/.exec(text(value)); if (!match || !BASE64.test(match[2]) || !match[1].startsWith(nonce) || match[1].length <= nonce.length || Number(match[3]) > 100000) { throw new Error('Invalid SCRAM server-first'); } } function finalMessage(value, name) { const final = text(value); if (name !== 'SCRAM-SHA-512') throw new Error('Unsupported mechanism'); const length = 88; if (!final.startsWith('v=') || final.length !== length + 2 || !BASE64.test(final.slice(2))) { throw new Error('Invalid SCRAM server-final'); } } export default function validatedSasl(client) { registerSha512(client); let active; // Public plugin + named feature registration replaces the stock handler before use. client.registerFeature('sasl', 100, async (features, done) => { if (active) active(); const names = features.sasl.mechanisms.filter(name => SUPPORTED.has(name)); const mech = client.sasl.createMechanism(names); let phase = 'starting'; let credentials; let nonce; const cleanup = () => { client.off('sasl', handler); client.off('--reset-stream-features', reset); if (active === reset) active = undefined; }; const reset = () => { phase = 'closed'; cleanup(); client.features.negotiated.sasl = false; }; const fail = () => { if (phase === 'closed') return; phase = 'closed'; cleanup(); client.features.negotiated.sasl = false; if (client.transport) client.transport.authenticated = false; client.emit('auth:failed'); done('disconnect', 'SCRAM validation failed'); // Never log peer data/credentials. }; const handler = async sasl => { if (phase === 'closed') return; try { if (sasl.type === 'challenge' && phase === 'server-first') { phase = 'sending-proof'; // Reject duplicate challenges and racing success. firstMessage(sasl.value, nonce); mech.processChallenge(sasl.value); const response = mech.createResponse(credentials); await client.send('sasl', {type: 'response', value: response}); if (phase !== 'closed') phase = 'server-final'; return; } if (sasl.type !== 'success' || phase !== 'server-final') return fail(); finalMessage(sasl.value, mech.name); mech.processSuccess(sasl.value); const result = mech.finalize(); if (!result.authenticated || !result.mutuallyAuthenticated) return fail(); phase = 'closed'; cleanup(); client.features.negotiated.sasl = true; if (client.transport) client.transport.authenticated = true; client.emit('auth:success', client.config.credentials); done('restart'); } catch { fail(); } }; active = reset; client.features.negotiated.sasl = false; client.on('sasl', handler); client.once('--reset-stream-features', reset); try { if (!mech || !SUPPORTED.has(mech.name)) return fail(); credentials = await client.getCredentials(); if (phase === 'closed') return; // Restrict this experiment to non-PLUS; never invent TLS binding bytes. if (credentials.tlsUnique) return fail(); const response = mech.createResponse(credentials); nonce = /,r=([^,]+)$/.exec(response.toString('utf8'))?.[1]; if (!nonce) return fail(); await client.send('sasl', {type: 'auth', mechanism: mech.name, value: response}); if (phase !== 'closed') phase = 'server-first'; } catch { fail(); } }); }