forked from mystiq/hydrogen-web
integrate session backup with session class
This commit is contained in:
parent
3cebd17cbe
commit
9d622434fb
7 changed files with 97 additions and 40 deletions
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue