You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-25 05:23:13 +03:00
Implement isSecretStorageReady in rust (#3730)
* Implement isSecretStorageReady in rust * refactor extract common code to check 4S access * fix incomplete mocks * code review * Remove keyId para from secretStorageCanAccessSecrets * use map instead of array * code review
This commit is contained in:
@@ -39,6 +39,7 @@ import { TestClient } from "../../TestClient";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
Category,
|
||||
ClientEvent,
|
||||
createClient,
|
||||
CryptoEvent,
|
||||
IClaimOTKsResult,
|
||||
@@ -68,7 +69,7 @@ import {
|
||||
mockSetupCrossSigningRequests,
|
||||
mockSetupMegolmBackupRequests,
|
||||
} from "../../test-utils/mockEndpoints";
|
||||
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
|
||||
import { CrossSigningKey, CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { DecryptionError } from "../../../src/crypto/algorithms";
|
||||
@@ -2283,6 +2284,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
|
||||
describe("Secret Storage and Key Backup", () => {
|
||||
/**
|
||||
* The account data events to be returned by the sync.
|
||||
* Will be updated when fecthMock intercepts calls to PUT `/_matrix/client/v3/user/:userId/account_data/`.
|
||||
* Will be used by `sendSyncResponseWithUpdatedAccountData`
|
||||
*/
|
||||
let accountDataEvents: Map<String, any>;
|
||||
|
||||
/**
|
||||
* Create a fake secret storage key
|
||||
* Async because `bootstrapSecretStorage` expect an async method
|
||||
@@ -2294,11 +2302,35 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
beforeEach(async () => {
|
||||
createSecretStorageKey.mockClear();
|
||||
|
||||
accountDataEvents = new Map();
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
});
|
||||
|
||||
function mockGetAccountData() {
|
||||
fetchMock.get(
|
||||
`path:/_matrix/client/v3/user/:userId/account_data/:type`,
|
||||
(url) => {
|
||||
const type = url.split("/").pop();
|
||||
const existing = accountDataEvents.get(type!);
|
||||
if (existing) {
|
||||
// return it
|
||||
return {
|
||||
status: 200,
|
||||
body: existing.content,
|
||||
};
|
||||
} else {
|
||||
// 404
|
||||
return {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock to respond to the PUT request `/_matrix/client/v3/user/:userId/account_data/m.cross_signing.${key}`
|
||||
* Resolved when the cross signing key is uploaded
|
||||
@@ -2311,6 +2343,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/m.cross_signing.${key}`,
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const type = url.split("/").pop();
|
||||
// update account data for sync response
|
||||
accountDataEvents.set(type!, content);
|
||||
resolve(content.encrypted);
|
||||
return {};
|
||||
},
|
||||
@@ -2319,32 +2354,24 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Send in the sync response the provided `secretStorageKey` into the account_data field
|
||||
* The key is set for the `m.secret_storage.default_key` and `m.secret_storage.key.${secretStorageKey}` events
|
||||
* https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv3sync
|
||||
* @param secretStorageKey
|
||||
* Send in the sync response the current account data events, as stored by `accountDataEvents`.
|
||||
*/
|
||||
function sendSyncResponse(secretStorageKey: string) {
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
account_data: {
|
||||
events: [
|
||||
{
|
||||
type: "m.secret_storage.default_key",
|
||||
content: {
|
||||
key: secretStorageKey,
|
||||
},
|
||||
},
|
||||
// Needed for secretStorage.getKey or secretStorage.hasKey
|
||||
{
|
||||
type: `m.secret_storage.key.${secretStorageKey}`,
|
||||
content: {
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
function sendSyncResponseWithUpdatedAccountData() {
|
||||
try {
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
account_data: {
|
||||
events: Array.from(accountDataEvents, ([type, content]) => ({
|
||||
type: type,
|
||||
content: content,
|
||||
})),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
// Might fail with "Cannot queue more than one /sync response" if called too often.
|
||||
// It's ok if it fails here, the sync response is cumulative and will contain
|
||||
// the latest account data.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2359,10 +2386,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
fetchMock.put(
|
||||
"express:/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)",
|
||||
(url: string, options: RequestInit) => {
|
||||
const type = url.split("/").pop();
|
||||
const content = JSON.parse(options.body as string);
|
||||
|
||||
// update account data for sync response
|
||||
accountDataEvents.set(type!, content);
|
||||
|
||||
if (content.key) {
|
||||
resolve(content.key);
|
||||
}
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
return {};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
@@ -2377,6 +2410,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/m.megolm_backup.v1`,
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
// update account data for sync response
|
||||
accountDataEvents.set("m.megolm_backup.v1", content);
|
||||
resolve(content.encrypted);
|
||||
return {};
|
||||
},
|
||||
@@ -2385,6 +2420,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
}
|
||||
|
||||
function awaitAccountDataUpdate(type: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
aliceClient.on(ClientEvent.AccountData, (ev: MatrixEvent): void => {
|
||||
if (ev.getType() === type) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all mocks needed to setup cross-signing, key backup, 4S and then
|
||||
* configure the account to have recovery.
|
||||
@@ -2422,32 +2467,41 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
|
||||
// Wait for the key to be uploaded in the account data
|
||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Wait for the cross signing keys to be uploaded
|
||||
await Promise.all(setupPromises);
|
||||
|
||||
// wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
|
||||
// Finally ensure backup is working
|
||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||
|
||||
await backupStatusUpdate;
|
||||
}
|
||||
|
||||
describe("bootstrapSecretStorage", () => {
|
||||
// Doesn't work with legacy crypto, which will try to bootstrap even without private key, which is buggy.
|
||||
newBackendOnly(
|
||||
"should throw an error if we are unable to create a key because createSecretStorageKey is not set",
|
||||
async () => {
|
||||
await expect(
|
||||
aliceClient.getCrypto()!.bootstrapSecretStorage({ setupNewSecretStorage: true }),
|
||||
).rejects.toThrow("unable to create a new secret storage key, createSecretStorageKey is not set");
|
||||
|
||||
expect(await aliceClient.getCrypto()!.isSecretStorageReady()).toStrictEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("should create a new key", async () => {
|
||||
it("Should create a 4S key", async () => {
|
||||
mockGetAccountData();
|
||||
|
||||
const awaitAccountData = awaitAccountDataUpdate("m.secret_storage.default_key");
|
||||
|
||||
const bootstrapPromise = aliceClient
|
||||
.getCrypto()!
|
||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||
@@ -2456,11 +2510,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
|
||||
// Finally, wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
|
||||
// await account data updated before getting default key.
|
||||
await awaitAccountData;
|
||||
|
||||
const defaultKeyId = await aliceClient.secretStorage.getDefaultKeyId();
|
||||
// Check that the uploaded key in stored in the secret storage
|
||||
expect(await aliceClient.secretStorage.hasKey(secretStorageKey)).toBeTruthy();
|
||||
@@ -2468,29 +2525,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(defaultKeyId).toBe(secretStorageKey);
|
||||
});
|
||||
|
||||
newBackendOnly(
|
||||
"should do nothing if an AES key is already in the secret storage and setupNewSecretStorage is not set",
|
||||
async () => {
|
||||
const bootstrapPromise = aliceClient
|
||||
.getCrypto()!
|
||||
.bootstrapSecretStorage({ createSecretStorageKey });
|
||||
it("should do nothing if an AES key is already in the secret storage and setupNewSecretStorage is not set", async () => {
|
||||
const awaitAccountDataClientUpdate = awaitAccountDataUpdate("m.secret_storage.default_key");
|
||||
|
||||
// Wait for the key to be uploaded in the account data
|
||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
const bootstrapPromise = aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
// Wait for the key to be uploaded in the account data
|
||||
await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
|
||||
// Call again bootstrapSecretStorage
|
||||
await aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
|
||||
// Wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
|
||||
// createSecretStorageKey should be called only on the first run of bootstrapSecretStorage
|
||||
expect(createSecretStorageKey).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
);
|
||||
// On legacy crypto we need to wait for ClientEvent.AccountData before calling bootstrap again.
|
||||
await awaitAccountDataClientUpdate;
|
||||
|
||||
// Call again bootstrapSecretStorage
|
||||
await aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
|
||||
|
||||
// createSecretStorageKey should be called only on the first run of bootstrapSecretStorage
|
||||
expect(createSecretStorageKey).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage", async () => {
|
||||
let bootstrapPromise = aliceClient
|
||||
@@ -2498,10 +2555,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||
|
||||
// Wait for the key to be uploaded in the account data
|
||||
let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
|
||||
// Wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
@@ -2512,10 +2569,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||
|
||||
// Wait for the key to be uploaded in the account data
|
||||
secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
|
||||
// Wait for bootstrapSecretStorage to finished
|
||||
await bootstrapPromise;
|
||||
@@ -2539,7 +2596,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||
|
||||
// Return the newly created key in the sync response
|
||||
sendSyncResponse(secretStorageKey);
|
||||
sendSyncResponseWithUpdatedAccountData();
|
||||
|
||||
// Wait for the cross signing keys to be uploaded
|
||||
const [masterKey, userSigningKey, selfSigningKey] = await Promise.all([
|
||||
@@ -2561,6 +2618,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const backupVersion = "abc";
|
||||
await bootstrapSecurity(backupVersion);
|
||||
|
||||
expect(await aliceClient.getCrypto()!.isSecretStorageReady()).toStrictEqual(true);
|
||||
|
||||
// Expect a backup to be available and used
|
||||
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
|
||||
expect(activeBackup).toStrictEqual(backupVersion);
|
||||
@@ -2596,7 +2655,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
{
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
@@ -212,6 +212,7 @@ describe("RustCrypto", () => {
|
||||
it("returns sensible values on a default client", async () => {
|
||||
const secretStorage = {
|
||||
isStored: jest.fn().mockResolvedValue(null),
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue("key"),
|
||||
} as unknown as Mocked<ServerSideSecretStorage>;
|
||||
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
|
||||
|
||||
@@ -232,6 +233,7 @@ describe("RustCrypto", () => {
|
||||
it("throws if `stop` is called mid-call", async () => {
|
||||
const secretStorage = {
|
||||
isStored: jest.fn().mockResolvedValue(null),
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue(null),
|
||||
} as unknown as Mocked<ServerSideSecretStorage>;
|
||||
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
|
||||
|
||||
@@ -258,7 +260,10 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
|
||||
it("isSecretStorageReady", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
const mockSecretStorage = {
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue(null),
|
||||
} as unknown as Mocked<ServerSideSecretStorage>;
|
||||
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, mockSecretStorage);
|
||||
await expect(rustCrypto.isSecretStorageReady()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { secretStorageContainsCrossSigningKeys } from "../../../src/rust-crypto/secret-storage";
|
||||
import {
|
||||
secretStorageCanAccessSecrets,
|
||||
secretStorageContainsCrossSigningKeys,
|
||||
} from "../../../src/rust-crypto/secret-storage";
|
||||
import { ServerSideSecretStorage } from "../../../src/secret-storage";
|
||||
|
||||
describe("secret-storage", () => {
|
||||
@@ -22,6 +25,7 @@ describe("secret-storage", () => {
|
||||
it("should return false when the master cross-signing key is not stored in secret storage", async () => {
|
||||
const secretStorage = {
|
||||
isStored: jest.fn().mockReturnValue(false),
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue("SFQ3TbqGOdaaRVfxHtNkn0tvhx0rVj9S"),
|
||||
} as unknown as ServerSideSecretStorage;
|
||||
|
||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||
@@ -35,6 +39,7 @@ describe("secret-storage", () => {
|
||||
if (type === "m.cross_signing.master") return { secretStorageKey: {} };
|
||||
else return { secretStorageKey2: {} };
|
||||
},
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue("SFQ3TbqGOdaaRVfxHtNkn0tvhx0rVj9S"),
|
||||
} as unknown as ServerSideSecretStorage;
|
||||
|
||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||
@@ -51,19 +56,73 @@ describe("secret-storage", () => {
|
||||
return { secretStorageKey2: {} };
|
||||
}
|
||||
},
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue("secretStorageKey"),
|
||||
} as unknown as ServerSideSecretStorage;
|
||||
|
||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true when there is shared secret storage key between master, user signing and self signing keys", async () => {
|
||||
it("should return true when master, user signing and self signing keys are all encrypted with default key", async () => {
|
||||
const secretStorage = {
|
||||
isStored: jest.fn().mockReturnValue({ secretStorageKey: {} }),
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue("secretStorageKey"),
|
||||
} as unknown as ServerSideSecretStorage;
|
||||
|
||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false when master, user signing and self signing keys are all encrypted with a non-default key", async () => {
|
||||
const secretStorage = {
|
||||
isStored: jest.fn().mockResolvedValue({ defaultKey: {} }),
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue("anotherCommonKey"),
|
||||
} as unknown as ServerSideSecretStorage;
|
||||
|
||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("Check canAccessSecrets", async () => {
|
||||
const secretStorage = {
|
||||
isStored: jest.fn((secretName) => {
|
||||
if (secretName == "secretA") {
|
||||
return { aaaa: {} };
|
||||
} else if (secretName == "secretB") {
|
||||
return { bbbb: {} };
|
||||
} else if (secretName == "secretC") {
|
||||
return { cccc: {} };
|
||||
} else if (secretName == "secretD") {
|
||||
return { aaaa: {} };
|
||||
} else if (secretName == "secretE") {
|
||||
return { aaaa: {}, bbbb: {} };
|
||||
} else {
|
||||
null;
|
||||
}
|
||||
}),
|
||||
getDefaultKeyId: jest.fn().mockResolvedValue("aaaa"),
|
||||
} as unknown as ServerSideSecretStorage;
|
||||
|
||||
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretE"])).toStrictEqual(true);
|
||||
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA"])).toStrictEqual(true);
|
||||
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretC"])).toStrictEqual(false);
|
||||
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretD"])).toStrictEqual(true);
|
||||
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretC"])).toStrictEqual(false);
|
||||
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretC", "secretA"])).toStrictEqual(false);
|
||||
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretD", "secretB"])).toStrictEqual(
|
||||
false,
|
||||
);
|
||||
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretD", "Unknown"])).toStrictEqual(
|
||||
false,
|
||||
);
|
||||
|
||||
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretD", "secretE"])).toStrictEqual(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretC", "secretD", "secretE"]),
|
||||
).toStrictEqual(false);
|
||||
expect(await secretStorageCanAccessSecrets(secretStorage, [])).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -480,7 +480,6 @@ export class BackupManager {
|
||||
const delay = Math.random() * maxDelay;
|
||||
await sleep(delay);
|
||||
if (!this.clientRunning) {
|
||||
logger.debug("Key backup send aborted, client stopped");
|
||||
this.sendingBackups = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
|
||||
import { Device, DeviceMap } from "../models/device";
|
||||
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../secret-storage";
|
||||
import { CrossSigningIdentity } from "./CrossSigningIdentity";
|
||||
import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
|
||||
import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } from "./secret-storage";
|
||||
import { keyFromPassphrase } from "../crypto/key_passphrase";
|
||||
import { encodeRecoveryKey } from "../crypto/recoverykey";
|
||||
import { crypto } from "../crypto/crypto";
|
||||
@@ -623,7 +623,20 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
* Implementation of {@link CryptoApi#isSecretStorageReady}
|
||||
*/
|
||||
public async isSecretStorageReady(): Promise<boolean> {
|
||||
return false;
|
||||
// make sure that the cross-signing keys are stored
|
||||
const secretsToCheck = [
|
||||
"m.cross_signing.master",
|
||||
"m.cross_signing.user_signing",
|
||||
"m.cross_signing.self_signing",
|
||||
];
|
||||
|
||||
// if key backup is active, we also need to check that the backup decryption key is stored
|
||||
const keyBackupEnabled = (await this.backupManager.getActiveBackupVersion()) != null;
|
||||
if (keyBackupEnabled) {
|
||||
secretsToCheck.push("m.megolm_backup.v1");
|
||||
}
|
||||
|
||||
return secretStorageCanAccessSecrets(this.secretStorage, secretsToCheck);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import { ServerSideSecretStorage } from "../secret-storage";
|
||||
|
||||
/**
|
||||
* Check that the private cross signing keys (master, self signing, user signing) are stored in the secret storage and encrypted with the same secret storage key.
|
||||
* Check that the private cross signing keys (master, self signing, user signing) are stored in the secret storage and encrypted with the default secret storage key.
|
||||
*
|
||||
* @param secretStorage - The secret store using account data
|
||||
* @returns True if the cross-signing keys are all stored and encrypted with the same secret storage key.
|
||||
@@ -25,20 +25,36 @@ import { ServerSideSecretStorage } from "../secret-storage";
|
||||
* @internal
|
||||
*/
|
||||
export async function secretStorageContainsCrossSigningKeys(secretStorage: ServerSideSecretStorage): Promise<boolean> {
|
||||
// Check if the master cross-signing key is stored in secret storage
|
||||
const secretStorageMasterKeys = await secretStorage.isStored("m.cross_signing.master");
|
||||
|
||||
// Master key not stored
|
||||
if (!secretStorageMasterKeys) return false;
|
||||
|
||||
// Get the user signing keys stored into the secret storage
|
||||
const secretStorageUserSigningKeys = (await secretStorage.isStored(`m.cross_signing.user_signing`)) || {};
|
||||
// Get the self signing keys stored into the secret storage
|
||||
const secretStorageSelfSigningKeys = (await secretStorage.isStored(`m.cross_signing.self_signing`)) || {};
|
||||
|
||||
// Check that one of the secret storage keys used to encrypt the master key was also used to encrypt the user-signing and self-signing keys
|
||||
return Object.keys(secretStorageMasterKeys).some(
|
||||
(secretStorageKey) =>
|
||||
secretStorageUserSigningKeys[secretStorageKey] && secretStorageSelfSigningKeys[secretStorageKey],
|
||||
);
|
||||
return secretStorageCanAccessSecrets(secretStorage, [
|
||||
"m.cross_signing.master",
|
||||
"m.cross_signing.user_signing",
|
||||
"m.cross_signing.self_signing",
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Check that the secret storage can access the given secrets using the default key.
|
||||
*
|
||||
* @param secretStorage - The secret store using account data
|
||||
* @param secretNames - The secret names to check
|
||||
* @returns True if all the given secrets are accessible and encrypted with the given key.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export async function secretStorageCanAccessSecrets(
|
||||
secretStorage: ServerSideSecretStorage,
|
||||
secretNames: string[],
|
||||
): Promise<boolean> {
|
||||
const defaultKeyId = await secretStorage.getDefaultKeyId();
|
||||
if (!defaultKeyId) return false;
|
||||
|
||||
for (const secretName of secretNames) {
|
||||
// check which keys this particular secret is encrypted with
|
||||
const record = (await secretStorage.isStored(secretName)) || {};
|
||||
// if it's not encrypted with the right key, there is no point continuing
|
||||
if (!(defaultKeyId in record)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user