diff --git a/src/matrix/Session.js b/src/matrix/Session.js index dcf1eabd..b5e0a4b7 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -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) { diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index f337875f..b1c5b02a 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -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 diff --git a/src/matrix/e2ee/megolm/Decryption.js b/src/matrix/e2ee/megolm/Decryption.js index b53a345e..1723351a 100644 --- a/src/matrix/e2ee/megolm/Decryption.js +++ b/src/matrix/e2ee/megolm/Decryption.js @@ -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 ( diff --git a/src/matrix/e2ee/megolm/SessionBackup.js b/src/matrix/e2ee/megolm/SessionBackup.js index 3c6854f2..e7a94d7e 100644 --- a/src/matrix/e2ee/megolm/SessionBackup.js +++ b/src/matrix/e2ee/megolm/SessionBackup.js @@ -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}); } } } diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 5976242d..1a65c1fd 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -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; } diff --git a/src/matrix/ssss/SecretStorage.js b/src/matrix/ssss/SecretStorage.js index a6ce2353..6e7b3ae7 100644 --- a/src/matrix/ssss/SecretStorage.js +++ b/src/matrix/ssss/SecretStorage.js @@ -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; diff --git a/src/matrix/ssss/index.js b/src/matrix/ssss/index.js index 120a0909..9918da7a 100644 --- a/src/matrix/ssss/index.js +++ b/src/matrix/ssss/index.js @@ -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) {