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 {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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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();
|
|
||||||
try {
|
|
||||||
decryption.init_with_private_key(this._privateKey);
|
|
||||||
const sessionInfo = this._decryption.decrypt(
|
const sessionInfo = this._decryption.decrypt(
|
||||||
sessionResponse.session_data.ephemeral,
|
sessionResponse.session_data.ephemeral,
|
||||||
sessionResponse.session_data.mac,
|
sessionResponse.session_data.mac,
|
||||||
sessionResponse.session_data.ciphertext,
|
sessionResponse.session_data.ciphertext,
|
||||||
);
|
);
|
||||||
return JSON.parse(sessionInfo);
|
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});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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([
|
|
||||||
storage.storeNames.session
|
|
||||||
]);
|
|
||||||
try {
|
|
||||||
txn.session.set("ssssKey", {id: key.id, binaryKey: key.binaryKey});
|
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) {
|
||||||
|
|
Loading…
Reference in a new issue