From c53b07a35a73d8a83c0e1bfaebdb0284e03b398d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 24 Mar 2020 15:49:51 +0000 Subject: [PATCH 1/9] Add logging when secrets are missing from cache --- src/CrossSigningManager.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 5c254bbd00..097464ee43 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -149,9 +149,15 @@ const onSecretRequested = async function({ if (!callbacks.getCrossSigningKeyCache) return; if (name === "m.cross_signing.self_signing") { const key = await callbacks.getCrossSigningKeyCache("self_signing"); + if (!key) { + console.log(`self_signing requested by ${deviceId}, but not found in cache`); + } return key && encodeBase64(key); } else if (name === "m.cross_signing.user_signing") { const key = await callbacks.getCrossSigningKeyCache("user_signing"); + if (!key) { + console.log(`user_signing requested by ${deviceId}, but not found in cache`); + } return key && encodeBase64(key); } console.warn("onSecretRequested didn't recognise the secret named ", name); From 1c802cc6afd6e27effc791238e07da13e8dcbf86 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 24 Mar 2020 15:50:08 +0000 Subject: [PATCH 2/9] Show private key cache state in debug panel --- .../views/settings/CrossSigningPanel.js | 17 +++++++++++++++++ src/i18n/strings/en_EN.json | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index cf47c797fc..b960434ca1 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -32,6 +32,8 @@ export default class CrossSigningPanel extends React.PureComponent { error: null, crossSigningPublicKeysOnDevice: false, crossSigningPrivateKeysInStorage: false, + selfSigningPrivateKeyCached: false, + userSigningPrivateKeyCached: false, secretStorageKeyInAccount: false, secretStorageKeyNeedsUpgrade: null, }; @@ -71,10 +73,13 @@ export default class CrossSigningPanel extends React.PureComponent { async _getUpdatedStatus() { const cli = MatrixClientPeg.get(); + const pkCache = cli.getCrossSigningCacheCallbacks(); const crossSigning = cli._crypto._crossSigningInfo; const secretStorage = cli._crypto._secretStorage; const crossSigningPublicKeysOnDevice = crossSigning.getId(); const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage); + const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing")); + const userSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing")); const secretStorageKeyInAccount = await secretStorage.hasKey(); const homeserverSupportsCrossSigning = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); @@ -84,6 +89,8 @@ export default class CrossSigningPanel extends React.PureComponent { this.setState({ crossSigningPublicKeysOnDevice, crossSigningPrivateKeysInStorage, + selfSigningPrivateKeyCached, + userSigningPrivateKeyCached, secretStorageKeyInAccount, homeserverSupportsCrossSigning, crossSigningReady, @@ -130,6 +137,8 @@ export default class CrossSigningPanel extends React.PureComponent { error, crossSigningPublicKeysOnDevice, crossSigningPrivateKeysInStorage, + selfSigningPrivateKeyCached, + userSigningPrivateKeyCached, secretStorageKeyInAccount, homeserverSupportsCrossSigning, crossSigningReady, @@ -209,6 +218,14 @@ export default class CrossSigningPanel extends React.PureComponent { {_t("Cross-signing private keys:")} {crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")} + + {_t("Self signing private key:")} + {selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")} + + + {_t("User signing private key:")} + {userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")} + {_t("Secret storage public key:")} {secretStorageKeyInAccount ? _t("in account data") : _t("not found")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 57b39309b0..12bd462937 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -582,6 +582,10 @@ "not found": "not found", "Cross-signing private keys:": "Cross-signing private keys:", "in secret storage": "in secret storage", + "Self signing private key:": "Self signing private key:", + "cached locally": "cached locally", + "not found locally": "not found locally", + "User signing private key:": "User signing private key:", "Secret storage public key:": "Secret storage public key:", "in account data": "in account data", "Homeserver feature support:": "Homeserver feature support:", From bdcb65de77107a45875c0854441594720439cb84 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 24 Mar 2020 09:55:54 -0600 Subject: [PATCH 3/9] Support and send the config over to capable widgets For https://github.com/vector-im/riot-web/pull/12845 --- src/FromWidgetPostMessageApi.js | 11 +++++++- src/WidgetMessaging.js | 11 ++++++++ src/components/views/elements/AppTile.js | 6 +++++ src/utils/WidgetUtils.js | 8 ++++-- src/widgets/WidgetApi.ts | 34 +++++++++++++++++++++--- 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 64caba0fdf..ea76c85643 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -24,6 +24,8 @@ import {MatrixClientPeg} from "./MatrixClientPeg"; import RoomViewStore from "./stores/RoomViewStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore from "./settings/SettingsStore"; +import {Capability, KnownWidgetActions} from "./widgets/WidgetApi"; +import SdkConfig from "./SdkConfig"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -213,11 +215,18 @@ export default class FromWidgetPostMessageApi { const data = event.data.data; const val = data.value; - if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) { + if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) { ActiveWidgetStore.setWidgetPersistence(widgetId, val); } } else if (action === 'get_openid') { // Handled by caller + } else if (action === KnownWidgetActions.GetRiotWebConfig) { + if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.GetRiotWebConfig)) { + this.sendResponse(event, { + api: INBOUND_API_NAME, + config: SdkConfig.get(), + }); + } } else { console.warn('Widget postMessage event unhandled'); this.sendError(event, {message: 'The postMessage was unhandled'}); diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index d40a8ab637..b0cfe963f3 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -27,6 +27,7 @@ import {MatrixClientPeg} from "./MatrixClientPeg"; import SettingsStore from "./settings/SettingsStore"; import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog"; import WidgetUtils from "./utils/WidgetUtils"; +import {KnownWidgetActions} from "./widgets/WidgetApi"; if (!global.mxFromWidgetMessaging) { global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); @@ -75,6 +76,16 @@ export default class WidgetMessaging { }); } + /** + * Tells the widget that the client is ready to handle further widget requests. + */ + flagReadyToContinue() { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.ClientReady, + }); + } + /** * Request a screenshot from a widget * @return {Promise} To be resolved with screenshot data when it has been generated diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index a26478c461..0a8bf7443b 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -419,6 +419,12 @@ export default class AppTile extends React.Component { if (this.props.onCapabilityRequest) { this.props.onCapabilityRequest(requestedCapabilities); } + + // We only tell Jitsi widgets that we're ready because they're realistically the only ones + // using this custom extension to the widget API. + if (this.props.type === 'jitsi') { + widgetMessaging.flagReadyToContinue(); + } }).catch((err) => { console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err); }); diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 74e5f82c35..eea995cfea 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -28,6 +28,7 @@ const WIDGET_WAIT_TIME = 20000; import SettingsStore from "../settings/SettingsStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; +import {Capability} from "../widgets/WidgetApi"; /** * Encodes a URI according to a set of template variables. Variables will be @@ -454,12 +455,15 @@ export default class WidgetUtils { static getCapWhitelistForAppTypeInRoomId(appType, roomId) { const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId); - const capWhitelist = enableScreenshots ? ["m.capability.screenshot"] : []; + const capWhitelist = enableScreenshots ? [Capability.Screenshot] : []; // Obviously anyone that can add a widget can claim it's a jitsi widget, // so this doesn't really offer much over the set of domains we load // widgets from at all, but it probably makes sense for sanity. - if (appType == 'jitsi') capWhitelist.push("m.always_on_screen"); + if (appType === 'jitsi') { + capWhitelist.push(Capability.AlwaysOnScreen); + capWhitelist.push(Capability.GetRiotWebConfig); + } return capWhitelist; } diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts index c19e34ae43..d6d1c79a99 100644 --- a/src/widgets/WidgetApi.ts +++ b/src/widgets/WidgetApi.ts @@ -23,6 +23,7 @@ export enum Capability { Screenshot = "m.capability.screenshot", Sticker = "m.sticker", AlwaysOnScreen = "m.always_on_screen", + GetRiotWebConfig = "im.vector.web.riot_config", } export enum KnownWidgetActions { @@ -33,7 +34,10 @@ export enum KnownWidgetActions { UpdateVisibility = "visibility", ReceiveOpenIDCredentials = "openid_credentials", SetAlwaysOnScreen = "set_always_on_screen", + GetRiotWebConfig = "im.vector.web.riot_config", + ClientReady = "im.vector.ready", } + export type WidgetAction = KnownWidgetActions | string; export enum WidgetApiType { @@ -63,10 +67,15 @@ export interface FromWidgetRequest extends WidgetRequest { */ export class WidgetApi { private origin: string; - private inFlightRequests: {[requestId: string]: (reply: FromWidgetRequest) => void} = {}; + private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {}; private readyPromise: Promise; private readyPromiseResolve: () => void; + /** + * Set this to true if your widget is expecting a ready message from the client. False otherwise (default). + */ + public expectingExplicitReady = false; + constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) { this.origin = new URL(currentUrl).origin; @@ -83,7 +92,14 @@ export class WidgetApi { if (payload.action === KnownWidgetActions.GetCapabilities) { this.onCapabilitiesRequest(payload); + if (!this.expectingExplicitReady) { + this.readyPromiseResolve(); + } + } else if (payload.action === KnownWidgetActions.ClientReady) { this.readyPromiseResolve(); + + // Automatically acknowledge so we can move on + this.replyToRequest(payload, {}); } else { console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`); } @@ -126,7 +142,10 @@ export class WidgetApi { data: payload, response: {}, // Not used at this layer - it's used when the client responds }; - this.inFlightRequests[request.requestId] = callback; + + if (callback) { + this.inFlightRequests[request.requestId] = callback; + } console.log(`[WidgetAPI] Sending request: `, request); window.parent.postMessage(request, "*"); @@ -134,7 +153,16 @@ export class WidgetApi { public setAlwaysOnScreen(onScreen: boolean): Promise { return new Promise(resolve => { - this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, resolve); + this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, null); + resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change. + }); + } + + public getRiotConfig(): Promise { + return new Promise(resolve => { + this.callAction(KnownWidgetActions.GetRiotWebConfig, {}, response => { + resolve(response.response.config); + }); }); } } From 7ea61e41055fef1ba199e78af6420dbd7405dce4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 24 Mar 2020 10:05:57 -0600 Subject: [PATCH 4/9] Appease the linter --- src/WidgetMessaging.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index b0cfe963f3..30c2389b1e 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -78,6 +78,7 @@ export default class WidgetMessaging { /** * Tells the widget that the client is ready to handle further widget requests. + * @returns {Promise<*>} Resolves after the widget has acknowledged the ready message. */ flagReadyToContinue() { return this.messageToWidget({ From f6e9c32c48081be68a6b9bbf5ff3b86a0d56704a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Mar 2020 12:04:09 +0100 Subject: [PATCH 5/9] fall back to non-standard persisted api for Safari --- src/rageshake/submit-rageshake.js | 6 ++++++ src/utils/StorageManager.js | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index 53e9f24788..5a7039a913 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -119,6 +119,12 @@ export default async function sendBugReport(bugReportEndpoint, opts) { body.append("storageManager_persisted", await navigator.storage.persisted()); } catch (e) {} } + // Safari + if (document.hasStorageAccess) { + try { + body.append("storageManager_persisted", await document.hasStorageAccess()); + } catch (e) {} + } if (navigator.storage && navigator.storage.estimate) { try { const estimate = await navigator.storage.estimate(); diff --git a/src/utils/StorageManager.js b/src/utils/StorageManager.js index 4ed118da8a..175772903d 100644 --- a/src/utils/StorageManager.js +++ b/src/utils/StorageManager.js @@ -48,6 +48,11 @@ export function tryPersistStorage() { navigator.storage.persist().then(persistent => { console.log("StorageManager: Persistent?", persistent); }); + } else if (document.requestStorageAccess) { //Safari + document.requestStorageAccess().then( + () => console.log("StorageManager: Persistent?", true), + () => console.log("StorageManager: Persistent?", false), + ); } else { console.log("StorageManager: Persistence unsupported"); } From 9a5f4d9b22b2dadaddcdca3e0e17acbd969bc9dd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Mar 2020 12:07:11 +0100 Subject: [PATCH 6/9] fall back, don't do both on FF --- src/rageshake/submit-rageshake.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index 5a7039a913..00ef87f89c 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -118,9 +118,7 @@ export default async function sendBugReport(bugReportEndpoint, opts) { try { body.append("storageManager_persisted", await navigator.storage.persisted()); } catch (e) {} - } - // Safari - if (document.hasStorageAccess) { + } else if (document.hasStorageAccess) { // Safari try { body.append("storageManager_persisted", await document.hasStorageAccess()); } catch (e) {} From 4d63c11f260f78e93d01f025d55ae028a2897cd8 Mon Sep 17 00:00:00 2001 From: Zoe Date: Wed, 25 Mar 2020 14:06:47 +0000 Subject: [PATCH 7/9] Respond to backup key sharing requests --- src/CrossSigningManager.js | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 097464ee43..5def8d9fd5 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -145,18 +145,33 @@ const onSecretRequested = async function({ console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`); return; } - const callbacks = client.getCrossSigningCacheCallbacks(); - if (!callbacks.getCrossSigningKeyCache) return; - if (name === "m.cross_signing.self_signing") { - const key = await callbacks.getCrossSigningKeyCache("self_signing"); - if (!key) { - console.log(`self_signing requested by ${deviceId}, but not found in cache`); + if (name.startsWith("m.cross_signing")) { + const callbacks = client.getCrossSigningCacheCallbacks(); + if (!callbacks.getCrossSigningKeyCache) return; + /* Explicit enumeration here is deliberate – never share the master key! */ + if (name === "m.cross_signing.self_signing") { + const key = await callbacks.getCrossSigningKeyCache("self_signing"); + if (!key) { + console.log( + `self_signing requested by ${deviceId}, but not found in cache` + ); + } + return key && encodeBase64(key); + } else if (name === "m.cross_signing.user_signing") { + const key = await callbacks.getCrossSigningKeyCache("user_signing"); + if (!key) { + console.log( + `user_signing requested by ${deviceId}, but not found in cache` + ); + } + return key && encodeBase64(key); } - return key && encodeBase64(key); - } else if (name === "m.cross_signing.user_signing") { - const key = await callbacks.getCrossSigningKeyCache("user_signing"); + } else if (name === "m.megolm_backup.v1") { + const key = await client._crypto.getSessionBackupPrivateKey(); if (!key) { - console.log(`user_signing requested by ${deviceId}, but not found in cache`); + console.log( + `session backup key requested by ${deviceId}, but not found in cache` + ); } return key && encodeBase64(key); } From f891f3e9fa70ffc83fb679d5364d5dfc909ad42b Mon Sep 17 00:00:00 2001 From: Zoe Date: Wed, 25 Mar 2020 16:08:26 +0000 Subject: [PATCH 8/9] lint --- src/CrossSigningManager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 5def8d9fd5..29eb3cb8be 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -153,7 +153,7 @@ const onSecretRequested = async function({ const key = await callbacks.getCrossSigningKeyCache("self_signing"); if (!key) { console.log( - `self_signing requested by ${deviceId}, but not found in cache` + `self_signing requested by ${deviceId}, but not found in cache`, ); } return key && encodeBase64(key); @@ -161,7 +161,7 @@ const onSecretRequested = async function({ const key = await callbacks.getCrossSigningKeyCache("user_signing"); if (!key) { console.log( - `user_signing requested by ${deviceId}, but not found in cache` + `user_signing requested by ${deviceId}, but not found in cache`, ); } return key && encodeBase64(key); @@ -170,7 +170,7 @@ const onSecretRequested = async function({ const key = await client._crypto.getSessionBackupPrivateKey(); if (!key) { console.log( - `session backup key requested by ${deviceId}, but not found in cache` + `session backup key requested by ${deviceId}, but not found in cache`, ); } return key && encodeBase64(key); From 0097134ade4c3953d57fa64e33c70d3b45d92c5f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Mar 2020 17:56:35 +0000 Subject: [PATCH 9/9] Update src/utils/StorageManager.js Co-Authored-By: J. Ryan Stinnett --- src/utils/StorageManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/StorageManager.js b/src/utils/StorageManager.js index 175772903d..e29b6d9b0e 100644 --- a/src/utils/StorageManager.js +++ b/src/utils/StorageManager.js @@ -48,7 +48,7 @@ export function tryPersistStorage() { navigator.storage.persist().then(persistent => { console.log("StorageManager: Persistent?", persistent); }); - } else if (document.requestStorageAccess) { //Safari + } else if (document.requestStorageAccess) { // Safari document.requestStorageAccess().then( () => console.log("StorageManager: Persistent?", true), () => console.log("StorageManager: Persistent?", false),