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 { logger } from "../../../src/logger";
|
||||||
import {
|
import {
|
||||||
Category,
|
Category,
|
||||||
|
ClientEvent,
|
||||||
createClient,
|
createClient,
|
||||||
CryptoEvent,
|
CryptoEvent,
|
||||||
IClaimOTKsResult,
|
IClaimOTKsResult,
|
||||||
@@ -68,7 +69,7 @@ import {
|
|||||||
mockSetupCrossSigningRequests,
|
mockSetupCrossSigningRequests,
|
||||||
mockSetupMegolmBackupRequests,
|
mockSetupMegolmBackupRequests,
|
||||||
} from "../../test-utils/mockEndpoints";
|
} 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 { CrossSigningKey, CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api";
|
||||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||||
import { DecryptionError } from "../../../src/crypto/algorithms";
|
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", () => {
|
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
|
* Create a fake secret storage key
|
||||||
* Async because `bootstrapSecretStorage` expect an async method
|
* Async because `bootstrapSecretStorage` expect an async method
|
||||||
@@ -2294,11 +2302,35 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
createSecretStorageKey.mockClear();
|
createSecretStorageKey.mockClear();
|
||||||
|
accountDataEvents = new Map();
|
||||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||||
await startClientAndAwaitFirstSync();
|
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}`
|
* 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
|
* 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}`,
|
`express:/_matrix/client/v3/user/:userId/account_data/m.cross_signing.${key}`,
|
||||||
(url: string, options: RequestInit) => {
|
(url: string, options: RequestInit) => {
|
||||||
const content = JSON.parse(options.body as string);
|
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);
|
resolve(content.encrypted);
|
||||||
return {};
|
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
|
* Send in the sync response the current account data events, as stored by `accountDataEvents`.
|
||||||
* 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
|
|
||||||
*/
|
*/
|
||||||
function sendSyncResponse(secretStorageKey: string) {
|
function sendSyncResponseWithUpdatedAccountData() {
|
||||||
syncResponder.sendOrQueueSyncResponse({
|
try {
|
||||||
next_batch: 1,
|
syncResponder.sendOrQueueSyncResponse({
|
||||||
account_data: {
|
next_batch: 1,
|
||||||
events: [
|
account_data: {
|
||||||
{
|
events: Array.from(accountDataEvents, ([type, content]) => ({
|
||||||
type: "m.secret_storage.default_key",
|
type: type,
|
||||||
content: {
|
content: content,
|
||||||
key: secretStorageKey,
|
})),
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
// Needed for secretStorage.getKey or secretStorage.hasKey
|
} catch (err) {
|
||||||
{
|
// Might fail with "Cannot queue more than one /sync response" if called too often.
|
||||||
type: `m.secret_storage.key.${secretStorageKey}`,
|
// It's ok if it fails here, the sync response is cumulative and will contain
|
||||||
content: {
|
// the latest account data.
|
||||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2359,10 +2386,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
fetchMock.put(
|
fetchMock.put(
|
||||||
"express:/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)",
|
"express:/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)",
|
||||||
(url: string, options: RequestInit) => {
|
(url: string, options: RequestInit) => {
|
||||||
|
const type = url.split("/").pop();
|
||||||
const content = JSON.parse(options.body as string);
|
const content = JSON.parse(options.body as string);
|
||||||
|
|
||||||
|
// update account data for sync response
|
||||||
|
accountDataEvents.set(type!, content);
|
||||||
|
|
||||||
if (content.key) {
|
if (content.key) {
|
||||||
resolve(content.key);
|
resolve(content.key);
|
||||||
}
|
}
|
||||||
|
sendSyncResponseWithUpdatedAccountData();
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
{ overwriteRoutes: true },
|
{ 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`,
|
`express:/_matrix/client/v3/user/:userId/account_data/m.megolm_backup.v1`,
|
||||||
(url: string, options: RequestInit) => {
|
(url: string, options: RequestInit) => {
|
||||||
const content = JSON.parse(options.body as string);
|
const content = JSON.parse(options.body as string);
|
||||||
|
// update account data for sync response
|
||||||
|
accountDataEvents.set("m.megolm_backup.v1", content);
|
||||||
resolve(content.encrypted);
|
resolve(content.encrypted);
|
||||||
return {};
|
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
|
* Add all mocks needed to setup cross-signing, key backup, 4S and then
|
||||||
* configure the account to have recovery.
|
* 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
|
// Wait for the key to be uploaded in the account data
|
||||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
await awaitSecretStorageKeyStoredInAccountData();
|
||||||
|
|
||||||
// Return the newly created key in the sync response
|
|
||||||
sendSyncResponse(secretStorageKey);
|
|
||||||
|
|
||||||
// Wait for the cross signing keys to be uploaded
|
// Wait for the cross signing keys to be uploaded
|
||||||
await Promise.all(setupPromises);
|
await Promise.all(setupPromises);
|
||||||
|
|
||||||
// wait for bootstrapSecretStorage to finished
|
// wait for bootstrapSecretStorage to finished
|
||||||
await bootstrapPromise;
|
await bootstrapPromise;
|
||||||
|
|
||||||
|
// Return the newly created key in the sync response
|
||||||
|
sendSyncResponseWithUpdatedAccountData();
|
||||||
|
|
||||||
// Finally ensure backup is working
|
// Finally ensure backup is working
|
||||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||||
|
|
||||||
await backupStatusUpdate;
|
await backupStatusUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("bootstrapSecretStorage", () => {
|
describe("bootstrapSecretStorage", () => {
|
||||||
|
// Doesn't work with legacy crypto, which will try to bootstrap even without private key, which is buggy.
|
||||||
newBackendOnly(
|
newBackendOnly(
|
||||||
"should throw an error if we are unable to create a key because createSecretStorageKey is not set",
|
"should throw an error if we are unable to create a key because createSecretStorageKey is not set",
|
||||||
async () => {
|
async () => {
|
||||||
await expect(
|
await expect(
|
||||||
aliceClient.getCrypto()!.bootstrapSecretStorage({ setupNewSecretStorage: true }),
|
aliceClient.getCrypto()!.bootstrapSecretStorage({ setupNewSecretStorage: true }),
|
||||||
).rejects.toThrow("unable to create a new secret storage key, createSecretStorageKey is not set");
|
).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
|
const bootstrapPromise = aliceClient
|
||||||
.getCrypto()!
|
.getCrypto()!
|
||||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||||
@@ -2456,11 +2510,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||||
|
|
||||||
// Return the newly created key in the sync response
|
// Return the newly created key in the sync response
|
||||||
sendSyncResponse(secretStorageKey);
|
sendSyncResponseWithUpdatedAccountData();
|
||||||
|
|
||||||
// Finally, wait for bootstrapSecretStorage to finished
|
// Finally, wait for bootstrapSecretStorage to finished
|
||||||
await bootstrapPromise;
|
await bootstrapPromise;
|
||||||
|
|
||||||
|
// await account data updated before getting default key.
|
||||||
|
await awaitAccountData;
|
||||||
|
|
||||||
const defaultKeyId = await aliceClient.secretStorage.getDefaultKeyId();
|
const defaultKeyId = await aliceClient.secretStorage.getDefaultKeyId();
|
||||||
// Check that the uploaded key in stored in the secret storage
|
// Check that the uploaded key in stored in the secret storage
|
||||||
expect(await aliceClient.secretStorage.hasKey(secretStorageKey)).toBeTruthy();
|
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);
|
expect(defaultKeyId).toBe(secretStorageKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
newBackendOnly(
|
it("should do nothing if an AES key is already in the secret storage and setupNewSecretStorage is not set", async () => {
|
||||||
"should do nothing if an AES key is already in the secret storage and setupNewSecretStorage is not set",
|
const awaitAccountDataClientUpdate = awaitAccountDataUpdate("m.secret_storage.default_key");
|
||||||
async () => {
|
|
||||||
const bootstrapPromise = aliceClient
|
|
||||||
.getCrypto()!
|
|
||||||
.bootstrapSecretStorage({ createSecretStorageKey });
|
|
||||||
|
|
||||||
// Wait for the key to be uploaded in the account data
|
const bootstrapPromise = aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
|
||||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
|
||||||
|
|
||||||
// Return the newly created key in the sync response
|
// Wait for the key to be uploaded in the account data
|
||||||
sendSyncResponse(secretStorageKey);
|
await awaitSecretStorageKeyStoredInAccountData();
|
||||||
|
|
||||||
// Wait for bootstrapSecretStorage to finished
|
// Return the newly created key in the sync response
|
||||||
await bootstrapPromise;
|
sendSyncResponseWithUpdatedAccountData();
|
||||||
|
|
||||||
// Call again bootstrapSecretStorage
|
// Wait for bootstrapSecretStorage to finished
|
||||||
await aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
|
await bootstrapPromise;
|
||||||
|
|
||||||
// createSecretStorageKey should be called only on the first run of bootstrapSecretStorage
|
// On legacy crypto we need to wait for ClientEvent.AccountData before calling bootstrap again.
|
||||||
expect(createSecretStorageKey).toHaveBeenCalledTimes(1);
|
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 () => {
|
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
|
let bootstrapPromise = aliceClient
|
||||||
@@ -2498,10 +2555,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||||
|
|
||||||
// Wait for the key to be uploaded in the account data
|
// 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
|
// Return the newly created key in the sync response
|
||||||
sendSyncResponse(secretStorageKey);
|
sendSyncResponseWithUpdatedAccountData();
|
||||||
|
|
||||||
// Wait for bootstrapSecretStorage to finished
|
// Wait for bootstrapSecretStorage to finished
|
||||||
await bootstrapPromise;
|
await bootstrapPromise;
|
||||||
@@ -2512,10 +2569,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||||
|
|
||||||
// Wait for the key to be uploaded in the account data
|
// 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
|
// Return the newly created key in the sync response
|
||||||
sendSyncResponse(secretStorageKey);
|
sendSyncResponseWithUpdatedAccountData();
|
||||||
|
|
||||||
// Wait for bootstrapSecretStorage to finished
|
// Wait for bootstrapSecretStorage to finished
|
||||||
await bootstrapPromise;
|
await bootstrapPromise;
|
||||||
@@ -2539,7 +2596,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
|
||||||
|
|
||||||
// Return the newly created key in the sync response
|
// Return the newly created key in the sync response
|
||||||
sendSyncResponse(secretStorageKey);
|
sendSyncResponseWithUpdatedAccountData();
|
||||||
|
|
||||||
// Wait for the cross signing keys to be uploaded
|
// Wait for the cross signing keys to be uploaded
|
||||||
const [masterKey, userSigningKey, selfSigningKey] = await Promise.all([
|
const [masterKey, userSigningKey, selfSigningKey] = await Promise.all([
|
||||||
@@ -2561,6 +2618,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
const backupVersion = "abc";
|
const backupVersion = "abc";
|
||||||
await bootstrapSecurity(backupVersion);
|
await bootstrapSecurity(backupVersion);
|
||||||
|
|
||||||
|
expect(await aliceClient.getCrypto()!.isSecretStorageReady()).toStrictEqual(true);
|
||||||
|
|
||||||
// Expect a backup to be available and used
|
// Expect a backup to be available and used
|
||||||
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
|
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
|
||||||
expect(activeBackup).toStrictEqual(backupVersion);
|
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",
|
"path:/_matrix/client/v3/room_keys/version",
|
||||||
{
|
{
|
||||||
status: 404,
|
status: 404,
|
||||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||||
},
|
},
|
||||||
{ overwriteRoutes: true },
|
{ overwriteRoutes: true },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ describe("RustCrypto", () => {
|
|||||||
it("returns sensible values on a default client", async () => {
|
it("returns sensible values on a default client", async () => {
|
||||||
const secretStorage = {
|
const secretStorage = {
|
||||||
isStored: jest.fn().mockResolvedValue(null),
|
isStored: jest.fn().mockResolvedValue(null),
|
||||||
|
getDefaultKeyId: jest.fn().mockResolvedValue("key"),
|
||||||
} as unknown as Mocked<ServerSideSecretStorage>;
|
} as unknown as Mocked<ServerSideSecretStorage>;
|
||||||
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
|
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
|
||||||
|
|
||||||
@@ -232,6 +233,7 @@ describe("RustCrypto", () => {
|
|||||||
it("throws if `stop` is called mid-call", async () => {
|
it("throws if `stop` is called mid-call", async () => {
|
||||||
const secretStorage = {
|
const secretStorage = {
|
||||||
isStored: jest.fn().mockResolvedValue(null),
|
isStored: jest.fn().mockResolvedValue(null),
|
||||||
|
getDefaultKeyId: jest.fn().mockResolvedValue(null),
|
||||||
} as unknown as Mocked<ServerSideSecretStorage>;
|
} as unknown as Mocked<ServerSideSecretStorage>;
|
||||||
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
|
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
|
||||||
|
|
||||||
@@ -258,7 +260,10 @@ describe("RustCrypto", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("isSecretStorageReady", async () => {
|
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);
|
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.
|
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";
|
import { ServerSideSecretStorage } from "../../../src/secret-storage";
|
||||||
|
|
||||||
describe("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 () => {
|
it("should return false when the master cross-signing key is not stored in secret storage", async () => {
|
||||||
const secretStorage = {
|
const secretStorage = {
|
||||||
isStored: jest.fn().mockReturnValue(false),
|
isStored: jest.fn().mockReturnValue(false),
|
||||||
|
getDefaultKeyId: jest.fn().mockResolvedValue("SFQ3TbqGOdaaRVfxHtNkn0tvhx0rVj9S"),
|
||||||
} as unknown as ServerSideSecretStorage;
|
} as unknown as ServerSideSecretStorage;
|
||||||
|
|
||||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||||
@@ -35,6 +39,7 @@ describe("secret-storage", () => {
|
|||||||
if (type === "m.cross_signing.master") return { secretStorageKey: {} };
|
if (type === "m.cross_signing.master") return { secretStorageKey: {} };
|
||||||
else return { secretStorageKey2: {} };
|
else return { secretStorageKey2: {} };
|
||||||
},
|
},
|
||||||
|
getDefaultKeyId: jest.fn().mockResolvedValue("SFQ3TbqGOdaaRVfxHtNkn0tvhx0rVj9S"),
|
||||||
} as unknown as ServerSideSecretStorage;
|
} as unknown as ServerSideSecretStorage;
|
||||||
|
|
||||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||||
@@ -51,19 +56,73 @@ describe("secret-storage", () => {
|
|||||||
return { secretStorageKey2: {} };
|
return { secretStorageKey2: {} };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getDefaultKeyId: jest.fn().mockResolvedValue("secretStorageKey"),
|
||||||
} as unknown as ServerSideSecretStorage;
|
} as unknown as ServerSideSecretStorage;
|
||||||
|
|
||||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||||
expect(result).toBeFalsy();
|
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 = {
|
const secretStorage = {
|
||||||
isStored: jest.fn().mockReturnValue({ secretStorageKey: {} }),
|
isStored: jest.fn().mockReturnValue({ secretStorageKey: {} }),
|
||||||
|
getDefaultKeyId: jest.fn().mockResolvedValue("secretStorageKey"),
|
||||||
} as unknown as ServerSideSecretStorage;
|
} as unknown as ServerSideSecretStorage;
|
||||||
|
|
||||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||||
expect(result).toBeTruthy();
|
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;
|
const delay = Math.random() * maxDelay;
|
||||||
await sleep(delay);
|
await sleep(delay);
|
||||||
if (!this.clientRunning) {
|
if (!this.clientRunning) {
|
||||||
logger.debug("Key backup send aborted, client stopped");
|
|
||||||
this.sendingBackups = false;
|
this.sendingBackups = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
|
|||||||
import { Device, DeviceMap } from "../models/device";
|
import { Device, DeviceMap } from "../models/device";
|
||||||
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../secret-storage";
|
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../secret-storage";
|
||||||
import { CrossSigningIdentity } from "./CrossSigningIdentity";
|
import { CrossSigningIdentity } from "./CrossSigningIdentity";
|
||||||
import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
|
import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } from "./secret-storage";
|
||||||
import { keyFromPassphrase } from "../crypto/key_passphrase";
|
import { keyFromPassphrase } from "../crypto/key_passphrase";
|
||||||
import { encodeRecoveryKey } from "../crypto/recoverykey";
|
import { encodeRecoveryKey } from "../crypto/recoverykey";
|
||||||
import { crypto } from "../crypto/crypto";
|
import { crypto } from "../crypto/crypto";
|
||||||
@@ -623,7 +623,20 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
|||||||
* Implementation of {@link CryptoApi#isSecretStorageReady}
|
* Implementation of {@link CryptoApi#isSecretStorageReady}
|
||||||
*/
|
*/
|
||||||
public async isSecretStorageReady(): Promise<boolean> {
|
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";
|
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
|
* @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.
|
* @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
|
* @internal
|
||||||
*/
|
*/
|
||||||
export async function secretStorageContainsCrossSigningKeys(secretStorage: ServerSideSecretStorage): Promise<boolean> {
|
export async function secretStorageContainsCrossSigningKeys(secretStorage: ServerSideSecretStorage): Promise<boolean> {
|
||||||
// Check if the master cross-signing key is stored in secret storage
|
return secretStorageCanAccessSecrets(secretStorage, [
|
||||||
const secretStorageMasterKeys = await secretStorage.isStored("m.cross_signing.master");
|
"m.cross_signing.master",
|
||||||
|
"m.cross_signing.user_signing",
|
||||||
// Master key not stored
|
"m.cross_signing.self_signing",
|
||||||
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 the secret storage can access the given secrets using the default key.
|
||||||
|
*
|
||||||
// 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
|
* @param secretStorage - The secret store using account data
|
||||||
return Object.keys(secretStorageMasterKeys).some(
|
* @param secretNames - The secret names to check
|
||||||
(secretStorageKey) =>
|
* @returns True if all the given secrets are accessible and encrypted with the given key.
|
||||||
secretStorageUserSigningKeys[secretStorageKey] && secretStorageSelfSigningKeys[secretStorageKey],
|
*
|
||||||
);
|
* @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