From bc97c564d3990594fe85c210efc22a1c1ec62706 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sun, 24 May 2026 14:17:30 -0600 Subject: [PATCH] Fix libolm->rust crypto migration crash on localStorage stores matrix-js-sdk's LocalStorageCryptoStore.getEndToEndSessionsBatch() returns olm sessions without the deviceKey/sessionId fields, since those live only in the storage key path. initRustCrypto()'s migration then assigns the undefined deviceKey to PickledSession.senderKey and crashes in the WASM setter with "Cannot read properties of undefined (reading 'length')". The IndexedDB backend embeds those fields in the record and is unaffected, which is why this only hit installs that were started on the legacy localStorage crypto store. Patch the instance to re-derive deviceKey/sessionId from the crypto.sessions/ keys before returning a batch, so both the migration and its delete-after-migrate step work. --- src/matrix-crypto-store.js | 75 ++++++++++++++++++++++++++++++++++++- src/matrix-server-config.js | 16 ++++++-- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/matrix-crypto-store.js b/src/matrix-crypto-store.js index eb74cf3..55577d3 100644 --- a/src/matrix-crypto-store.js +++ b/src/matrix-crypto-store.js @@ -17,6 +17,13 @@ const v8 = require('v8'); let shimInstalled = false; +// Mirrors matrix-js-sdk's localStorage-crypto-store.js: olm sessions live under +// "crypto.sessions/" and migration batches are capped at 50. +const E2E_PREFIX = 'crypto.'; +const SESSION_KEY_PREFIX = E2E_PREFIX + 'sessions/'; +const SESSION_BATCH_SIZE = 50; +const OLM_BATCH_PATCH_MARKER = Symbol.for('node-red-contrib-matrix-chat.olmSessionBatchPatched'); + /** * Install the in-memory IndexedDB shim onto globalThis. Idempotent. Must be * called before MatrixClient.initRustCrypto(). @@ -172,4 +179,70 @@ async function snapshotCryptoStore(filePath, dbNamePrefix) { return true; } -module.exports = { ensureIndexedDBShim, restoreCryptoStore, snapshotCryptoStore }; +/** + * Patch a LocalStorageCryptoStore instance so its getEndToEndSessionsBatch() + * returns olm sessions with deviceKey/sessionId attached. + * + * Why: matrix-js-sdk's LocalStorageCryptoStore stores olm sessions as + * `{ session, lastReceivedMessageTs }` with the curve25519 deviceKey encoded + * only in the localStorage key ("crypto.sessions/"). On read, + * getEndToEndSessionsBatch() returns the bare session value without injecting + * the deviceKey or sessionId, so initRustCrypto()'s libolm-to-rust migration + * crashes at PickledSession.senderKey = session.deviceKey (undefined). The + * IndexedDB backend stores those fields in the record and is unaffected. + * + * Idempotent and safe to call on a store with no legacy sessions. + */ +function patchLocalStorageCryptoStoreForRustMigration(cryptoStore) { + if (!cryptoStore || cryptoStore[OLM_BATCH_PATCH_MARKER]) { + return cryptoStore; + } + const store = cryptoStore.store; + if (!store || typeof store.length !== 'number' || typeof store.key !== 'function') { + return cryptoStore; + } + cryptoStore.getEndToEndSessionsBatch = async function() { + const result = []; + for (let i = 0; i < store.length; i++) { + const key = store.key(i); + if (!key || !key.startsWith(SESSION_KEY_PREFIX)) { + continue; + } + const deviceKey = key.slice(SESSION_KEY_PREFIX.length); + let sessions; + try { + const raw = store.getItem(key); + sessions = raw ? JSON.parse(raw) : null; + } catch (e) { + sessions = null; + } + if (!sessions || typeof sessions !== 'object') { + continue; + } + for (const [sessionId, val] of Object.entries(sessions)) { + if (val === null || val === undefined) { + continue; + } + // Mirrors LocalStorageCryptoStore._getEndToEndSessions: very old + // entries were stored as bare base64 pickle strings. + const sessionInfo = (typeof val === 'string') + ? { session: val, lastReceivedMessageTs: 0 } + : val; + result.push({ ...sessionInfo, deviceKey, sessionId }); + if (result.length >= SESSION_BATCH_SIZE) { + return result; + } + } + } + return result.length === 0 ? null : result; + }; + cryptoStore[OLM_BATCH_PATCH_MARKER] = true; + return cryptoStore; +} + +module.exports = { + ensureIndexedDBShim, + restoreCryptoStore, + snapshotCryptoStore, + patchLocalStorageCryptoStoreForRustMigration, +}; diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index 702bc1a..88afc75 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -8,7 +8,12 @@ const cryptoApiPromise = import("matrix-js-sdk/lib/crypto-api/index.js"); const fs = require("fs-extra"); const { resolve } = require('path'); const { LocalStorage } = require('node-localstorage'); -const { ensureIndexedDBShim, restoreCryptoStore, snapshotCryptoStore } = require('./matrix-crypto-store'); +const { + ensureIndexedDBShim, + restoreCryptoStore, + snapshotCryptoStore, + patchLocalStorageCryptoStoreForRustMigration, +} = require('./matrix-crypto-store'); require("abort-controller/polyfill"); // polyfill abort-controller if we don't have it if (!globalThis.fetch) { // polyfill fetch if we don't have it @@ -453,8 +458,13 @@ module.exports = function(RED) { if(node.e2ee) { // Provide the legacy (pre-v37 libolm) crypto store so that // initRustCrypto() can perform a one-time migration of any - // existing crypto state into the Rust crypto store. - clientOpts.cryptoStore = new LocalStorageCryptoStore(localStorage); + // existing crypto state into the Rust crypto store. Patch + // the store because matrix-js-sdk's LocalStorageCryptoStore + // omits deviceKey/sessionId from getEndToEndSessionsBatch(), + // which breaks the libolm->rust olm-session migration. + clientOpts.cryptoStore = patchLocalStorageCryptoStoreForRustMigration( + new LocalStorageCryptoStore(localStorage) + ); } node.matrixClient = sdk.createClient(clientOpts);