Files
Haven/test/groupSignerHistory.test.js

86 lines
3.6 KiB
JavaScript

'use strict';
// A group message is shown only when it verifies under a signing key its
// author has on record. A key reset used to hide every message the author
// had sent before it, because only the current key was tried.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');
const { webcrypto } = require('node:crypto');
const ROOT = path.join(__dirname, '..');
const G = require(path.join(ROOT, 'public/js/e2e-group.js'));
const SOURCE = fs.readFileSync(path.join(ROOT, 'public/js/modules/app-groups.js'), 'utf8');
function loadGroups() {
const context = vm.createContext({
module: { exports: {} }, HavenGroupCrypto: G, crypto: webcrypto, console, setTimeout, clearTimeout,
t: (key) => key, localStorage: { getItem: () => null, setItem() {}, removeItem() {} },
});
vm.runInContext(SOURCE.replace(/^export default/m, 'module.exports ='), context, { filename: 'app-groups.js' });
return context.module.exports;
}
async function signer() {
const pair = await G.generateSigningKeyPair();
return { pair, jwk: await G.exportPublicJwk(pair.publicKey) };
}
// An app with one group (channel 5, code g1) whose epoch 1 key it holds, and
// a server that reports `recorded` as the author's signing keys.
async function makeApp(recorded) {
const app = Object.assign(Object.create(null), loadGroups(), {
user: { id: 1 },
channels: [{ id: 5, code: 'g1', is_dm: 1, is_group: 1 }],
socket: { on() {}, off() {}, emit() {} },
_groupFetchKeys: async () => {},
_groupReq: async (ev, payload) => (ev === 'get-signing-key'
? { ev: 'signing-key-result', data: { userId: payload.userId, jwk: recorded[0], keys: recorded } }
: null),
});
const epochKey = await G.generateEpochKey();
app._groupState('g1').keys.set(1, epochKey);
const send = async (text, pair) => JSON.stringify(await G.encryptGroupMessage(text, {
epochKey, epoch: 1, channelId: 5, senderId: 2, prev: null, signingPrivateKey: pair.privateKey,
}));
return { app, send };
}
test('history signed before a key reset still verifies', async () => {
const before = await signer();
const after = await signer();
const { app, send } = await makeApp([after.jwk, before.jwk]);
const old = await app._groupDecrypt('g1', await send('from before the reset', before.pair), 2);
assert.equal(old.ok, true);
assert.equal(old.plaintext, 'from before the reset');
const now = await app._groupDecrypt('g1', await send('after', after.pair), 2);
assert.equal(now.ok, true);
});
test('a message under a key the author never published stays hidden', async () => {
const real = await signer();
const forged = await signer();
const { app, send } = await makeApp([real.jwk]);
const r = await app._groupDecrypt('g1', await send('forged', forged.pair), 2);
assert.equal(r.ok, false);
assert.equal(r.plaintext ?? null, null);
});
test('a message open in the main view and a pop-out is checked once, not hidden', async () => {
const s = await signer();
const { app, send } = await makeApp([s.jwk]);
const ch = app.channels[0];
const msg = { id: 7, user_id: 2, content: await send('seen twice', s.pair) };
await app._decryptGroupMessages([msg], ch);
await app._decryptGroupMessages([msg], ch);
assert.equal(msg.content, 'seen twice');
assert.equal(msg._e2eVerified, true);
// Plain text from the server is still refused, whatever it claims.
const forged = { id: 8, user_id: 2, content: 'not signed', _e2eVerified: true };
await app._decryptGroupMessages([forged], ch);
assert.equal(forged.content, 'groups.cannot_verify');
});