Skip to content

Commit 1e0cce2

Browse files
committed
feat(secret storage): keyId in SecretStorage.setDefaultKeyId can be set at null in order to delete an exising recovery key
1 parent 9134471 commit 1e0cce2

File tree

3 files changed

+110
-10
lines changed

3 files changed

+110
-10
lines changed

spec/unit/secret-storage.spec.ts

+80-1
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ import {
2323
SecretStorageCallbacks,
2424
SecretStorageKeyDescriptionAesV1,
2525
SecretStorageKeyDescriptionCommon,
26+
ServerSideSecretStorage,
2627
ServerSideSecretStorageImpl,
2728
trimTrailingEquals,
2829
} from "../../src/secret-storage";
2930
import { randomString } from "../../src/randomstring";
3031
import { SecretInfo } from "../../src/secret-storage.ts";
31-
import { AccountDataEvents } from "../../src";
32+
import { AccountDataEvents, ClientEvent, MatrixEvent, TypedEventEmitter } from "../../src";
33+
import { defer, IDeferred } from "../../src/utils";
3234

3335
declare module "../../src/@types/event" {
3436
interface SecretStorageAccountDataEvents {
@@ -273,6 +275,78 @@ describe("ServerSideSecretStorageImpl", function () {
273275
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("unknown algorithm"));
274276
});
275277
});
278+
279+
describe("setDefaultKeyId", function () {
280+
let secretStorage: ServerSideSecretStorage;
281+
let accountDataAdapter: Mocked<AccountDataClient>;
282+
let accountDataPromise: IDeferred<void>;
283+
beforeEach(() => {
284+
accountDataAdapter = mockAccountDataClient();
285+
accountDataPromise = defer();
286+
accountDataAdapter.setAccountData.mockImplementation(() => {
287+
accountDataPromise.resolve();
288+
return Promise.resolve({});
289+
});
290+
291+
secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
292+
});
293+
294+
it("should set the default key id", async function () {
295+
const setDefaultPromise = secretStorage.setDefaultKeyId("keyId");
296+
await accountDataPromise.promise;
297+
298+
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith("m.secret_storage.default_key", {
299+
key: "keyId",
300+
});
301+
302+
accountDataAdapter.emit(
303+
ClientEvent.AccountData,
304+
new MatrixEvent({
305+
type: "m.secret_storage.default_key",
306+
content: { key: "keyId" },
307+
}),
308+
);
309+
await setDefaultPromise;
310+
});
311+
312+
it("should set the default key id with a null key id", async function () {
313+
const setDefaultPromise = secretStorage.setDefaultKeyId(null);
314+
await accountDataPromise.promise;
315+
316+
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith("m.secret_storage.default_key", {});
317+
318+
accountDataAdapter.emit(
319+
ClientEvent.AccountData,
320+
new MatrixEvent({
321+
type: "m.secret_storage.default_key",
322+
content: {},
323+
}),
324+
);
325+
await setDefaultPromise;
326+
});
327+
});
328+
329+
describe("getDefaultKeyId", function () {
330+
it("should return null when there is no key", async function () {
331+
const accountDataAdapter = mockAccountDataClient();
332+
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
333+
expect(await secretStorage.getDefaultKeyId()).toBe(null);
334+
});
335+
336+
it("should return the key id when there is a key", async function () {
337+
const accountDataAdapter = mockAccountDataClient();
338+
accountDataAdapter.getAccountDataFromServer.mockResolvedValue({ key: "keyId" });
339+
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
340+
expect(await secretStorage.getDefaultKeyId()).toBe("keyId");
341+
});
342+
343+
it("should return null when an empty object is in the account data", async function () {
344+
const accountDataAdapter = mockAccountDataClient();
345+
accountDataAdapter.getAccountDataFromServer.mockResolvedValue({});
346+
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
347+
expect(await secretStorage.getDefaultKeyId()).toBe(null);
348+
});
349+
});
276350
});
277351

278352
describe("trimTrailingEquals", () => {
@@ -291,8 +365,13 @@ describe("trimTrailingEquals", () => {
291365
});
292366

293367
function mockAccountDataClient(): Mocked<AccountDataClient> {
368+
const eventEmitter = new TypedEventEmitter();
294369
return {
295370
getAccountDataFromServer: jest.fn().mockResolvedValue(null),
296371
setAccountData: jest.fn().mockResolvedValue({}),
372+
on: eventEmitter.on.bind(eventEmitter),
373+
off: eventEmitter.off.bind(eventEmitter),
374+
removeListener: eventEmitter.removeListener.bind(eventEmitter),
375+
emit: eventEmitter.emit.bind(eventEmitter),
297376
} as unknown as Mocked<AccountDataClient>;
298377
}

src/crypto/EncryptionSetup.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,10 @@ class AccountDataClientAdapter
264264
return event?.getContent<AccountDataEvents[K]>() ?? null;
265265
}
266266

267-
public setAccountData<K extends keyof AccountDataEvents>(type: K, content: AccountDataEvents[K]): Promise<{}> {
267+
public setAccountData<K extends keyof AccountDataEvents>(
268+
type: K,
269+
content: AccountDataEvents[K] | Record<string, never>,
270+
): Promise<{}> {
268271
const event = new MatrixEvent({ type, content });
269272
const lastEvent = this.values.get(type);
270273
this.values.set(type, event);

src/secret-storage.ts

+26-8
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,10 @@ export interface AccountDataClient extends TypedEventEmitter<ClientEvent.Account
148148
* @param content - the content object to be set
149149
* @returns an empty object
150150
*/
151-
setAccountData: <K extends keyof AccountDataEvents>(eventType: K, content: AccountDataEvents[K]) => Promise<{}>;
151+
setAccountData: <K extends keyof AccountDataEvents>(
152+
eventType: K,
153+
content: AccountDataEvents[K] | Record<string, never>,
154+
) => Promise<{}>;
152155
}
153156

154157
/**
@@ -316,9 +319,12 @@ export interface ServerSideSecretStorage {
316319
/**
317320
* Set the default key ID for encrypting secrets.
318321
*
322+
* If keyId is `null`, the default key id value in the account data will be set to an empty object.
323+
* This is considered as "disabling" the default key.
324+
*
319325
* @param keyId - The new default key ID
320326
*/
321-
setDefaultKeyId(keyId: string): Promise<void>;
327+
setDefaultKeyId(keyId: string | null): Promise<void>;
322328
}
323329

324330
/**
@@ -357,21 +363,33 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
357363
}
358364

359365
/**
360-
* Set the default key ID for encrypting secrets.
361-
*
362-
* @param keyId - The new default key ID
366+
* Implementation of {@link ServerSideSecretStorage#setDefaultKeyId}.
363367
*/
364-
public setDefaultKeyId(keyId: string): Promise<void> {
368+
public setDefaultKeyId(keyId: string | null): Promise<void> {
365369
return new Promise<void>((resolve, reject) => {
366370
const listener = (ev: MatrixEvent): void => {
367-
if (ev.getType() === "m.secret_storage.default_key" && ev.getContent().key === keyId) {
371+
if (ev.getType() !== "m.secret_storage.default_key") {
372+
// Different account data item
373+
return;
374+
}
375+
376+
// If keyId === null, the content should be an empty object.
377+
// Otherwise, the `key` in the content object should match keyId.
378+
const content = ev.getContent();
379+
const isSameKey = keyId === null ? Object.keys(content).length === 0 : content.key === keyId;
380+
if (isSameKey) {
368381
this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
369382
resolve();
370383
}
371384
};
372385
this.accountDataAdapter.on(ClientEvent.AccountData, listener);
373386

374-
this.accountDataAdapter.setAccountData("m.secret_storage.default_key", { key: keyId }).catch((e) => {
387+
// The spec [1] says that the value of the account data entry should be an object with a `key` property.
388+
// It doesn't specify how to delete the default key; we do it by setting the account data to an empty object.
389+
//
390+
// [1]: https://spec.matrix.org/v1.13/client-server-api/#key-storage
391+
const newValue: Record<string, never> | { key: string } = keyId === null ? {} : { key: keyId };
392+
this.accountDataAdapter.setAccountData("m.secret_storage.default_key", newValue).catch((e) => {
375393
this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
376394
reject(e);
377395
});

0 commit comments

Comments
 (0)