From b944456ab5da585c8943d9da28bcb0e05dcb41e6 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Sun, 25 Oct 2026 19:38:10 -0400 Subject: [PATCH] chore(types): typed secret storage key cache; drop the cryptoCallbacks cast (#210) secretStorageKeys was untyped JS, passed to createClient behind `as any`, which hid that getSecretStorageKey returned undefined where the SDK type says null. Now TypeScript against the SDK's CryptoCallbacks, returns null, and storePrivateKey keeps its own copy of the key. Unit test added. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5 --- src/client/initMatrix.ts | 2 +- src/client/secretStorageKeys.js | 37 ---------------------------- src/client/secretStorageKeys.test.ts | 26 +++++++++++++++++++ src/client/secretStorageKeys.ts | 27 ++++++++++++++++++++ 4 files changed, 54 insertions(+), 38 deletions(-) delete mode 100644 src/client/secretStorageKeys.js create mode 100644 src/client/secretStorageKeys.test.ts create mode 100644 src/client/secretStorageKeys.ts diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts index f49d53e46..b563af1f8 100644 --- a/src/client/initMatrix.ts +++ b/src/client/initMatrix.ts @@ -87,7 +87,7 @@ export const initClient = async (session: Session): Promise => { // the user (KeyUploadBanner) instead of retrying silently forever. fetchFn: watchKeyUploads((input, init) => globalThis.fetch(input, init)), timelineSupport: true, - cryptoCallbacks: cryptoCallbacks as any, + cryptoCallbacks, // SAS (emoji) + QR-code verification (show/scan/reciprocate). verificationMethods: ['m.sas.v1', 'm.qr_code.show.v1', 'm.qr_code.scan.v1', 'm.reciprocate.v1'], oauthClientId: oidcIdentity?.oidc.clientId, diff --git a/src/client/secretStorageKeys.js b/src/client/secretStorageKeys.js deleted file mode 100644 index 78c0cb4cb..000000000 --- a/src/client/secretStorageKeys.js +++ /dev/null @@ -1,37 +0,0 @@ -const secretStorageKeys = new Map(); - -export function storePrivateKey(keyId, privateKey) { - if (privateKey instanceof Uint8Array === false) { - throw new Error('Unable to store, privateKey is invalid.'); - } - secretStorageKeys.set(keyId, privateKey); -} - -function hasPrivateKey(keyId) { - return secretStorageKeys.get(keyId) instanceof Uint8Array; -} - -function getPrivateKey(keyId) { - return secretStorageKeys.get(keyId); -} - -export function clearSecretStorageKeys() { - secretStorageKeys.clear(); -} - -async function getSecretStorageKey({ keys }) { - const keyIds = Object.keys(keys); - const keyId = keyIds.find(hasPrivateKey); - if (!keyId) return undefined; - const privateKey = getPrivateKey(keyId); - return [keyId, privateKey]; -} - -function cacheSecretStorageKey(keyId, keyInfo, privateKey) { - secretStorageKeys.set(keyId, privateKey); -} - -export const cryptoCallbacks = { - getSecretStorageKey, - cacheSecretStorageKey, -}; diff --git a/src/client/secretStorageKeys.test.ts b/src/client/secretStorageKeys.test.ts new file mode 100644 index 000000000..6d7193c4f --- /dev/null +++ b/src/client/secretStorageKeys.test.ts @@ -0,0 +1,26 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { clearSecretStorageKeys, cryptoCallbacks, storePrivateKey } from './secretStorageKeys'; + +const ask = (...ids: string[]) => + cryptoCallbacks.getSecretStorageKey!( + { keys: Object.fromEntries(ids.map((id) => [id, {} as never])) }, + 'm.cross_signing.master', + ); + +test('[#210] secret storage key cache: null when unknown, key when stored, copies input', async () => { + clearSecretStorageKeys(); + assert.equal(await ask('k1'), null); + + const key = new Uint8Array([1, 2, 3]); + storePrivateKey('k1', key); + key[0] = 9; // the cache keeps its own copy + assert.deepEqual(await ask('k0', 'k1'), ['k1', new Uint8Array([1, 2, 3])]); + + cryptoCallbacks.cacheSecretStorageKey!('k2', {} as never, new Uint8Array([7])); + assert.deepEqual(await ask('k2'), ['k2', new Uint8Array([7])]); + + clearSecretStorageKeys(); + assert.equal(await ask('k1', 'k2'), null); + assert.throws(() => storePrivateKey('k3', 'nope' as never), /invalid/); +}); diff --git a/src/client/secretStorageKeys.ts b/src/client/secretStorageKeys.ts new file mode 100644 index 000000000..8d805d6f7 --- /dev/null +++ b/src/client/secretStorageKeys.ts @@ -0,0 +1,27 @@ +import type { CryptoCallbacks } from 'matrix-js-sdk/lib/crypto-api'; + +const secretStorageKeys = new Map>(); + +export function storePrivateKey(keyId: string, privateKey: Uint8Array) { + if (privateKey instanceof Uint8Array === false) { + throw new Error('Unable to store, privateKey is invalid.'); + } + // Our own copy, on a plain ArrayBuffer (what the SDK's callback type wants). + secretStorageKeys.set(keyId, new Uint8Array(privateKey)); +} + +export function clearSecretStorageKeys() { + secretStorageKeys.clear(); +} + +export const cryptoCallbacks: CryptoCallbacks = { + async getSecretStorageKey({ keys }) { + const keyId = Object.keys(keys).find((id) => secretStorageKeys.get(id) instanceof Uint8Array); + const privateKey = keyId ? secretStorageKeys.get(keyId) : undefined; + // The SDK expects null (not undefined) when no key is cached. + return keyId && privateKey ? [keyId, privateKey] : null; + }, + cacheSecretStorageKey(keyId, keyInfo, privateKey) { + secretStorageKeys.set(keyId, privateKey); + }, +}; -- 2.47.3