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); + }, +};