Skip to content

Commit

Permalink
support backup creation in rust
Browse files Browse the repository at this point in the history
  • Loading branch information
BillCarsonFr committed Sep 1, 2023
1 parent b39741a commit dcdbbee
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 37 deletions.
74 changes: 40 additions & 34 deletions spec/integ/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import { logger } from "../../../src/logger";
import {
Category,
createClient,
CryptoEvent,
IClaimOTKsResult,
IContent,
IDownloadKeyResult,
Expand All @@ -55,6 +54,7 @@ import {
Room,
RoomMember,
RoomStateEvent,
CryptoEvent,
} from "../../../src/matrix";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
Expand All @@ -68,7 +68,7 @@ import {
mockSetupMegolmBackupRequests,
} from "../../test-utils/mockEndpoints";
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api";
import { CrossSigningKey, CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";

afterEach(() => {
Expand Down Expand Up @@ -2202,9 +2202,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
"express:/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)",
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);

if (content.key) {
resolve(content.key);
}

return {};
},
{ overwriteRoutes: true },
Expand Down Expand Up @@ -2289,7 +2291,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
await bootstrapPromise;
// Finally ensure backup is working
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();

await backupStatusUpdate;
}

Expand Down Expand Up @@ -2340,7 +2341,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
},
);

newBackendOnly("should create a new key", async () => {
it("should create a new key", async () => {
const bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
Expand Down Expand Up @@ -2383,46 +2384,43 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
},
);

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 });
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
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });

// Wait for the key to be uploaded in the account data
let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
// Wait for the key to be uploaded in the account data
let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();

// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);

// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;

// Call again bootstrapSecretStorage
bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
// Call again bootstrapSecretStorage
bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });

// Wait for the key to be uploaded in the account data
secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
// Wait for the key to be uploaded in the account data
secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();

// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);

// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;

// createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
},
);
// createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
});

newBackendOnly("should upload cross signing keys", async () => {
it("should upload cross signing keys", async () => {
mockSetupCrossSigningRequests();

// Before setting up secret-storage, bootstrap cross-signing, so that the client has cross-signing keys.
await aliceClient.getCrypto()?.bootstrapCrossSigning({});
await aliceClient.getCrypto()!.bootstrapCrossSigning({});

// Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded.
const bootstrapPromise = aliceClient
Expand Down Expand Up @@ -2451,16 +2449,24 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expect(selfSigningKey[secretStorageKey]).toBeDefined();
});

oldBackendOnly("should create a new megolm backup", async () => {
it("should create a new megolm backup", async () => {
const backupVersion = "abc";
await bootstrapSecurity(backupVersion);

// Expect a backup to be available and used
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
expect(activeBackup).toStrictEqual(backupVersion);

// check that there is a MSK signature
const signatures = (await aliceClient.getCrypto()!.checkKeyBackupAndEnable())!.backupInfo.auth_data!
.signatures;
expect(signatures).toBeDefined();
expect(signatures![aliceClient.getUserId()!]).toBeDefined();
const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!;
expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined();
});

oldBackendOnly("Reset key backup should create a new backup and update 4S", async () => {
it("Reset key backup should create a new backup and update 4S", async () => {
// First set up recovery
const backupVersion = "1";
await bootstrapSecurity(backupVersion);
Expand Down
112 changes: 112 additions & 0 deletions src/rust-crypto/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,37 @@ import { logger } from "../logger";
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api";
import { CryptoEvent } from "../crypto";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { encodeUri } from "../utils";
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
import { sleep } from "../utils";

/**
* prepareKeyBackupVersion result.
*/
interface PreparedKeyBackupVersion {
/** The prepared algorithm version */
algorithm: string;
/** The auth data of the algorithm */
/* eslint-disable-next-line camelcase */
auth_data: AuthData;
/** The generated private key */
decryptionKey: RustSdkCryptoJs.BackupDecryptionKey;
}

/** Authentification of the backup info, depends on algorithm */
type AuthData = KeyBackupInfo["auth_data"];

/**
* Holds information of a created keybackup.
* Useful to get the generated private key material and save it securely somewhere.
*/
interface KeyBackupCreationInfo {
version: string;
algorithm: string;
authData: AuthData;
decryptionKey: RustSdkCryptoJs.BackupDecryptionKey;
}

/**
* @internal
*/
Expand Down Expand Up @@ -280,6 +308,90 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
}
}
}

/**
* Creates a new key backup by generating a new random private key.
*
* If there is an existing backup server side it will be deleted and replaced
* by the new one.
*
* @param signObject - Method that should sign the backup with existing device and
* existing identity.
* @returns a KeyBackupCreationInfo - All information related to the backup.
*/
public async setupKeyBackup(signObject: (authData: AuthData) => Promise<void>): Promise<KeyBackupCreationInfo> {
// Cleanup any existing backup
await this.deleteAllKeyBackupVersions();

const version = await this.prepareKeyBackupVersion();
await signObject(version.auth_data);

const res = await this.http.authedRequest<{ version: string }>(
Method.Post,
"/room_keys/version",
undefined,
{
algorithm: version.algorithm,
auth_data: version.auth_data,
},
{
prefix: ClientPrefix.V3,
},
);

this.olmMachine.saveBackupDecryptionKey(version.decryptionKey, res.version);

return {
version: res.version,
algorithm: version.algorithm,
authData: version.auth_data,
decryptionKey: version.decryptionKey,
};
}

/**
* Deletes all key backups.
*
* Will call the API to delete active backup until there is no more present.
*/
public async deleteAllKeyBackupVersions(): Promise<void> {
// there could be several backup versions. Delete all to be safe.
let current = (await this.requestKeyBackupVersion())?.version ?? null;
while (current != null) {
await this.deleteKeyBackupVersion(current);
current = (await this.requestKeyBackupVersion())?.version ?? null;
}

// XXX: Should this also update Secret Storage and delete any existing keys?
}

/**
* Deletes the given key backup.
*
* @param version - The backup version to delete.
*/
public async deleteKeyBackupVersion(version: string): Promise<void> {
logger.debug(`deleteKeyBackupVersion v:${version}`);
const path = encodeUri("/room_keys/version/$version", { $version: version });
await this.http.authedRequest<void>(Method.Delete, path, undefined, undefined, {
prefix: ClientPrefix.V3,
});
}

/**
* Prepare the keybackup version data, auth_data not signed at this point
* @returns a {@link PreparedKeyBackupVersion} with all information about the creation.
*/
private async prepareKeyBackupVersion(): Promise<PreparedKeyBackupVersion> {
const randomKey = RustSdkCryptoJs.BackupDecryptionKey.createRandomKey();
const pubKey = randomKey.megolmV1PublicKey;

return {
algorithm: pubKey.algorithm,
auth_data: { public_key: pubKey.publicKeyBase64 },
decryptionKey: randomKey,
};
}
}

export type RustBackupCryptoEvents =
Expand Down
56 changes: 53 additions & 3 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ 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.
*/
/**
* Utilities common to olm encryption algorithms
*/

import anotherjson from "another-json";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";

import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
Expand Down Expand Up @@ -63,9 +67,15 @@ import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } f
import { TypedReEmitter } from "../ReEmitter";
import { randomString } from "../randomstring";
import { ClientStoppedError } from "../errors";
import { ISignatures } from "../@types/signed";

const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];

interface ISignableObject {
signatures?: ISignatures;
unsigned?: object;
}

/**
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
*
Expand Down Expand Up @@ -555,6 +565,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
public async bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage,
setupNewKeyBackup,
}: CreateSecretStorageOpts = {}): Promise<void> {
// 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
Expand Down Expand Up @@ -598,19 +609,58 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
await this.secretStorage.store("m.cross_signing.master", crossSigningPrivateKeys.masterKey);
await this.secretStorage.store("m.cross_signing.user_signing", crossSigningPrivateKeys.userSigningKey);
await this.secretStorage.store("m.cross_signing.self_signing", crossSigningPrivateKeys.self_signing_key);

if (setupNewKeyBackup) {
await this.resetKeyBackup();
}
}
}

/**
* Deletes the given key backup.
*
* @param version - The backup version to delete.
*/
public async deleteKeyBackupVersion(version: string): Promise<void> {
await this.backupManager.deleteKeyBackupVersion(version);
}

/**
* Implementation of {@link CryptoApi#resetKeyBackup}.
*/
public async resetKeyBackup(): Promise<void> {
// stub
const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o));

// we want to store the private key in 4S
// need to check if 4S is set up?
if (await this.secretStorageHasAESKey()) {
await this.secretStorage.store("m.megolm_backup.v1", backupInfo.decryptionKey.toBase64());
}

// we can check and start async
this.checkKeyBackupAndEnable();
}

public async deleteKeyBackupVersion(version: string): Promise<void> {
// stub
private async signObject<T extends ISignableObject & object>(obj: T): Promise<void> {
const sigs = new Map(Object.entries(obj.signatures || {}));
const unsigned = obj.unsigned;

delete obj.signatures;
delete obj.unsigned;

const userSignatures = sigs.get(this.userId) || {};

const canonalizedJson = anotherjson.stringify(obj);
const signatures: RustSdkCryptoJs.Signatures = await this.olmMachine.sign(canonalizedJson);

const map = JSON.parse(signatures.asJSON());

sigs.set(this.userId, { ...userSignatures, ...map[this.userId] });

if (unsigned !== undefined) obj.unsigned = unsigned;
obj.signatures = Object.fromEntries(sigs.entries());
}

/**
* Add the secretStorage key to the secret storage
* - The secret storage key must have the `keyInfo` field filled
Expand Down

0 comments on commit dcdbbee

Please sign in to comment.