1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Add CryptoApi.getSecretStorageStatus (#5054)

* Add `CryptoApi.getSecretStorageStatus`

`isSecretStorageReady` is a bit of a blunt instrument: it's hard to see from
logs *why* the secret storage isn't ready.

Add a new method which returns a bit more data.

* Update src/rust-crypto/rust-crypto.ts

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>

---------

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
This commit is contained in:
Richard van der Hoff
2025-10-23 13:04:28 +02:00
committed by GitHub
parent 977d0322da
commit b0cbe22f64
3 changed files with 78 additions and 4 deletions

View File

@@ -854,9 +854,27 @@ describe("RustCrypto", () => {
}); });
}); });
it("getSecretStorageStatus", async () => {
const mockSecretStorage = {
getDefaultKeyId: jest.fn().mockResolvedValue("blah"),
isStored: jest.fn().mockResolvedValue({ blah: {} }),
} as unknown as Mocked<ServerSideSecretStorage>;
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, mockSecretStorage);
await expect(rustCrypto.getSecretStorageStatus()).resolves.toEqual({
defaultKeyId: "blah",
ready: true,
secretStorageKeyValidityMap: {
"m.cross_signing.master": true,
"m.cross_signing.self_signing": true,
"m.cross_signing.user_signing": true,
},
});
});
it("isSecretStorageReady", async () => { it("isSecretStorageReady", async () => {
const mockSecretStorage = { const mockSecretStorage = {
getDefaultKeyId: jest.fn().mockResolvedValue(null), getDefaultKeyId: jest.fn().mockResolvedValue(null),
isStored: jest.fn().mockResolvedValue(null),
} as unknown as Mocked<ServerSideSecretStorage>; } as unknown as Mocked<ServerSideSecretStorage>;
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, mockSecretStorage); const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, mockSecretStorage);
await expect(rustCrypto.isSecretStorageReady()).resolves.toBe(false); await expect(rustCrypto.isSecretStorageReady()).resolves.toBe(false);

View File

@@ -20,7 +20,7 @@ import type { ToDeviceBatch, ToDevicePayload } from "../models/ToDeviceMessage.t
import { type Room } from "../models/room.ts"; import { type Room } from "../models/room.ts";
import { type DeviceMap } from "../models/device.ts"; import { type DeviceMap } from "../models/device.ts";
import { type UIAuthCallback } from "../interactive-auth.ts"; import { type UIAuthCallback } from "../interactive-auth.ts";
import { type PassphraseInfo, type SecretStorageKeyDescription } from "../secret-storage.ts"; import { type PassphraseInfo, type SecretStorageKey, type SecretStorageKeyDescription } from "../secret-storage.ts";
import { type VerificationRequest } from "./verification.ts"; import { type VerificationRequest } from "./verification.ts";
import { import {
type BackupTrustInfo, type BackupTrustInfo,
@@ -369,6 +369,11 @@ export interface CryptoApi {
*/ */
isSecretStorageReady(): Promise<boolean>; isSecretStorageReady(): Promise<boolean>;
/**
* Inspect the status of secret storage, in more detail than {@link isSecretStorageReady}.
*/
getSecretStorageStatus(): Promise<SecretStorageStatus>;
/** /**
* Bootstrap [secret storage](https://spec.matrix.org/v1.12/client-server-api/#storage). * Bootstrap [secret storage](https://spec.matrix.org/v1.12/client-server-api/#storage).
* *
@@ -1148,6 +1153,30 @@ export interface CryptoCallbacks {
cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void; cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void;
} }
/**
* The result of a call to {@link CryptoApi.getSecretStorageStatus}.
*/
export interface SecretStorageStatus {
/** Whether secret storage is fully populated. The same as {@link CryptoApi.isSecretStorageReady}. */
ready: boolean;
/** The ID of the current default secret storage key. */
defaultKeyId: string | null;
/**
* For each secret that we checked whether it is correctly stored in secret storage with the default secret storage key.
*
* Note that we will only check that the key backup key is stored if key backup is currently enabled (i.e. that
* {@link CryptoApi.getActiveSessionBackupVersion} returns non-null). `m.megolm_backup.v1` will only be present in that case.
*
* (This is an object rather than a `Map` so that it JSON.stringify()s nicely, since its main purpose is to end up
* in logs.)
*/
secretStorageKeyValidityMap: {
[P in SecretStorageKey]?: boolean;
};
}
/** /**
* Parameter of {@link CryptoApi#bootstrapSecretStorage} * Parameter of {@link CryptoApi#bootstrapSecretStorage}
*/ */

View File

@@ -65,6 +65,7 @@ import {
type KeyBackupRestoreOpts, type KeyBackupRestoreOpts,
type KeyBackupRestoreResult, type KeyBackupRestoreResult,
type OwnDeviceKeys, type OwnDeviceKeys,
type SecretStorageStatus,
type StartDehydrationOpts, type StartDehydrationOpts,
UserVerificationStatus, UserVerificationStatus,
type VerificationRequest, type VerificationRequest,
@@ -78,7 +79,7 @@ import {
type ServerSideSecretStorage, type ServerSideSecretStorage,
} from "../secret-storage.ts"; } from "../secret-storage.ts";
import { CrossSigningIdentity } from "./CrossSigningIdentity.ts"; import { CrossSigningIdentity } from "./CrossSigningIdentity.ts";
import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } from "./secret-storage.ts"; import { secretStorageContainsCrossSigningKeys } from "./secret-storage.ts";
import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts"; import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts";
import { EventType, MsgType } from "../@types/event.ts"; import { EventType, MsgType } from "../@types/event.ts";
import { TypedEventEmitter } from "../models/typed-event-emitter.ts"; import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
@@ -827,6 +828,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Implementation of {@link CryptoApi#isSecretStorageReady} * Implementation of {@link CryptoApi#isSecretStorageReady}
*/ */
public async isSecretStorageReady(): Promise<boolean> { public async isSecretStorageReady(): Promise<boolean> {
return (await this.getSecretStorageStatus()).ready;
}
/**
* Implementation of {@link CryptoApi#getSecretStorageStatus}
*/
public async getSecretStorageStatus(): Promise<SecretStorageStatus> {
// make sure that the cross-signing keys are stored // make sure that the cross-signing keys are stored
const secretsToCheck: SecretStorageKey[] = [ const secretsToCheck: SecretStorageKey[] = [
"m.cross_signing.master", "m.cross_signing.master",
@@ -834,13 +842,32 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
"m.cross_signing.self_signing", "m.cross_signing.self_signing",
]; ];
// if key backup is active, we also need to check that the backup decryption key is stored // If key backup is active, we also need to check that the backup decryption key is stored
const keyBackupEnabled = (await this.backupManager.getActiveBackupVersion()) != null; const keyBackupEnabled = (await this.backupManager.getActiveBackupVersion()) != null;
if (keyBackupEnabled) { if (keyBackupEnabled) {
secretsToCheck.push("m.megolm_backup.v1"); secretsToCheck.push("m.megolm_backup.v1");
} }
return secretStorageCanAccessSecrets(this.secretStorage, secretsToCheck); const defaultKeyId = await this.secretStorage.getDefaultKeyId();
const result: SecretStorageStatus = {
// Assume we have all secrets until proven otherwise
ready: true,
defaultKeyId,
secretStorageKeyValidityMap: {},
};
for (const secretName of secretsToCheck) {
// Check which keys this particular secret is encrypted with
const record = (await this.secretStorage.isStored(secretName)) || {};
// If it's encrypted with the right key, it is valid
const secretStored = !!defaultKeyId && defaultKeyId in record;
result.secretStorageKeyValidityMap[secretName] = secretStored;
result.ready = result.ready && secretStored;
}
return result;
} }
/** /**