Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for key backups #22

Merged
merged 18 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const dmTarget = creds?.['dmTarget'] ?? "@admin:localhost";
const homeserverUrl = creds?.['homeserverUrl'] ?? "http://localhost:8008";
const accessToken = creds?.['accessToken'] ?? 'YOUR_TOKEN';
const storage = new SimpleFsStorageProvider("./examples/storage/bot.json");
const crypto = new RustSdkCryptoStorageProvider("./examples/storage/bot_sled", StoreType.Sled);
const crypto = new RustSdkCryptoStorageProvider("./examples/storage/bot_sqlite", StoreType.Sqlite);

const client = new MatrixClient(homeserverUrl, accessToken, storage, crypto);
AutojoinRoomsMixin.setupOnClient(client);
Expand Down
2 changes: 1 addition & 1 deletion examples/encryption_appservice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ try {
const dmTarget = creds?.['dmTarget'] ?? "@admin:localhost";
const homeserverUrl = creds?.['homeserverUrl'] ?? "http://localhost:8008";
const storage = new SimpleFsStorageProvider("./examples/storage/encryption_appservice.json");
const crypto = new RustSdkAppserviceCryptoStorageProvider("./examples/storage/encryption_appservice_sled", StoreType.Sled);
const crypto = new RustSdkAppserviceCryptoStorageProvider("./examples/storage/encryption_appservice_sqlite", StoreType.Sqlite);
const worksImage = fs.readFileSync("./examples/static/it-works.png");

const registration: IAppserviceRegistration = {
Expand Down
2 changes: 1 addition & 1 deletion examples/encryption_bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const dmTarget = creds?.['dmTarget'] ?? "@admin:localhost";
const homeserverUrl = creds?.['homeserverUrl'] ?? "http://localhost:8008";
const accessToken = creds?.['accessToken'] ?? 'YOUR_TOKEN';
const storage = new SimpleFsStorageProvider("./examples/storage/encryption_bot.json");
const crypto = new RustSdkCryptoStorageProvider("./examples/storage/encryption_bot_sled", StoreType.Sled);
const crypto = new RustSdkCryptoStorageProvider("./examples/storage/encryption_bot_sqlite", StoreType.Sqlite);
const worksImage = fs.readFileSync("./examples/static/it-works.png");

const client = new MatrixClient(homeserverUrl, accessToken, storage, crypto);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"tsconfig.json"
],
"dependencies": {
"@matrix-org/matrix-sdk-crypto-nodejs": "0.1.0-beta.6",
"@matrix-org/matrix-sdk-crypto-nodejs": "0.1.0-beta.9",
"@types/express": "^4.17.13",
"another-json": "^0.2.0",
"async-lock": "^1.3.2",
Expand Down
75 changes: 75 additions & 0 deletions src/MatrixClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import { DMs } from "./DMs";
import { ServerVersions } from "./models/ServerVersions";
import { RoomCreateOptions } from "./models/CreateRoom";
import { PresenceState } from './models/events/PresenceEvent';
import { IKeyBackupInfo, IKeyBackupInfoRetrieved, IKeyBackupInfoUpdate, IKeyBackupVersion, KeyBackupVersion } from "./models/KeyBackup";
import { MatrixError } from "./models/MatrixError";

const SYNC_BACKOFF_MIN_MS = 5000;
const SYNC_BACKOFF_MAX_MS = 15000;
Expand Down Expand Up @@ -1962,6 +1964,79 @@ export class MatrixClient extends EventEmitter {
});
}

/**
* Get information about the latest room key backup version.
* @returns {Promise<IKeyBackupInfoRetrieved|null>} Resolves to the retrieved key backup info,
* or null if there is no existing backup.
*/
public async getKeyBackupVersion(): Promise<IKeyBackupInfoRetrieved|null> {
try {
return await this.doRequest("GET", "/_matrix/client/v3/room_keys/version");
} catch (e) {
if (e instanceof MatrixError && e.errcode === "M_NOT_FOUND") {
return null;
} else {
throw e;
}
}
}

/**
* Create a new room key backup.
* @param {IKeyBackupInfo} info The properties of the key backup to create.
* @returns {Promise<IKeyBackupVersion>} Resolves to the version id of the new backup.
*/
public async createKeyBackupVersion(info: IKeyBackupInfo): Promise<IKeyBackupVersion> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}

const data = {
...info,
signatures: this.crypto.sign(info),
};
return await this.doRequest("POST", "/_matrix/client/v3/room_keys/version", null, data);
}

/**
* Update an existing room key backup.
* @param {KeyBackupVersion} version The key backup version to update.
* @param {IKeyBackupInfoUpdate} info The properties of the key backup to be applied.
* @returns {Promise<void>} Resolves when complete.
*/
public async updateKeyBackupVersion(version: KeyBackupVersion, info: IKeyBackupInfoUpdate): Promise<void> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}

const data = {
...info,
signatures: this.crypto.sign(info),
};
await this.doRequest("PUT", `/_matrix/client/v3/room_keys/version/${version}`, null, data);
}

/**
* Enable backing up of room keys.
* @param {IKeyBackupInfoRetrieved} info The configuration for key backup behaviour,
* as returned by {@link getKeyBackupVersion}.
* @returns {Promise<void>} Resolves when complete.
*/
public async enableKeyBackup(info: IKeyBackupInfoRetrieved): Promise<void> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}

this.crypto.enableKeyBackup(info);
}

/**
* Disable backing up of room keys.
*/
public disableKeyBackup(): void {
this.crypto?.disableKeyBackup();
}

/**
* Get relations for a given event.
* @param {string} roomId The room ID to for the given event.
Expand Down
36 changes: 32 additions & 4 deletions src/e2ee/CryptoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { EncryptedFile } from "../models/events/MessageEvent";
import { RustSdkCryptoStorageProvider } from "../storage/RustSdkCryptoStorageProvider";
import { RustEngine, SYNC_LOCK_NAME } from "./RustEngine";
import { MembershipEvent } from "../models/events/MembershipEvent";
import { IKeyBackupInfoRetrieved } from "../models/KeyBackup";

/**
* Manages encryption for a MatrixClient. Get an instance from a MatrixClient directly
Expand Down Expand Up @@ -168,12 +169,13 @@ export class CryptoClient {
leftDeviceLists.map(u => new UserId(u)));

await this.engine.lock.acquire(SYNC_LOCK_NAME, async () => {
const syncResp = await this.engine.machine.receiveSyncChanges(deviceMessages, deviceLists, otkCounts, unusedFallbackKeyAlgs);
const decryptedToDeviceMessages = JSON.parse(syncResp);
if (Array.isArray(decryptedToDeviceMessages)) {
for (const msg of decryptedToDeviceMessages) {
const syncResp = JSON.parse(await this.engine.machine.receiveSyncChanges(deviceMessages, deviceLists, otkCounts, unusedFallbackKeyAlgs));
if (Array.isArray(syncResp) && syncResp.length === 2 && Array.isArray(syncResp[0])) {
for (const msg of syncResp[0] as IToDeviceMessage[]) {
this.client.emit("to_device.decrypted", msg);
}
} else {
LogService.error("CryptoClient", "OlmMachine.receiveSyncChanges did not return an expected value of [to-device events, room key changes]");
}

await this.engine.run();
Expand Down Expand Up @@ -284,4 +286,30 @@ export class CryptoClient {
const decrypted = Attachment.decrypt(encrypted);
return Buffer.from(decrypted);
}

/**
* Enable backing up of room keys.
* @param {IKeyBackupInfoRetrieved} info The configuration for key backup behaviour,
* as returned by {@link MatrixClient#getKeyBackupVersion}.
* @returns {Promise<void>} Resolves when complete.
*/
public async enableKeyBackup(info: IKeyBackupInfoRetrieved): Promise<void> {
this.client.on("to_device.decrypted", this.onToDeviceMessage);
await this.engine.enableKeyBackup(info);
this.engine.backupRoomKeys();
}

/**
* Disable backing up of room keys.
*/
public disableKeyBackup(): void {
this.engine.disableKeyBackup();
this.client.removeListener("to_device.decrypted", this.onToDeviceMessage);
}

private readonly onToDeviceMessage = (msg: IToDeviceMessage): void => {
if (msg.type === "m.room_key") {
this.engine.backupRoomKeys();
}
};
}
74 changes: 73 additions & 1 deletion src/e2ee/RustEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import {
KeysUploadRequest,
KeysQueryRequest,
ToDeviceRequest,
KeysBackupRequest,
} from "@matrix-org/matrix-sdk-crypto-nodejs";
import * as AsyncLock from "async-lock";

import { MatrixClient } from "../MatrixClient";
import { ICryptoRoomInformation } from "./ICryptoRoomInformation";
import { EncryptionAlgorithm } from "../models/Crypto";
import { EncryptionEvent } from "../models/events/EncryptionEvent";
import { ICurve25519AuthData, IKeyBackupInfoRetrieved, KeyBackupVersion } from "../models/KeyBackup";

/**
* @internal
Expand All @@ -29,6 +31,10 @@ export const SYNC_LOCK_NAME = "sync";
export class RustEngine {
public readonly lock = new AsyncLock();

private keyBackupVersion: KeyBackupVersion|undefined;
private keyBackupWaiter = Promise.resolve();
private isBackupEnabled = false;

public constructor(public readonly machine: OlmMachine, private client: MatrixClient) {
}

Expand Down Expand Up @@ -59,7 +65,8 @@ export class RustEngine {
case RequestType.SignatureUpload:
throw new Error("Bindings error: Backup feature not possible");
case RequestType.KeysBackup:
throw new Error("Bindings error: Backup feature not possible");
await this.processKeysBackupRequest(request);
break;
default:
throw new Error("Bindings error: Unrecognized request type: " + request.type);
}
Expand Down Expand Up @@ -117,6 +124,7 @@ export class RustEngine {
const keysClaim = await this.machine.getMissingSessions(members);
if (keysClaim) {
await this.processKeysClaimRequest(keysClaim);
this.backupRoomKeysIfEnabled();
AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
}
});

Expand All @@ -128,6 +136,56 @@ export class RustEngine {
});
}

public enableKeyBackup(info: IKeyBackupInfoRetrieved) {
this.keyBackupWaiter = this.keyBackupWaiter.then(async () => {
if (this.isBackupEnabled) {
AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
await this.actuallyDisableKeyBackup();
}
// TODO Error with message if the key backup uses an unsupported auth_data type
AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
await this.machine.enableBackupV1((info.auth_data as ICurve25519AuthData).public_key, info.version);
this.keyBackupVersion = info.version;
this.isBackupEnabled = true;
});
return this.keyBackupWaiter;
}

public disableKeyBackup() {
this.keyBackupWaiter = this.keyBackupWaiter.then(this.actuallyDisableKeyBackup);
return this.keyBackupWaiter;
}

private readonly actuallyDisableKeyBackup = async () => {
AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
await this.machine.disableBackup();
this.keyBackupVersion = undefined;
this.isBackupEnabled = false;
};

public async backupRoomKeys() {
this.keyBackupWaiter = this.keyBackupWaiter.then(async () => {
if (!this.isBackupEnabled) {
throw new Error("Key backup error: attempted to create a backup before having enabled backups");
}
await this.actuallyBackupRoomKeys();
});
return this.keyBackupWaiter;
}

private async backupRoomKeysIfEnabled() {
this.keyBackupWaiter = this.keyBackupWaiter.then(async () => {
if (this.isBackupEnabled) {
await this.actuallyBackupRoomKeys();
}
});
return this.keyBackupWaiter;
}

private readonly actuallyBackupRoomKeys = async () => {
const request = await this.machine.backupRoomKeys();
if (request) {
await this.processKeysBackupRequest(request);
}
};

private async processKeysClaimRequest(request: KeysClaimRequest) {
const resp = await this.client.doRequest("POST", "/_matrix/client/v3/keys/claim", null, JSON.parse(request.body));
await this.machine.markRequestAsSent(request.id, request.type, JSON.stringify(resp));
Expand All @@ -154,4 +212,18 @@ export class RustEngine {
const resp = await this.client.sendToDevices(type, messages);
await this.machine.markRequestAsSent(id, RequestType.ToDevice, JSON.stringify(resp));
}

private async processKeysBackupRequest(request: KeysBackupRequest) {
let resp: Awaited<ReturnType<MatrixClient["doRequest"]>>;
try {
if (!this.keyBackupVersion) {
throw new Error("Key backup version missing");
}
resp = await this.client.doRequest("PUT", "/_matrix/client/v3/room_keys/keys", { version: this.keyBackupVersion }, JSON.parse(request.body));
} catch (e) {
this.client.emit("crypto.failed_backup", e);
return;
}
await this.machine.markRequestAsSent(request.id, request.type, JSON.stringify(resp));
}
}
36 changes: 36 additions & 0 deletions src/models/KeyBackup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Signatures } from "./Crypto";

/**
* The kinds of key backup encryption algorithms allowed by the spec.
* @category Models
*/
export enum KeyBackupEncryptionAlgorithm {
MegolmBackupV1Curve25519AesSha2 = "m.megolm_backup.v1.curve25519-aes-sha2",
}

/**
* Information about a server-side key backup.
*/
export interface IKeyBackupInfo {
algorithm: string | KeyBackupEncryptionAlgorithm;
auth_data: object;
}

export type KeyBackupVersion = string;

export interface IKeyBackupVersion {
version: KeyBackupVersion;
}

export interface IKeyBackupInfoRetrieved extends IKeyBackupInfo, IKeyBackupVersion {
count: number;
etag: string;
}

export type IKeyBackupInfoUpdate = IKeyBackupInfo & Partial<IKeyBackupVersion>;

export interface ICurve25519AuthDataUnsigned {
public_key: string;
}

export type ICurve25519AuthData = ICurve25519AuthDataUnsigned & Signatures;
4 changes: 2 additions & 2 deletions src/storage/RustSdkCryptoStorageProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class RustSdkCryptoStorageProvider implements ICryptoStorageProvider {
*/
public constructor(
public readonly storagePath: string,
public readonly storageType: RustSdkCryptoStoreType = RustSdkCryptoStoreType.Sled,
public readonly storageType: RustSdkCryptoStoreType = RustSdkCryptoStoreType.Sqlite,
) {
this.storagePath = path.resolve(this.storagePath);
mkdirp.sync(storagePath);
Expand Down Expand Up @@ -69,7 +69,7 @@ export class RustSdkAppserviceCryptoStorageProvider extends RustSdkCryptoStorage
* @param baseStoragePath The *directory* to persist database details to.
* @param storageType The storage type to use. Must be supported by the rust-sdk.
*/
public constructor(private baseStoragePath: string, storageType: RustSdkCryptoStoreType = RustSdkCryptoStoreType.Sled) {
public constructor(private baseStoragePath: string, storageType: RustSdkCryptoStoreType = RustSdkCryptoStoreType.Sqlite) {
super(path.join(baseStoragePath, "_default"), storageType);
}

Expand Down
10 changes: 4 additions & 6 deletions test/MatrixClientTest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as tmp from "tmp";
import * as simple from "simple-mock";
import { StoreType } from "@matrix-org/matrix-sdk-crypto-nodejs";

import {
EventKind,
Expand Down Expand Up @@ -48,13 +47,13 @@ describe('MatrixClient', () => {
expect(client.accessToken).toEqual(accessToken);
});

it('should create a crypto client when requested', () => {
it('should create a crypto client when requested', () => testCryptoStores(async (cryptoStoreType) => {
const homeserverUrl = "https://example.org";
const accessToken = "example_token";

const client = new MatrixClient(homeserverUrl, accessToken, null, new RustSdkCryptoStorageProvider(tmp.dirSync().name, StoreType.Sled));
const client = new MatrixClient(homeserverUrl, accessToken, null, new RustSdkCryptoStorageProvider(tmp.dirSync().name, cryptoStoreType));
expect(client.crypto).toBeDefined();
});
}));

it('should NOT create a crypto client when requested', () => {
const homeserverUrl = "https://example.org";
Expand Down Expand Up @@ -1401,8 +1400,7 @@ describe('MatrixClient', () => {
describe('processSync', () => {
interface ProcessSyncClient {
userId: string;

processSync(raw: any): Promise<any>;
processSync(raw: any): MatrixClient["processSync"];
}

it('should process non-room account data', async () => {
Expand Down
Loading