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
84 lines
4.6 KiB
JavaScript
84 lines
4.6 KiB
JavaScript
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)}));
|
|
});
|