mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-05 06:50:10 +00:00
Add official-compatible mobile and desktop connector flows, preserve exact .html asset paths, and cover the protocol and framing behavior with regression tests. Fixes #326
127 lines
4.9 KiB
JavaScript
127 lines
4.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
buildCredentialData,
|
|
normalizePublicKeyOptions,
|
|
parseConnectorRequest,
|
|
resolveParentChannel,
|
|
} from '../webapp/public/webauthn-connector.js';
|
|
|
|
function encodeBase64Utf8(value) {
|
|
return Buffer.from(value, 'utf8').toString('base64');
|
|
}
|
|
|
|
const publicKeyOptions = {
|
|
challenge: 'AQID',
|
|
allowCredentials: [{ id: 'BAUG', type: 'public-key', transports: ['usb'] }],
|
|
timeout: 60000,
|
|
rpId: 'vault.example.test',
|
|
};
|
|
|
|
test('parses the official desktop/browser V1 connector request', () => {
|
|
const params = new URLSearchParams({
|
|
data: encodeBase64Utf8(JSON.stringify(publicKeyOptions)),
|
|
parent: encodeURIComponent('file:///C:/Program Files/Bitwarden/resources/app/index.html'),
|
|
btnText: encodeURIComponent('Read security key'),
|
|
btnAwaitingInteractionText: encodeURIComponent('Awaiting security key interaction...'),
|
|
v: '1',
|
|
});
|
|
const request = parseConnectorRequest(params);
|
|
assert.equal(request.parentUrl, 'file:///C:/Program Files/Bitwarden/resources/app/index.html');
|
|
assert.equal(request.parentProtocol, 'file:');
|
|
assert.deepEqual(JSON.parse(request.webauthnJson), publicKeyOptions);
|
|
assert.equal(request.buttonText, 'Read security key');
|
|
assert.equal(request.awaitingText, 'Awaiting security key interaction...');
|
|
});
|
|
|
|
test('keeps V2 parsing compatible with the shared official connector protocol', () => {
|
|
const params = new URLSearchParams({
|
|
data: encodeBase64Utf8(JSON.stringify({ data: JSON.stringify(publicKeyOptions) })),
|
|
parent: encodeURIComponent('chrome-extension://nngceckbapebfimnlniiiahkandclblb/popup/index.html'),
|
|
v: '2',
|
|
});
|
|
assert.deepEqual(JSON.parse(parseConnectorRequest(params).webauthnJson), publicKeyOptions);
|
|
});
|
|
|
|
test('normalizes WebAuthn challenge and allowed credential IDs', () => {
|
|
const normalized = normalizePublicKeyOptions(JSON.stringify(publicKeyOptions));
|
|
assert.deepEqual(Array.from(normalized.challenge), [1, 2, 3]);
|
|
assert.deepEqual(Array.from(normalized.allowCredentials[0].id), [4, 5, 6]);
|
|
});
|
|
|
|
test('emits the exact assertion shape consumed by official Bitwarden clients', () => {
|
|
const output = JSON.parse(buildCredentialData({
|
|
id: 'credential-id',
|
|
rawId: Uint8Array.from([1, 2, 3]).buffer,
|
|
type: 'public-key',
|
|
getClientExtensionResults: () => ({ appid: false }),
|
|
response: {
|
|
authenticatorData: Uint8Array.from([4, 5]).buffer,
|
|
clientDataJSON: Uint8Array.from([6, 7]).buffer,
|
|
signature: Uint8Array.from([8, 9]).buffer,
|
|
},
|
|
}));
|
|
assert.deepEqual(output, {
|
|
id: 'credential-id',
|
|
rawId: 'AQID',
|
|
type: 'public-key',
|
|
extensions: { appid: false },
|
|
response: {
|
|
authenticatorData: 'BAU',
|
|
clientDataJson: 'Bgc',
|
|
signature: 'CAk',
|
|
},
|
|
});
|
|
});
|
|
|
|
test('accepts legacy file and current official desktop parent origins', () => {
|
|
assert.deepEqual(resolveParentChannel({
|
|
parentProtocol: 'file:',
|
|
parentUrl: 'file:///C:/Bitwarden/index.html',
|
|
}, 'https://vault.example.test'), {
|
|
eventOrigin: 'null',
|
|
targetOrigin: 'file:///C:/Bitwarden/index.html',
|
|
});
|
|
assert.deepEqual(resolveParentChannel({
|
|
parentProtocol: 'bw-desktop-file:',
|
|
parentUrl: 'bw-desktop-file://bundle/index.html',
|
|
}, 'https://vault.example.test'), {
|
|
eventOrigin: 'bw-desktop-file://bundle',
|
|
targetOrigin: 'bw-desktop-file://bundle/index.html',
|
|
});
|
|
});
|
|
|
|
test('accepts configured official extension origins and rejects arbitrary parents', () => {
|
|
const extension = 'chrome-extension://nngceckbapebfimnlniiiahkandclblb';
|
|
assert.deepEqual(resolveParentChannel({
|
|
parentProtocol: 'chrome-extension:',
|
|
parentUrl: `${extension}/popup/index.html`,
|
|
}, 'https://vault.example.test', [extension]), {
|
|
eventOrigin: extension,
|
|
targetOrigin: extension,
|
|
});
|
|
assert.throws(() => resolveParentChannel({
|
|
parentProtocol: 'https:',
|
|
parentUrl: 'https://attacker.example/frame',
|
|
}, 'https://vault.example.test', []), /Untrusted parent/);
|
|
});
|
|
|
|
test('uses the official postMessage message contract and iframe-sized fallback styling', async () => {
|
|
const [html, source, viteConfig] = await Promise.all([
|
|
readFile(new URL('../webapp/public/webauthn-connector.html', import.meta.url), 'utf8'),
|
|
readFile(new URL('../webapp/public/webauthn-connector.js', import.meta.url), 'utf8'),
|
|
readFile(new URL('../webapp/vite.config.ts', import.meta.url), 'utf8'),
|
|
]);
|
|
assert.match(html, /id="webauthn-button"/);
|
|
assert.match(html, /min-height:\s*40px/);
|
|
assert.match(html, /background:\s*#2563eb/);
|
|
assert.match(source, /post\('info\|ready'\)/);
|
|
assert.match(source, /post\(`success\|\$\{buildCredentialData\(credential\)\}`\)/);
|
|
assert.match(source, /post\(`error\|\$\{browserErrorMessage\(error\)\}`\)/);
|
|
assert.match(source, /event\.data === 'stop'/);
|
|
assert.match(source, /event\.data === 'start'/);
|
|
assert.match(viteConfig, /endsWith\('-connector\.html'\)/);
|
|
});
|