mirror of
https://github.com/ancsemi/Haven.git
synced 2026-10-27 00:54:56 +00:00
86 lines
3.6 KiB
JavaScript
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');
|
|
});
|