mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-22 19:14:52 +01:00
draft of session backup writing + some refactoring
This commit is contained in:
parent
ffece4f357
commit
933a1b4636
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user