You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
ElementR: Add CryptoApi#bootstrapSecretStorage (#3483)
* Add WIP bootstrapSecretStorage * Add new test if `createSecretStorageKey` is not set * Remove old comments * Add docs for `crypto-api.bootstrapSecretStorage` * Remove default parameter for `createSecretStorageKey` * Move `bootstrapSecretStorage` next to `isSecretStorageReady` * Deprecate `bootstrapSecretStorage` in `MatrixClient` * Update documentations * Raise error if missing `keyInfo` * Update behavior around `setupNewSecretStorage` * Move `ICreateSecretStorageOpts` to `rust-crypto` * Move `ICryptoCallbacks` to `rust-crypto` * Update `bootstrapSecretStorage` documentation * Add partial `CryptoCallbacks` documentation * Fix typo * Review changes * Review changes
This commit is contained in:
@@ -74,7 +74,7 @@ module.exports = {
|
|||||||
"jest/no-standalone-expect": [
|
"jest/no-standalone-expect": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
|
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly", "newBackendOnly"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { escapeRegExp } from "../../../src/utils";
|
|||||||
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
|
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
|
||||||
import { flushPromises } from "../../test-utils/flushPromises";
|
import { flushPromises } from "../../test-utils/flushPromises";
|
||||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||||
|
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||||
|
|
||||||
const ROOM_ID = "!room:id";
|
const ROOM_ID = "!room:id";
|
||||||
|
|
||||||
@@ -402,6 +403,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
|
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
|
||||||
// Rust backend. Once we have full support in the rust sdk, it will go away.
|
// Rust backend. Once we have full support in the rust sdk, it will go away.
|
||||||
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
|
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
|
||||||
|
const newBackendOnly = backend !== "rust-sdk" ? test.skip : test;
|
||||||
|
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
|
|
||||||
@@ -2169,4 +2171,163 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("bootstrapSecretStorage", () => {
|
||||||
|
/**
|
||||||
|
* Create a fake secret storage key
|
||||||
|
* Async because `bootstrapSecretStorage` expect an async method
|
||||||
|
*/
|
||||||
|
const createSecretStorageKey = jest.fn().mockResolvedValue({
|
||||||
|
keyInfo: {}, // Returning undefined here used to cause a crash
|
||||||
|
privateKey: Uint8Array.of(32, 33),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock to respond to the PUT request `/_matrix/client/r0/user/:userId/account_data/:type`
|
||||||
|
* Resolved when a key is uploaded (ie in `body.content.key`)
|
||||||
|
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
|
||||||
|
*/
|
||||||
|
function awaitKeyStoredInAccountData(): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// This url is called multiple times during the secret storage bootstrap process
|
||||||
|
// When we received the newly generated key, we return it
|
||||||
|
fetchMock.put(
|
||||||
|
"express:/_matrix/client/r0/user/:userId/account_data/:type",
|
||||||
|
(url: string, options: RequestInit) => {
|
||||||
|
const content = JSON.parse(options.body as string);
|
||||||
|
|
||||||
|
if (content.key) {
|
||||||
|
resolve(content.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
{ overwriteRoutes: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
function sendSyncResponse(secretStorageKey: string) {
|
||||||
|
syncResponder.sendOrQueueSyncResponse({
|
||||||
|
next_batch: 1,
|
||||||
|
account_data: {
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
type: "m.secret_storage.default_key",
|
||||||
|
content: {
|
||||||
|
key: secretStorageKey,
|
||||||
|
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Needed for secretStorage.getKey or secretStorage.hasKey
|
||||||
|
{
|
||||||
|
type: `m.secret_storage.key.${secretStorageKey}`,
|
||||||
|
content: {
|
||||||
|
key: secretStorageKey,
|
||||||
|
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
createSecretStorageKey.mockClear();
|
||||||
|
|
||||||
|
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||||
|
await startClientAndAwaitFirstSync();
|
||||||
|
});
|
||||||
|
|
||||||
|
newBackendOnly("should do no nothing if createSecretStorageKey is not set", async () => {
|
||||||
|
await aliceClient.getCrypto()!.bootstrapSecretStorage({ setupNewSecretStorage: true });
|
||||||
|
|
||||||
|
// No key was created
|
||||||
|
expect(createSecretStorageKey).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
newBackendOnly("should create a new key", async () => {
|
||||||
|
const bootstrapPromise = aliceClient
|
||||||
|
.getCrypto()!
|
||||||
|
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||||
|
|
||||||
|
// Wait for the key to be uploaded in the account data
|
||||||
|
const secretStorageKey = await awaitKeyStoredInAccountData();
|
||||||
|
|
||||||
|
// Return the newly created key in the sync response
|
||||||
|
sendSyncResponse(secretStorageKey);
|
||||||
|
|
||||||
|
// Finally, wait for bootstrapSecretStorage to finished
|
||||||
|
await bootstrapPromise;
|
||||||
|
|
||||||
|
const defaultKeyId = await aliceClient.secretStorage.getDefaultKeyId();
|
||||||
|
// Check that the uploaded key in stored in the secret storage
|
||||||
|
expect(await aliceClient.secretStorage.hasKey(secretStorageKey)).toBeTruthy();
|
||||||
|
// Check that the uploaded key is the default key
|
||||||
|
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 });
|
||||||
|
|
||||||
|
// Wait for the key to be uploaded in the account data
|
||||||
|
const secretStorageKey = await awaitKeyStoredInAccountData();
|
||||||
|
|
||||||
|
// Return the newly created key in the sync response
|
||||||
|
sendSyncResponse(secretStorageKey);
|
||||||
|
|
||||||
|
// Wait for bootstrapSecretStorage to finished
|
||||||
|
await bootstrapPromise;
|
||||||
|
|
||||||
|
// Call again bootstrapSecretStorage
|
||||||
|
await aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
|
||||||
|
|
||||||
|
// createSecretStorageKey should be called only on the first run of bootstrapSecretStorage
|
||||||
|
expect(createSecretStorageKey).toHaveBeenCalledTimes(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
newBackendOnly(
|
||||||
|
"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
|
||||||
|
.getCrypto()!
|
||||||
|
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||||
|
|
||||||
|
// Wait for the key to be uploaded in the account data
|
||||||
|
let secretStorageKey = await awaitKeyStoredInAccountData();
|
||||||
|
|
||||||
|
// Return the newly created key in the sync response
|
||||||
|
sendSyncResponse(secretStorageKey);
|
||||||
|
|
||||||
|
// Wait for bootstrapSecretStorage to finished
|
||||||
|
await bootstrapPromise;
|
||||||
|
|
||||||
|
// Call again bootstrapSecretStorage
|
||||||
|
bootstrapPromise = aliceClient
|
||||||
|
.getCrypto()!
|
||||||
|
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||||
|
|
||||||
|
// Wait for the key to be uploaded in the account data
|
||||||
|
secretStorageKey = await awaitKeyStoredInAccountData();
|
||||||
|
|
||||||
|
// Return the newly created key in the sync response
|
||||||
|
sendSyncResponse(secretStorageKey);
|
||||||
|
|
||||||
|
// Wait for bootstrapSecretStorage to finished
|
||||||
|
await bootstrapPromise;
|
||||||
|
|
||||||
|
// createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call
|
||||||
|
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
|
|||||||
import { IEventDecryptionResult } from "../../../src/@types/crypto";
|
import { IEventDecryptionResult } from "../../../src/@types/crypto";
|
||||||
import { OutgoingRequest, OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
import { OutgoingRequest, OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||||
import { ServerSideSecretStorage } from "../../../src/secret-storage";
|
import { ServerSideSecretStorage } from "../../../src/secret-storage";
|
||||||
import { ImportRoomKeysOpts } from "../../../src/crypto-api";
|
import { CryptoCallbacks, ImportRoomKeysOpts } from "../../../src/crypto-api";
|
||||||
import * as testData from "../../test-utils/test-data";
|
import * as testData from "../../test-utils/test-data";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -212,6 +212,7 @@ describe("RustCrypto", () => {
|
|||||||
TEST_USER,
|
TEST_USER,
|
||||||
TEST_DEVICE_ID,
|
TEST_DEVICE_ID,
|
||||||
{} as ServerSideSecretStorage,
|
{} as ServerSideSecretStorage,
|
||||||
|
{} as CryptoCallbacks,
|
||||||
);
|
);
|
||||||
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
|
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
|
||||||
});
|
});
|
||||||
@@ -334,6 +335,7 @@ describe("RustCrypto", () => {
|
|||||||
TEST_USER,
|
TEST_USER,
|
||||||
TEST_DEVICE_ID,
|
TEST_DEVICE_ID,
|
||||||
{} as ServerSideSecretStorage,
|
{} as ServerSideSecretStorage,
|
||||||
|
{} as CryptoCallbacks,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -430,6 +432,7 @@ async function makeTestRustCrypto(
|
|||||||
userId: string = TEST_USER,
|
userId: string = TEST_USER,
|
||||||
deviceId: string = TEST_DEVICE_ID,
|
deviceId: string = TEST_DEVICE_ID,
|
||||||
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
|
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
|
||||||
|
cryptoCallbacks: CryptoCallbacks = {} as CryptoCallbacks,
|
||||||
): Promise<RustCrypto> {
|
): Promise<RustCrypto> {
|
||||||
return await initRustCrypto(http, userId, deviceId, secretStorage);
|
return await initRustCrypto(http, userId, deviceId, secretStorage, cryptoCallbacks);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2223,7 +2223,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
// importing rust-crypto will download the webassembly, so we delay it until we know it will be
|
// importing rust-crypto will download the webassembly, so we delay it until we know it will be
|
||||||
// needed.
|
// needed.
|
||||||
const RustCrypto = await import("./rust-crypto");
|
const RustCrypto = await import("./rust-crypto");
|
||||||
const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId, this.secretStorage);
|
const rustCrypto = await RustCrypto.initRustCrypto(
|
||||||
|
this.http,
|
||||||
|
userId,
|
||||||
|
deviceId,
|
||||||
|
this.secretStorage,
|
||||||
|
this.cryptoCallbacks,
|
||||||
|
);
|
||||||
this.cryptoBackend = rustCrypto;
|
this.cryptoBackend = rustCrypto;
|
||||||
|
|
||||||
// attach the event listeners needed by RustCrypto
|
// attach the event listeners needed by RustCrypto
|
||||||
@@ -2874,6 +2880,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
|
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
|
||||||
* algorithm is found
|
* algorithm is found
|
||||||
*
|
*
|
||||||
|
* @deprecated Use {@link CryptoApi#bootstrapSecretStorage}.
|
||||||
*/
|
*/
|
||||||
public bootstrapSecretStorage(opts: ICreateSecretStorageOpts): Promise<void> {
|
public bootstrapSecretStorage(opts: ICreateSecretStorageOpts): Promise<void> {
|
||||||
if (!this.crypto) {
|
if (!this.crypto) {
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ import type { IMegolmSessionData } from "./@types/crypto";
|
|||||||
import { Room } from "./models/room";
|
import { Room } from "./models/room";
|
||||||
import { DeviceMap } from "./models/device";
|
import { DeviceMap } from "./models/device";
|
||||||
import { UIAuthCallback } from "./interactive-auth";
|
import { UIAuthCallback } from "./interactive-auth";
|
||||||
import { AddSecretStorageKeyOpts } from "./secret-storage";
|
import { AddSecretStorageKeyOpts, SecretStorageCallbacks, SecretStorageKeyDescription } from "./secret-storage";
|
||||||
import { VerificationRequest } from "./crypto-api/verification";
|
import { VerificationRequest } from "./crypto-api/verification";
|
||||||
|
import { KeyBackupInfo } from "./crypto-api/keybackup";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public interface to the cryptography parts of the js-sdk
|
* Public interface to the cryptography parts of the js-sdk
|
||||||
@@ -190,6 +191,18 @@ export interface CryptoApi {
|
|||||||
*/
|
*/
|
||||||
isSecretStorageReady(): Promise<boolean>;
|
isSecretStorageReady(): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap the secret storage by creating a new secret storage key and store it in the secret storage.
|
||||||
|
*
|
||||||
|
* - Do nothing if an AES key is already stored in the secret storage and `setupNewKeyBackup` is not set;
|
||||||
|
* - Generate a new key {@link GeneratedSecretStorageKey} with `createSecretStorageKey`.
|
||||||
|
* - Store this key in the secret storage and set it as the default key.
|
||||||
|
* - Call `cryptoCallbacks.cacheSecretStorageKey` if provided.
|
||||||
|
*
|
||||||
|
* @param opts - Options object.
|
||||||
|
*/
|
||||||
|
bootstrapSecretStorage(opts: CreateSecretStorageOpts): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the status of our cross-signing keys.
|
* Get the status of our cross-signing keys.
|
||||||
*
|
*
|
||||||
@@ -377,6 +390,72 @@ export interface CrossSigningStatus {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crypto callbacks provided by the application
|
||||||
|
*/
|
||||||
|
export interface CryptoCallbacks extends SecretStorageCallbacks {
|
||||||
|
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
|
||||||
|
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
|
||||||
|
shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
|
||||||
|
/**
|
||||||
|
* Called by {@link CryptoApi#bootstrapSecretStorage}
|
||||||
|
* @param keyId - secret storage key id
|
||||||
|
* @param keyInfo - secret storage key info
|
||||||
|
* @param key - private key to store
|
||||||
|
*/
|
||||||
|
cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void;
|
||||||
|
onSecretRequested?: (
|
||||||
|
userId: string,
|
||||||
|
deviceId: string,
|
||||||
|
requestId: string,
|
||||||
|
secretName: string,
|
||||||
|
deviceTrust: DeviceVerificationStatus,
|
||||||
|
) => Promise<string | undefined>;
|
||||||
|
getDehydrationKey?: (
|
||||||
|
keyInfo: SecretStorageKeyDescription,
|
||||||
|
checkFunc: (key: Uint8Array) => void,
|
||||||
|
) => Promise<Uint8Array>;
|
||||||
|
getBackupKey?: () => Promise<Uint8Array>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameter of {@link CryptoApi#bootstrapSecretStorage}
|
||||||
|
*/
|
||||||
|
export interface CreateSecretStorageOpts {
|
||||||
|
/**
|
||||||
|
* Function called to await a secret storage key creation flow.
|
||||||
|
* @returns Promise resolving to an object with public key metadata, encoded private
|
||||||
|
* recovery key which should be disposed of after displaying to the user,
|
||||||
|
* and raw private key to avoid round tripping if needed.
|
||||||
|
*/
|
||||||
|
createSecretStorageKey?: () => Promise<GeneratedSecretStorageKey>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current key backup object. If passed,
|
||||||
|
* the passphrase and recovery key from this backup will be used.
|
||||||
|
*/
|
||||||
|
keyBackupInfo?: KeyBackupInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, a new key backup version will be
|
||||||
|
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
|
||||||
|
* is supplied.
|
||||||
|
*/
|
||||||
|
setupNewKeyBackup?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset even if keys already exist.
|
||||||
|
*/
|
||||||
|
setupNewSecretStorage?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called to get the user's
|
||||||
|
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
|
||||||
|
* containing the key, or rejects if the key cannot be obtained.
|
||||||
|
*/
|
||||||
|
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
|
||||||
|
}
|
||||||
|
|
||||||
/** Types of cross-signing key */
|
/** Types of cross-signing key */
|
||||||
export enum CrossSigningKey {
|
export enum CrossSigningKey {
|
||||||
Master = "master",
|
Master = "master",
|
||||||
@@ -396,3 +475,4 @@ export interface GeneratedSecretStorageKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export * from "./crypto-api/verification";
|
export * from "./crypto-api/verification";
|
||||||
|
export * from "./crypto-api/keybackup";
|
||||||
|
|||||||
42
src/crypto-api/keybackup.ts
Normal file
42
src/crypto-api/keybackup.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ISigned } from "../@types/signed";
|
||||||
|
|
||||||
|
export interface Curve25519AuthData {
|
||||||
|
public_key: string;
|
||||||
|
private_key_salt?: string;
|
||||||
|
private_key_iterations?: number;
|
||||||
|
private_key_bits?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Aes256AuthData {
|
||||||
|
iv: string;
|
||||||
|
mac: string;
|
||||||
|
private_key_salt?: string;
|
||||||
|
private_key_iterations?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra info of a recovery key
|
||||||
|
*/
|
||||||
|
export interface KeyBackupInfo {
|
||||||
|
algorithm: string;
|
||||||
|
auth_data: ISigned & (Curve25519AuthData | Aes256AuthData);
|
||||||
|
count?: number;
|
||||||
|
etag?: string;
|
||||||
|
version?: string; // number contained within
|
||||||
|
}
|
||||||
@@ -15,13 +15,14 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DeviceInfo } from "./deviceinfo";
|
import { DeviceInfo } from "./deviceinfo";
|
||||||
import { IKeyBackupInfo } from "./keybackup";
|
|
||||||
import { GeneratedSecretStorageKey } from "../crypto-api";
|
|
||||||
|
|
||||||
/* re-exports for backwards compatibility. */
|
/* re-exports for backwards compatibility. */
|
||||||
// CrossSigningKey is used as a value in `client.ts`, we can't export it as a type
|
// CrossSigningKey is used as a value in `client.ts`, we can't export it as a type
|
||||||
export { CrossSigningKey } from "../crypto-api";
|
export { CrossSigningKey } from "../crypto-api";
|
||||||
export type { GeneratedSecretStorageKey as IRecoveryKey } from "../crypto-api";
|
export type {
|
||||||
|
GeneratedSecretStorageKey as IRecoveryKey,
|
||||||
|
CreateSecretStorageOpts as ICreateSecretStorageOpts,
|
||||||
|
} from "../crypto-api";
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
ImportRoomKeyProgressData as IImportOpts,
|
ImportRoomKeyProgressData as IImportOpts,
|
||||||
@@ -67,38 +68,3 @@ export interface IEncryptedEventInfo {
|
|||||||
*/
|
*/
|
||||||
mismatchedSender: boolean;
|
mismatchedSender: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreateSecretStorageOpts {
|
|
||||||
/**
|
|
||||||
* Function called to await a secret storage key creation flow.
|
|
||||||
* @returns Promise resolving to an object with public key metadata, encoded private
|
|
||||||
* recovery key which should be disposed of after displaying to the user,
|
|
||||||
* and raw private key to avoid round tripping if needed.
|
|
||||||
*/
|
|
||||||
createSecretStorageKey?: () => Promise<GeneratedSecretStorageKey>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current key backup object. If passed,
|
|
||||||
* the passphrase and recovery key from this backup will be used.
|
|
||||||
*/
|
|
||||||
keyBackupInfo?: IKeyBackupInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, a new key backup version will be
|
|
||||||
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
|
|
||||||
* is supplied.
|
|
||||||
*/
|
|
||||||
setupNewKeyBackup?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset even if keys already exist.
|
|
||||||
*/
|
|
||||||
setupNewSecretStorage?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function called to get the user's
|
|
||||||
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
|
|
||||||
* containing the key, or rejects if the key cannot be obtained.
|
|
||||||
*/
|
|
||||||
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ import {
|
|||||||
AccountDataClient,
|
AccountDataClient,
|
||||||
AddSecretStorageKeyOpts,
|
AddSecretStorageKeyOpts,
|
||||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||||
SecretStorageCallbacks,
|
|
||||||
SecretStorageKeyDescription,
|
SecretStorageKeyDescription,
|
||||||
SecretStorageKeyObject,
|
SecretStorageKeyObject,
|
||||||
SecretStorageKeyTuple,
|
SecretStorageKeyTuple,
|
||||||
@@ -97,7 +96,10 @@ import { Device, DeviceMap } from "../models/device";
|
|||||||
import { deviceInfoToDevice } from "./device-converter";
|
import { deviceInfoToDevice } from "./device-converter";
|
||||||
|
|
||||||
/* re-exports for backwards compatibility */
|
/* re-exports for backwards compatibility */
|
||||||
export type { BootstrapCrossSigningOpts as IBootstrapCrossSigningOpts } from "../crypto-api";
|
export type {
|
||||||
|
BootstrapCrossSigningOpts as IBootstrapCrossSigningOpts,
|
||||||
|
CryptoCallbacks as ICryptoCallbacks,
|
||||||
|
} from "../crypto-api";
|
||||||
|
|
||||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||||
|
|
||||||
@@ -134,25 +136,6 @@ interface IInitOpts {
|
|||||||
pickleKey?: string;
|
pickleKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICryptoCallbacks extends SecretStorageCallbacks {
|
|
||||||
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
|
|
||||||
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
|
|
||||||
shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
|
|
||||||
cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void;
|
|
||||||
onSecretRequested?: (
|
|
||||||
userId: string,
|
|
||||||
deviceId: string,
|
|
||||||
requestId: string,
|
|
||||||
secretName: string,
|
|
||||||
deviceTrust: DeviceTrustLevel,
|
|
||||||
) => Promise<string | undefined>;
|
|
||||||
getDehydrationKey?: (
|
|
||||||
keyInfo: SecretStorageKeyDescription,
|
|
||||||
checkFunc: (key: Uint8Array) => void,
|
|
||||||
) => Promise<Uint8Array>;
|
|
||||||
getBackupKey?: () => Promise<Uint8Array>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
interface IRoomKey {
|
interface IRoomKey {
|
||||||
room_id: string;
|
room_id: string;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ISigned } from "../@types/signed";
|
|
||||||
import { IEncryptedPayload } from "./aes";
|
import { IEncryptedPayload } from "./aes";
|
||||||
|
|
||||||
export interface Curve25519SessionData {
|
export interface Curve25519SessionData {
|
||||||
@@ -35,27 +34,13 @@ export interface IKeyBackupRoomSessions {
|
|||||||
[sessionId: string]: IKeyBackupSession;
|
[sessionId: string]: IKeyBackupSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICurve25519AuthData {
|
// Export for backward compatibility
|
||||||
public_key: string;
|
export type {
|
||||||
private_key_salt?: string;
|
Curve25519AuthData as ICurve25519AuthData,
|
||||||
private_key_iterations?: number;
|
Aes256AuthData as IAes256AuthData,
|
||||||
private_key_bits?: number;
|
KeyBackupInfo as IKeyBackupInfo,
|
||||||
}
|
} from "../crypto-api/keybackup";
|
||||||
|
|
||||||
export interface IAes256AuthData {
|
|
||||||
iv: string;
|
|
||||||
mac: string;
|
|
||||||
private_key_salt?: string;
|
|
||||||
private_key_iterations?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IKeyBackupInfo {
|
|
||||||
algorithm: string;
|
|
||||||
auth_data: ISigned & (ICurve25519AuthData | IAes256AuthData);
|
|
||||||
count?: number;
|
|
||||||
etag?: string;
|
|
||||||
version?: string; // number contained within
|
|
||||||
}
|
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
export interface IKeyBackupPrepareOpts {
|
export interface IKeyBackupPrepareOpts {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { logger } from "../logger";
|
|||||||
import { RUST_SDK_STORE_PREFIX } from "./constants";
|
import { RUST_SDK_STORE_PREFIX } from "./constants";
|
||||||
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
||||||
import { ServerSideSecretStorage } from "../secret-storage";
|
import { ServerSideSecretStorage } from "../secret-storage";
|
||||||
|
import { ICryptoCallbacks } from "../crypto";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new `RustCrypto` implementation
|
* Create a new `RustCrypto` implementation
|
||||||
@@ -30,12 +31,14 @@ import { ServerSideSecretStorage } from "../secret-storage";
|
|||||||
* @param userId - The local user's User ID.
|
* @param userId - The local user's User ID.
|
||||||
* @param deviceId - The local user's Device ID.
|
* @param deviceId - The local user's Device ID.
|
||||||
* @param secretStorage - Interface to server-side secret storage.
|
* @param secretStorage - Interface to server-side secret storage.
|
||||||
|
* @param cryptoCallbacks - Crypto callbacks provided by the application
|
||||||
*/
|
*/
|
||||||
export async function initRustCrypto(
|
export async function initRustCrypto(
|
||||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||||
userId: string,
|
userId: string,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
secretStorage: ServerSideSecretStorage,
|
secretStorage: ServerSideSecretStorage,
|
||||||
|
cryptoCallbacks: ICryptoCallbacks,
|
||||||
): Promise<RustCrypto> {
|
): Promise<RustCrypto> {
|
||||||
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
|
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
|
||||||
await RustSdkCryptoJs.initAsync();
|
await RustSdkCryptoJs.initAsync();
|
||||||
@@ -49,7 +52,7 @@ export async function initRustCrypto(
|
|||||||
|
|
||||||
// TODO: use the pickle key for the passphrase
|
// TODO: use the pickle key for the passphrase
|
||||||
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, RUST_SDK_STORE_PREFIX, "test pass");
|
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, RUST_SDK_STORE_PREFIX, "test pass");
|
||||||
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId, secretStorage);
|
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId, secretStorage, cryptoCallbacks);
|
||||||
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
|
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
|
||||||
rustCrypto.onRoomKeysUpdated(sessions),
|
rustCrypto.onRoomKeysUpdated(sessions),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,11 +39,13 @@ import {
|
|||||||
ImportRoomKeyProgressData,
|
ImportRoomKeyProgressData,
|
||||||
ImportRoomKeysOpts,
|
ImportRoomKeysOpts,
|
||||||
VerificationRequest,
|
VerificationRequest,
|
||||||
|
CreateSecretStorageOpts,
|
||||||
|
CryptoCallbacks,
|
||||||
} from "../crypto-api";
|
} from "../crypto-api";
|
||||||
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
|
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
|
||||||
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
|
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
|
||||||
import { Device, DeviceMap } from "../models/device";
|
import { Device, DeviceMap } from "../models/device";
|
||||||
import { AddSecretStorageKeyOpts, 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 { secretStorageContainsCrossSigningKeys } from "./secret-storage";
|
||||||
import { keyFromPassphrase } from "../crypto/key_passphrase";
|
import { keyFromPassphrase } from "../crypto/key_passphrase";
|
||||||
@@ -90,6 +92,9 @@ export class RustCrypto implements CryptoBackend {
|
|||||||
|
|
||||||
/** Interface to server-side secret storage */
|
/** Interface to server-side secret storage */
|
||||||
private readonly secretStorage: ServerSideSecretStorage,
|
private readonly secretStorage: ServerSideSecretStorage,
|
||||||
|
|
||||||
|
/** Crypto callbacks provided by the application */
|
||||||
|
private readonly cryptoCallbacks: CryptoCallbacks,
|
||||||
) {
|
) {
|
||||||
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
|
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
|
||||||
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
|
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
|
||||||
@@ -375,6 +380,49 @@ export class RustCrypto implements CryptoBackend {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link CryptoApi#bootstrapSecretStorage}
|
||||||
|
*/
|
||||||
|
public async bootstrapSecretStorage({
|
||||||
|
createSecretStorageKey,
|
||||||
|
setupNewSecretStorage,
|
||||||
|
}: CreateSecretStorageOpts = {}): Promise<void> {
|
||||||
|
// If createSecretStorageKey is not set, we stop
|
||||||
|
if (!createSecretStorageKey) return;
|
||||||
|
|
||||||
|
// See if we already have an AES secret-storage key.
|
||||||
|
const secretStorageKeyTuple = await this.secretStorage.getKey();
|
||||||
|
|
||||||
|
if (secretStorageKeyTuple) {
|
||||||
|
const [, keyInfo] = secretStorageKeyTuple;
|
||||||
|
|
||||||
|
// If an AES Key is already stored in the secret storage and setupNewSecretStorage is not set
|
||||||
|
// we don't want to create a new key
|
||||||
|
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES && !setupNewSecretStorage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const recoveryKey = await createSecretStorageKey();
|
||||||
|
|
||||||
|
// keyInfo is required to continue
|
||||||
|
if (!recoveryKey.keyInfo) {
|
||||||
|
throw new Error("missing keyInfo field in the secret storage key created by createSecretStorageKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretStorageKeyObject = await this.secretStorage.addKey(
|
||||||
|
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||||
|
recoveryKey.keyInfo,
|
||||||
|
);
|
||||||
|
await this.secretStorage.setDefaultKeyId(secretStorageKeyObject.keyId);
|
||||||
|
|
||||||
|
this.cryptoCallbacks.cacheSecretStorageKey?.(
|
||||||
|
secretStorageKeyObject.keyId,
|
||||||
|
secretStorageKeyObject.keyInfo,
|
||||||
|
recoveryKey.privateKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of {@link CryptoApi#getCrossSigningStatus}
|
* Implementation of {@link CryptoApi#getCrossSigningStatus}
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user