{sub}
;
@@ -283,7 +292,7 @@ export default class CallHandler {
} else if (payload.type === 'screensharing') {
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
if (screenCapErrorString) {
- this.setCallState(undefined, newCall.roomId, "ended");
+ this.removeCallForRoom(newCall.roomId);
console.log("Can't capture screen: " + screenCapErrorString);
Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
title: _t('Unable to capture screen'),
@@ -351,6 +360,14 @@ export default class CallHandler {
console.info("Place conference call in %s", payload.room_id);
this.startCallApp(payload.room_id, payload.type);
break;
+ case 'end_conference':
+ console.info("Terminating conference call in %s", payload.room_id);
+ this.terminateCallApp(payload.room_id);
+ break;
+ case 'hangup_conference':
+ console.info("Leaving conference call in %s", payload.room_id);
+ this.hangupCallApp(payload.room_id);
+ break;
case 'incoming_call':
{
if (this.getAnyActiveCall()) {
@@ -376,7 +393,7 @@ export default class CallHandler {
return; // no call to hangup
}
this.calls.get(payload.room_id).hangup();
- this.setCallState(null, payload.room_id, "ended");
+ this.removeCallForRoom(payload.room_id);
break;
case 'answer':
if (!this.calls.get(payload.room_id)) {
@@ -398,10 +415,12 @@ export default class CallHandler {
show: true,
});
+ // prevent double clicking the call button
const room = MatrixClientPeg.get().getRoom(roomId);
const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
-
- if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) {
+ const hasJitsi = currentJitsiWidgets.length > 0
+ || WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI);
+ if (hasJitsi) {
Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
title: _t('Call in Progress'),
description: _t('A call is currently being placed!'),
@@ -409,33 +428,6 @@ export default class CallHandler {
return;
}
- if (currentJitsiWidgets.length > 0) {
- console.warn(
- "Refusing to start conference call widget in " + roomId +
- " a conference call widget is already present",
- );
-
- if (WidgetUtils.canUserModifyWidgets(roomId)) {
- Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, {
- title: _t('End Call'),
- description: _t('Remove the group call from the room?'),
- button: _t('End Call'),
- cancelButton: _t('Cancel'),
- onFinished: (endCall) => {
- if (endCall) {
- WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']);
- }
- },
- });
- } else {
- Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
- title: _t('Call in Progress'),
- description: _t("You don't have permission to remove the call from the room"),
- });
- }
- return;
- }
-
const jitsiDomain = Jitsi.getInstance().preferredDomain;
const jitsiAuth = await Jitsi.getInstance().getJitsiAuth();
let confId;
@@ -484,4 +476,38 @@ export default class CallHandler {
console.error(e);
});
}
+
+ private terminateCallApp(roomId: string) {
+ Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, {
+ hasCancelButton: true,
+ title: _t("End conference"),
+ description: _t("This will end the conference for everyone. Continue?"),
+ button: _t("End conference"),
+ onFinished: (proceed) => {
+ if (!proceed) return;
+
+ // We'll just obliterate them all. There should only ever be one, but might as well
+ // be safe.
+ const roomInfo = WidgetStore.instance.getRoom(roomId);
+ const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
+ jitsiWidgets.forEach(w => {
+ // setting invalid content removes it
+ WidgetUtils.setRoomWidget(roomId, w.id);
+ });
+ },
+ });
+ }
+
+ private hangupCallApp(roomId: string) {
+ const roomInfo = WidgetStore.instance.getRoom(roomId);
+ if (!roomInfo) return; // "should never happen" clauses go here
+
+ const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
+ jitsiWidgets.forEach(w => {
+ const messaging = WidgetMessagingStore.instance.getMessagingForId(w.id);
+ if (!messaging) return; // more "should never happen" words
+
+ messaging.transport.send(ElementWidgetActions.HangupCall, {});
+ });
+ }
}
diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
deleted file mode 100644
index d5d7c08d50..0000000000
--- a/src/FromWidgetPostMessageApi.js
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 Travis Ralston
-Copyright 2019 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 URL from 'url';
-import dis from './dispatcher/dispatcher';
-import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
-import ActiveWidgetStore from './stores/ActiveWidgetStore';
-import {MatrixClientPeg} from "./MatrixClientPeg";
-import RoomViewStore from "./stores/RoomViewStore";
-import {IntegrationManagers} from "./integrations/IntegrationManagers";
-import SettingsStore from "./settings/SettingsStore";
-import {Capability} from "./widgets/WidgetApi";
-import {objectClone} from "./utils/objects";
-
-const WIDGET_API_VERSION = '0.0.2'; // Current API version
-const SUPPORTED_WIDGET_API_VERSIONS = [
- '0.0.1',
- '0.0.2',
-];
-const INBOUND_API_NAME = 'fromWidget';
-
-// Listen for and handle incoming requests using the 'fromWidget' postMessage
-// API and initiate responses
-export default class FromWidgetPostMessageApi {
- constructor() {
- this.widgetMessagingEndpoints = [];
- this.widgetListeners = {}; // {action: func[]}
-
- this.start = this.start.bind(this);
- this.stop = this.stop.bind(this);
- this.onPostMessage = this.onPostMessage.bind(this);
- }
-
- start() {
- window.addEventListener('message', this.onPostMessage);
- }
-
- stop() {
- window.removeEventListener('message', this.onPostMessage);
- }
-
- /**
- * Adds a listener for a given action
- * @param {string} action The action to listen for.
- * @param {Function} callbackFn A callback function to be called when the action is
- * encountered. Called with two parameters: the interesting request information and
- * the raw event received from the postMessage API. The raw event is meant to be used
- * for sendResponse and similar functions.
- */
- addListener(action, callbackFn) {
- if (!this.widgetListeners[action]) this.widgetListeners[action] = [];
- this.widgetListeners[action].push(callbackFn);
- }
-
- /**
- * Removes a listener for a given action.
- * @param {string} action The action that was subscribed to.
- * @param {Function} callbackFn The original callback function that was used to subscribe
- * to updates.
- */
- removeListener(action, callbackFn) {
- if (!this.widgetListeners[action]) return;
-
- const idx = this.widgetListeners[action].indexOf(callbackFn);
- if (idx !== -1) this.widgetListeners[action].splice(idx, 1);
- }
-
- /**
- * Register a widget endpoint for trusted postMessage communication
- * @param {string} widgetId Unique widget identifier
- * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
- */
- addEndpoint(widgetId, endpointUrl) {
- const u = URL.parse(endpointUrl);
- if (!u || !u.protocol || !u.host) {
- console.warn('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl);
- return;
- }
-
- const origin = u.protocol + '//' + u.host;
- const endpoint = new WidgetMessagingEndpoint(widgetId, origin);
- if (this.widgetMessagingEndpoints.some(function(ep) {
- return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
- })) {
- // Message endpoint already registered
- console.warn('Add FromWidgetPostMessageApi - Endpoint already registered');
- return;
- } else {
- console.log(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint);
- this.widgetMessagingEndpoints.push(endpoint);
- }
- }
-
- /**
- * De-register a widget endpoint from trusted communication sources
- * @param {string} widgetId Unique widget identifier
- * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
- * @return {boolean} True if endpoint was successfully removed
- */
- removeEndpoint(widgetId, endpointUrl) {
- const u = URL.parse(endpointUrl);
- if (!u || !u.protocol || !u.host) {
- console.warn('Remove widget messaging endpoint - Invalid origin');
- return;
- }
-
- const origin = u.protocol + '//' + u.host;
- if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) {
- const length = this.widgetMessagingEndpoints.length;
- this.widgetMessagingEndpoints = this.widgetMessagingEndpoints
- .filter((endpoint) => endpoint.widgetId !== widgetId || endpoint.endpointUrl !== origin);
- return (length > this.widgetMessagingEndpoints.length);
- }
- return false;
- }
-
- /**
- * Handle widget postMessage events
- * Messages are only handled where a valid, registered messaging endpoints
- * @param {Event} event Event to handle
- * @return {undefined}
- */
- onPostMessage(event) {
- if (!event.origin) { // Handle chrome
- event.origin = event.originalEvent.origin;
- }
-
- // Event origin is empty string if undefined
- if (
- event.origin.length === 0 ||
- !this.trustedEndpoint(event.origin) ||
- event.data.api !== INBOUND_API_NAME ||
- !event.data.widgetId
- ) {
- return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
- }
-
- // Call any listeners we have registered
- if (this.widgetListeners[event.data.action]) {
- for (const fn of this.widgetListeners[event.data.action]) {
- fn(event.data, event);
- }
- }
-
- // Although the requestId is required, we don't use it. We'll be nice and process the message
- // if the property is missing, but with a warning for widget developers.
- if (!event.data.requestId) {
- console.warn("fromWidget action '" + event.data.action + "' does not have a requestId");
- }
-
- const action = event.data.action;
- const widgetId = event.data.widgetId;
- if (action === 'content_loaded') {
- console.log('Widget reported content loaded for', widgetId);
- dis.dispatch({
- action: 'widget_content_loaded',
- widgetId: widgetId,
- });
- this.sendResponse(event, {success: true});
- } else if (action === 'supported_api_versions') {
- this.sendResponse(event, {
- api: INBOUND_API_NAME,
- supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
- });
- } else if (action === 'api_version') {
- this.sendResponse(event, {
- api: INBOUND_API_NAME,
- version: WIDGET_API_VERSION,
- });
- } else if (action === 'm.sticker') {
- // console.warn('Got sticker message from widget', widgetId);
- // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
- const data = event.data.data || event.data.widgetData;
- dis.dispatch({action: 'm.sticker', data: data, widgetId: event.data.widgetId});
- } else if (action === 'integration_manager_open') {
- // Close the stickerpicker
- dis.dispatch({action: 'stickerpicker_close'});
- // Open the integration manager
- // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
- const data = event.data.data || event.data.widgetData;
- const integType = (data && data.integType) ? data.integType : null;
- const integId = (data && data.integId) ? data.integId : null;
-
- // TODO: Open the right integration manager for the widget
- if (SettingsStore.getValue("feature_many_integration_managers")) {
- IntegrationManagers.sharedInstance().openAll(
- MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
- `type_${integType}`,
- integId,
- );
- } else {
- IntegrationManagers.sharedInstance().getPrimaryManager().open(
- MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
- `type_${integType}`,
- integId,
- );
- }
- } else if (action === 'set_always_on_screen') {
- // This is a new message: there is no reason to support the deprecated widgetData here
- const data = event.data.data;
- const val = data.value;
-
- if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
- ActiveWidgetStore.setWidgetPersistence(widgetId, val);
- }
- } else if (action === 'get_openid') {
- // Handled by caller
- } else {
- console.warn('Widget postMessage event unhandled');
- this.sendError(event, {message: 'The postMessage was unhandled'});
- }
- }
-
- /**
- * Check if message origin is registered as trusted
- * @param {string} origin PostMessage origin to check
- * @return {boolean} True if trusted
- */
- trustedEndpoint(origin) {
- if (!origin) {
- return false;
- }
-
- return this.widgetMessagingEndpoints.some((endpoint) => {
- // TODO / FIXME -- Should this also check the widgetId?
- return endpoint.endpointUrl === origin;
- });
- }
-
- /**
- * Send a postmessage response to a postMessage request
- * @param {Event} event The original postMessage request event
- * @param {Object} res Response data
- */
- sendResponse(event, res) {
- const data = objectClone(event.data);
- data.response = res;
- event.source.postMessage(data, event.origin);
- }
-
- /**
- * Send an error response to a postMessage request
- * @param {Event} event The original postMessage request event
- * @param {string} msg Error message
- * @param {Error} nestedError Nested error event (optional)
- */
- sendError(event, msg, nestedError) {
- console.error('Action:' + event.data.action + ' failed with message: ' + msg);
- const data = objectClone(event.data);
- data.response = {
- error: {
- message: msg,
- },
- };
- if (nestedError) {
- data.response.error._error = nestedError;
- }
- event.source.postMessage(data, event.origin);
- }
-}
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index f991d2df5d..c503247bf7 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -53,7 +53,7 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
-const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
+export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
/*
* Return true if the given string contains emoji
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index 3a48de5eef..dc04e47535 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -186,6 +186,8 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
console.log("Logged in with token");
return _clearStorage().then(() => {
_persistCredentialsToLocalStorage(creds);
+ // remember that we just logged in
+ sessionStorage.setItem("mx_fresh_login", true);
return true;
});
}).catch((err) => {
@@ -312,6 +314,9 @@ async function _restoreFromLocalStorage(opts) {
console.log("No pickle key available");
}
+ const freshLogin = sessionStorage.getItem("mx_fresh_login");
+ sessionStorage.removeItem("mx_fresh_login");
+
console.log(`Restoring session for ${userId}`);
await _doSetLoggedIn({
userId: userId,
@@ -321,6 +326,7 @@ async function _restoreFromLocalStorage(opts) {
identityServerUrl: isUrl,
guest: isGuest,
pickleKey: pickleKey,
+ freshLogin: freshLogin,
}, false);
return true;
} else {
@@ -364,6 +370,7 @@ async function _handleLoadSessionFailure(e) {
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
*/
export async function setLoggedIn(credentials) {
+ credentials.freshLogin = true;
stopMatrixClient();
const pickleKey = credentials.userId && credentials.deviceId
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
@@ -429,6 +436,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
" guest: " + credentials.guest +
" hs: " + credentials.homeserverUrl +
" softLogout: " + softLogout,
+ " freshLogin: " + credentials.freshLogin,
);
// This is dispatched to indicate that the user is still in the process of logging in
@@ -462,10 +470,28 @@ async function _doSetLoggedIn(credentials, clearStorage) {
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
+ MatrixClientPeg.replaceUsingCreds(credentials);
+ const client = MatrixClientPeg.get();
+
+ if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) {
+ // If we just logged in, try to rehydrate a device instead of using a
+ // new device. If it succeeds, we'll get a new device ID, so make sure
+ // we persist that ID to localStorage
+ const newDeviceId = await client.rehydrateDevice();
+ if (newDeviceId) {
+ credentials.deviceId = newDeviceId;
+ }
+
+ delete credentials.freshLogin;
+ }
+
if (localStorage) {
try {
_persistCredentialsToLocalStorage(credentials);
+ // make sure we don't think that it's a fresh login any more
+ sessionStorage.removeItem("mx_fresh_login");
+
// The user registered as a PWLU (PassWord-Less User), the generated password
// is cached here such that the user can change it at a later time.
if (credentials.password) {
@@ -482,12 +508,10 @@ async function _doSetLoggedIn(credentials, clearStorage) {
console.warn("No local storage available: can't persist session!");
}
- MatrixClientPeg.replaceUsingCreds(credentials);
-
dis.dispatch({ action: 'on_logged_in' });
await startMatrixClient(/*startSyncing=*/!softLogout);
- return MatrixClientPeg.get();
+ return client;
}
function _showStorageEvictedDialog() {
diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index 9589130e7f..69e586c58d 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
import * as StorageManager from './utils/StorageManager';
import IdentityAuthClient from './IdentityAuthClient';
-import { crossSigningCallbacks } from './SecurityManager';
+import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
export interface IMatrixClientCreds {
@@ -42,6 +42,7 @@ export interface IMatrixClientCreds {
accessToken: string;
guest: boolean;
pickleKey?: string;
+ freshLogin?: boolean;
}
// TODO: Move this to the js-sdk
@@ -192,6 +193,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
this.matrixClient.setCryptoTrustCrossSignedDevices(
!SettingsStore.getValue('e2ee.manuallyVerifyAllSessions'),
);
+ await tryToUnlockSecretStorageWithDehydrationKey(this.matrixClient);
StorageManager.setCryptoInitialised(true);
}
} catch (e) {
diff --git a/src/SecurityManager.js b/src/SecurityManager.js
index f6b9c993d0..3272c0f015 100644
--- a/src/SecurityManager.js
+++ b/src/SecurityManager.js
@@ -24,6 +24,7 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
import { isSecureBackupRequired } from './utils/WellKnownUtils';
import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
+import SettingsStore from "./settings/SettingsStore";
// This stores the secret storage private keys in memory for the JS SDK. This is
// only meant to act as a cache to avoid prompting the user multiple times
@@ -31,8 +32,13 @@ import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreK
// single secret storage operation, as it will clear the cached keys once the
// operation ends.
let secretStorageKeys = {};
+let secretStorageKeyInfo = {};
let secretStorageBeingAccessed = false;
+let nonInteractive = false;
+
+let dehydrationCache = {};
+
function isCachingAllowed() {
return secretStorageBeingAccessed;
}
@@ -66,6 +72,20 @@ async function confirmToDismiss() {
return !sure;
}
+function makeInputToKey(keyInfo) {
+ return async ({ passphrase, recoveryKey }) => {
+ if (passphrase) {
+ return deriveKey(
+ passphrase,
+ keyInfo.passphrase.salt,
+ keyInfo.passphrase.iterations,
+ );
+ } else {
+ return decodeRecoveryKey(recoveryKey);
+ }
+ };
+}
+
async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
const keyInfoEntries = Object.entries(keyInfos);
if (keyInfoEntries.length > 1) {
@@ -78,17 +98,18 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
return [keyId, secretStorageKeys[keyId]];
}
- const inputToKey = async ({ passphrase, recoveryKey }) => {
- if (passphrase) {
- return deriveKey(
- passphrase,
- keyInfo.passphrase.salt,
- keyInfo.passphrase.iterations,
- );
- } else {
- return decodeRecoveryKey(recoveryKey);
+ if (dehydrationCache.key) {
+ if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationCache.key, keyInfo)) {
+ cacheSecretStorageKey(keyId, dehydrationCache.key, keyInfo);
+ return [keyId, dehydrationCache.key];
}
- };
+ }
+
+ if (nonInteractive) {
+ throw new Error("Could not unlock non-interactively");
+ }
+
+ const inputToKey = makeInputToKey(keyInfo);
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
AccessSecretStorageDialog,
/* props= */
@@ -118,14 +139,56 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
const key = await inputToKey(input);
// Save to cache to avoid future prompts in the current session
- cacheSecretStorageKey(keyId, key);
+ cacheSecretStorageKey(keyId, key, keyInfo);
return [keyId, key];
}
-function cacheSecretStorageKey(keyId, key) {
+export async function getDehydrationKey(keyInfo, checkFunc) {
+ const inputToKey = makeInputToKey(keyInfo);
+ const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
+ AccessSecretStorageDialog,
+ /* props= */
+ {
+ keyInfo,
+ checkPrivateKey: async (input) => {
+ const key = await inputToKey(input);
+ try {
+ checkFunc(key);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+ },
+ /* className= */ null,
+ /* isPriorityModal= */ false,
+ /* isStaticModal= */ false,
+ /* options= */ {
+ onBeforeClose: async (reason) => {
+ if (reason === "backgroundClick") {
+ return confirmToDismiss();
+ }
+ return true;
+ },
+ },
+ );
+ const [input] = await finished;
+ if (!input) {
+ throw new AccessCancelledError();
+ }
+ const key = await inputToKey(input);
+
+ // need to copy the key because rehydration (unpickling) will clobber it
+ dehydrationCache = {key: new Uint8Array(key), keyInfo};
+
+ return key;
+}
+
+function cacheSecretStorageKey(keyId, key, keyInfo) {
if (isCachingAllowed()) {
secretStorageKeys[keyId] = key;
+ secretStorageKeyInfo[keyId] = keyInfo;
}
}
@@ -176,6 +239,7 @@ export const crossSigningCallbacks = {
getSecretStorageKey,
cacheSecretStorageKey,
onSecretRequested,
+ getDehydrationKey,
};
export async function promptForBackupPassphrase() {
@@ -262,6 +326,18 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
await cli.bootstrapSecretStorage({
getKeyBackupPassphrase: promptForBackupPassphrase,
});
+
+ const keyId = Object.keys(secretStorageKeys)[0];
+ if (keyId && SettingsStore.getValue("feature_dehydration")) {
+ const dehydrationKeyInfo =
+ secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase
+ ? {passphrase: secretStorageKeyInfo[keyId].passphrase}
+ : {};
+ console.log("Setting dehydration key");
+ await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device");
+ } else {
+ console.log("Not setting dehydration key: no SSSS key found");
+ }
}
// `return await` needed here to ensure `finally` block runs after the
@@ -272,6 +348,57 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
secretStorageBeingAccessed = false;
if (!isCachingAllowed()) {
secretStorageKeys = {};
+ secretStorageKeyInfo = {};
+ }
+ }
+}
+
+// FIXME: this function name is a bit of a mouthful
+export async function tryToUnlockSecretStorageWithDehydrationKey(client) {
+ const key = dehydrationCache.key;
+ let restoringBackup = false;
+ if (key && await client.isSecretStorageReady()) {
+ console.log("Trying to set up cross-signing using dehydration key");
+ secretStorageBeingAccessed = true;
+ nonInteractive = true;
+ try {
+ await client.checkOwnCrossSigningTrust();
+
+ // we also need to set a new dehydrated device to replace the
+ // device we rehydrated
+ const dehydrationKeyInfo =
+ dehydrationCache.keyInfo && dehydrationCache.keyInfo.passphrase
+ ? {passphrase: dehydrationCache.keyInfo.passphrase}
+ : {};
+ await client.setDehydrationKey(key, dehydrationKeyInfo, "Backup device");
+
+ // and restore from backup
+ const backupInfo = await client.getKeyBackupVersion();
+ if (backupInfo) {
+ restoringBackup = true;
+ // don't await, because this can take a long time
+ client.restoreKeyBackupWithSecretStorage(backupInfo)
+ .finally(() => {
+ secretStorageBeingAccessed = false;
+ nonInteractive = false;
+ if (!isCachingAllowed()) {
+ secretStorageKeys = {};
+ secretStorageKeyInfo = {};
+ }
+ });
+ }
+ } finally {
+ dehydrationCache = {};
+ // the secret storage cache is needed for restoring from backup, so
+ // don't clear it yet if we're restoring from backup
+ if (!restoringBackup) {
+ secretStorageBeingAccessed = false;
+ nonInteractive = false;
+ if (!isCachingAllowed()) {
+ secretStorageKeys = {};
+ secretStorageKeyInfo = {};
+ }
+ }
}
}
}
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index f9cda23650..34d40bf1fd 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -18,7 +18,6 @@ import { _t } from './languageHandler';
import * as Roles from './Roles';
import {isValid3pidInvite} from "./RoomInvite";
import SettingsStore from "./settings/SettingsStore";
-import {WidgetType} from "./widgets/WidgetType";
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
function textForMemberEvent(ev) {
@@ -464,10 +463,6 @@ function textForWidgetEvent(event) {
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
const {name, type, url} = event.getContent() || {};
- if (WidgetType.JITSI.matches(type) || WidgetType.JITSI.matches(prevType)) {
- return textForJitsiWidgetEvent(event, senderName, url, prevUrl);
- }
-
let widgetName = name || prevName || type || prevType || '';
// Apply sentence case to widget name
if (widgetName && widgetName.length > 0) {
@@ -493,24 +488,6 @@ function textForWidgetEvent(event) {
}
}
-function textForJitsiWidgetEvent(event, senderName, url, prevUrl) {
- if (url) {
- if (prevUrl) {
- return _t('Group call modified by %(senderName)s', {
- senderName,
- });
- } else {
- return _t('Group call started by %(senderName)s', {
- senderName,
- });
- }
- } else {
- return _t('Group call ended by %(senderName)s', {
- senderName,
- });
- }
-}
-
function textForMjolnirEvent(event) {
const senderName = event.getSender();
const {entity: prevEntity} = event.getPrevContent();
diff --git a/src/ToWidgetPostMessageApi.js b/src/ToWidgetPostMessageApi.js
deleted file mode 100644
index 00309d252c..0000000000
--- a/src/ToWidgetPostMessageApi.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-
-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.
-*/
-
-// const OUTBOUND_API_NAME = 'toWidget';
-
-// Initiate requests using the "toWidget" postMessage API and handle responses
-// NOTE: ToWidgetPostMessageApi only handles message events with a data payload with a
-// response field
-export default class ToWidgetPostMessageApi {
- constructor(timeoutMs) {
- this._timeoutMs = timeoutMs || 5000; // default to 5s timer
- this._counter = 0;
- this._requestMap = {
- // $ID: {resolve, reject}
- };
- this.start = this.start.bind(this);
- this.stop = this.stop.bind(this);
- this.onPostMessage = this.onPostMessage.bind(this);
- }
-
- start() {
- window.addEventListener('message', this.onPostMessage);
- }
-
- stop() {
- window.removeEventListener('message', this.onPostMessage);
- }
-
- onPostMessage(ev) {
- // THIS IS ALL UNSAFE EXECUTION.
- // We do not verify who the sender of `ev` is!
- const payload = ev.data;
- // NOTE: Workaround for running in a mobile WebView where a
- // postMessage immediately triggers this callback even though it is
- // not the response.
- if (payload.response === undefined) {
- return;
- }
- const promise = this._requestMap[payload.requestId];
- if (!promise) {
- return;
- }
- delete this._requestMap[payload.requestId];
- promise.resolve(payload);
- }
-
- // Initiate outbound requests (toWidget)
- exec(action, targetWindow, targetOrigin) {
- targetWindow = targetWindow || window.parent; // default to parent window
- targetOrigin = targetOrigin || "*";
- this._counter += 1;
- action.requestId = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter;
-
- return new Promise((resolve, reject) => {
- this._requestMap[action.requestId] = {resolve, reject};
- targetWindow.postMessage(action, targetOrigin);
-
- if (this._timeoutMs > 0) {
- setTimeout(() => {
- if (!this._requestMap[action.requestId]) {
- return;
- }
- console.error("postMessage request timed out. Sent object: " + JSON.stringify(action),
- this._requestMap);
- this._requestMap[action.requestId].reject(new Error("Timed out"));
- delete this._requestMap[action.requestId];
- }, this._timeoutMs);
- }
- });
- }
-}
diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js
deleted file mode 100644
index c68e926ac1..0000000000
--- a/src/WidgetMessaging.js
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
-Copyright 2017 New Vector Ltd
-Copyright 2019 Travis Ralston
-
-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.
-*/
-
-/*
-* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for
-* spec. details / documentation.
-*/
-
-import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
-import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
-import Modal from "./Modal";
-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();
- global.mxFromWidgetMessaging.start();
-}
-if (!global.mxToWidgetMessaging) {
- global.mxToWidgetMessaging = new ToWidgetPostMessageApi();
- global.mxToWidgetMessaging.start();
-}
-
-const OUTBOUND_API_NAME = 'toWidget';
-
-export default class WidgetMessaging {
- /**
- * @param {string} widgetId The widget's ID
- * @param {string} wurl The raw URL of the widget as in the event (the 'wURL')
- * @param {string} renderedUrl The url used in the widget's iframe (either similar to the wURL
- * or a different URL of the clients choosing if it is using its own impl).
- * @param {bool} isUserWidget If true, the widget is a user widget, otherwise it's a room widget
- * @param {object} target Where widget messages should be sent (eg. the iframe object)
- */
- constructor(widgetId, wurl, renderedUrl, isUserWidget, target) {
- this.widgetId = widgetId;
- this.wurl = wurl;
- this.renderedUrl = renderedUrl;
- this.isUserWidget = isUserWidget;
- this.target = target;
- this.fromWidget = global.mxFromWidgetMessaging;
- this.toWidget = global.mxToWidgetMessaging;
- this._onOpenIdRequest = this._onOpenIdRequest.bind(this);
- this.start();
- }
-
- messageToWidget(action) {
- action.widgetId = this.widgetId; // Required to be sent for all outbound requests
-
- return this.toWidget.exec(action, this.target).then((data) => {
- // Check for errors and reject if found
- if (data.response === undefined) { // null is valid
- throw new Error("Missing 'response' field");
- }
- if (data.response && data.response.error) {
- const err = data.response.error;
- const msg = String(err.message ? err.message : "An error was returned");
- if (err._error) {
- console.error(err._error);
- }
- // Potential XSS attack if 'msg' is not appropriately sanitized,
- // as it is untrusted input by our parent window (which we assume is Element).
- // We can't aggressively sanitize [A-z0-9] since it might be a translation.
- throw new Error(msg);
- }
- // Return the response field for the request
- return data.response;
- });
- }
-
- /**
- * 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,
- });
- }
-
- /**
- * Tells the widget that it should terminate now.
- * @returns {Promise<*>} Resolves when widget has acknowledged the message.
- */
- terminate() {
- return this.messageToWidget({
- api: OUTBOUND_API_NAME,
- action: KnownWidgetActions.Terminate,
- });
- }
-
- /**
- * Request a screenshot from a widget
- * @return {Promise} To be resolved with screenshot data when it has been generated
- */
- getScreenshot() {
- console.log('Requesting screenshot for', this.widgetId);
- return this.messageToWidget({
- api: OUTBOUND_API_NAME,
- action: "screenshot",
- })
- .catch((error) => new Error("Failed to get screenshot: " + error.message))
- .then((response) => response.screenshot);
- }
-
- /**
- * Request capabilities required by the widget
- * @return {Promise} To be resolved with an array of requested widget capabilities
- */
- getCapabilities() {
- console.log('Requesting capabilities for', this.widgetId);
- return this.messageToWidget({
- api: OUTBOUND_API_NAME,
- action: "capabilities",
- }).then((response) => {
- console.log('Got capabilities for', this.widgetId, response.capabilities);
- return response.capabilities;
- });
- }
-
- sendVisibility(visible) {
- return this.messageToWidget({
- api: OUTBOUND_API_NAME,
- action: "visibility",
- visible,
- })
- .catch((error) => {
- console.error("Failed to send visibility: ", error);
- });
- }
-
- start() {
- this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl);
- this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
- }
-
- stop() {
- this.fromWidget.removeEndpoint(this.widgetId, this.renderedUrl);
- this.fromWidget.removeListener("get_openid", this._onOpenIdRequest);
- }
-
- async _onOpenIdRequest(ev, rawEv) {
- if (ev.widgetId !== this.widgetId) return; // not interesting
-
- const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.wurl, this.isUserWidget);
-
- const settings = SettingsStore.getValue("widgetOpenIDPermissions");
- if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
- this.fromWidget.sendResponse(rawEv, {state: "blocked"});
- return;
- }
- if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
- const responseBody = {state: "allowed"};
- const credentials = await MatrixClientPeg.get().getOpenIdToken();
- Object.assign(responseBody, credentials);
- this.fromWidget.sendResponse(rawEv, responseBody);
- return;
- }
-
- // Confirm that we received the request
- this.fromWidget.sendResponse(rawEv, {state: "request"});
-
- // Actually ask for permission to send the user's data
- Modal.createTrackedDialog("OpenID widget permissions", '',
- WidgetOpenIDPermissionsDialog, {
- widgetUrl: this.wurl,
- widgetId: this.widgetId,
- isUserWidget: this.isUserWidget,
-
- onFinished: async (confirm) => {
- const responseBody = {
- // Legacy (early draft) fields
- success: confirm,
-
- // New style MSC1960 fields
- state: confirm ? "allowed" : "blocked",
- original_request_id: ev.requestId, // eslint-disable-line camelcase
- };
- if (confirm) {
- const credentials = await MatrixClientPeg.get().getOpenIdToken();
- Object.assign(responseBody, credentials);
- }
- this.messageToWidget({
- api: OUTBOUND_API_NAME,
- action: "openid_credentials",
- data: responseBody,
- }).catch((error) => {
- console.error("Failed to send OpenID credentials: ", error);
- });
- },
- },
- );
- }
-}
diff --git a/src/WidgetMessagingEndpoint.js b/src/WidgetMessagingEndpoint.js
deleted file mode 100644
index 9114e12137..0000000000
--- a/src/WidgetMessagingEndpoint.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-
-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.
-*/
-
-
-/**
- * Represents mapping of widget instance to URLs for trusted postMessage communication.
- */
-export default class WidgetMessageEndpoint {
- /**
- * Mapping of widget instance to URL for trusted postMessage communication.
- * @param {string} widgetId Unique widget identifier
- * @param {string} endpointUrl Widget wurl origin.
- */
- constructor(widgetId, endpointUrl) {
- if (!widgetId) {
- throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
- }
- if (!endpointUrl) {
- throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
- }
- this.widgetId = widgetId;
- this.endpointUrl = endpointUrl;
- }
-}
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index a638ad6de1..19418df414 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -1019,7 +1019,7 @@ export default class MatrixChat extends React.PureComponent