S00-019: Validate browser SCRAM-SHA-512 through public mechanism composition
Changes: Add an isolated pinned mechanism wrapper using Stanza's public constructor and factory APIs, preserve the SCRAM-SHA-512 wire name and fail-closed final guard, and document the proposed ADR, browser bundle evidence, independent Scramp vectors, notices and limits. Leave parent selection, dependency experiments, specs and shared contracts unchanged. Validation: Fresh-cache npm ci and Vite browser build pass; 87 focused tests pass, including RFC HMAC vectors, original defect reproduction, independent full SHA-512 exchanges and rejected invalid finals/order/reset paths. Hashed Scramp oracle fixture comparison passes. Dependency check verifies 49 locked entries, 25 installed pins/licenses and 340 unmodified Stanza files. make check passes 72 tooling tests. Native browsers, WSS/live server interoperability and production readiness are untested; independent security review remains pending. Task: S00-019 Agent: auto-crypto-c68801170fa4 Role: crypto Action: claim Status: review Task title: Validate browser-compatible SCRAM-SHA-512 through a public mechanism API
This commit is contained in:
1 parent
e69372c6db
commit
07c9967d02
26 files changed
+6399
-8
No files matched your search
@@ -0,0 +1,83 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import {createHash as oracleHash, createHmac as oracleHmac} from 'node:crypto';
|
||||
import {Buffer as BrowserBuffer, createHash, createHmac, backend, StockScram, Sha512Mechanism, randomBytes} from '../dist/browser.mjs';
|
||||
const hmacVectors = JSON.parse(readFileSync(new URL('./hmac-vectors.json', import.meta.url)));
|
||||
const scramVectors = JSON.parse(readFileSync(new URL('./scramp-vectors.json', import.meta.url)));
|
||||
|
||||
test('bundle resolves browser backend and contains no external runtime imports', () => {
|
||||
assert.equal(backend, 'browser');
|
||||
const modules = JSON.parse(readFileSync(new URL('../dist/modules.json', import.meta.url)));
|
||||
assert.ok(modules.some(id => id.endsWith('/platform/browser/crypto/Hmac.js')));
|
||||
assert.ok(modules.some(id => id.endsWith('/platform/browser/crypto/SHA-512.js')));
|
||||
assert.ok(!modules.some(id => /platform\/node\/|^node:|browser-external/.test(id)));
|
||||
const bundle = readFileSync(new URL('../dist/browser.mjs', import.meta.url), 'utf8');
|
||||
assert.ok(!/^import\s|\brequire\(/m.test(bundle));
|
||||
});
|
||||
|
||||
for (const vector of hmacVectors) {
|
||||
for (const algorithm of ['sha512', 'SHA512']) {
|
||||
test(`RFC 4231 case ${vector.case}, browser ${algorithm}`, () => {
|
||||
const key = BrowserBuffer.from(vector.key, 'hex');
|
||||
const data = BrowserBuffer.from(vector.data, 'hex');
|
||||
const actual = createHmac(algorithm, key).update(data).digest('hex');
|
||||
assert.equal(actual.slice(0, vector.digest.length), vector.digest);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test('reproduce browser hyphenated HMAC mismatch, keep Node independent control', () => {
|
||||
const vector = hmacVectors[0];
|
||||
const key = BrowserBuffer.from(vector.key, 'hex');
|
||||
const data = BrowserBuffer.from(vector.data, 'hex');
|
||||
const broken = createHmac('sha-512', key).update(data).digest('hex');
|
||||
assert.notEqual(broken, vector.digest);
|
||||
assert.equal(broken, '9656975ee5de55e75f2976ecce9a04501060b9dc22a6eda2eaef638966280182477fe09f080b2bf564649cad42af8607a2bd8d02979df3a980f15e2326a0a22a');
|
||||
assert.equal(oracleHmac('sha512', Buffer.from(vector.key, 'hex')).update(Buffer.from(vector.data, 'hex')).digest('hex'), vector.digest);
|
||||
});
|
||||
|
||||
const abc = 'ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192' +
|
||||
'992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f';
|
||||
for (const algorithm of ['sha512', 'sha-512', 'SHA512', 'SHA-512']) {
|
||||
test(`fixed SHA512 abc digest, browser ${algorithm}`, () => {
|
||||
assert.equal(createHash(algorithm).update('abc').digest('hex'), abc);
|
||||
});
|
||||
}
|
||||
|
||||
for (const length of [0, 1, 63, 64, 65, 127, 128, 129, 131, 256]) {
|
||||
test(`browser HMAC alias key length ${length} agrees with Node crypto`, () => {
|
||||
const key = Buffer.alloc(length, 0xab);
|
||||
const data = Buffer.alloc(257, 0xcd);
|
||||
assert.equal(createHmac('sha512', BrowserBuffer.from(key)).update(BrowserBuffer.from(data)).digest('hex'),
|
||||
oracleHmac('sha512', key).update(data).digest('hex'));
|
||||
assert.equal(createHash('sha512').update(BrowserBuffer.from(data)).digest('hex'),
|
||||
oracleHash('sha512').update(data).digest('hex'));
|
||||
});
|
||||
}
|
||||
|
||||
test('stock browser SHA512 proof mismatch; public alias delegate matches independent vector', () => {
|
||||
const vector = scramVectors[0];
|
||||
const credentials = {username: vector.username, password: vector.password, clientNonce: vector.nonce};
|
||||
const stock = new StockScram('SCRAM-SHA-512');
|
||||
stock.createResponse(credentials);
|
||||
stock.processChallenge(BrowserBuffer.from(vector.first));
|
||||
assert.notEqual(stock.createResponse(credentials).toString(), vector.proof);
|
||||
const selected = new Sha512Mechanism('SCRAM-SHA-512');
|
||||
assert.equal(selected.name, vector.mechanism);
|
||||
assert.equal(selected.createResponse(credentials).toString(), vector.initial);
|
||||
selected.processChallenge(BrowserBuffer.from(vector.first));
|
||||
assert.equal(selected.createResponse(credentials).toString(), vector.proof);
|
||||
selected.processSuccess(BrowserBuffer.from(vector.final));
|
||||
assert.deepEqual(selected.finalize(), {authenticated: true, mutuallyAuthenticated: true});
|
||||
assert.equal(selected.getCacheableCredentials(), null);
|
||||
});
|
||||
|
||||
test('native browser RNG path works without fixed nonce, no invented channel binding', () => {
|
||||
assert.equal(randomBytes(32).length, 32);
|
||||
const selected = new Sha512Mechanism('SCRAM-SHA-512');
|
||||
const initial = selected.createResponse({username: 'user', password: 'pencil'}).toString();
|
||||
assert.match(initial, /^n,,n=user,r=[0-9a-f]{64}$/);
|
||||
assert.throws(() => new Sha512Mechanism('SCRAM-SHA-512-PLUS'));
|
||||
assert.throws(() => new Sha512Mechanism('SCRAM-SHA-512').createResponse({tlsUnique: BrowserBuffer.alloc(32)}));
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
[
|
||||
{
|
||||
"case": 1,
|
||||
"key": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
|
||||
"data": "4869205468657265",
|
||||
"digest": "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854"
|
||||
},
|
||||
{
|
||||
"case": 2,
|
||||
"key": "4a656665",
|
||||
"data": "7768617420646f2079612077616e7420666f72206e6f7468696e673f",
|
||||
"digest": "164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737"
|
||||
},
|
||||
{
|
||||
"case": 3,
|
||||
"key": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"data": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||
"digest": "fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb"
|
||||
},
|
||||
{
|
||||
"case": 4,
|
||||
"key": "0102030405060708090a0b0c0d0e0f10111213141516171819",
|
||||
"data": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
||||
"digest": "b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd"
|
||||
},
|
||||
{
|
||||
"case": 5,
|
||||
"key": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
|
||||
"data": "546573742057697468205472756e636174696f6e",
|
||||
"digest": "415fad6271580a531d4179bc891d87a6"
|
||||
},
|
||||
{
|
||||
"case": 6,
|
||||
"key": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"data": "54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a65204b6579202d2048617368204b6579204669727374",
|
||||
"digest": "80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598"
|
||||
},
|
||||
{
|
||||
"case": 7,
|
||||
"key": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"data": "5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e",
|
||||
"digest": "e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,302 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import {Client, parse, validatedSasl, StockScram} from '../dist/browser.mjs';
|
||||
const vectors = JSON.parse(readFileSync(new URL('./scramp-vectors.json', import.meta.url)));
|
||||
const tick = () => new Promise(resolve => setImmediate(resolve));
|
||||
const deferred = () => {
|
||||
let resolve;
|
||||
const promise = new Promise(r => {resolve = r;});
|
||||
return {promise, resolve};
|
||||
};
|
||||
|
||||
function harness(vector = vectors[1], fixed = true, mechanisms = [vector.mechanism]) {
|
||||
const client = new Client({jid: 'user@example.invalid', autoReconnect: false,
|
||||
credentials: {username: vector.username, password: vector.password, clientNonce: vector.nonce}});
|
||||
const result = {sent: [], success: 0, failed: 0, restart: 0, disconnect: 0, verification: []};
|
||||
// Capture fixture effects; use the real Client, XML registry, feature dispatcher,
|
||||
// mechanism factory and SCRAM crypto. Never connect to a real provider.
|
||||
client.send = async (kind, stanza) => {result.sent.push({kind, stanza});};
|
||||
client.transport = {authenticated: false, restart: () => {result.restart++;}};
|
||||
client.disconnect = async () => {result.disconnect++;};
|
||||
client.on('auth:success', () => {result.success++;});
|
||||
client.on('auth:failed', () => {result.failed++;});
|
||||
if (!fixed) client.sasl.register('SCRAM-SHA-512', StockScram, 1000);
|
||||
if (fixed) {
|
||||
client.use(validatedSasl);
|
||||
const original = client.sasl.createMechanism.bind(client.sasl);
|
||||
client.sasl.createMechanism = names => {
|
||||
const mech = original(names);
|
||||
if (mech) for (const method of ['processSuccess', 'finalize']) {
|
||||
const fn = mech[method].bind(mech);
|
||||
mech[method] = (...args) => {result.verification.push(method); return fn(...args);};
|
||||
}
|
||||
return mech;
|
||||
};
|
||||
}
|
||||
const receive = async (type, value) => {
|
||||
const body = value === undefined ? '' : Buffer.from(value).toString('base64');
|
||||
const xml = parse(`<${type} xmlns="urn:ietf:params:xml:ns:xmpp-sasl">${body}</${type}>`);
|
||||
const stanza = client.stanzas.import(xml);
|
||||
assert.equal(stanza.type, type);
|
||||
client.emit('sasl', stanza);
|
||||
await tick();
|
||||
};
|
||||
const start = async () => {
|
||||
client.emit('features', {sasl: {mechanisms}});
|
||||
await tick();
|
||||
};
|
||||
return {client, result, receive, start};
|
||||
}
|
||||
function rejected(h) {
|
||||
assert.equal(h.result.success, 0);
|
||||
assert.equal(h.result.restart, 0);
|
||||
assert.equal(h.result.failed, 1);
|
||||
assert.equal(h.result.disconnect, 1);
|
||||
assert.equal(h.client.features.negotiated.sasl, false);
|
||||
assert.equal(h.client.transport.authenticated, false);
|
||||
assert.equal(h.client.listenerCount('sasl'), 0);
|
||||
}
|
||||
|
||||
for (const vector of vectors) {
|
||||
test(`${vector.mechanism}: independent Scramp proof and verified final through feature dispatcher`, async () => {
|
||||
const h = harness(vector);
|
||||
await h.start();
|
||||
assert.equal(h.result.sent[0].stanza.mechanism, vector.mechanism);
|
||||
assert.equal(h.result.sent[0].stanza.value.toString(), `n,,n=user,r=${vector.nonce}`);
|
||||
await h.receive('challenge', vector.first);
|
||||
assert.equal(h.result.sent[1].stanza.value.toString(), vector.proof);
|
||||
assert.equal(h.result.success, 0);
|
||||
assert.equal(h.result.restart, 0);
|
||||
assert.equal(h.client.transport.authenticated, false);
|
||||
await h.receive('success', vector.final);
|
||||
assert.deepEqual(h.result.verification, ['processSuccess', 'finalize']);
|
||||
assert.equal(h.result.success, 1);
|
||||
assert.equal(h.result.restart, 1);
|
||||
assert.equal(h.result.disconnect, 0);
|
||||
assert.equal(h.client.features.negotiated.sasl, true);
|
||||
assert.equal(h.client.transport.authenticated, true);
|
||||
await h.receive('success', vector.final);
|
||||
assert.equal(h.result.success, 1);
|
||||
assert.equal(h.result.restart, 1);
|
||||
});
|
||||
for (const [label, final] of [
|
||||
['missing', undefined],
|
||||
['forged', 'v=' + (vector.final[2] === 'A' ? 'B' : 'A') + vector.final.slice(3)],
|
||||
['error', 'e=invalid-proof'],
|
||||
['conflicting error', `${vector.final},e=invalid-proof`],
|
||||
['duplicate verifier', `${vector.final},${vector.final}`],
|
||||
['bad base64', 'v=***'],
|
||||
['oversized', 'v=' + 'A'.repeat(4096)]
|
||||
]) {
|
||||
test(`${vector.mechanism}: reject ${label} server-final after proof`, async () => {
|
||||
const h = harness(vector);
|
||||
await h.start();
|
||||
await h.receive('challenge', vector.first);
|
||||
await h.receive('success', final);
|
||||
rejected(h);
|
||||
if (label === 'forged') assert.deepEqual(h.result.verification, ['processSuccess', 'finalize']);
|
||||
await h.receive('success', vector.final);
|
||||
rejected(h);
|
||||
});
|
||||
}
|
||||
test(`${vector.mechanism}: reject valid-looking final before proof`, async () => {
|
||||
const h = harness(vector);
|
||||
await h.start();
|
||||
await h.receive('success', vector.final);
|
||||
rejected(h);
|
||||
assert.deepEqual(h.result.verification, []);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [label, final] of [['missing', undefined], ['forged', 'v=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=']]) {
|
||||
test(`stock 12.22.1 reproduces ${label} premature acceptance`, async () => {
|
||||
const h = harness(vectors[1], false);
|
||||
await h.start();
|
||||
await h.receive('success', final);
|
||||
assert.equal(h.result.success, 1);
|
||||
assert.equal(h.result.restart, 1);
|
||||
assert.equal(h.client.transport.authenticated, true);
|
||||
});
|
||||
test(`replacement rejects original ${label} premature defect`, async () => {
|
||||
const h = harness();
|
||||
await h.start();
|
||||
await h.receive('success', final);
|
||||
rejected(h);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [label, first] of [
|
||||
['wrong nonce', vectors[1].first.replace('r=rOpr', 'r=wrong')],
|
||||
['unextended nonce', `r=${vectors[1].nonce},s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096`],
|
||||
['iteration overflow', vectors[1].first.replace('i=4096', 'i=100001')],
|
||||
['invalid iterations', vectors[1].first.replace('i=4096', 'i=NaN')],
|
||||
['duplicate attribute', vectors[1].first + ',i=4096'],
|
||||
['mandatory extension', 'm=required,' + vectors[1].first]
|
||||
]) {
|
||||
test(`reject ${label} before library proof work`, async () => {
|
||||
const h = harness();
|
||||
await h.start();
|
||||
await h.receive('challenge', first);
|
||||
rejected(h);
|
||||
assert.equal(h.result.sent.length, 1);
|
||||
});
|
||||
}
|
||||
|
||||
test('reject success while proof send is pending; no late revival', async () => {
|
||||
const h = harness();
|
||||
await h.start();
|
||||
const pending = deferred();
|
||||
h.client.send = () => pending.promise;
|
||||
await h.receive('challenge', vectors[1].first);
|
||||
await h.receive('success', vectors[1].final);
|
||||
rejected(h);
|
||||
pending.resolve();
|
||||
await tick();
|
||||
await h.receive('success', vectors[1].final);
|
||||
rejected(h);
|
||||
});
|
||||
|
||||
test('duplicate challenge while sending proof fails closed', async () => {
|
||||
const h = harness();
|
||||
await h.start();
|
||||
const pending = deferred();
|
||||
h.client.send = () => pending.promise;
|
||||
await h.receive('challenge', vectors[1].first);
|
||||
await h.receive('challenge', vectors[1].first);
|
||||
rejected(h);
|
||||
pending.resolve();
|
||||
await tick();
|
||||
rejected(h);
|
||||
});
|
||||
|
||||
test('reset while awaiting credentials prevents late auth and listener leak', async () => {
|
||||
const h = harness();
|
||||
const pending = deferred();
|
||||
h.client.getCredentials = () => pending.promise;
|
||||
await h.start();
|
||||
h.client.emit('--reset-stream-features');
|
||||
pending.resolve({username: 'user', password: 'pencil', clientNonce: vectors[1].nonce});
|
||||
await tick();
|
||||
await h.receive('success', vectors[1].final);
|
||||
assert.equal(h.result.sent.length, 0);
|
||||
assert.equal(h.result.success, 0);
|
||||
assert.equal(h.result.restart, 0);
|
||||
assert.equal(h.client.listenerCount('sasl'), 0);
|
||||
});
|
||||
|
||||
test('reset after proof, new negotiation succeeds without old handlers', async () => {
|
||||
const h = harness();
|
||||
await h.start();
|
||||
await h.receive('challenge', vectors[1].first);
|
||||
h.client.emit('--reset-stream-features');
|
||||
await h.receive('success', vectors[1].final);
|
||||
assert.equal(h.result.success, 0);
|
||||
assert.equal(h.client.listenerCount('sasl'), 0);
|
||||
await h.start();
|
||||
assert.equal(h.client.listenerCount('sasl'), 1);
|
||||
await h.receive('challenge', vectors[1].first);
|
||||
await h.receive('success', vectors[1].final);
|
||||
assert.equal(h.result.success, 1);
|
||||
assert.equal(h.result.restart, 1);
|
||||
});
|
||||
|
||||
test('fixture policy filters PLUS and plaintext; strongest available non-PLUS selected', async () => {
|
||||
const h = harness(vectors[1], true, ['PLAIN', 'SCRAM-SHA-1', 'SCRAM-SHA-512-PLUS', 'SCRAM-SHA-256', 'SCRAM-SHA-512']);
|
||||
await h.start();
|
||||
assert.equal(h.result.sent[0].stanza.mechanism, 'SCRAM-SHA-512');
|
||||
});
|
||||
for (const names of [['PLAIN'], ['SCRAM-SHA-512-PLUS'], []]) {
|
||||
test(`no allowed mechanism (${names.join(',')}) disconnects`, async () => {
|
||||
const h = harness(vectors[1], true, names);
|
||||
await h.start();
|
||||
rejected(h);
|
||||
assert.equal(h.result.sent.length, 0);
|
||||
});
|
||||
}
|
||||
for (const type of ['failure', 'abort']) {
|
||||
test(`${type} is terminal; valid final cannot revive it`, async () => {
|
||||
const h = harness();
|
||||
await h.start();
|
||||
await h.receive(type);
|
||||
await h.receive('success', vectors[1].final);
|
||||
rejected(h);
|
||||
});
|
||||
}
|
||||
|
||||
test('success during pending initial credentials fails closed', async () => {
|
||||
const h = harness();
|
||||
const pending = deferred();
|
||||
h.client.getCredentials = () => pending.promise;
|
||||
await h.start();
|
||||
await h.receive('success', vectors[1].final);
|
||||
rejected(h);
|
||||
pending.resolve({username: 'user', password: 'pencil', clientNonce: vectors[1].nonce});
|
||||
await tick();
|
||||
assert.equal(h.result.sent.length, 0);
|
||||
rejected(h);
|
||||
});
|
||||
|
||||
test('reset during proof send cancels continuation', async () => {
|
||||
const h = harness();
|
||||
await h.start();
|
||||
const pending = deferred();
|
||||
h.client.send = () => pending.promise;
|
||||
await h.receive('challenge', vectors[1].first);
|
||||
h.client.emit('--reset-stream-features');
|
||||
pending.resolve();
|
||||
await tick();
|
||||
await h.receive('success', vectors[1].final);
|
||||
assert.equal(h.result.success, 0);
|
||||
assert.equal(h.result.restart, 0);
|
||||
assert.equal(h.client.listenerCount('sasl'), 0);
|
||||
});
|
||||
|
||||
for (const stage of ['credentials', 'initial send', 'proof send']) {
|
||||
test(`${stage} rejection disconnects without success`, async () => {
|
||||
const h = harness();
|
||||
const reject = async () => {throw new Error('synthetic fixture failure');};
|
||||
if (stage === 'credentials') h.client.getCredentials = reject;
|
||||
if (stage === 'initial send') h.client.send = reject;
|
||||
await h.start();
|
||||
if (stage === 'proof send') {
|
||||
h.client.send = reject;
|
||||
await h.receive('challenge', vectors[1].first);
|
||||
}
|
||||
rejected(h);
|
||||
});
|
||||
}
|
||||
|
||||
test('repeat challenge after completed proof is rejected', async () => {
|
||||
const h = harness();
|
||||
await h.start();
|
||||
await h.receive('challenge', vectors[1].first);
|
||||
await h.receive('challenge', vectors[1].first);
|
||||
rejected(h);
|
||||
});
|
||||
|
||||
|
||||
test('synthetic binding input fails without auth or plaintext fallback', async () => {
|
||||
const h = harness();
|
||||
h.client.config.credentials.tlsUnique = Buffer.alloc(32);
|
||||
await h.start();
|
||||
rejected(h);
|
||||
assert.equal(h.result.sent.length, 0);
|
||||
});
|
||||
|
||||
test('server silence after proof never authenticates; reset discards attempt', async () => {
|
||||
const h = harness();
|
||||
let cacheEvents = 0;
|
||||
h.client.on('credentials:update', () => {cacheEvents++;});
|
||||
await h.start();
|
||||
await h.receive('challenge', vectors[0].first);
|
||||
assert.equal(h.result.success, 0);
|
||||
assert.equal(h.result.restart, 0);
|
||||
assert.equal(h.client.transport.authenticated, false);
|
||||
assert.equal(cacheEvents, 0);
|
||||
assert.equal(h.client.config.credentials.saltedPassword, undefined);
|
||||
h.client.emit('--reset-stream-features');
|
||||
await h.receive('success', vectors[0].final);
|
||||
assert.equal(h.result.success, 0);
|
||||
assert.equal(h.result.restart, 0);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"mechanism": "SCRAM-SHA-512",
|
||||
"username": "user",
|
||||
"password": "pencil",
|
||||
"nonce": "rOprNGfwEbeRWgbNEkqO",
|
||||
"initial": "n,,n=user,r=rOprNGfwEbeRWgbNEkqO",
|
||||
"first": "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096",
|
||||
"proof": "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=gMGXRcevScNtxZ6/8lQYpGtnsNAc3mGcmNomv+xnoOMw+3R2xNJdMNnzMlTN8PPC6wdp6dybEmDYXYTxwnYPJQ==",
|
||||
"final": "v=ZQnYEgWQMFmmsM8aQMF0nDDCy/AgCzkwk8CmMZYcMg0vSVlKDanekLtifDSeVGT4+5ZxXnJq199RVG2rR7N7Zw=="
|
||||
},
|
||||
{
|
||||
"mechanism": "SCRAM-SHA-512",
|
||||
"username": "user",
|
||||
"password": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"nonce": "rOprNGfwEbeRWgbNEkqO",
|
||||
"initial": "n,,n=user,r=rOprNGfwEbeRWgbNEkqO",
|
||||
"first": "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096",
|
||||
"proof": "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=FQ+Ds6nMR5kU+jnkwyhJGBK8lA2hTFKTqEBVSUCvSgmexvd4GGT4uAqD3iACaCkLYP0Bc6JHwnHMWkpGNOX2rg==",
|
||||
"final": "v=dXX1mz64UYbnX15NloHSoWfqoMVVvFg23i8KXwHigIVa+U6w+eJBBwhNg47qeIqkRLhhVnRnvcVvQMXpqav+4A=="
|
||||
},
|
||||
{
|
||||
"mechanism": "SCRAM-SHA-512",
|
||||
"username": "user",
|
||||
"password": "p\u00e9ncil",
|
||||
"nonce": "rOprNGfwEbeRWgbNEkqO",
|
||||
"initial": "n,,n=user,r=rOprNGfwEbeRWgbNEkqO",
|
||||
"first": "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096",
|
||||
"proof": "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=Jwk58tl/01ikMIUvnr0MXiTPWZJOiqtTyIAKx2A7IL85TXsL9S/fvBEIYvd/a0QIAQrNp47BSvResR5QB63TSg==",
|
||||
"final": "v=QwFhP0JOInTkJ/uTsZ2DRt00V0dQtkZKotpq815cU8X2SpF/5qbOd8sd6TCj8NjX/GPke8Xj4yg6whLHu7KNlw=="
|
||||
}
|
||||
]
|
||||
Reference in new issue
Block a user