1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-29 16:43:09 +03:00

Merge pull request #597 from matrix-org/dbkr/e2e_rooms_indexeddb

Migrate room encryption store to crypto store
This commit is contained in:
David Baker
2018-02-06 10:29:29 +00:00
committed by GitHub
8 changed files with 207 additions and 49 deletions

View File

@@ -42,6 +42,7 @@ const MatrixBaseApis = require("./base-apis");
const MatrixError = httpApi.MatrixError; const MatrixError = httpApi.MatrixError;
import ReEmitter from './ReEmitter'; import ReEmitter from './ReEmitter';
import RoomList from './crypto/RoomList';
const SCROLLBACK_DELAY_MS = 3000; const SCROLLBACK_DELAY_MS = 3000;
let CRYPTO_ENABLED = false; let CRYPTO_ENABLED = false;
@@ -181,6 +182,11 @@ function MatrixClient(opts) {
if (CRYPTO_ENABLED) { if (CRYPTO_ENABLED) {
this.olmVersion = Crypto.getOlmVersion(); this.olmVersion = Crypto.getOlmVersion();
} }
// List of which rooms have encryption enabled: separate from crypto because
// we still want to know which rooms are encrypted even if crypto is disabled:
// we don't want to start sending unencrypted events to them.
this._roomList = new RoomList(this._cryptoStore, this._sessionStore);
} }
utils.inherits(MatrixClient, EventEmitter); utils.inherits(MatrixClient, EventEmitter);
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
@@ -351,13 +357,6 @@ MatrixClient.prototype.initCrypto = async function() {
return; return;
} }
if (!CRYPTO_ENABLED) {
throw new Error(
`End-to-end encryption not supported in this js-sdk build: did ` +
`you remember to load the olm library?`,
);
}
if (!this._sessionStore) { if (!this._sessionStore) {
// this is temporary, the sessionstore is supposed to be going away // this is temporary, the sessionstore is supposed to be going away
throw new Error(`Cannot enable encryption: no sessionStore provided`); throw new Error(`Cannot enable encryption: no sessionStore provided`);
@@ -367,6 +366,16 @@ MatrixClient.prototype.initCrypto = async function() {
throw new Error(`Cannot enable encryption: no cryptoStore provided`); throw new Error(`Cannot enable encryption: no cryptoStore provided`);
} }
// initialise the list of encrypted rooms (whether or not crypto is enabled)
await this._roomList.init();
if (!CRYPTO_ENABLED) {
throw new Error(
`End-to-end encryption not supported in this js-sdk build: did ` +
`you remember to load the olm library?`,
);
}
const userId = this.getUserId(); const userId = this.getUserId();
if (userId === null) { if (userId === null) {
throw new Error( throw new Error(
@@ -387,6 +396,7 @@ MatrixClient.prototype.initCrypto = async function() {
userId, this.deviceId, userId, this.deviceId,
this.store, this.store,
this._cryptoStore, this._cryptoStore,
this._roomList,
); );
this.reEmitter.reEmit(crypto, [ this.reEmitter.reEmit(crypto, [
@@ -646,11 +656,7 @@ MatrixClient.prototype.isRoomEncrypted = function(roomId) {
// we don't have an m.room.encrypted event, but that might be because // we don't have an m.room.encrypted event, but that might be because
// the server is hiding it from us. Check the store to see if it was // the server is hiding it from us. Check the store to see if it was
// previously encrypted. // previously encrypted.
if (!this._sessionStore) { return this._roomList.isRoomEncrypted(roomId);
return false;
}
return Boolean(this._sessionStore.getEndToEndRoom(roomId));
}; };
/** /**

81
src/crypto/RoomList.js Normal file
View File

@@ -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.
*/
/**
* @module crypto/RoomList
*
* Manages the list of encrypted rooms
*/
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
/**
* @alias module:crypto/RoomList
*/
export default class RoomList {
constructor(cryptoStore, sessionStore) {
this._cryptoStore = cryptoStore;
this._sessionStore = sessionStore;
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
this._roomEncryption = {};
}
async init() {
let removeSessionStoreRooms = false;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this._cryptoStore.getEndToEndRooms(txn, (result) => {
if (result === null || Object.keys(result).length === 0) {
// migrate from session store, if there's data there
const sessStoreRooms = this._sessionStore.getAllEndToEndRooms();
if (sessStoreRooms !== null) {
for (const roomId of Object.keys(sessStoreRooms)) {
this._cryptoStore.storeEndToEndRoom(
roomId, sessStoreRooms[roomId], txn,
);
}
}
this._roomEncryption = sessStoreRooms;
removeSessionStoreRooms = true;
} else {
this._roomEncryption = result;
}
});
},
);
if (removeSessionStoreRooms) {
this._sessionStore.removeAllEndToEndRooms();
}
}
getRoomEncryption(roomId) {
return this._roomEncryption[roomId] || null;
}
isRoomEncrypted(roomId) {
return Boolean(this.getRoomEncryption(roomId));
}
async setRoomEncryption(roomId, roomInfo) {
this._roomEncryption[roomId] = roomInfo;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this._cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
},
);
}
}

View File

@@ -59,15 +59,18 @@ import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
* *
* @param {module:crypto/store/base~CryptoStore} cryptoStore * @param {module:crypto/store/base~CryptoStore} cryptoStore
* storage for the crypto layer. * storage for the crypto layer.
*
* @param {RoomList} roomList An initialised RoomList object
*/ */
function Crypto(baseApis, sessionStore, userId, deviceId, function Crypto(baseApis, sessionStore, userId, deviceId,
clientStore, cryptoStore) { clientStore, cryptoStore, roomList) {
this._baseApis = baseApis; this._baseApis = baseApis;
this._sessionStore = sessionStore; this._sessionStore = sessionStore;
this._userId = userId; this._userId = userId;
this._deviceId = deviceId; this._deviceId = deviceId;
this._clientStore = clientStore; this._clientStore = clientStore;
this._cryptoStore = cryptoStore; this._cryptoStore = cryptoStore;
this._roomList = roomList;
this._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._olmDevice = new OlmDevice(sessionStore, cryptoStore);
this._deviceList = new DeviceList( this._deviceList = new DeviceList(
@@ -587,7 +590,6 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
return device; return device;
}; };
/** /**
* Configure a room to use encryption (ie, save a flag in the sessionstore). * Configure a room to use encryption (ie, save a flag in the sessionstore).
* *
@@ -601,21 +603,19 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) { Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) {
// if we already have encryption in this room, we should ignore this event // if we already have encryption in this room, we should ignore this event
// (for now at least. maybe we should alert the user somehow?) // (for now at least. maybe we should alert the user somehow?)
const existingConfig = this._sessionStore.getEndToEndRoom(roomId); const existingConfig = this._roomList.getRoomEncryption(roomId);
if (existingConfig) { if (existingConfig && JSON.stringify(existingConfig) != JSON.stringify(config)) {
if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
console.error("Ignoring m.room.encryption event which requests " + console.error("Ignoring m.room.encryption event which requests " +
"a change of config in " + roomId); "a change of config in " + roomId);
return; return;
} }
}
const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm]; const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm];
if (!AlgClass) { if (!AlgClass) {
throw new Error("Unable to encrypt with " + config.algorithm); throw new Error("Unable to encrypt with " + config.algorithm);
} }
this._sessionStore.storeEndToEndRoom(roomId, config); await this._roomList.setRoomEncryption(roomId, config);
const alg = new AlgClass({ const alg = new AlgClass({
userId: this._userId, userId: this._userId,
@@ -693,16 +693,6 @@ Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
); );
}; };
/**
* Whether encryption is enabled for a room.
* @param {string} roomId the room id to query.
* @return {bool} whether encryption is enabled.
*/
Crypto.prototype.isRoomEncrypted = function(roomId) {
return Boolean(this._roomEncryptors[roomId]);
};
/** /**
* Get a list containing all of the room keys * Get a list containing all of the room keys
* *

View File

@@ -18,7 +18,7 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import utils from '../../utils'; import utils from '../../utils';
export const VERSION = 5; export const VERSION = 6;
/** /**
* Implementation of a CryptoStore which is backed by an existing * Implementation of a CryptoStore which is backed by an existing
@@ -425,6 +425,30 @@ export class Backend {
objectStore.put(deviceData, "-"); objectStore.put(deviceData, "-");
} }
storeEndToEndRoom(roomId, roomInfo, txn) {
const objectStore = txn.objectStore("rooms");
objectStore.put(roomInfo, roomId);
}
getEndToEndRooms(txn, func) {
const rooms = {};
const objectStore = txn.objectStore("rooms");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
rooms[cursor.key] = cursor.value;
cursor.continue();
} else {
try {
func(rooms);
} catch (e) {
abortWithException(txn, e);
}
}
};
}
doTxn(mode, stores, func) { doTxn(mode, stores, func) {
const txn = this._db.transaction(stores, mode); const txn = this._db.transaction(stores, mode);
const promise = promiseifyTxn(txn); const promise = promiseifyTxn(txn);
@@ -460,6 +484,9 @@ export function upgradeDatabase(db, oldVersion) {
if (oldVersion < 5) { if (oldVersion < 5) {
db.createObjectStore("device_data"); db.createObjectStore("device_data");
} }
if (oldVersion < 6) {
db.createObjectStore("rooms");
}
// Expand as needed. // Expand as needed.
} }

View File

@@ -386,6 +386,27 @@ export default class IndexedDBCryptoStore {
this._backendPromise.value().getEndToEndDeviceData(txn, func); this._backendPromise.value().getEndToEndDeviceData(txn, func);
} }
// End to End Rooms
/**
* Store the end-to-end state for a room.
* @param {string} roomId The room's ID.
* @param {object} roomInfo The end-to-end info for the room.
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndRoom(roomId, roomInfo, txn) {
this._backendPromise.value().storeEndToEndRoom(roomId, roomInfo, txn);
}
/**
* Get an object of roomId->roomInfo for all e2e rooms in the store
* @param {*} txn An active transaction. See doTxn().
* @param {function(Object)} func Function called with the end to end encrypted rooms
*/
getEndToEndRooms(txn, func) {
this._backendPromise.value().getEndToEndRooms(txn, func);
}
/** /**
* Perform a transaction on the crypto store. Any store methods * Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may * that require a transaction (txn) object to be passed in may
@@ -418,3 +439,4 @@ IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data'; IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';

View File

@@ -31,6 +31,7 @@ const E2E_PREFIX = "crypto.";
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
function keyEndToEndSessions(deviceKey) { function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey; return E2E_PREFIX + "sessions/" + deviceKey;
@@ -40,6 +41,10 @@ function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId; return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
} }
function keyEndToEndRoomsPrefix(roomId) {
return KEY_ROOMS_PREFIX + roomId;
}
/** /**
* @implements {module:crypto/store/base~CryptoStore} * @implements {module:crypto/store/base~CryptoStore}
*/ */
@@ -140,6 +145,26 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
); );
} }
storeEndToEndRoom(roomId, roomInfo, txn) {
setJsonItem(
this.store, keyEndToEndRoomsPrefix(roomId), roomInfo,
);
}
getEndToEndRooms(txn, func) {
const result = {};
const prefix = keyEndToEndRoomsPrefix('');
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key.startsWith(prefix)) {
const roomId = key.substr(prefix.length);
result[roomId] = getJsonItem(this.store, key);
}
}
func(result);
}
/** /**
* Delete all data from this store. * Delete all data from this store.
* *

View File

@@ -39,6 +39,8 @@ export default class MemoryCryptoStore {
this._inboundGroupSessions = {}; this._inboundGroupSessions = {};
// Opaque device data object // Opaque device data object
this._deviceData = null; this._deviceData = null;
// roomId -> Opaque roomInfo object
this._rooms = {};
} }
/** /**
@@ -283,6 +285,15 @@ export default class MemoryCryptoStore {
this._deviceData = deviceData; this._deviceData = deviceData;
} }
// E2E rooms
storeEndToEndRoom(roomId, roomInfo, txn) {
this._rooms[roomId] = roomInfo;
}
getEndToEndRooms(txn, func) {
func(this._rooms);
}
doTxn(mode, stores, func) { doTxn(mode, stores, func) {
return Promise.resolve(func(null)); return Promise.resolve(func(null));

View File

@@ -174,21 +174,21 @@ WebStorageSessionStore.prototype = {
}, },
/** /**
* Store the end-to-end state for a room. * Get the end-to-end state for all rooms
* @param {string} roomId The room's ID. * @return {object} roomId -> object with the end-to-end info for the room.
* @param {object} roomInfo The end-to-end info for the room.
*/ */
storeEndToEndRoom: function(roomId, roomInfo) { getAllEndToEndRooms: function() {
setJsonItem(this.store, keyEndToEndRoom(roomId), roomInfo); const roomKeys = getKeysWithPrefix(this.store, keyEndToEndRoom(''));
const results = {};
for (const k of roomKeys) {
const unprefixedKey = k.substr(keyEndToEndRoom('').length);
results[unprefixedKey] = getJsonItem(this.store, k);
}
return results;
}, },
/** removeAllEndToEndRooms: function() {
* Get the end-to-end state for a room removeByPrefix(this.store, keyEndToEndRoom(''));
* @param {string} roomId The room's ID.
* @return {object} The end-to-end info for the room.
*/
getEndToEndRoom: function(roomId) {
return getJsonItem(this.store, keyEndToEndRoom(roomId));
}, },
}; };
@@ -224,10 +224,6 @@ function getJsonItem(store, key) {
return null; return null;
} }
function setJsonItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
}
function getKeysWithPrefix(store, prefix) { function getKeysWithPrefix(store, prefix) {
const results = []; const results = [];
for (let i = 0; i < store.length; ++i) { for (let i = 0; i < store.length; ++i) {