Implement secret sharing

This commit is contained in:
RMidhunSuresh 2023-06-05 11:52:54 +05:30
parent e6a9e39c7d
commit 631b2f059f
4 changed files with 396 additions and 36 deletions

View File

@ -43,9 +43,11 @@ import {
readKey as ssssReadKey, readKey as ssssReadKey,
writeKey as ssssWriteKey, writeKey as ssssWriteKey,
removeKey as ssssRemoveKey, removeKey as ssssRemoveKey,
keyFromDehydratedDeviceKey as createSSSSKeyFromDehydratedDeviceKey keyFromDehydratedDeviceKey as createSSSSKeyFromDehydratedDeviceKey,
SecretStorage,
SharedSecret,
SecretFetcher
} from "./ssss/index"; } from "./ssss/index";
import {SecretStorage} from "./ssss/SecretStorage";
import {ObservableValue, RetainedObservableValue} from "../observable/value"; import {ObservableValue, RetainedObservableValue} from "../observable/value";
import {CallHandler} from "./calls/CallHandler"; import {CallHandler} from "./calls/CallHandler";
import {RoomStateHandlerSet} from "./room/state/RoomStateHandlerSet"; import {RoomStateHandlerSet} from "./room/state/RoomStateHandlerSet";
@ -109,6 +111,7 @@ export class Session {
this._createRoomEncryption = this._createRoomEncryption.bind(this); this._createRoomEncryption = this._createRoomEncryption.bind(this);
this._forgetArchivedRoom = this._forgetArchivedRoom.bind(this); this._forgetArchivedRoom = this._forgetArchivedRoom.bind(this);
this.needsKeyBackup = new ObservableValue(false); this.needsKeyBackup = new ObservableValue(false);
this.secretFetcher = new SecretFetcher();
} }
get fingerprintKey() { get fingerprintKey() {
@ -198,6 +201,20 @@ export class Session {
}); });
this._megolmDecryption = new MegOlmDecryption(this._keyLoader, this._olmWorker); this._megolmDecryption = new MegOlmDecryption(this._keyLoader, this._olmWorker);
this._deviceMessageHandler.enableEncryption({olmDecryption, megolmDecryption: this._megolmDecryption}); this._deviceMessageHandler.enableEncryption({olmDecryption, megolmDecryption: this._megolmDecryption});
this._sharedSecret = new SharedSecret({
hsApi: this._hsApi,
storage: this._storage,
deviceMessageHandler: this._deviceMessageHandler,
deviceTracker: this._deviceTracker,
ourUserId: this.userId,
olmEncryption: this._olmEncryption,
crypto: this._platform.crypto,
encoding: this._platform.encoding,
crossSigning: this._crossSigning,
logger: this._platform.logger,
});
this.secretFetcher.setSecretSharing(this._sharedSecret);
} }
_createRoomEncryption(room, encryptionParams) { _createRoomEncryption(room, encryptionParams) {
@ -251,11 +268,11 @@ export class Session {
this._keyBackup.get().dispose(); this._keyBackup.get().dispose();
this._keyBackup.set(undefined); this._keyBackup.set(undefined);
} }
const crossSigning = this._crossSigning.get(); // const crossSigning = this._crossSigning.get();
if (crossSigning) { // if (crossSigning) {
crossSigning.dispose(); // crossSigning.dispose();
this._crossSigning.set(undefined); // this._crossSigning.set(undefined);
} // }
const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm);
if (await this._tryLoadSecretStorage(key, log)) { if (await this._tryLoadSecretStorage(key, log)) {
// only after having read a secret, write the key // only after having read a secret, write the key
@ -331,7 +348,9 @@ export class Session {
const isValid = await secretStorage.hasValidKeyForAnyAccountData(); const isValid = await secretStorage.hasValidKeyForAnyAccountData();
log.set("isValid", isValid); log.set("isValid", isValid);
if (isValid) { if (isValid) {
await this._loadSecretStorageServices(secretStorage, log); this._secretStorage = secretStorage;
await this._loadSecretStorageService(log);
this.secretFetcher.setSecretStorage(secretStorage);
} }
return isValid; return isValid;
}); });
@ -359,29 +378,6 @@ export class Session {
log.set("no_backup", true); log.set("no_backup", true);
} }
}); });
if (this._features.crossSigning) {
await log.wrap("enable cross-signing", async log => {
const crossSigning = new CrossSigning({
storage: this._storage,
secretStorage,
platform: this._platform,
olm: this._olm,
olmUtil: this._olmUtil,
deviceTracker: this._deviceTracker,
deviceMessageHandler: this._deviceMessageHandler,
hsApi: this._hsApi,
ownUserId: this.userId,
e2eeAccount: this._e2eeAccount,
deviceId: this.deviceId,
});
if (await crossSigning.load(log)) {
this._crossSigning.set(crossSigning);
}
else {
crossSigning.dispose();
}
});
}
} catch (err) { } catch (err) {
log.catch(err); log.catch(err);
} }
@ -588,6 +584,32 @@ export class Session {
} }
}); });
} }
if (this._features.crossSigning) {
this._platform.logger.run("enable cross-signing", async log => {
const crossSigning = new CrossSigning({
storage: this._storage,
// secretStorage: this._secretStorage,
secretFetcher: this.secretFetcher,
platform: this._platform,
olm: this._olm,
olmUtil: this._olmUtil,
deviceTracker: this._deviceTracker,
deviceMessageHandler: this._deviceMessageHandler,
hsApi: this._hsApi,
ownUserId: this.userId,
e2eeAccount: this._e2eeAccount,
deviceId: this.deviceId,
});
this._crossSigning.set(crossSigning);
// if (await crossSigning.load(log)) {
// this._crossSigning.set(crossSigning);
// }
// else {
// crossSigning.dispose();
// }
});
}
await this._keyBackup.get()?.start(log); await this._keyBackup.get()?.start(log);
await this._crossSigning.get()?.start(log); await this._crossSigning.get()?.start(log);

View File

@ -0,0 +1,46 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type {SecretStorage} from "./SecretStorage";
import type {SharedSecret} from "./SharedSecret";
/**
* This is a wrapper around SecretStorage and SecretSharing so that
* you don't need to always check both sources for something.
*/
export class SecretFetcher {
public secretStorage: SecretStorage;
public secretSharing: SharedSecret;
async getSecret(name: string): Promise<string | undefined> {
;
return await this.secretStorage?.readSecret(name) ??
await this.secretSharing?.getLocallyStoredSecret(name);
// note that we don't ask another device for secret here
// that should be done explicitly since it can take arbitrary
// amounts of time to be fulfilled as the other devices may
// be offline etc...
}
setSecretStorage(storage: SecretStorage) {
this.secretStorage = storage;
}
setSecretSharing(sharing: SharedSecret) {
this.secretSharing = sharing;
this.secretSharing.setSecretFetcher(this);
}
}

View File

@ -0,0 +1,277 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type {HomeServerApi} from "../net/HomeServerApi";
import type {Storage} from "../storage/idb/Storage";
import type {DeviceMessageHandler} from "../DeviceMessageHandler.js"
import type {DeviceTracker} from "../e2ee/DeviceTracker";
import type {ILogger, ILogItem} from "../../logging/types";
import type {Encryption as OlmEncryption} from "../e2ee/olm/Encryption";
import type {Crypto} from "../../platform/web/dom/Crypto.js";
import type {Encoding} from "../../platform/web/utils/Encoding.js";
import type {CrossSigning} from "../verification/CrossSigning";
import type {SecretFetcher} from "./SecretFetcher";
import type {ObservableValue} from "../../observable/value";
import {makeTxnId, formatToDeviceMessagesPayload} from "../common.js";
import {Deferred} from "../../utils/Deferred";
import {StoreNames} from "../storage/common";
import {SESSION_E2EE_KEY_PREFIX} from "../e2ee/common";
type Options = {
hsApi: HomeServerApi;
storage: Storage;
deviceMessageHandler: DeviceMessageHandler;
deviceTracker: DeviceTracker;
ourUserId: string;
olmEncryption: OlmEncryption;
crypto: Crypto;
encoding: Encoding;
crossSigning: ObservableValue<CrossSigning | undefined>;
logger: ILogger;
};
const enum EVENT_TYPE {
REQUEST = "m.secret.request",
SEND = "m.secret.send",
}
export class SharedSecret {
private readonly hsApi: HomeServerApi;
private readonly storage: Storage;
private readonly deviceMessageHandler: DeviceMessageHandler;
private readonly deviceTracker: DeviceTracker;
private readonly ourUserId: string;
private readonly olmEncryption: OlmEncryption;
private readonly waitMap: Map<string, Deferred<any>> = new Map();
private readonly crypto: Crypto;
private readonly encoding: Encoding;
private readonly aesEncryption: AESEncryption;
private readonly crossSigning: ObservableValue<CrossSigning | undefined>;
private readonly logger: ILogger;
private secretFetcher: SecretFetcher;
constructor(options: Options) {
this.hsApi = options.hsApi;
this.storage = options.storage;
this.deviceMessageHandler = options.deviceMessageHandler;
this.deviceTracker = options.deviceTracker;
this.ourUserId = options.ourUserId;
this.olmEncryption = options.olmEncryption;
this.crypto = options.crypto;
this.encoding = options.encoding;
this.crossSigning = options.crossSigning;
this.logger = options.logger;
this.aesEncryption = new AESEncryption(this.storage, this.crypto, this.encoding);
(window as any).foo = this;
this.init();
}
private async init() {
this.deviceMessageHandler.on("message", ({ encrypted }) => {
const type: EVENT_TYPE = encrypted?.event.type;
switch (type) {
case EVENT_TYPE.REQUEST: {
this._respondToRequest(encrypted);
}
case EVENT_TYPE.SEND: {
const { request_id } = encrypted.event.content;
const deffered = this.waitMap.get(request_id);
deffered?.resolve(encrypted);
this.waitMap.delete(request_id);
break;
}
}
});
await this.aesEncryption.load();
}
private async _respondToRequest(request) {
await this.logger.run("SharedSecret.respondToRequest", async (log) => {
if (!this.shouldRespondToRequest(request, log)) {
return;
}
const requestContent = request.event.content;
const id = requestContent.request_id;
const deviceId = requestContent.requesting_device_id;
const name = requestContent.name;
const secret = await this.secretFetcher.getSecret(name);
if (!secret) {
// Can't share a secret that we don't know about.
log.log({ l: "Secret not available to share" });
return;
}
const content = { secret, request_id: id };
const device = await this.deviceTracker.deviceForId(this.ourUserId, deviceId, this.hsApi, log);
if (!device) {
log.log({ l: "Cannot find device", deviceId });
return;
}
const messages = await log.wrap("olm encrypt", log => this.olmEncryption.encrypt(
EVENT_TYPE.SEND, content, [device], this.hsApi, log));
console.log("messages", messages);
const payload = formatToDeviceMessagesPayload(messages);
console.log("payload", payload);
await this.hsApi.sendToDevice("m.room.encrypted", payload, makeTxnId(), {log}).response();
});
}
private async shouldRespondToRequest(request: any, log: ILogItem): Promise<boolean> {
return log.wrap("SecretSharing.shouldRespondToRequest", async () => {
const crossSigning = this.crossSigning.get();
if (!crossSigning) {
// We're not in a position to respond to this request
log.log({ crossSigningNotAvailable: true });
return false;
}
const content = request.event.content;
if (
request.event.sender !== this.ourUserId ||
!(
content.name &&
content.action &&
content.requesting_device_id &&
content.request_id
) ||
content.action === "request_cancellation"
) {
// 1. Ensure that the message came from the same user as us
// 2. Validate message format
// 3. Check if this is a cancellation
return false;
}
// 3. Check that the device is verified
const deviceId = content.requesting_device_id;
const device = await this.deviceTracker.deviceForId(this.ourUserId, deviceId, this.hsApi, log);
if (!device) {
log.log({ l: "Device could not be acquired", deviceId });
return false;
}
if (!await crossSigning.isOurUserDeviceTrusted(device, log)) {
log.log({ l: "Device not trusted, returning" });
return false;
}
return true;
})
}
async getLocallyStoredSecret(name: string): Promise<any> {
const txn = await this.storage.readTxn([
this.storage.storeNames.sharedSecrets,
]);
const storedSecret = await txn.sharedSecrets.get(name);
if (storedSecret) {
const secret = await this.aesEncryption.decrypt(storedSecret.encrypted);
return secret;
}
}
// todo: this will break if two different pieces of code call this method
requestSecret(name: string, log: ILogItem): Promise<string> {
return log.wrap("SharedSecret.requestSecret", async (_log) => {
const request_id = makeTxnId();
const promise = this.trackSecretRequest(request_id);
await this.sendRequestForSecret(name, request_id, _log);
const result = await promise;
const secret = result.event.content.secret;
await this.writeToStorage(name, secret);
return secret;
});
}
private async writeToStorage(name:string, secret: any) {
const encrypted = await this.aesEncryption.encrypt(secret);
const txn = await this.storage.readWriteTxn([StoreNames.sharedSecrets]);
txn.sharedSecrets.set(name, { encrypted });
}
private trackSecretRequest(request_id: string): Promise<any> {
const deferred = new Deferred();
this.waitMap.set(request_id, deferred);
return deferred.promise;
}
private async sendRequestForSecret(name: string, request_id: string, log: ILogItem) {
const content = {
action: "request",
name,
request_id,
requesting_device_id: this.deviceTracker.ownDeviceId,
}
let devices = await this.deviceTracker.devicesForUsers([this.ourUserId], this.hsApi, log);
devices = devices.filter(d => d.device_id !== this.deviceTracker.ownDeviceId);
const messages = await log.wrap("olm encrypt", log => this.olmEncryption.encrypt(
EVENT_TYPE.REQUEST, content, devices, this.hsApi, log));
console.log("messages", messages);
const payload = formatToDeviceMessagesPayload(messages);
console.log("payload", payload);
await this.hsApi.sendToDevice("m.room.encrypted", payload, makeTxnId(), {log}).response();
}
setSecretFetcher(secretFetcher: SecretFetcher): void {
this.secretFetcher = secretFetcher;
}
}
class AESEncryption {
private key: JsonWebKey;
private iv: Uint8Array;
constructor(private storage: Storage, private crypto: Crypto, private encoding: Encoding) { };
async load() {
const storageKey = `${SESSION_E2EE_KEY_PREFIX}localAESKey`;
// 1. Check if we're already storing the AES key
const txn = await this.storage.readTxn([StoreNames.session]);
let { key, iv } = await txn.session.get(storageKey) ?? {};
// 2. If no key, create it and store in session store
if (!key) {
key = await this.crypto.aes.generateKey("jwk");
iv = await this.crypto.aes.generateIV();
const txn = await this.storage.readWriteTxn([StoreNames.session]);
txn.session.set(storageKey, { key, iv });
}
// 3. Set props
this.key = key;
this.iv = iv;
}
async encrypt(secret: string): Promise<Uint8Array> {
const data = this.encoding.utf8.encode(secret);
const encrypted = await this.crypto.aes.encryptCTR({
jwkKey: this.key,
iv: this.iv,
data,
});
return encrypted;
}
async decrypt(ciphertext: Uint8Array): Promise<string> {
const buffer = await this.crypto.aes.decryptCTR({
jwkKey: this.key,
iv: this.iv,
data: ciphertext,
});
const secret = this.encoding.utf8.decode(buffer);
return secret;
}
}

View File

@ -22,7 +22,7 @@ import {ToDeviceChannel} from "./SAS/channel/Channel";
import {VerificationEventType} from "./SAS/channel/types"; import {VerificationEventType} from "./SAS/channel/types";
import {ObservableMap} from "../../observable/map"; import {ObservableMap} from "../../observable/map";
import {SASRequest} from "./SAS/SASRequest"; import {SASRequest} from "./SAS/SASRequest";
import type {SecretStorage} from "../ssss/SecretStorage"; import {SecretFetcher} from "../ssss";
import type {Storage} from "../storage/idb/Storage"; import type {Storage} from "../storage/idb/Storage";
import type {Platform} from "../../platform/web/Platform"; import type {Platform} from "../../platform/web/Platform";
import type {DeviceTracker} from "../e2ee/DeviceTracker"; import type {DeviceTracker} from "../e2ee/DeviceTracker";
@ -80,7 +80,7 @@ enum MSKVerification {
export class CrossSigning { export class CrossSigning {
private readonly storage: Storage; private readonly storage: Storage;
private readonly secretStorage: SecretStorage; private readonly secretFetcher: SecretFetcher;
private readonly platform: Platform; private readonly platform: Platform;
private readonly deviceTracker: DeviceTracker; private readonly deviceTracker: DeviceTracker;
private readonly olm: Olm; private readonly olm: Olm;
@ -97,7 +97,7 @@ export class CrossSigning {
constructor(options: { constructor(options: {
storage: Storage, storage: Storage,
secretStorage: SecretStorage, secretFetcher: SecretFetcher,
deviceTracker: DeviceTracker, deviceTracker: DeviceTracker,
platform: Platform, platform: Platform,
olm: Olm, olm: Olm,
@ -109,7 +109,7 @@ export class CrossSigning {
deviceMessageHandler: DeviceMessageHandler, deviceMessageHandler: DeviceMessageHandler,
}) { }) {
this.storage = options.storage; this.storage = options.storage;
this.secretStorage = options.secretStorage; this.secretFetcher = options.secretFetcher;
this.platform = options.platform; this.platform = options.platform;
this.deviceTracker = options.deviceTracker; this.deviceTracker = options.deviceTracker;
this.olm = options.olm; this.olm = options.olm;
@ -208,6 +208,7 @@ export class CrossSigning {
} }
private handleSASDeviceMessage({ unencrypted: event }) { private handleSASDeviceMessage({ unencrypted: event }) {
if (!event) { return; }
const txnId = event.content.transaction_id; const txnId = event.content.transaction_id;
/** /**
* If we receive an event for the current/previously finished * If we receive an event for the current/previously finished
@ -304,6 +305,20 @@ export class CrossSigning {
}); });
} }
async isOurUserDeviceTrusted(device: DeviceKey, log: ILogItem): Promise<boolean> {
return await log.wrap("CrossSigning.getDeviceTrust", async () => {
const ourSSK = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.SelfSigning, this.hsApi, log);
if (!ourSSK) {
return false;
}
const verification = this.hasValidSignatureFrom(device, ourSSK, log);
if (verification === SignatureVerification.Valid) {
return true;
}
return false;
});
}
getUserTrust(userId: string, log: ILogItem): Promise<UserTrust> { getUserTrust(userId: string, log: ILogItem): Promise<UserTrust> {
return log.wrap("CrossSigning.getUserTrust", async log => { return log.wrap("CrossSigning.getUserTrust", async log => {
log.set("id", userId); log.set("id", userId);
@ -421,7 +436,7 @@ export class CrossSigning {
} }
private async getSigningKey(usage: KeyUsage): Promise<Uint8Array | undefined> { private async getSigningKey(usage: KeyUsage): Promise<Uint8Array | undefined> {
const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`); const seedStr = await this.secretFetcher.getSecret(`m.cross_signing.${usage}`);
if (seedStr) { if (seedStr) {
return new Uint8Array(this.platform.encoding.base64.decode(seedStr)); return new Uint8Array(this.platform.encoding.base64.decode(seedStr));
} }