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

Symmetric backup #1775

Merged
merged 14 commits into from
Jul 9, 2021
156 changes: 141 additions & 15 deletions spec/unit/crypto/backup.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const ENCRYPTED_EVENT = new MatrixEvent({
origin_server_ts: 1507753886000,
});

const KEY_BACKUP_DATA = {
const CURVE25519_KEY_BACKUP_DATA = {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
Expand All @@ -73,14 +73,41 @@ const KEY_BACKUP_DATA = {
},
};

const BACKUP_INFO = {
const AES256_KEY_BACKUP_DATA = {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
session_data: {
iv: 'b3Jqqvm5S9QdmXrzssspLQ',
ciphertext: 'GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce'
+ '7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd'
+ 'EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0'
+ 'WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r'
+ 'KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P'
+ 'vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K'
+ 'YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd'
+ 'fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA'
+ 'RgaDHkfzoA3g3aeQ',
mac: 'uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU',
},
};

const CURVE25519_BACKUP_INFO = {
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};

const AES256_BACKUP_INFO = {
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: 1,
auth_data: {
// FIXME: add iv and mac
},
};

const keys = {};

function getCrossSigningKey(type) {
Expand Down Expand Up @@ -144,7 +171,7 @@ describe("MegolmBackup", function() {
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
);
mockCrypto.backupInfo = BACKUP_INFO;
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;

mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
Expand Down Expand Up @@ -228,7 +255,7 @@ describe("MegolmBackup", function() {
});
});

it('sends backups to the server', function() {
it('sends backups to the server (Curve25519 version)', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
Expand Down Expand Up @@ -306,6 +333,88 @@ describe("MegolmBackup", function() {
});
});

it('sends backups to the server (AES-256 version)', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());

const client = makeTestClient(sessionStore, cryptoStore);

megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});

megolmDecryption.olmlib = mockOlmLib;

return client.initCrypto()
.then(() => {
return client.crypto.storeSessionBackupPrivateKey(new Uint8Array(32));
})
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: 1,
auth_data: {
iv: "PsCAtR7gMc4xBd9YS3A9Ow",
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({});
};
client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
);
}).then(() => {
expect(numCalls).toBe(1);
});
});
});

it('signs backups with the cross-signing master key', async function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
Expand Down Expand Up @@ -512,38 +621,55 @@ describe("MegolmBackup", function() {
client.stopClient();
});

it('can restore from backup', function() {
it('can restore from backup (Curve25519 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
CURVE25519_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted
});
});

it('can restore from backup (AES-256 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
return Promise.resolve(AES256_KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BACKUP_INFO,
AES256_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
expect(res.untrusted).toBeTruthy(); // keys from backup are untrusted
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted
});
});

it('can restore backup by room', function() {
it('can restore backup by room (Curve25519 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve({
rooms: {
[ROOM_ID]: {
sessions: {
[SESSION_ID]: KEY_BACKUP_DATA,
[SESSION_ID]: CURVE25519_KEY_BACKUP_DATA,
},
},
},
});
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null, null, BACKUP_INFO,
null, null, CURVE25519_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
Expand All @@ -562,14 +688,14 @@ describe("MegolmBackup", function() {
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
expect(cachedNull).toBeNull();
client.http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
};
await new Promise((resolve) => {
client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BACKUP_INFO,
CURVE25519_BACKUP_INFO,
{ cacheCompleteCallback: resolve },
);
});
Expand All @@ -578,11 +704,11 @@ describe("MegolmBackup", function() {
});

it("fails if an known algorithm is used", async function() {
const BAD_BACKUP_INFO = Object.assign({}, BACKUP_INFO, {
const BAD_BACKUP_INFO = Object.assign({}, CURVE25519_BACKUP_INFO, {
algorithm: "this.algorithm.does.not.exist",
});
client.http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
};

await expect(client.restoreKeyBackupWithRecoveryKey(
Expand Down
36 changes: 18 additions & 18 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2138,28 +2138,26 @@ export class MatrixClient extends EventEmitter {
* Get information about the current key backup.
* @returns {Promise} Information object from API or null
*/
public getKeyBackupVersion(): Promise<IKeyBackupInfo> {
return this.http.authedRequest(
undefined, "GET", "/room_keys/version", undefined, undefined,
{ prefix: PREFIX_UNSTABLE },
).then((res) => {
if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) {
const err = "Unknown backup algorithm: " + res.algorithm;
return Promise.reject(err);
} else if (!(typeof res.auth_data === "object")
|| !res.auth_data.public_key) {
const err = "Invalid backup data returned";
return Promise.reject(err);
} else {
return res;
}
}).catch((e) => {
public async getKeyBackupVersion(): Promise<IKeyBackupInfo> {
let res;
try {
res = await this.http.authedRequest(
undefined, "GET", "/room_keys/version", undefined, undefined,
{ prefix: PREFIX_UNSTABLE },
);
} catch (e) {
if (e.errcode === 'M_NOT_FOUND') {
return null;
} else {
throw e;
}
});
}
try {
BackupManager.checkBackupVersion(res);
} catch (e) {
throw e;
}
return res;
}

/**
Expand Down Expand Up @@ -2570,6 +2568,8 @@ export class MatrixClient extends EventEmitter {

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

const untrusted = algorithm.untrusted;

try {
// If the pubkey computed from the private data we've been given
// doesn't match the one in the auth_data, the user has entered
Expand Down Expand Up @@ -2637,7 +2637,7 @@ export class MatrixClient extends EventEmitter {

await this.importRoomKeys(keys, {
progressCallback,
untrusted: true,
untrusted,
source: "backup",
});

Expand Down
12 changes: 3 additions & 9 deletions src/crypto/SecretStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@ import { EventEmitter } from 'events';
import { logger } from '../logger';
import * as olmlib from './olmlib';
import { randomString } from '../randomstring';
import { encryptAES, decryptAES } from './aes';
import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
import { encodeBase64 } from "./olmlib";

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

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";

/**
* Implements Secure Secret Storage and Sharing (MSC1946)
* @module crypto/SecretStorage
Expand Down Expand Up @@ -99,7 +97,7 @@ export class SecretStorage extends EventEmitter {
keyInfo.passphrase = opts.passphrase;
}
if (opts.key) {
const { iv, mac } = await SecretStorage._calculateKeyCheck(opts.key);
const { iv, mac } = await calculateKeyCheck(opts.key);
keyInfo.iv = iv;
keyInfo.mac = mac;
}
Expand Down Expand Up @@ -171,7 +169,7 @@ export class SecretStorage extends EventEmitter {
async checkKey(key, info) {
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (info.mac) {
const { mac } = await SecretStorage._calculateKeyCheck(key, info.iv);
const { mac } = await calculateKeyCheck(key, info.iv);
return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, '');
} else {
// if we have no information, we have to assume the key is right
Expand All @@ -182,10 +180,6 @@ export class SecretStorage extends EventEmitter {
}
}

static async _calculateKeyCheck(key, iv) {
return await encryptAES(ZERO_STR, key, "", iv);
}

/**
* Store an encrypted secret on the server
*
Expand Down
13 changes: 13 additions & 0 deletions src/crypto/aes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,16 @@ export function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: strin
return subtleCrypto ? decryptBrowser(data, key, name) : decryptNode(data, key, name);
}

// string of zeroes, for calculating the key check
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";

/** Calculate the MAC for checking the key.
*
* @param {Uint8Array} key the key to use
* @param {string} [iv] The initialization vector as a base64-encoded string.
* If omitted, a random initialization vector will be created.
* @return {Promise<object>} An object that contains, `mac` and `iv` properties.
*/
export function calculateKeyCheck(key, iv = undefined) {
return encryptAES(ZERO_STR, key, "", iv);
uhoreg marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 1 addition & 1 deletion src/crypto/algorithms/megolm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1670,7 +1670,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
*/
public importRoomKey(session: IMegolmSessionData, opts: any = {}): Promise<void> {
const extraSessionData: any = {};
if (opts.untrusted) {
if (opts.untrusted || session.untrusted) {
extraSessionData.untrusted = true;
}
if (session["org.matrix.msc3061.shared_history"]) {
Expand Down
Loading