draft of session backup writing + some refactoring

This commit is contained in:
Bruno Windels 2022-01-25 18:48:19 +01:00
parent ffece4f357
commit 933a1b4636
2 changed files with 179 additions and 25 deletions

View file

@ -253,6 +253,7 @@ export class Session {
secretStorage,
this._hsApi,
this._keyLoader,
this._storage,
txn
);
if (this._sessionBackup) {
@ -580,6 +581,11 @@ export class Session {
if (preparation) {
await log.wrap("deviceMsgs", log => this._deviceMessageHandler.writeSync(preparation, txn, log));
// this should come after the deviceMessageHandler, so the room keys are already written and their
// isBetter property has been checked
if (this._sessionBackup) {
this._sessionBackup.writeKeys(preparation.newRoomKeys, txn, log);
}
}
// store account data
@ -617,6 +623,9 @@ export class Session {
await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, false, log));
}
}
if (this._sessionBackup) {
this._sessionBackup.flush();
}
}
applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates) {

View file

@ -14,10 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {StoreNames} from "../../storage/common";
import {LRUCache} from "../../../utils/LRUCache";
import {keyFromStorage, keyFromBackup} from "./decryption/RoomKey";
import {MEGOLM_ALGORITHM} from "../common";
import type {HomeServerApi} from "../../net/HomeServerApi";
import type {RoomKey} from "./decryption/RoomKey";
import type {IncomingRoomKey, RoomKey} from "./decryption/RoomKey";
import type {KeyLoader} from "./decryption/KeyLoader";
import type {SecretStorage} from "../../ssss/SecretStorage";
import type {Storage} from "../../storage/idb/Storage";
import type {DeviceIdentity} from "../../storage/idb/stores/DeviceIdentityStore";
import type {ILogItem} from "../../../logging/types";
import type {Platform} from "../../../platform/web/Platform";
import type {Transaction} from "../../storage/idb/Transaction";
@ -47,6 +54,7 @@ interface OtherBackupInfo extends BaseBackupInfo {
type BackupInfo = Curve25519BackupInfo | OtherBackupInfo;
interface Curve25519AuthData {
public_key: string,
signatures: SignatureMap
@ -54,50 +62,139 @@ interface Curve25519AuthData {
type AuthData = Curve25519AuthData;
type SessionInfo = {
first_message_index: number,
forwarded_count: number,
is_verified: boolean,
session_data: Curve29915SessionData | any
}
type Curve29915SessionData = {
ciphertext: string,
mac: string,
ephemeral: string,
}
type MegOlmSessionKeyInfo = {
algorithm: MEGOLM_ALGORITHM,
sender_key: string,
sender_claimed_keys: {[algorithm: string]: string},
forwarding_curve25519_key_chain: string[],
session_key: string
}
type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string};
export class SessionBackup {
constructor(
private readonly backupInfo: BackupInfo,
private readonly decryption: Olm.PkDecryption,
private readonly algorithm: Curve25519,
private readonly hsApi: HomeServerApi,
private readonly keyLoader: KeyLoader
private readonly keyLoader: KeyLoader,
private readonly storage: Storage,
private readonly platform: Platform,
) {}
async getSession(roomId: string, sessionId: string, log: ILogItem) {
async getRoomKey(roomId: string, sessionId: string, log: ILogItem): Promise<IncomingRoomKey | undefined> {
const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response();
const sessionInfo = this.decryption.decrypt(
sessionResponse.session_data.ephemeral,
sessionResponse.session_data.mac,
sessionResponse.session_data.ciphertext,
);
return JSON.parse(sessionInfo);
if (!sessionResponse.session_data) {
return;
}
const sessionKeyInfo = this.algorithm.decryptRoomKey(sessionResponse.session_data);
if (sessionKeyInfo?.algorithm === MEGOLM_ALGORITHM) {
return keyFromBackup(roomId, sessionId, sessionKeyInfo);
} else if (sessionKeyInfo?.algorithm) {
log.set("unknown algorithm", sessionKeyInfo.algorithm);
}
}
get version() {
writeKeys(roomKeys: IncomingRoomKey[], txn: Transaction): boolean {
let hasBetter = false;
for (const key of roomKeys) {
if (key.isBetter) {
txn.sessionsNeedingBackup.set(key.roomId, key.senderKey, key.sessionId);
hasBetter = true;
}
}
return hasBetter;
}
async flush() {
while (true) {
await this.platform.clock.createTimeout(this.platform.random() * 10000).elapsed();
const txn = await this.storage.readTxn([
StoreNames.sessionsNeedingBackup,
StoreNames.inboundGroupSessions,
]);
const keysNeedingBackup = await txn.sessionsNeedingBackup.getFirstEntries(20);
if (keysNeedingBackup.length === 0) {
return;
}
const roomKeys = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn)));
const payload: {
rooms: {
[roomId: string]: {
sessions: {[sessionId: string]: SessionInfo}
}
}
} = { rooms: {} };
const payloadRooms = payload.rooms;
for (const key of roomKeys) {
if (key) {
let roomPayload = payloadRooms[key.roomId];
if (!roomPayload) {
roomPayload = payloadRooms[key.roomId] = { sessions: {} };
}
roomPayload.sessions[key.sessionId] = await this.encodeRoomKey(key);
}
}
await this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload).response();
{
const txn = await this.storage.readWriteTxn([
StoreNames.sessionsNeedingBackup,
]);
try {
for (const key of keysNeedingBackup) {
txn.sessionsNeedingBackup.remove(key.roomId, key.senderKey, key.sessionId);
}
} catch (err) {
txn.abort();
throw err;
}
await txn.complete();
}
}
}
private async encodeRoomKey(roomKey: RoomKey): Promise<SessionInfo> {
return await this.keyLoader.useKey(roomKey, session => {
const firstMessageIndex = session.first_known_index();
const sessionKey = session.export_session(firstMessageIndex);
return {
first_message_index: firstMessageIndex,
forwarded_count: 0,
is_verified: false,
session_data: this.algorithm.encryptRoomKey(roomKey, sessionKey)
};
});
}
get version(): string {
return this.backupInfo.version;
}
dispose() {
this.decryption.free();
this.algorithm.dispose();
}
static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, txn: Transaction) {
static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction) {
const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn);
if (base64PrivateKey) {
const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey));
const backupInfo = await hsApi.roomKeysVersion().response() as BackupInfo;
if (backupInfo.algorithm === Curve25519Algorithm) {
const expectedPubKey = backupInfo.auth_data.public_key;
const decryption = new olm.PkDecryption();
try {
const pubKey = decryption.init_with_private_key(privateKey);
if (pubKey !== expectedPubKey) {
throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`);
}
} catch(err) {
decryption.free();
throw err;
}
return new SessionBackup(backupInfo, decryption, hsApi, keyLoader);
const algorithm = Curve25519.fromAuthData(backupInfo.auth_data, privateKey, olm);
return new SessionBackup(backupInfo, algorithm, hsApi, keyLoader, storage, platform);
} else {
throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`);
}
@ -105,3 +202,51 @@ export class SessionBackup {
}
}
class Curve25519 {
constructor(
private readonly encryption: Olm.PkEncryption,
private readonly decryption: Olm.PkDecryption
) {}
static fromAuthData(authData: Curve25519AuthData, privateKey: Uint8Array, olm: Olm): Curve25519 {
const expectedPubKey = authData.public_key;
const decryption = new olm.PkDecryption();
const encryption = new olm.PkEncryption();
try {
const pubKey = decryption.init_with_private_key(privateKey);
if (pubKey !== expectedPubKey) {
throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`);
}
encryption.set_recipient_key(pubKey);
} catch(err) {
decryption.free();
throw err;
}
return new Curve25519(encryption, decryption);
}
decryptRoomKey(sessionData: Curve29915SessionData): SessionKeyInfo {
const sessionInfo = this.decryption.decrypt(
sessionData.ephemeral,
sessionData.mac,
sessionData.ciphertext,
);
return JSON.parse(sessionInfo) as SessionKeyInfo;
}
encryptRoomKey(key: RoomKey, sessionKey: string): Curve29915SessionData {
const sessionInfo: SessionKeyInfo = {
algorithm: MEGOLM_ALGORITHM,
sender_key: key.senderKey,
sender_claimed_keys: {ed25519: key.claimedEd25519Key},
forwarding_curve25519_key_chain: [],
session_key: sessionKey
};
return this.encryption.encrypt(JSON.stringify(sessionInfo)) as Curve29915SessionData;
}
dispose() {
this.decryption.free();
this.encryption.free();
}
}