From 44d99277fe600c23272ec27c8ef4513ce1d977de Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 20 Nov 2018 13:09:59 +0000 Subject: [PATCH 1/6] Support passphrase-based e2e key backups --- src/client.js | 50 +++++++++++++++++---- src/crypto/backup_password.js | 81 +++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 src/crypto/backup_password.js diff --git a/src/client.js b/src/client.js index 9ff1762c0e7..da7d5218a17 100644 --- a/src/client.js +++ b/src/client.js @@ -49,6 +49,7 @@ import RoomList from './crypto/RoomList'; import Crypto from './crypto'; import { isCryptoAvailable } from './crypto'; import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey'; +import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password'; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -860,22 +861,37 @@ MatrixClient.prototype.disableKeyBackup = function() { * Set up the data required to create a new backup version. The backup version * will not be created and enabled until createKeyBackupVersion is called. * + * @param {string} password Passphrase string that can be entered by the user + * when restoring the backup as an alternative to entering the recovery key. + * Optional. + * * @returns {object} Object that can be passed to createKeyBackupVersion and * additionally has a 'recovery_key' member with the user-facing recovery key string. */ -MatrixClient.prototype.prepareKeyBackupVersion = function() { +MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } const decryption = new global.Olm.PkDecryption(); try { - const publicKey = decryption.generate_key(); + let privateKey; + let publicKey; + let authData = {}; + if (password) { + const keyInfo = await keyForNewBackup(password); + publicKey = decryption.init_with_private_key(keyInfo.key); + authData.private_key_salt = keyInfo.salt; + authData.private_key_iterations = keyInfo.iterations; + } else { + publicKey = decryption.generate_key(); + } + + authData.public_key = publicKey; + return { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - auth_data: { - public_key: publicKey, - }, + auth_data: authData, recovery_key: encodeRecoveryKey(decryption.get_private_key()), }; } finally { @@ -992,8 +1008,28 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { } }; -MatrixClient.prototype.restoreKeyBackups = function( +MatrixClient.prototype.restoreKeyBackupWithPassword = async function( + password, targetRoomId, targetSessionId, version, +) { + const backupInfo = await this.getKeyBackupVersion(); + + const privKey = keyForExistingBackup(backupInfo, password); + return this._restoreKeyBackup( + privKey, targetRoomId, targetSessionId, version, + ); +}; + +MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function( recoveryKey, targetRoomId, targetSessionId, version, +) { + const privKey = decodeRecoveryKey(recoveryKey); + return this._restoreKeyBackup( + privKey, targetRoomId, targetSessionId, version, + ); +}; + +MatrixClient.prototype._restoreKeyBackup = function( + privKey, targetRoomId, targetSessionId, version, ) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); @@ -1003,8 +1039,6 @@ MatrixClient.prototype.restoreKeyBackups = function( const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); - // FIXME: see the FIXME in createKeyBackupVersion - const privkey = decodeRecoveryKey(recoveryKey); const decryption = new global.Olm.PkDecryption(); try { decryption.init_with_private_key(privkey); diff --git a/src/crypto/backup_password.js b/src/crypto/backup_password.js new file mode 100644 index 00000000000..946b996816b --- /dev/null +++ b/src/crypto/backup_password.js @@ -0,0 +1,81 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + +import { randomString } from '../randomstring'; + +const DEFAULT_ITERATIONS = 500000; + +export async function keyForExistingBackup(backupData, password) { + if (!global.Olm) { + throw new Error("Olm is not available"); + } + + const authData = backupData.auth_data; + + if (!authData.private_key_salt || !authData.private_key_iterations) { + throw new Error( + "Salt and/or iterations not found: " + + "this backup cannot be restored with a passphrase", + ); + } + + return await deriveKey( + password, backupData.private_key_salt, + backupData.private_key_iterations, + ); +} + +export async function keyForNewBackup(password) { + if (!global.Olm) { + throw new Error("Olm is not available"); + } + + const salt = randomString(32); + + const key = await deriveKey(password, salt, DEFAULT_ITERATIONS); + + return { key, salt, iterations: DEFAULT_ITERATIONS }; +} + +async function deriveKey(password, salt, iterations) { + const subtleCrypto = global.crypto.subtle; + const TextEncoder = global.TextEncoder; + if (!subtleCrypto || !TextEncoder) { + // TODO: Implement this for node + throw new Error("Password-based backup is not avaiable on this platform"); + } + + const key = await subtleCrypto.importKey( + 'raw', + new TextEncoder().encode(password), + {name: 'PBKDF2'}, + false, + ['deriveBits'], + ); + + const keybits = await subtleCrypto.deriveBits( + { + name: 'PBKDF2', + salt: new TextEncoder().encode(salt), + iterations: iterations, + hash: 'SHA-512', + }, + key, + global.Olm.PRIVATE_KEY_LENGTH * 8, + ); + + return new Uint8Array(keybits); +} From cb51799246579fa45c3290b7659e417c71b98d4d Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 20 Nov 2018 16:15:29 +0000 Subject: [PATCH 2/6] Make backup restore work --- src/client.js | 4 ++-- src/crypto/backup_password.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client.js b/src/client.js index da7d5218a17..cd236aedbf3 100644 --- a/src/client.js +++ b/src/client.js @@ -1013,7 +1013,7 @@ MatrixClient.prototype.restoreKeyBackupWithPassword = async function( ) { const backupInfo = await this.getKeyBackupVersion(); - const privKey = keyForExistingBackup(backupInfo, password); + const privKey = await keyForExistingBackup(backupInfo, password); return this._restoreKeyBackup( privKey, targetRoomId, targetSessionId, version, ); @@ -1041,7 +1041,7 @@ MatrixClient.prototype._restoreKeyBackup = function( const decryption = new global.Olm.PkDecryption(); try { - decryption.init_with_private_key(privkey); + decryption.init_with_private_key(privKey); } catch(e) { decryption.free(); throw e; diff --git a/src/crypto/backup_password.js b/src/crypto/backup_password.js index 946b996816b..1a6d1f28426 100644 --- a/src/crypto/backup_password.js +++ b/src/crypto/backup_password.js @@ -33,8 +33,8 @@ export async function keyForExistingBackup(backupData, password) { } return await deriveKey( - password, backupData.private_key_salt, - backupData.private_key_iterations, + password, backupData.auth_data.private_key_salt, + backupData.auth_data.private_key_iterations, ); } From 6047838f53092aa44b3ea3f10a7a7dfdfd353079 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 20 Nov 2018 16:17:58 +0000 Subject: [PATCH 3/6] lint --- src/client.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index cd236aedbf3..8ac8b6a1f3a 100644 --- a/src/client.js +++ b/src/client.js @@ -875,9 +875,8 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { const decryption = new global.Olm.PkDecryption(); try { - let privateKey; let publicKey; - let authData = {}; + const authData = {}; if (password) { const keyInfo = await keyForNewBackup(password); publicKey = decryption.init_with_private_key(keyInfo.key); From eeea70640e6b5c5e769f393e8f50535726e0fa83 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 20 Nov 2018 16:28:29 +0000 Subject: [PATCH 4/6] Add randomString factored out from client secret --- src/client.js | 10 ++-------- src/randomstring.js | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 src/randomstring.js diff --git a/src/client.js b/src/client.js index 8ac8b6a1f3a..daaaf52c97d 100644 --- a/src/client.js +++ b/src/client.js @@ -50,6 +50,7 @@ import Crypto from './crypto'; import { isCryptoAvailable } from './crypto'; import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey'; import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password'; +import { randomString } from './randomstring'; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -3862,14 +3863,7 @@ MatrixClient.prototype.getEventMapper = function() { * @return {string} A new client secret */ MatrixClient.prototype.generateClientSecret = function() { - let ret = ""; - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (let i = 0; i < 32; i++) { - ret += chars.charAt(Math.floor(Math.random() * chars.length)); - } - - return ret; + return randomString(32); }; /** */ diff --git a/src/randomstring.js b/src/randomstring.js new file mode 100644 index 00000000000..7ebe4ed78ac --- /dev/null +++ b/src/randomstring.js @@ -0,0 +1,26 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + +export function randomString(len) { + let ret = ""; + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for (let i = 0; i < len; ++i) { + ret += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return ret; +} From abd2ac71688074b3d333c38e590c28c027a1cdb4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 20 Nov 2018 16:34:04 +0000 Subject: [PATCH 5/6] Rename backup API call in test --- spec/unit/crypto/backup.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index bf9fe8eddd1..a654312b7db 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -435,7 +435,7 @@ describe("MegolmBackup", function() { client._http.authedRequest = function() { return Promise.resolve(KEY_BACKUP_DATA); }; - return client.restoreKeyBackups( + return client.restoreKeyBackupWithRecoveryKey( "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", ROOM_ID, SESSION_ID, @@ -458,7 +458,7 @@ describe("MegolmBackup", function() { }, }); }; - return client.restoreKeyBackups( + return client.restoreKeyBackupWithRecoveryKey( "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", ).then(() => { return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); From 092f4217b02f31c17f437895dac48af62a4f2c65 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 21 Nov 2018 17:56:02 +0000 Subject: [PATCH 6/6] docs --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index daaaf52c97d..1840b3cac16 100644 --- a/src/client.js +++ b/src/client.js @@ -866,7 +866,7 @@ MatrixClient.prototype.disableKeyBackup = function() { * when restoring the backup as an alternative to entering the recovery key. * Optional. * - * @returns {object} Object that can be passed to createKeyBackupVersion and + * @returns {Promise} Object that can be passed to createKeyBackupVersion and * additionally has a 'recovery_key' member with the user-facing recovery key string. */ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) {