integrate session backup with session class

This commit is contained in:
Bruno Windels 2020-09-17 15:58:46 +02:00
parent 3cebd17cbe
commit 9d622434fb
7 changed files with 97 additions and 40 deletions

View file

@ -23,12 +23,19 @@ import {Account as E2EEAccount} from "./e2ee/Account.js";
import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption.js"; import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption.js";
import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js"; import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js";
import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption.js"; import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption.js";
import {SessionBackup} from "./e2ee/megolm/SessionBackup.js";
import {Encryption as MegOlmEncryption} from "./e2ee/megolm/Encryption.js"; import {Encryption as MegOlmEncryption} from "./e2ee/megolm/Encryption.js";
import {MEGOLM_ALGORITHM} from "./e2ee/common.js"; import {MEGOLM_ALGORITHM} from "./e2ee/common.js";
import {RoomEncryption} from "./e2ee/RoomEncryption.js"; import {RoomEncryption} from "./e2ee/RoomEncryption.js";
import {DeviceTracker} from "./e2ee/DeviceTracker.js"; import {DeviceTracker} from "./e2ee/DeviceTracker.js";
import {LockMap} from "../utils/LockMap.js"; import {LockMap} from "../utils/LockMap.js";
import {groupBy} from "../utils/groupBy.js"; import {groupBy} from "../utils/groupBy.js";
import {
keyFromCredential as ssssKeyFromCredential,
readKey as ssssReadKey,
writeKey as ssssWriteKey,
SecretStorage
} from "./ssss/index.js"
const PICKLE_KEY = "DEFAULT_KEY"; const PICKLE_KEY = "DEFAULT_KEY";
@ -54,6 +61,7 @@ export class Session {
this._megolmDecryption = null; this._megolmDecryption = null;
this._getSyncToken = () => this.syncToken; this._getSyncToken = () => this.syncToken;
this._olmWorker = olmWorker; this._olmWorker = olmWorker;
this._cryptoDriver = cryptoDriver;
if (olm) { if (olm) {
this._olmUtil = new olm.Utility(); this._olmUtil = new olm.Utility();
@ -130,10 +138,54 @@ export class Session {
megolmEncryption: this._megolmEncryption, megolmEncryption: this._megolmEncryption,
megolmDecryption: this._megolmDecryption, megolmDecryption: this._megolmDecryption,
storage: this._storage, storage: this._storage,
encryptionParams sessionBackup: this._sessionBackup,
encryptionParams,
}); });
} }
/**
* Enable secret storage by providing the secret storage credential.
* This will also see if there is a megolm session backup and try to enable that if so.
*
* @param {string} type either "passphrase" or "recoverykey"
* @param {string} credential either the passphrase or the recovery key, depending on the type
* @return {Promise} resolves or rejects after having tried to enable secret storage
*/
async enableSecretStorage(type, credential) {
if (!this._olm) {
throw new Error("olm required");
}
const key = ssssKeyFromCredential(type, credential, this._storage, this._cryptoDriver);
// write the key
let txn = await this._storage.readWriteTxn([
this._storage.storeNames.session,
]);
try {
ssssWriteKey(key, txn);
} catch (err) {
txn.abort();
throw err;
}
await txn.complete();
// and create session backup, which needs to read from accountData
txn = await this._storage.readTxn([
this._storage.storeNames.accountData,
]);
await this._createSessionBackup(key, txn);
}
async _createSessionBackup(ssssKey, txn) {
const secretStorage = new SecretStorage({key: ssssKey, cryptoDriver: this._cryptoDriver});
this._sessionBackup = await SessionBackup.fromSecretStorage({olm: this._olm, secretStorage, hsApi: this._hsApi, txn});
if (this._sessionBackup) {
for (const room of this._rooms.values()) {
if (room.isEncrypted) {
room.enableSessionBackup(this._sessionBackup);
}
}
}
}
// called after load // called after load
async beforeFirstSync(isNewLogin) { async beforeFirstSync(isNewLogin) {
if (this._olm) { if (this._olm) {
@ -155,6 +207,17 @@ export class Session {
await this._e2eeAccount.generateOTKsIfNeeded(this._storage); await this._e2eeAccount.generateOTKsIfNeeded(this._storage);
await this._e2eeAccount.uploadKeys(this._storage); await this._e2eeAccount.uploadKeys(this._storage);
await this._deviceMessageHandler.decryptPending(this.rooms); await this._deviceMessageHandler.decryptPending(this.rooms);
const txn = await this._storage.readTxn([
this._storage.storeNames.session,
this._storage.storeNames.accountData,
]);
// try set up session backup if we stored the ssss key
const ssssKey = await ssssReadKey(txn);
if (ssssKey) {
// txn will end here as this does a network request
await this._createSessionBackup(ssssKey, txn);
}
} }
} }
@ -200,6 +263,7 @@ export class Session {
stop() { stop() {
this._olmWorker?.dispose(); this._olmWorker?.dispose();
this._sendScheduler.stop(); this._sendScheduler.stop();
this._sessionBackup?.dispose();
} }
async start(lastVersionResponse) { async start(lastVersionResponse) {

View file

@ -22,7 +22,7 @@ import {makeTxnId} from "../common.js";
const ENCRYPTED_TYPE = "m.room.encrypted"; const ENCRYPTED_TYPE = "m.room.encrypted";
export class RoomEncryption { export class RoomEncryption {
constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage}) { constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, sessionBackup}) {
this._room = room; this._room = room;
this._deviceTracker = deviceTracker; this._deviceTracker = deviceTracker;
this._olmEncryption = olmEncryption; this._olmEncryption = olmEncryption;
@ -37,10 +37,13 @@ export class RoomEncryption {
this._eventIdsByMissingSession = new Map(); this._eventIdsByMissingSession = new Map();
this._senderDeviceCache = new Map(); this._senderDeviceCache = new Map();
this._storage = storage; this._storage = storage;
this._sessionBackup = null; this._sessionBackup = sessionBackup;
} }
setSessionBackup(sessionBackup) { enableSessionBackup(sessionBackup) {
if (this._sessionBackup) {
return;
}
this._sessionBackup = sessionBackup; this._sessionBackup = sessionBackup;
// TODO: query session backup for all missing sessions so far // TODO: query session backup for all missing sessions so far
// can we query multiple? no, only for sessionId, all for room, or all // can we query multiple? no, only for sessionId, all for room, or all

View file

@ -159,6 +159,7 @@ export class Decryption {
async addRoomKeyFromBackup(roomId, sessionId, sessionInfo, txn) { async addRoomKeyFromBackup(roomId, sessionId, sessionInfo, txn) {
const sessionKey = sessionInfo["session_key"]; const sessionKey = sessionInfo["session_key"];
const senderKey = sessionInfo["sender_key"]; const senderKey = sessionInfo["sender_key"];
// TODO: can we just trust this?
const claimedEd25519Key = sessionInfo["sender_claimed_keys"]?.["ed25519"]; const claimedEd25519Key = sessionInfo["sender_claimed_keys"]?.["ed25519"];
if ( if (

View file

@ -17,31 +17,28 @@ limitations under the License.
import {base64} from "../../utils/base-encoding.js"; import {base64} from "../../utils/base-encoding.js";
export class SessionBackup { export class SessionBackup {
constructor({olm, backupInfo, privateKey, hsApi}) { constructor({backupInfo, decryption, hsApi}) {
this._olm = olm;
this._backupInfo = backupInfo; this._backupInfo = backupInfo;
this._privateKey = privateKey; this._decryption = decryption;
this._hsApi = hsApi; this._hsApi = hsApi;
} }
async getSession(roomId, sessionId) { async getSession(roomId, sessionId) {
const sessionResponse = await this._hsApi.roomKeyForRoomAndSession(this._backupInfo.version, roomId, sessionId).response(); const sessionResponse = await this._hsApi.roomKeyForRoomAndSession(this._backupInfo.version, roomId, sessionId).response();
const decryption = new this._olm.PkDecryption(); const sessionInfo = this._decryption.decrypt(
try { sessionResponse.session_data.ephemeral,
decryption.init_with_private_key(this._privateKey); sessionResponse.session_data.mac,
const sessionInfo = this._decryption.decrypt( sessionResponse.session_data.ciphertext,
sessionResponse.session_data.ephemeral, );
sessionResponse.session_data.mac, return JSON.parse(sessionInfo);
sessionResponse.session_data.ciphertext,
);
return JSON.parse(sessionInfo);
} finally {
decryption.free();
}
} }
static async fromSecretStorage({olm, secretStorage, hsApi}) { dispose() {
const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1"); this._decryption.free();
}
static async fromSecretStorage({olm, secretStorage, hsApi, txn}) {
const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn);
if (base64PrivateKey) { if (base64PrivateKey) {
const privateKey = base64.decode(base64PrivateKey); const privateKey = base64.decode(base64PrivateKey);
const backupInfo = await hsApi.roomKeysVersion().response(); const backupInfo = await hsApi.roomKeysVersion().response();
@ -52,10 +49,11 @@ export class SessionBackup {
if (pubKey !== expectedPubKey) { if (pubKey !== expectedPubKey) {
throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`);
} }
} finally { } catch(err) {
decryption.free(); decryption.free();
throw err;
} }
return new SessionBackup({olm, backupInfo, privateKey, hsApi}); return new SessionBackup({backupInfo, decryption, hsApi});
} }
} }
} }

View file

@ -429,6 +429,10 @@ export class Room extends EventEmitter {
return !!this._summary.encryption; return !!this._summary.encryption;
} }
enableSessionBackup(sessionBackup) {
this._roomEncryption?.enableSessionBackup(sessionBackup);
}
get isTrackingMembers() { get isTrackingMembers() {
return this._summary.isTrackingMembers; return this._summary.isTrackingMembers;
} }

View file

@ -17,16 +17,12 @@ limitations under the License.
import {base64} from "../../utils/base-encoding.js"; import {base64} from "../../utils/base-encoding.js";
export class SecretStorage { export class SecretStorage {
constructor({key, storage, cryptoDriver}) { constructor({key, cryptoDriver}) {
this._key = key; this._key = key;
this._storage = storage;
this._cryptoDriver = cryptoDriver; this._cryptoDriver = cryptoDriver;
} }
async readSecret(name) { async readSecret(name, txn) {
const txn = await this._storage.readTxn([
this._storage.storeNames.accountData
]);
const accountData = await txn.accountData.get(name); const accountData = await txn.accountData.get(name);
if (!accountData) { if (!accountData) {
return; return;

View file

@ -34,17 +34,8 @@ async function readDefaultKeyDescription(storage) {
return new KeyDescription(id, keyAccountData); return new KeyDescription(id, keyAccountData);
} }
export async function writeKey(storage, key) { export async function writeKey(key, txn) {
const txn = await storage.readWriteTxn([ txn.session.set("ssssKey", {id: key.id, binaryKey: key.binaryKey});
storage.storeNames.session
]);
try {
txn.session.set("ssssKey", {id: key.id, binaryKey: key.binaryKey});
} catch (err) {
txn.abort();
throw err;
}
await txn.complete();
} }
export async function readKey(txn) { export async function readKey(txn) {