Skip to content

Commit

Permalink
Add support for fallback keys
Browse files Browse the repository at this point in the history
  • Loading branch information
turt2live committed Aug 17, 2021
1 parent 161829b commit 8e2188d
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 1 deletion.
34 changes: 33 additions & 1 deletion src/MatrixClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ import {
DeviceKeyAlgorithm,
DeviceKeyLabel,
EncryptionAlgorithm,
FallbackKey,
MultiUserDeviceListResponse,
OTKAlgorithm,
OTKClaimResponse,
OTKCounts,
OTKs
OTKs,
} from "./models/Crypto";
import { requiresCrypto } from "./e2ee/decorators";
import { ICryptoStorageProvider } from "./storage/ICryptoStorageProvider";
Expand Down Expand Up @@ -666,6 +667,19 @@ export class MatrixClient extends EventEmitter {
this.crypto?.updateCounts(raw['device_one_time_keys_count']);
}

let unusedFallbacks: string[] = null;
if (raw['org.matrix.msc2732.device_unused_fallback_key_types']) {
unusedFallbacks = raw['org.matrix.msc2732.device_unused_fallback_key_types'];
} else if (raw['device_unused_fallback_key_types']) {
unusedFallbacks = raw['device_unused_fallback_key_types'];
}

// XXX: We should be able to detect the presence of the array, but Synapse doesn't tell us about
// feature support if we didn't upload one, so assume we're on a latest version of Synapse at least.
if (!unusedFallbacks?.includes(OTKAlgorithm.Signed)) {
await this.crypto?.updateFallbackKey();
}

if (raw['device_lists']) {
const changed = raw['device_lists']['changed'];
const removed = raw['device_lists']['left'];
Expand Down Expand Up @@ -1732,6 +1746,24 @@ export class MatrixClient extends EventEmitter {
.then(r => r['one_time_key_counts']);
}

/**
* Uploads a fallback One Time Key to the server for usage. This will replace the existing fallback
* key.
* @param {FallbackKey} fallbackKey The fallback key.
* @returns {Promise<OTKCounts>} Resolves to the One Time Key counts.
*/
@timedMatrixClientFunctionCall()
@requiresCrypto()
public async uploadFallbackKey(fallbackKey: FallbackKey): Promise<OTKCounts> {
const keyObj = {
[`${OTKAlgorithm.Signed}:${fallbackKey.keyId}`]: fallbackKey.key,
};
return this.doRequest("POST", "/_matrix/client/r0/keys/upload", null, {
"org.matrix.msc2732.fallback_keys": keyObj,
"fallback_keys": keyObj,
}).then(r => r['one_time_key_counts']);
}

/**
* Gets <b>unverified</b> device lists for the given users. The caller is expected to validate
* and verify the device lists, including that the returned devices belong to the claimed users.
Expand Down
32 changes: 32 additions & 0 deletions src/e2ee/CryptoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as anotherJson from "another-json";
import {
DeviceKeyAlgorithm,
EncryptionAlgorithm,
FallbackKey,
IMegolmEncrypted,
IMRoomKey,
IOlmEncrypted,
Expand All @@ -16,6 +17,7 @@ import {
OTKCounts,
OTKs,
Signatures,
SignedCurve25519OTK,
UserDevice,
} from "../models/Crypto";
import { requiresReady } from "./decorators";
Expand Down Expand Up @@ -192,6 +194,36 @@ export class CryptoClient {
}
}

/**
* Updates the client's fallback key.
* @returns {Promise<void>} Resolves when complete.
*/
@requiresReady()
public async updateFallbackKey(): Promise<void> {
const account = await this.getOlmAccount();
try {
account.generate_fallback_key();

const key = JSON.parse(account.fallback_key());
const keyId = Object.keys(key[OTKAlgorithm.Unsigned])[0];
const obj: Partial<SignedCurve25519OTK> = {
key: key[OTKAlgorithm.Unsigned][keyId],
fallback: true,
};
const signatures = await this.sign(obj);
const fallback: FallbackKey = {
keyId: keyId,
key: {
...obj,
signatures: signatures,
} as SignedCurve25519OTK & {fallback: true},
};
await this.client.uploadFallbackKey(fallback);
} finally {
await this.storeAndFreeOlmAccount(account);
}
}

/**
* Signs an object using the device keys.
* @param {object} obj The object to sign.
Expand Down
10 changes: 10 additions & 0 deletions src/models/Crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ export interface Signatures {
export interface SignedCurve25519OTK {
key: string;
signatures: Signatures;
fallback?: boolean;
}

/**
* A fallback key.
* @category Models
*/
export interface FallbackKey {
keyId: string;
key: SignedCurve25519OTK & {fallback: true};
}

/**
Expand Down
44 changes: 44 additions & 0 deletions test/MatrixClientTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2069,6 +2069,9 @@ describe('MatrixClient', () => {
const { client } = createTestClient(null, "@user:example.org", true);
const syncClient = <ProcessSyncClient>(<any>client);

// Override to fix test as we aren't testing this here.
client.crypto.updateFallbackKey = async () => null;

const deviceMessage = {
type: "m.room.encrypted",
content: {
Expand Down Expand Up @@ -2103,10 +2106,51 @@ describe('MatrixClient', () => {
expect(processSpy.callCount).toBe(1);
});

it('should handle fallback key updates', async () => {
const { client } = createTestClient(null, "@user:example.org", true);
const syncClient = <ProcessSyncClient>(<any>client);

await client.cryptoStore.setDeviceId(TEST_DEVICE_ID);
await feedStaticOlmAccount(client);
client.uploadDeviceKeys = () => Promise.resolve({});
client.uploadDeviceOneTimeKeys = () => Promise.resolve({});
client.checkOneTimeKeyCounts = () => Promise.resolve({});

await client.crypto.prepare([]);

const updateSpy = simple.stub();
client.crypto.updateFallbackKey = updateSpy;

// Test workaround for https://github.com/matrix-org/synapse/issues/10618
await syncClient.processSync({
// no content
});
expect(updateSpy.callCount).toBe(1);
updateSpy.reset();

// Test "no more fallback keys" state
await syncClient.processSync({
"org.matrix.msc2732.device_unused_fallback_key_types": [],
"device_unused_fallback_key_types": [],
});
expect(updateSpy.callCount).toBe(1);
updateSpy.reset();

// Test "has remaining fallback keys"
await syncClient.processSync({
"org.matrix.msc2732.device_unused_fallback_key_types": ["signed_curve25519"],
"device_unused_fallback_key_types": ["signed_curve25519"],
});
expect(updateSpy.callCount).toBe(0);
});

it('should decrypt timeline events', async () => {
const {client: realClient} = await createPreparedCryptoTestClient("@alice:example.org");
const client = <ProcessSyncClient>(<any>realClient);

// Override to fix test as we aren't testing this here.
realClient.crypto.updateFallbackKey = async () => null;

const userId = "@syncing:example.org";
const roomId = "!testing:example.org";
const events = [
Expand Down
53 changes: 53 additions & 0 deletions test/encryption/CryptoClientTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3417,4 +3417,57 @@ describe('CryptoClient', () => {
expect(downloadSpy.callCount).toBe(1);
});
});

describe('updateFallbackKey', () => {
const userId = "@alice:example.org";
let client: MatrixClient;

beforeEach(async () => {
const { client: mclient } = createTestClient(null, userId, true);
client = mclient;

await client.cryptoStore.setDeviceId(TEST_DEVICE_ID);
await feedStaticOlmAccount(client);
client.uploadDeviceKeys = () => Promise.resolve({});
client.uploadDeviceOneTimeKeys = () => Promise.resolve({});
client.checkOneTimeKeyCounts = () => Promise.resolve({});

// client crypto not prepared for the one test which wants that state
});

it('should fail when the crypto has not been prepared', async () => {
try {
await client.crypto.updateFallbackKey();

// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect(e.message).toEqual("End-to-end encryption has not initialized");
}
});

it('should create new keys', async () => {
await client.crypto.prepare([]);

const uploadSpy = simple.stub().callFn(async (k) => {
expect(k).toMatchObject({
keyId: expect.any(String),
key: {
key: expect.any(String),
fallback: true,
signatures: {
[userId]: {
[`${DeviceKeyAlgorithm.Ed25519}:${TEST_DEVICE_ID}`]: expect.any(String),
},
},
},
});
return null; // return not used
});
client.uploadFallbackKey = uploadSpy;

await client.crypto.updateFallbackKey();
expect(uploadSpy.callCount).toBe(1);
});
});
});

0 comments on commit 8e2188d

Please sign in to comment.