fix(security): bookmarks/reminders stop storing decrypted text server-side
For encrypted rooms, bookmarks persist only {roomId, eventId, savedAt}
and reminders only their non-text fields; the preview, room name and
sender resolve locally at render/fire time from the timeline (with a
"Message unavailable" fallback). A one-time, loop-guarded cleanup strips
text from existing entries in currently-encrypted rooms. Unencrypted
rooms are unchanged. Unit-tested.
Fixes #10
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
Bookmark,
|
||||
cleanupEncryptedBookmarks,
|
||||
hasBookmarkText,
|
||||
stripBookmarkText,
|
||||
toStorableBookmark,
|
||||
} from './useBookmarks';
|
||||
import { cleanupEncryptedReminders, Reminder, toStorableReminder } from './useReminders';
|
||||
|
||||
// E2EE policy for bookmarks/reminders (Gitea #10): account data is stored
|
||||
// unencrypted on the homeserver, so entries for encrypted rooms must carry no
|
||||
// message text or names. `isEncryptedRoom` is injected to keep these pure.
|
||||
const isEncrypted = (roomId: string) => roomId === '!enc';
|
||||
|
||||
const full = (roomId: string): Bookmark => ({
|
||||
roomId,
|
||||
eventId: `$ev-${roomId}`,
|
||||
savedAt: 100,
|
||||
previewText: 'secret text',
|
||||
roomName: 'Room',
|
||||
senderName: 'Alice',
|
||||
});
|
||||
|
||||
test('stripBookmarkText keeps only roomId/eventId/savedAt', () => {
|
||||
const stripped = stripBookmarkText(full('!enc'));
|
||||
assert.deepEqual(stripped, { roomId: '!enc', eventId: '$ev-!enc', savedAt: 100, roomName: '' });
|
||||
assert.equal(hasBookmarkText(stripped), false);
|
||||
assert.equal(hasBookmarkText(full('!enc')), true);
|
||||
});
|
||||
|
||||
test('toStorableBookmark strips text for encrypted rooms only', () => {
|
||||
const plain = full('!plain');
|
||||
assert.equal(toStorableBookmark(plain, isEncrypted), plain);
|
||||
const enc = toStorableBookmark(full('!enc'), isEncrypted);
|
||||
assert.equal(enc.previewText, undefined);
|
||||
assert.equal(enc.senderName, undefined);
|
||||
assert.equal(enc.roomName, '');
|
||||
});
|
||||
|
||||
test('cleanupEncryptedBookmarks returns undefined when nothing to strip', () => {
|
||||
const already = stripBookmarkText(full('!enc'));
|
||||
assert.equal(cleanupEncryptedBookmarks([full('!plain'), already], isEncrypted), undefined);
|
||||
assert.equal(cleanupEncryptedBookmarks([], isEncrypted), undefined);
|
||||
});
|
||||
|
||||
test('cleanupEncryptedBookmarks strips legacy encrypted entries and keeps the rest', () => {
|
||||
const plain = full('!plain');
|
||||
const out = cleanupEncryptedBookmarks([plain, full('!enc')], isEncrypted);
|
||||
assert.ok(out);
|
||||
assert.equal(out[0], plain);
|
||||
assert.deepEqual(out[1], stripBookmarkText(full('!enc')));
|
||||
// Running it again on the result is a no-op (no write loop).
|
||||
assert.equal(cleanupEncryptedBookmarks(out, isEncrypted), undefined);
|
||||
});
|
||||
|
||||
const reminder = (roomId: string): Reminder => ({
|
||||
roomId,
|
||||
eventId: `$ev-${roomId}`,
|
||||
timestamp: 200,
|
||||
message: 'secret text',
|
||||
});
|
||||
|
||||
test('toStorableReminder drops message for encrypted rooms only', () => {
|
||||
const plain = reminder('!plain');
|
||||
assert.equal(toStorableReminder(plain, isEncrypted), plain);
|
||||
assert.deepEqual(toStorableReminder(reminder('!enc'), isEncrypted), {
|
||||
roomId: '!enc',
|
||||
eventId: '$ev-!enc',
|
||||
timestamp: 200,
|
||||
});
|
||||
});
|
||||
|
||||
test('cleanupEncryptedReminders strips legacy entries once', () => {
|
||||
const out = cleanupEncryptedReminders([reminder('!plain'), reminder('!enc')], isEncrypted);
|
||||
assert.ok(out);
|
||||
assert.equal(out[0].message, 'secret text');
|
||||
assert.equal(out[1].message, undefined);
|
||||
assert.equal(cleanupEncryptedReminders(out, isEncrypted), undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user