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 {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.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 {MEGOLM_ALGORITHM} from "./e2ee/common.js";
import {RoomEncryption} from "./e2ee/RoomEncryption.js";
import {DeviceTracker} from "./e2ee/DeviceTracker.js";
import {LockMap} from "../utils/LockMap.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";
@ -54,6 +61,7 @@ export class Session {
this._megolmDecryption = null;
this._getSyncToken = () => this.syncToken;
this._olmWorker = olmWorker;
this._cryptoDriver = cryptoDriver;
if (olm) {
this._olmUtil = new olm.Utility();
@ -130,10 +138,54 @@ export class Session {
megolmEncryption: this._megolmEncryption,
megolmDecryption: this._megolmDecryption,
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
async beforeFirstSync(isNewLogin) {
if (this._olm) {
@ -155,6 +207,17 @@ export class Session {
await this._e2eeAccount.generateOTKsIfNeeded(this._storage);
await this._e2eeAccount.uploadKeys(this._storage);
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() {
this._olmWorker?.dispose();
this._sendScheduler.stop();
this._sessionBackup?.dispose();
}
async start(lastVersionResponse) {

View file

@ -22,7 +22,7 @@ import {makeTxnId} from "../common.js";
const ENCRYPTED_TYPE = "m.room.encrypted";
export class RoomEncryption {
constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage}) {
constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, sessionBackup}) {
this._room = room;
this._deviceTracker = deviceTracker;
this._olmEncryption = olmEncryption;
@ -37,10 +37,13 @@ export class RoomEncryption {
this._eventIdsByMissingSession = new Map();
this._senderDeviceCache = new Map();
this._storage = storage;
this._sessionBackup = null;
this._sessionBackup = sessionBackup;
}
setSessionBackup(sessionBackup) {
enableSessionBackup(sessionBackup) {
if (this._sessionBackup) {
return;
}
this._sessionBackup = sessionBackup;
// TODO: query session backup for all missing sessions so far
// 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) {
const sessionKey = sessionInfo["session_key"];
const senderKey = sessionInfo["sender_key"];
// TODO: can we just trust this?
const claimedEd25519Key = sessionInfo["sender_claimed_keys"]?.["ed25519"];
if (

View file

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

View file

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

View file

@ -34,17 +34,8 @@ async function readDefaultKeyDescription(storage) {
return new KeyDescription(id, keyAccountData);
}
export async function writeKey(storage, key) {
const txn = await storage.readWriteTxn([
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 writeKey(key, txn) {
txn.session.set("ssssKey", {id: key.id, binaryKey: key.binaryKey});
}
export async function readKey(txn) {