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/<deviceKey> keys before returning a batch, so both
the migration and its delete-after-migrate step work.
This commit is contained in:
2026-05-24 14:17:30 -06:00
parent 5012c603aa
commit bc97c564d3
2 changed files with 87 additions and 4 deletions
+74 -1
View File
@@ -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/<deviceKey>" 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/<deviceKey>"). 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,
};
+13 -3
View File
@@ -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);