diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 5c254bbd00..29eb3cb8be 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -145,13 +145,34 @@ 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"); - return key && encodeBase64(key); - } else if (name === "m.cross_signing.user_signing") { - const key = await callbacks.getCrossSigningKeyCache("user_signing"); + 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); + } + } else if (name === "m.megolm_backup.v1") { + const key = await client._crypto.getSessionBackupPrivateKey(); + if (!key) { + console.log( + `session backup key requested by ${deviceId}, but not found in cache`, + ); + } return key && encodeBase64(key); } console.warn("onSecretRequested didn't recognise the secret named ", name); 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..30c2389b1e 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,17 @@ 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({ + 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/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 3602134f08..d60958236f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -583,6 +583,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:", diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index 53e9f24788..00ef87f89c 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -118,6 +118,10 @@ export default async function sendBugReport(bugReportEndpoint, opts) { try { body.append("storageManager_persisted", await navigator.storage.persisted()); } catch (e) {} + } else if (document.hasStorageAccess) { // Safari + try { + body.append("storageManager_persisted", await document.hasStorageAccess()); + } catch (e) {} } if (navigator.storage && navigator.storage.estimate) { try { diff --git a/src/utils/StorageManager.js b/src/utils/StorageManager.js index 4ed118da8a..e29b6d9b0e 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"); } 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); + }); }); } }