Skip to content

Commit 061199e

Browse files
authored
Merge pull request #1775 from uhoreg/symmetric_backup
Symmetric backup
2 parents 2d67a35 + d639a29 commit 061199e

File tree

10 files changed

+354
-63
lines changed

10 files changed

+354
-63
lines changed

spec/unit/crypto/backup.spec.js

Lines changed: 141 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const ENCRYPTED_EVENT = new MatrixEvent({
5252
origin_server_ts: 1507753886000,
5353
});
5454

55-
const KEY_BACKUP_DATA = {
55+
const CURVE25519_KEY_BACKUP_DATA = {
5656
first_message_index: 0,
5757
forwarded_count: 0,
5858
is_verified: false,
@@ -73,14 +73,41 @@ const KEY_BACKUP_DATA = {
7373
},
7474
};
7575

76-
const BACKUP_INFO = {
76+
const AES256_KEY_BACKUP_DATA = {
77+
first_message_index: 0,
78+
forwarded_count: 0,
79+
is_verified: false,
80+
session_data: {
81+
iv: 'b3Jqqvm5S9QdmXrzssspLQ',
82+
ciphertext: 'GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce'
83+
+ '7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd'
84+
+ 'EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0'
85+
+ 'WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r'
86+
+ 'KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P'
87+
+ 'vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K'
88+
+ 'YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd'
89+
+ 'fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA'
90+
+ 'RgaDHkfzoA3g3aeQ',
91+
mac: 'uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU',
92+
},
93+
};
94+
95+
const CURVE25519_BACKUP_INFO = {
7796
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
7897
version: 1,
7998
auth_data: {
8099
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
81100
},
82101
};
83102

103+
const AES256_BACKUP_INFO = {
104+
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
105+
version: 1,
106+
auth_data: {
107+
// FIXME: add iv and mac
108+
},
109+
};
110+
84111
const keys = {};
85112

86113
function getCrossSigningKey(type) {
@@ -144,7 +171,7 @@ describe("MegolmBackup", function() {
144171
mockCrypto.backupKey.set_recipient_key(
145172
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
146173
);
147-
mockCrypto.backupInfo = BACKUP_INFO;
174+
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
148175

149176
mockStorage = new MockStorageApi();
150177
sessionStore = new WebStorageSessionStore(mockStorage);
@@ -228,7 +255,7 @@ describe("MegolmBackup", function() {
228255
});
229256
});
230257

231-
it('sends backups to the server', function() {
258+
it('sends backups to the server (Curve25519 version)', function() {
232259
const groupSession = new Olm.OutboundGroupSession();
233260
groupSession.create();
234261
const ibGroupSession = new Olm.InboundGroupSession();
@@ -306,6 +333,88 @@ describe("MegolmBackup", function() {
306333
});
307334
});
308335

336+
it('sends backups to the server (AES-256 version)', function() {
337+
const groupSession = new Olm.OutboundGroupSession();
338+
groupSession.create();
339+
const ibGroupSession = new Olm.InboundGroupSession();
340+
ibGroupSession.create(groupSession.session_key());
341+
342+
const client = makeTestClient(sessionStore, cryptoStore);
343+
344+
megolmDecryption = new MegolmDecryption({
345+
userId: '@user:id',
346+
crypto: mockCrypto,
347+
olmDevice: olmDevice,
348+
baseApis: client,
349+
roomId: ROOM_ID,
350+
});
351+
352+
megolmDecryption.olmlib = mockOlmLib;
353+
354+
return client.initCrypto()
355+
.then(() => {
356+
return client.crypto.storeSessionBackupPrivateKey(new Uint8Array(32));
357+
})
358+
.then(() => {
359+
return cryptoStore.doTxn(
360+
"readwrite",
361+
[cryptoStore.STORE_SESSION],
362+
(txn) => {
363+
cryptoStore.addEndToEndInboundGroupSession(
364+
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
365+
groupSession.session_id(),
366+
{
367+
forwardingCurve25519KeyChain: undefined,
368+
keysClaimed: {
369+
ed25519: "SENDER_ED25519",
370+
},
371+
room_id: ROOM_ID,
372+
session: ibGroupSession.pickle(olmDevice._pickleKey),
373+
},
374+
txn);
375+
});
376+
})
377+
.then(() => {
378+
client.enableKeyBackup({
379+
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
380+
version: 1,
381+
auth_data: {
382+
iv: "PsCAtR7gMc4xBd9YS3A9Ow",
383+
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
384+
},
385+
});
386+
let numCalls = 0;
387+
return new Promise((resolve, reject) => {
388+
client.http.authedRequest = function(
389+
callback, method, path, queryParams, data, opts,
390+
) {
391+
++numCalls;
392+
expect(numCalls).toBeLessThanOrEqual(1);
393+
if (numCalls >= 2) {
394+
// exit out of retry loop if there's something wrong
395+
reject(new Error("authedRequest called too many timmes"));
396+
return Promise.resolve({});
397+
}
398+
expect(method).toBe("PUT");
399+
expect(path).toBe("/room_keys/keys");
400+
expect(queryParams.version).toBe(1);
401+
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
402+
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
403+
groupSession.session_id(),
404+
);
405+
resolve();
406+
return Promise.resolve({});
407+
};
408+
client.crypto.backupManager.backupGroupSession(
409+
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
410+
groupSession.session_id(),
411+
);
412+
}).then(() => {
413+
expect(numCalls).toBe(1);
414+
});
415+
});
416+
});
417+
309418
it('signs backups with the cross-signing master key', async function() {
310419
const groupSession = new Olm.OutboundGroupSession();
311420
groupSession.create();
@@ -512,38 +621,55 @@ describe("MegolmBackup", function() {
512621
client.stopClient();
513622
});
514623

515-
it('can restore from backup', function() {
624+
it('can restore from backup (Curve25519 version)', function() {
625+
client.http.authedRequest = function() {
626+
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
627+
};
628+
return client.restoreKeyBackupWithRecoveryKey(
629+
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
630+
ROOM_ID,
631+
SESSION_ID,
632+
CURVE25519_BACKUP_INFO,
633+
).then(() => {
634+
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
635+
}).then((res) => {
636+
expect(res.clearEvent.content).toEqual('testytest');
637+
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted
638+
});
639+
});
640+
641+
it('can restore from backup (AES-256 version)', function() {
516642
client.http.authedRequest = function() {
517-
return Promise.resolve(KEY_BACKUP_DATA);
643+
return Promise.resolve(AES256_KEY_BACKUP_DATA);
518644
};
519645
return client.restoreKeyBackupWithRecoveryKey(
520646
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
521647
ROOM_ID,
522648
SESSION_ID,
523-
BACKUP_INFO,
649+
AES256_BACKUP_INFO,
524650
).then(() => {
525651
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
526652
}).then((res) => {
527653
expect(res.clearEvent.content).toEqual('testytest');
528-
expect(res.untrusted).toBeTruthy(); // keys from backup are untrusted
654+
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted
529655
});
530656
});
531657

532-
it('can restore backup by room', function() {
658+
it('can restore backup by room (Curve25519 version)', function() {
533659
client.http.authedRequest = function() {
534660
return Promise.resolve({
535661
rooms: {
536662
[ROOM_ID]: {
537663
sessions: {
538-
[SESSION_ID]: KEY_BACKUP_DATA,
664+
[SESSION_ID]: CURVE25519_KEY_BACKUP_DATA,
539665
},
540666
},
541667
},
542668
});
543669
};
544670
return client.restoreKeyBackupWithRecoveryKey(
545671
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
546-
null, null, BACKUP_INFO,
672+
null, null, CURVE25519_BACKUP_INFO,
547673
).then(() => {
548674
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
549675
}).then((res) => {
@@ -562,14 +688,14 @@ describe("MegolmBackup", function() {
562688
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
563689
expect(cachedNull).toBeNull();
564690
client.http.authedRequest = function() {
565-
return Promise.resolve(KEY_BACKUP_DATA);
691+
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
566692
};
567693
await new Promise((resolve) => {
568694
client.restoreKeyBackupWithRecoveryKey(
569695
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
570696
ROOM_ID,
571697
SESSION_ID,
572-
BACKUP_INFO,
698+
CURVE25519_BACKUP_INFO,
573699
{ cacheCompleteCallback: resolve },
574700
);
575701
});
@@ -578,11 +704,11 @@ describe("MegolmBackup", function() {
578704
});
579705

580706
it("fails if an known algorithm is used", async function() {
581-
const BAD_BACKUP_INFO = Object.assign({}, BACKUP_INFO, {
707+
const BAD_BACKUP_INFO = Object.assign({}, CURVE25519_BACKUP_INFO, {
582708
algorithm: "this.algorithm.does.not.exist",
583709
});
584710
client.http.authedRequest = function() {
585-
return Promise.resolve(KEY_BACKUP_DATA);
711+
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
586712
};
587713

588714
await expect(client.restoreKeyBackupWithRecoveryKey(

src/@types/signed.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ export interface ISignatures {
1919
[keyId: string]: string;
2020
};
2121
}
22+
23+
export interface ISigned {
24+
signatures?: ISignatures;
25+
}

src/client.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2142,28 +2142,26 @@ export class MatrixClient extends EventEmitter {
21422142
* Get information about the current key backup.
21432143
* @returns {Promise} Information object from API or null
21442144
*/
2145-
public getKeyBackupVersion(): Promise<IKeyBackupInfo> {
2146-
return this.http.authedRequest(
2147-
undefined, "GET", "/room_keys/version", undefined, undefined,
2148-
{ prefix: PREFIX_UNSTABLE },
2149-
).then((res) => {
2150-
if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) {
2151-
const err = "Unknown backup algorithm: " + res.algorithm;
2152-
return Promise.reject(err);
2153-
} else if (!(typeof res.auth_data === "object")
2154-
|| !res.auth_data.public_key) {
2155-
const err = "Invalid backup data returned";
2156-
return Promise.reject(err);
2157-
} else {
2158-
return res;
2159-
}
2160-
}).catch((e) => {
2145+
public async getKeyBackupVersion(): Promise<IKeyBackupInfo> {
2146+
let res;
2147+
try {
2148+
res = await this.http.authedRequest(
2149+
undefined, "GET", "/room_keys/version", undefined, undefined,
2150+
{ prefix: PREFIX_UNSTABLE },
2151+
);
2152+
} catch (e) {
21612153
if (e.errcode === 'M_NOT_FOUND') {
21622154
return null;
21632155
} else {
21642156
throw e;
21652157
}
2166-
});
2158+
}
2159+
try {
2160+
BackupManager.checkBackupVersion(res);
2161+
} catch (e) {
2162+
throw e;
2163+
}
2164+
return res;
21672165
}
21682166

21692167
/**
@@ -2574,6 +2572,8 @@ export class MatrixClient extends EventEmitter {
25742572

25752573
const algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => { return privKey; });
25762574

2575+
const untrusted = algorithm.untrusted;
2576+
25772577
try {
25782578
// If the pubkey computed from the private data we've been given
25792579
// doesn't match the one in the auth_data, the user has entered
@@ -2641,7 +2641,7 @@ export class MatrixClient extends EventEmitter {
26412641

26422642
await this.importRoomKeys(keys, {
26432643
progressCallback,
2644-
untrusted: true,
2644+
untrusted,
26452645
source: "backup",
26462646
});
26472647

src/crypto/SecretStorage.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,14 @@ limitations under the License.
1717
import { logger } from '../logger';
1818
import * as olmlib from './olmlib';
1919
import { randomString } from '../randomstring';
20-
import { encryptAES, decryptAES, IEncryptedPayload } from './aes';
20+
import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './aes';
2121
import { encodeBase64 } from "./olmlib";
2222
import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix';
2323
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
2424
import { EventEmitter } from 'stream';
2525

2626
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
2727

28-
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
29-
3028
// Some of the key functions use a tuple and some use an object...
3129
export type SecretStorageKeyTuple = [keyId: string, keyInfo: ISecretStorageKeyInfo];
3230
export type SecretStorageKeyObject = {keyId: string, keyInfo: ISecretStorageKeyInfo};
@@ -139,7 +137,7 @@ export class SecretStorage {
139137
keyInfo.passphrase = opts.passphrase;
140138
}
141139
if (opts.key) {
142-
const { iv, mac } = await SecretStorage.calculateKeyCheck(opts.key);
140+
const { iv, mac } = await calculateKeyCheck(opts.key);
143141
keyInfo.iv = iv;
144142
keyInfo.mac = mac;
145143
}
@@ -212,7 +210,7 @@ export class SecretStorage {
212210
public async checkKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
213211
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
214212
if (info.mac) {
215-
const { mac } = await SecretStorage.calculateKeyCheck(key, info.iv);
213+
const { mac } = await calculateKeyCheck(key, info.iv);
216214
return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, '');
217215
} else {
218216
// if we have no information, we have to assume the key is right
@@ -223,10 +221,6 @@ export class SecretStorage {
223221
}
224222
}
225223

226-
public static async calculateKeyCheck(key: Uint8Array, iv?: string): Promise<IEncryptedPayload> {
227-
return await encryptAES(ZERO_STR, key, "", iv);
228-
}
229-
230224
/**
231225
* Store an encrypted secret on the server
232226
*

src/crypto/aes.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,16 @@ export function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: strin
261261
return subtleCrypto ? decryptBrowser(data, key, name) : decryptNode(data, key, name);
262262
}
263263

264+
// string of zeroes, for calculating the key check
265+
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
266+
267+
/** Calculate the MAC for checking the key.
268+
*
269+
* @param {Uint8Array} key the key to use
270+
* @param {string} [iv] The initialization vector as a base64-encoded string.
271+
* If omitted, a random initialization vector will be created.
272+
* @return {Promise<object>} An object that contains, `mac` and `iv` properties.
273+
*/
274+
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<IEncryptedPayload> {
275+
return encryptAES(ZERO_STR, key, "", iv);
276+
}

src/crypto/algorithms/megolm.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1670,7 +1670,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
16701670
*/
16711671
public importRoomKey(session: IMegolmSessionData, opts: any = {}): Promise<void> {
16721672
const extraSessionData: any = {};
1673-
if (opts.untrusted) {
1673+
if (opts.untrusted || session.untrusted) {
16741674
extraSessionData.untrusted = true;
16751675
}
16761676
if (session["org.matrix.msc3061.shared_history"]) {

0 commit comments

Comments
 (0)