/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018-2019 New Vector Ltd
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.
*/
/**
* This is an internal module. See {@link MatrixClient} for the public class.
* @module client
*/
import url from "url";
import { EventEmitter } from "events";
import { MatrixBaseApis } from "./base-apis";
import { Filter } from "./filter";
import { SyncApi } from "./sync";
import { EventStatus, MatrixEvent } from "./models/event";
import { EventTimeline } from "./models/event-timeline";
import { SearchResult } from "./models/search-result";
import { StubStore } from "./store/stub";
import { createNewMatrixCall } from "./webrtc/call";
import { CallEventHandler } from './webrtc/callEventHandler';
import * as utils from './utils';
import { sleep } from './utils';
import {
MatrixError,
PREFIX_MEDIA_R0,
PREFIX_UNSTABLE,
retryNetworkOperation,
} from "./http-api";
import { getHttpUriForMxc } from "./content-repo";
import * as ContentHelpers from "./content-helpers";
import * as olmlib from "./crypto/olmlib";
import { ReEmitter } from './ReEmitter';
import { RoomList } from './crypto/RoomList';
import { logger } from './logger';
import { Crypto, isCryptoAvailable, fixBackupKey } from './crypto';
import { decodeRecoveryKey } from './crypto/recoverykey';
import { keyFromAuthData } from './crypto/key_passphrase';
import { randomString } from './randomstring';
import { PushProcessor } from "./pushprocessor";
import { encodeBase64, decodeBase64 } from "./crypto/olmlib";
import { User } from "./models/user";
import { AutoDiscovery } from "./autodiscovery";
import { DEHYDRATION_ALGORITHM } from "./crypto/dehydration";
const SCROLLBACK_DELAY_MS = 3000;
export const CRYPTO_ENABLED = isCryptoAvailable();
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
function keysFromRecoverySession(sessions, decryptionKey, roomId) {
const keys = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
const decrypted = keyFromRecoverySession(sessionData, decryptionKey);
decrypted.session_id = sessionId;
decrypted.room_id = roomId;
keys.push(decrypted);
} catch (e) {
logger.log("Failed to decrypt megolm session from backup", e);
}
}
return keys;
}
function keyFromRecoverySession(session, decryptionKey) {
return JSON.parse(decryptionKey.decrypt(
session.session_data.ephemeral,
session.session_data.mac,
session.session_data.ciphertext,
));
}
/**
* Construct a Matrix Client. Only directly construct this if you want to use
* custom modules. Normally, {@link createClient} should be used
* as it specifies 'sensible' defaults for these modules.
* @constructor
* @extends {external:EventEmitter}
* @extends {module:base-apis~MatrixBaseApis}
*
* @param {Object} opts The configuration options for this client.
* @param {string} opts.baseUrl Required. The base URL to the client-server
* HTTP API.
* @param {string} opts.idBaseUrl Optional. The base identity server URL for
* identity server requests.
* @param {Function} opts.request Required. The function to invoke for HTTP
* requests. The value of this property is typically require("request")
* as it returns a function which meets the required interface. See
* {@link requestFunction} for more information.
*
* @param {string} opts.accessToken The access_token for this user.
*
* @param {string} opts.userId The user ID for this user.
*
* @param {Object} opts.deviceToImport Device data exported with
* "exportDevice" method that must be imported to recreate this device.
* Should only be useful for devices with end-to-end crypto enabled.
* If provided, opts.deviceId and opts.userId should **NOT** be provided
* (they are present in the exported data).
*
* @param {string} opts.pickleKey Key used to pickle olm objects or other
* sensitive data.
*
* @param {IdentityServerProvider} [opts.identityServer]
* Optional. A provider object with one function `getAccessToken`, which is a
* callback that returns a Promise of an identity access token to supply
* with identity requests. If the object is unset, no access token will be
* supplied.
* See also https://github.com/vector-im/element-web/issues/10615 which seeks to
* replace the previous approach of manual access tokens params with this
* callback throughout the SDK.
*
* @param {Object=} opts.store
* The data store used for sync data from the homeserver. If not specified,
* this client will not store any HTTP responses. The `createClient` helper
* will create a default store if needed.
*
* @param {module:store/session/webstorage~WebStorageSessionStore} opts.sessionStore
* A store to be used for end-to-end crypto session data. Most data has been
* migrated out of here to `cryptoStore` instead. If not specified,
* end-to-end crypto will be disabled. The `createClient` helper
* _will not_ create this store at the moment.
*
* @param {module:crypto.store.base~CryptoStore} opts.cryptoStore
* A store to be used for end-to-end crypto session data. If not specified,
* end-to-end crypto will be disabled. The `createClient` helper will create
* a default store if needed.
*
* @param {string=} opts.deviceId A unique identifier for this device; used for
* tracking things like crypto keys and access tokens. If not specified,
* end-to-end crypto will be disabled.
*
* @param {Object} opts.scheduler Optional. The scheduler to use. If not
* specified, this client will not retry requests on failure. This client
* will supply its own processing function to
* {@link module:scheduler~MatrixScheduler#setProcessFunction}.
*
* @param {Object} opts.queryParams Optional. Extra query parameters to append
* to all requests with this client. Useful for application services which require
* ?user_id=.
*
* @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of
* time to wait before timing out HTTP requests. If not specified, there is no timeout.
*
* @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
* Authorization header instead of query param to send the access token to the server.
*
* @param {boolean} [opts.timelineSupport = false] Set to true to enable
* improved timeline support ({@link
* module:client~MatrixClient#getEventTimeline getEventTimeline}). It is
* disabled by default for compatibility with older clients - in particular to
* maintain support for back-paginating the live timeline after a '/sync'
* result with a gap.
*
* @param {boolean} [opts.unstableClientRelationAggregation = false]
* Optional. Set to true to enable client-side aggregation of event relations
* via `EventTimelineSet#getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*
* @param {Array} [opts.verificationMethods] Optional. The verification method
* that the application can handle. Each element should be an item from {@link
* module:crypto~verificationMethods verificationMethods}, or a class that
* implements the {$link module:crypto/verification/Base verifier interface}.
*
* @param {boolean} [opts.forceTURN]
* Optional. Whether relaying calls through a TURN server should be forced.
*
* * @param {boolean} [opts.iceCandidatePoolSize]
* Optional. Up to this many ICE candidates will be gathered when an incoming call arrives.
* Gathering does not send data to the caller, but will communicate with the configured TURN
* server. Default 0.
*
* @param {boolean} [opts.supportsCallTransfer]
* Optional. True to advertise support for call transfers to other parties on Matrix calls.
*
* @param {boolean} [opts.fallbackICEServerAllowed]
* Optional. Whether to allow a fallback ICE server should be used for negotiating a
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to false.
*
* @param {boolean} [opts.usingExternalCrypto]
* Optional. Whether to allow sending messages to encrypted rooms when encryption
* is not available internally within this SDK. This is useful if you are using an external
* E2E proxy, for example. Defaults to false.
*
* @param {object} opts.cryptoCallbacks Optional. Callbacks for crypto and cross-signing.
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param {function} [opts.cryptoCallbacks.getCrossSigningKey]
* Optional. Function to call when a cross-signing private key is needed.
* Secure Secret Storage will be used by default if this is unset.
* Args:
* {string} type The type of key needed. Will be one of "master",
* "self_signing", or "user_signing"
* {Uint8Array} publicKey The public key matching the expected private key.
* This can be passed to checkPrivateKey() along with the private key
* in order to check that a given private key matches what is being
* requested.
* Should return a promise that resolves with the private key as a
* UInt8Array or rejects with an error.
*
* @param {function} [opts.cryptoCallbacks.saveCrossSigningKeys]
* Optional. Called when new private keys for cross-signing need to be saved.
* Secure Secret Storage will be used by default if this is unset.
* Args:
* {object} keys the private keys to save. Map of key name to private key
* as a UInt8Array. The getPrivateKey callback above will be called
* with the corresponding key name when the keys are required again.
*
* @param {function} [opts.cryptoCallbacks.shouldUpgradeDeviceVerifications]
* Optional. Called when there are device-to-device verifications that can be
* upgraded into cross-signing verifications.
* Args:
* {object} users The users whose device verifications can be
* upgraded to cross-signing verifications. This will be a map of user IDs
* to objects with the properties `devices` (array of the user's devices
* that verified their cross-signing key), and `crossSigningInfo` (the
* user's cross-signing information)
* Should return a promise which resolves with an array of the user IDs who
* should be cross-signed.
*
* @param {function} [opts.cryptoCallbacks.getSecretStorageKey]
* Optional. Function called when an encryption key for secret storage
* is required. One or more keys will be described in the keys object.
* The callback function should return a promise with an array of:
* [, ] or null if it cannot provide
* any of the keys.
* Args:
* {object} keys Information about the keys:
* {
* keys: {
* : {
* "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
* "passphrase": {
* "algorithm": "m.pbkdf2",
* "iterations": 500000,
* "salt": "..."
* },
* "iv": "...",
* "mac": "..."
* }, ...
* }
* }
* {string} name the name of the value we want to read out of SSSS, for UI purposes.
*
* @param {function} [opts.cryptoCallbacks.cacheSecretStorageKey]
* Optional. Function called when a new encryption key for secret storage
* has been created. This allows the application a chance to cache this key if
* desired to avoid user prompts.
* Args:
* {string} keyId the ID of the new key
* {object} keyInfo Infomation about the key as above for `getSecretStorageKey`
* {Uint8Array} key the new private key
*
* @param {function} [opts.cryptoCallbacks.onSecretRequested]
* Optional. Function called when a request for a secret is received from another
* device.
* Args:
* {string} name The name of the secret being requested.
* {string} userId The user ID of the client requesting
* {string} deviceId The device ID of the client requesting the secret.
* {string} requestId The ID of the request. Used to match a
* corresponding `crypto.secrets.request_cancelled`. The request ID will be
* unique per sender, device pair.
* {DeviceTrustLevel} deviceTrust: The trust status of the device requesting
* the secret as returned by {@link module:client~MatrixClient#checkDeviceTrust}.
*/
export function MatrixClient(opts) {
opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl);
opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl);
MatrixBaseApis.call(this, opts);
this.olmVersion = null; // Populated after initCrypto is done
this.reEmitter = new ReEmitter(this);
this.usingExternalCrypto = opts.usingExternalCrypto;
this.store = opts.store || new StubStore();
this.deviceId = opts.deviceId || null;
const userId = (opts.userId || null);
this.credentials = {
userId: userId,
};
if (opts.deviceToImport) {
if (this.deviceId) {
logger.warn(
'not importing device because'
+ ' device ID is provided to constructor'
+ ' independently of exported data',
);
} else if (this.credentials.userId) {
logger.warn(
'not importing device because'
+ ' user ID is provided to constructor'
+ ' independently of exported data',
);
} else if (!(opts.deviceToImport.deviceId)) {
logger.warn('not importing device because no device ID in exported data');
} else {
this.deviceId = opts.deviceToImport.deviceId;
this.credentials.userId = opts.deviceToImport.userId;
// will be used during async initialization of the crypto
this._exportedOlmDeviceToImport = opts.deviceToImport.olmDevice;
}
} else if (opts.pickleKey) {
this.pickleKey = opts.pickleKey;
}
this.scheduler = opts.scheduler;
if (this.scheduler) {
const self = this;
this.scheduler.setProcessFunction(async function(eventToSend) {
const room = self.getRoom(eventToSend.getRoomId());
if (eventToSend.status !== EventStatus.SENDING) {
_updatePendingEventStatus(room, eventToSend,
EventStatus.SENDING);
}
const res = await _sendEventHttpRequest(self, eventToSend);
if (room) {
// ensure we update pending event before the next scheduler run so that any listeners to event id
// updates on the synchronous event emitter get a chance to run first.
room.updatePendingEvent(eventToSend, EventStatus.SENT, res.event_id);
}
return res;
});
}
this.clientRunning = false;
// try constructing a MatrixCall to see if we are running in an environment
// which has WebRTC. If we are, listen for and handle m.call.* events.
const call = createNewMatrixCall(this);
this._supportsVoip = false;
if (call) {
this._callEventHandler = new CallEventHandler(this);
this._supportsVoip = true;
// Start listening for calls after the initial sync is done
// We do not need to backfill the call event buffer
// with encrypted events that might never get decrypted
this.on("sync", this._startCallEventHandler);
} else {
this._callEventHandler = null;
}
this._syncingRetry = null;
this._syncApi = null;
this._peekSync = null;
this._isGuest = false;
this._ongoingScrollbacks = {};
this.timelineSupport = Boolean(opts.timelineSupport);
this.urlPreviewCache = {}; // key=preview key, value=Promise for preview (may be an error)
this._notifTimelineSet = null;
this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
this._crypto = null;
this._cryptoStore = opts.cryptoStore;
this._sessionStore = opts.sessionStore;
this._verificationMethods = opts.verificationMethods;
this._cryptoCallbacks = opts.cryptoCallbacks || {};
this._forceTURN = opts.forceTURN || false;
this._iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize;
this._supportsCallTransfer = opts.supportsCallTransfer || false;
this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
// List of which rooms have encryption enabled: separate from crypto because
// we still want to know which rooms are encrypted even if crypto is disabled:
// we don't want to start sending unencrypted events to them.
this._roomList = new RoomList(this._cryptoStore);
// The pushprocessor caches useful things, so keep one and re-use it
this._pushProcessor = new PushProcessor(this);
// Promise to a response of the server's /versions response
// TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020
this._serverVersionsPromise = null;
this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp }
this._clientWellKnown = undefined;
this._clientWellKnownPromise = undefined;
this._turnServers = [];
this._turnServersExpiry = 0;
this._checkTurnServersIntervalID = null;
// The SDK doesn't really provide a clean way for events to recalculate the push
// actions for themselves, so we have to kinda help them out when they are encrypted.
// We do this so that push rules are correctly executed on events in their decrypted
// state, such as highlights when the user's name is mentioned.
this.on("Event.decrypted", (event) => {
const oldActions = event.getPushActions();
const actions = this._pushProcessor.actionsForEvent(event);
event.setPushActions(actions); // Might as well while we're here
const room = this.getRoom(event.getRoomId());
if (!room) return;
const currentCount = room.getUnreadNotificationCount("highlight");
// Ensure the unread counts are kept up to date if the event is encrypted
// We also want to make sure that the notification count goes up if we already
// have encrypted events to avoid other code from resetting 'highlight' to zero.
const oldHighlight = oldActions && oldActions.tweaks
? !!oldActions.tweaks.highlight : false;
const newHighlight = actions && actions.tweaks
? !!actions.tweaks.highlight : false;
if (oldHighlight !== newHighlight || currentCount > 0) {
// TODO: Handle mentions received while the client is offline
// See also https://github.com/vector-im/element-web/issues/9069
if (!room.hasUserReadEvent(this.getUserId(), event.getId())) {
let newCount = currentCount;
if (newHighlight && !oldHighlight) newCount++;
if (!newHighlight && oldHighlight) newCount--;
room.setUnreadNotificationCount("highlight", newCount);
// Fix 'Mentions Only' rooms from not having the right badge count
const totalCount = room.getUnreadNotificationCount('total');
if (totalCount < newCount) {
room.setUnreadNotificationCount('total', newCount);
}
}
}
});
// Like above, we have to listen for read receipts from ourselves in order to
// correctly handle notification counts on encrypted rooms.
// This fixes https://github.com/vector-im/element-web/issues/9421
this.on("Room.receipt", (event, room) => {
if (room && this.isRoomEncrypted(room.roomId)) {
// Figure out if we've read something or if it's just informational
const content = event.getContent();
const isSelf = Object.keys(content).filter(eid => {
return Object.keys(content[eid]['m.read']).includes(this.getUserId());
}).length > 0;
if (!isSelf) return;
// Work backwards to determine how many events are unread. We also set
// a limit for how back we'll look to avoid spinning CPU for too long.
// If we hit the limit, we assume the count is unchanged.
const maxHistory = 20;
const events = room.getLiveTimeline().getEvents();
let highlightCount = 0;
for (let i = events.length - 1; i >= 0; i--) {
if (i === events.length - maxHistory) return; // limit reached
const event = events[i];
if (room.hasUserReadEvent(this.getUserId(), event.getId())) {
// If the user has read the event, then the counting is done.
break;
}
const pushActions = this.getPushActionsForEvent(event);
highlightCount += pushActions.tweaks &&
pushActions.tweaks.highlight ? 1 : 0;
}
// Note: we don't need to handle 'total' notifications because the counts
// will come from the server.
room.setUnreadNotificationCount("highlight", highlightCount);
}
});
}
utils.inherits(MatrixClient, EventEmitter);
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
/**
* Try to rehydrate a device if available. The client must have been
* initialized with a `cryptoCallback.getDehydrationKey` option, and this
* function must be called before initCrypto and startClient are called.
*
* @return {Promise} Resolves to undefined if a device could not be dehydrated, or
* to the new device ID if the dehydration was successful.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.rehydrateDevice = async function() {
if (this._crypto) {
throw new Error("Cannot rehydrate device after crypto is initialized");
}
if (!this._cryptoCallbacks.getDehydrationKey) {
return;
}
const getDeviceResult = await this.getDehydratedDevice();
if (!getDeviceResult) {
return;
}
if (!getDeviceResult.device_data || !getDeviceResult.device_id) {
logger.info("no dehydrated device found");
return;
}
const account = new global.Olm.Account();
try {
const deviceData = getDeviceResult.device_data;
if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) {
logger.warn("Wrong algorithm for dehydrated device");
return;
}
logger.log("unpickling dehydrated device");
const key = await this._cryptoCallbacks.getDehydrationKey(
deviceData,
(k) => {
// copy the key so that it doesn't get clobbered
account.unpickle(new Uint8Array(k), deviceData.account);
},
);
account.unpickle(key, deviceData.account);
logger.log("unpickled device");
const rehydrateResult = await this._http.authedRequest(
undefined,
"POST",
"/dehydrated_device/claim",
undefined,
{
device_id: getDeviceResult.device_id,
},
{
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
},
);
if (rehydrateResult.success === true) {
this.deviceId = getDeviceResult.device_id;
logger.info("using dehydrated device");
const pickleKey = this.pickleKey || "DEFAULT_KEY";
this._exportedOlmDeviceToImport = {
pickledAccount: account.pickle(pickleKey),
sessions: [],
pickleKey: pickleKey,
};
account.free();
return this.deviceId;
} else {
account.free();
logger.info("not using dehydrated device");
return;
}
} catch (e) {
account.free();
logger.warn("could not unpickle", e);
}
};
/**
* Get the current dehydrated device, if any
* @return {Promise} A promise of an object containing the dehydrated device
*/
MatrixClient.prototype.getDehydratedDevice = async function() {
try {
return await this._http.authedRequest(
undefined,
"GET",
"/dehydrated_device",
undefined, undefined,
{
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
},
);
} catch (e) {
logger.info("could not get dehydrated device", e.toString());
return;
}
};
/**
* Set the dehydration key. This will also periodically dehydrate devices to
* the server.
*
* @param {Uint8Array} key the dehydration key
* @param {object} [keyInfo] Information about the key. Primarily for
* information about how to generate the key from a passphrase.
* @param {string} [deviceDisplayName] The device display name for the
* dehydrated device.
* @return {Promise} A promise that resolves when the dehydrated device is stored.
*/
MatrixClient.prototype.setDehydrationKey = async function(
key, keyInfo = {}, deviceDisplayName = undefined,
) {
if (!(this._crypto)) {
logger.warn('not dehydrating device if crypto is not enabled');
return;
}
return await this._crypto._dehydrationManager.setKeyAndQueueDehydration(
key, keyInfo, deviceDisplayName,
);
};
/**
* Creates a new dehydrated device (without queuing periodic dehydration)
* @param {Uint8Array} key the dehydration key
* @param {object} [keyInfo] Information about the key. Primarily for
* information about how to generate the key from a passphrase.
* @param {string} [deviceDisplayName] The device display name for the
* dehydrated device.
* @return {Promise} the device id of the newly created dehydrated device
*/
MatrixClient.prototype.createDehydratedDevice = async function(
key, keyInfo = {}, deviceDisplayName = undefined,
) {
if (!(this._crypto)) {
logger.warn('not dehydrating device if crypto is not enabled');
return;
}
await this._crypto._dehydrationManager.setKey(
key, keyInfo, deviceDisplayName,
);
return await this._crypto._dehydrationManager.dehydrateDevice();
};
MatrixClient.prototype.exportDevice = async function() {
if (!(this._crypto)) {
logger.warn('not exporting device if crypto is not enabled');
return;
}
return {
userId: this.credentials.userId,
deviceId: this.deviceId,
olmDevice: await this._crypto._olmDevice.export(),
};
};
/**
* Clear any data out of the persistent stores used by the client.
*
* @returns {Promise} Promise which resolves when the stores have been cleared.
*/
MatrixClient.prototype.clearStores = function() {
if (this._clientRunning) {
throw new Error("Cannot clear stores while client is running");
}
const promises = [];
promises.push(this.store.deleteAllData());
if (this._cryptoStore) {
promises.push(this._cryptoStore.deleteAllData());
}
return Promise.all(promises);
};
/**
* Get the user-id of the logged-in user
*
* @return {?string} MXID for the logged-in user, or null if not logged in
*/
MatrixClient.prototype.getUserId = function() {
if (this.credentials && this.credentials.userId) {
return this.credentials.userId;
}
return null;
};
/**
* Get the domain for this client's MXID
* @return {?string} Domain of this MXID
*/
MatrixClient.prototype.getDomain = function() {
if (this.credentials && this.credentials.userId) {
return this.credentials.userId.replace(/^.*?:/, '');
}
return null;
};
/**
* Get the local part of the current user ID e.g. "foo" in "@foo:bar".
* @return {?string} The user ID localpart or null.
*/
MatrixClient.prototype.getUserIdLocalpart = function() {
if (this.credentials && this.credentials.userId) {
return this.credentials.userId.split(":")[0].substring(1);
}
return null;
};
/**
* Get the device ID of this client
* @return {?string} device ID
*/
MatrixClient.prototype.getDeviceId = function() {
return this.deviceId;
};
/**
* Check if the runtime environment supports VoIP calling.
* @return {boolean} True if VoIP is supported.
*/
MatrixClient.prototype.supportsVoip = function() {
return this._supportsVoip;
};
/**
* Set whether VoIP calls are forced to use only TURN
* candidates. This is the same as the forceTURN option
* when creating the client.
* @param {bool} forceTURN True to force use of TURN servers
*/
MatrixClient.prototype.setForceTURN = function(forceTURN) {
this._forceTURN = forceTURN;
};
/**
* Set whether to advertise transfer support to other parties on Matrix calls.
* @param {bool} supportsCallTransfer True to advertise the 'm.call.transferee' capability
*/
MatrixClient.prototype.setSupportsCallTransfer = function(supportsCallTransfer) {
this._supportsCallTransfer = supportsCallTransfer;
};
/**
* Creates a new call.
* The place*Call methods on the returned call can be used to actually place a call
*
* @param {string} roomId The room the call is to be placed in.
* @return {MatrixCall} the call or null if the browser doesn't support calling.
*/
MatrixClient.prototype.createCall = function(roomId) {
return createNewMatrixCall(this, roomId);
};
/**
* Get the current sync state.
* @return {?string} the sync state, which may be null.
* @see module:client~MatrixClient#event:"sync"
*/
MatrixClient.prototype.getSyncState = function() {
if (!this._syncApi) {
return null;
}
return this._syncApi.getSyncState();
};
/**
* Returns the additional data object associated with
* the current sync state, or null if there is no
* such data.
* Sync errors, if available, are put in the 'error' key of
* this object.
* @return {?Object}
*/
MatrixClient.prototype.getSyncStateData = function() {
if (!this._syncApi) {
return null;
}
return this._syncApi.getSyncStateData();
};
/**
* Whether the initial sync has completed.
* @return {boolean} True if at least on sync has happened.
*/
MatrixClient.prototype.isInitialSyncComplete = function() {
const state = this.getSyncState();
if (!state) {
return false;
}
return state === "PREPARED" || state === "SYNCING";
};
/**
* Return whether the client is configured for a guest account.
* @return {boolean} True if this is a guest access_token (or no token is supplied).
*/
MatrixClient.prototype.isGuest = function() {
return this._isGuest;
};
/**
* Return the provided scheduler, if any.
* @return {?module:scheduler~MatrixScheduler} The scheduler or null
*/
MatrixClient.prototype.getScheduler = function() {
return this.scheduler;
};
/**
* Set whether this client is a guest account. This method is experimental
* and may change without warning.
* @param {boolean} isGuest True if this is a guest account.
*/
MatrixClient.prototype.setGuest = function(isGuest) {
// EXPERIMENTAL:
// If the token is a macaroon, it should be encoded in it that it is a 'guest'
// access token, which means that the SDK can determine this entirely without
// the dev manually flipping this flag.
this._isGuest = isGuest;
};
/**
* Retry a backed off syncing request immediately. This should only be used when
* the user explicitly attempts to retry their lost connection.
* @return {boolean} True if this resulted in a request being retried.
*/
MatrixClient.prototype.retryImmediately = function() {
return this._syncApi.retryImmediately();
};
/**
* Return the global notification EventTimelineSet, if any
*
* @return {EventTimelineSet} the globl notification EventTimelineSet
*/
MatrixClient.prototype.getNotifTimelineSet = function() {
return this._notifTimelineSet;
};
/**
* Set the global notification EventTimelineSet
*
* @param {EventTimelineSet} notifTimelineSet
*/
MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
this._notifTimelineSet = notifTimelineSet;
};
/**
* Gets the capabilities of the homeserver. Always returns an object of
* capability keys and their options, which may be empty.
* @param {boolean} fresh True to ignore any cached values.
* @return {Promise} Resolves to the capabilities of the homeserver
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getCapabilities = function(fresh=false) {
const now = new Date().getTime();
if (this._cachedCapabilities && !fresh) {
if (now < this._cachedCapabilities.expiration) {
logger.log("Returning cached capabilities");
return Promise.resolve(this._cachedCapabilities.capabilities);
}
}
// We swallow errors because we need a default object anyhow
return this._http.authedRequest(
undefined, "GET", "/capabilities",
).catch((e) => {
logger.error(e);
return null; // otherwise consume the error
}).then((r) => {
if (!r) r = {};
const capabilities = r["capabilities"] || {};
// If the capabilities missed the cache, cache it for a shorter amount
// of time to try and refresh them later.
const cacheMs = Object.keys(capabilities).length
? CAPABILITIES_CACHE_MS
: 60000 + (Math.random() * 5000);
this._cachedCapabilities = {
capabilities: capabilities,
expiration: now + cacheMs,
};
logger.log("Caching capabilities: ", capabilities);
return capabilities;
});
};
// Crypto bits
// ===========
/**
* Initialise support for end-to-end encryption in this client
*
* You should call this method after creating the matrixclient, but *before*
* calling `startClient`, if you want to support end-to-end encryption.
*
* It will return a Promise which will resolve when the crypto layer has been
* successfully initialised.
*/
MatrixClient.prototype.initCrypto = async function() {
if (!isCryptoAvailable()) {
throw new Error(
`End-to-end encryption not supported in this js-sdk build: did ` +
`you remember to load the olm library?`,
);
}
if (this._crypto) {
logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
return;
}
if (!this._sessionStore) {
// this is temporary, the sessionstore is supposed to be going away
throw new Error(`Cannot enable encryption: no sessionStore provided`);
}
if (!this._cryptoStore) {
// the cryptostore is provided by sdk.createClient, so this shouldn't happen
throw new Error(`Cannot enable encryption: no cryptoStore provided`);
}
logger.log("Crypto: Starting up crypto store...");
await this._cryptoStore.startup();
// initialise the list of encrypted rooms (whether or not crypto is enabled)
logger.log("Crypto: initialising roomlist...");
await this._roomList.init();
const userId = this.getUserId();
if (userId === null) {
throw new Error(
`Cannot enable encryption on MatrixClient with unknown userId: ` +
`ensure userId is passed in createClient().`,
);
}
if (this.deviceId === null) {
throw new Error(
`Cannot enable encryption on MatrixClient with unknown deviceId: ` +
`ensure deviceId is passed in createClient().`,
);
}
const crypto = new Crypto(
this,
this._sessionStore,
userId, this.deviceId,
this.store,
this._cryptoStore,
this._roomList,
this._verificationMethods,
);
this.reEmitter.reEmit(crypto, [
"crypto.keyBackupFailed",
"crypto.keyBackupSessionsRemaining",
"crypto.roomKeyRequest",
"crypto.roomKeyRequestCancellation",
"crypto.warning",
"crypto.devicesUpdated",
"crypto.willUpdateDevices",
"deviceVerificationChanged",
"userTrustStatusChanged",
"crossSigning.keysChanged",
]);
logger.log("Crypto: initialising crypto object...");
await crypto.init({
exportedOlmDevice: this._exportedOlmDeviceToImport,
pickleKey: this.pickleKey,
});
delete this._exportedOlmDeviceToImport;
this.olmVersion = Crypto.getOlmVersion();
// if crypto initialisation was successful, tell it to attach its event
// handlers.
crypto.registerEventHandlers(this);
this._crypto = crypto;
};
/**
* Is end-to-end crypto enabled for this client.
* @return {boolean} True if end-to-end is enabled.
*/
MatrixClient.prototype.isCryptoEnabled = function() {
return this._crypto !== null;
};
/**
* Get the Ed25519 key for this device
*
* @return {?string} base64-encoded ed25519 key. Null if crypto is
* disabled.
*/
MatrixClient.prototype.getDeviceEd25519Key = function() {
if (!this._crypto) {
return null;
}
return this._crypto.getDeviceEd25519Key();
};
/**
* Get the Curve25519 key for this device
*
* @return {?string} base64-encoded curve25519 key. Null if crypto is
* disabled.
*/
MatrixClient.prototype.getDeviceCurve25519Key = function() {
if (!this._crypto) {
return null;
}
return this._crypto.getDeviceCurve25519Key();
};
/**
* Upload the device keys to the homeserver.
* @return {object} A promise that will resolve when the keys are uploaded.
*/
MatrixClient.prototype.uploadKeys = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.uploadDeviceKeys();
};
/**
* Download the keys for a list of users and stores the keys in the session
* store.
* @param {Array} userIds The users to fetch.
* @param {bool} forceDownload Always download the keys even if cached.
*
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
* module:crypto~DeviceInfo|DeviceInfo}.
*/
MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
if (this._crypto === null) {
return Promise.reject(new Error("End-to-end encryption disabled"));
}
return this._crypto.downloadKeys(userIds, forceDownload);
};
/**
* Get the stored device keys for a user id
*
* @param {string} userId the user to list keys for.
*
* @return {module:crypto/deviceinfo[]} list of devices
*/
MatrixClient.prototype.getStoredDevicesForUser = function(userId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.getStoredDevicesForUser(userId) || [];
};
/**
* Get the stored device key for a user id and device id
*
* @param {string} userId the user to list keys for.
* @param {string} deviceId unique identifier for the device
*
* @return {module:crypto/deviceinfo} device or null
*/
MatrixClient.prototype.getStoredDevice = function(userId, deviceId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.getStoredDevice(userId, deviceId) || null;
};
/**
* Mark the given device as verified
*
* @param {string} userId owner of the device
* @param {string} deviceId unique identifier for the device or user's
* cross-signing public key ID.
*
* @param {boolean=} verified whether to mark the device as verified. defaults
* to 'true'.
*
* @returns {Promise}
*
* @fires module:client~event:MatrixClient"deviceVerificationChanged"
*/
MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified) {
if (verified === undefined) {
verified = true;
}
const prom = _setDeviceVerification(this, userId, deviceId, verified, null);
// if one of the user's own devices is being marked as verified / unverified,
// check the key backup status, since whether or not we use this depends on
// whether it has a signature from a verified device
if (userId == this.credentials.userId) {
this._crypto.checkKeyBackup();
}
return prom;
};
/**
* Mark the given device as blocked/unblocked
*
* @param {string} userId owner of the device
* @param {string} deviceId unique identifier for the device or user's
* cross-signing public key ID.
*
* @param {boolean=} blocked whether to mark the device as blocked. defaults
* to 'true'.
*
* @returns {Promise}
*
* @fires module:client~event:MatrixClient"deviceVerificationChanged"
*/
MatrixClient.prototype.setDeviceBlocked = function(userId, deviceId, blocked) {
if (blocked === undefined) {
blocked = true;
}
return _setDeviceVerification(this, userId, deviceId, null, blocked);
};
/**
* Mark the given device as known/unknown
*
* @param {string} userId owner of the device
* @param {string} deviceId unique identifier for the device or user's
* cross-signing public key ID.
*
* @param {boolean=} known whether to mark the device as known. defaults
* to 'true'.
*
* @returns {Promise}
*
* @fires module:client~event:MatrixClient"deviceVerificationChanged"
*/
MatrixClient.prototype.setDeviceKnown = function(userId, deviceId, known) {
if (known === undefined) {
known = true;
}
return _setDeviceVerification(this, userId, deviceId, null, null, known);
};
async function _setDeviceVerification(
client, userId, deviceId, verified, blocked, known,
) {
if (!client._crypto) {
throw new Error("End-to-End encryption disabled");
}
await client._crypto.setDeviceVerification(
userId, deviceId, verified, blocked, known,
);
}
/**
* Request a key verification from another user, using a DM.
*
* @param {string} userId the user to request verification with
* @param {string} roomId the room to use for verification
*
* @returns {Promise} resolves to a VerificationRequest
* when the request has been sent to the other party.
*/
MatrixClient.prototype.requestVerificationDM = function(userId, roomId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.requestVerificationDM(userId, roomId);
};
/**
* Finds a DM verification request that is already in progress for the given room id
*
* @param {string} roomId the room to use for verification
*
* @returns {module:crypto/verification/request/VerificationRequest?} the VerificationRequest that is in progress, if any
*/
MatrixClient.prototype.findVerificationRequestDMInProgress = function(roomId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.findVerificationRequestDMInProgress(roomId);
};
/**
* Returns all to-device verification requests that are already in progress for the given user id
*
* @param {string} userId the ID of the user to query
*
* @returns {module:crypto/verification/request/VerificationRequest[]} the VerificationRequests that are in progress
*/
MatrixClient.prototype.getVerificationRequestsToDeviceInProgress = function(userId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.getVerificationRequestsToDeviceInProgress(userId);
};
/**
* Request a key verification from another user.
*
* @param {string} userId the user to request verification with
* @param {Array} devices array of device IDs to send requests to. Defaults to
* all devices owned by the user
*
* @returns {Promise} resolves to a VerificationRequest
* when the request has been sent to the other party.
*/
MatrixClient.prototype.requestVerification = function(userId, devices) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.requestVerification(userId, devices);
};
/**
* Begin a key verification.
*
* @param {string} method the verification method to use
* @param {string} userId the user to verify keys with
* @param {string} deviceId the device to verify
*
* @returns {module:crypto/verification/Base} a verification object
*/
MatrixClient.prototype.beginKeyVerification = function(
method, userId, deviceId,
) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.beginKeyVerification(method, userId, deviceId);
};
/**
* Set the global override for whether the client should ever send encrypted
* messages to unverified devices. This provides the default for rooms which
* do not specify a value.
*
* @param {boolean} value whether to blacklist all unverified devices by default
*/
MatrixClient.prototype.setGlobalBlacklistUnverifiedDevices = function(value) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
this._crypto.setGlobalBlacklistUnverifiedDevices(value);
};
/**
* @return {boolean} whether to blacklist all unverified devices by default
*/
MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.getGlobalBlacklistUnverifiedDevices();
};
/**
* Set whether sendMessage in a room with unknown and unverified devices
* should throw an error and not send them message. This has 'Global' for
* symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
* no room-level equivalent for this setting.
*
* This API is currently UNSTABLE and may change or be removed without notice.
*
* @param {boolean} value whether error on unknown devices
*/
MatrixClient.prototype.setGlobalErrorOnUnknownDevices = function(value) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
this._crypto.setGlobalErrorOnUnknownDevices(value);
};
/**
* @return {boolean} whether to error on unknown devices
*
* This API is currently UNSTABLE and may change or be removed without notice.
*/
MatrixClient.prototype.getGlobalErrorOnUnknownDevices = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.getGlobalErrorOnUnknownDevices();
};
/**
* Add methods that call the corresponding method in this._crypto
*
* @param {class} MatrixClient the class to add the method to
* @param {string} names the names of the methods to call
*/
function wrapCryptoFuncs(MatrixClient, names) {
for (const name of names) {
MatrixClient.prototype[name] = function(...args) {
if (!this._crypto) { // eslint-disable-line no-invalid-this
throw new Error("End-to-end encryption disabled");
}
return this._crypto[name](...args); // eslint-disable-line no-invalid-this
};
}
}
/**
* Get the user's cross-signing key ID.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#getCrossSigningId
* @param {string} [type=master] The type of key to get the ID of. One of
* "master", "self_signing", or "user_signing". Defaults to "master".
*
* @returns {string} the key ID
*/
/**
* Get the cross signing information for a given user.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#getStoredCrossSigningForUser
* @param {string} userId the user ID to get the cross-signing info for.
*
* @returns {CrossSigningInfo} the cross signing information for the user.
*/
/**
* Check whether a given user is trusted.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#checkUserTrust
* @param {string} userId The ID of the user to check.
*
* @returns {UserTrustLevel}
*/
/**
* Check whether a given device is trusted.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#checkDeviceTrust
* @param {string} userId The ID of the user whose devices is to be checked.
* @param {string} deviceId The ID of the device to check
*
* @returns {DeviceTrustLevel}
*/
/**
* Check the copy of our cross-signing key that we have in the device list and
* see if we can get the private key. If so, mark it as trusted.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#checkOwnCrossSigningTrust
*/
/**
* Checks that a given cross-signing private key matches a given public key.
* This can be used by the getCrossSigningKey callback to verify that the
* private key it is about to supply is the one that was requested.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#checkCrossSigningPrivateKey
* @param {Uint8Array} privateKey The private key
* @param {string} expectedPublicKey The public key
* @returns {boolean} true if the key matches, otherwise false
*/
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @function module:client~MatrixClient#prepareToEncrypt
* @param {module:models/room} room the room the event is in
*/
/**
* Checks whether cross signing:
* - is enabled on this account and trusted by this device
* - has private keys either cached locally or stored in secret storage
*
* If this function returns false, bootstrapCrossSigning() can be used
* to fix things such that it returns true. That is to say, after
* bootstrapCrossSigning() completes successfully, this function should
* return true.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#isCrossSigningReady
* @return {bool} True if cross-signing is ready to be used on this device
*/
/**
* Bootstrap cross-signing by creating keys if needed. If everything is already
* set up, then no changes are made, so this is safe to run to ensure
* cross-signing is ready for use.
*
* This function:
* - creates new cross-signing keys if they are not found locally cached nor in
* secret storage (if it has been setup)
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#bootstrapCrossSigning
* @param {function} opts.authUploadDeviceSigningKeys Function
* called to await an interactive auth flow when uploading device signing keys.
* @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys
* already exist.
* Args:
* {function} A function that makes the request requiring auth. Receives the
* auth data as an object. Can be called multiple times, first with an empty
* authDict, to obtain the flows.
*/
wrapCryptoFuncs(MatrixClient, [
"getCrossSigningId",
"getStoredCrossSigningForUser",
"checkUserTrust",
"checkDeviceTrust",
"checkOwnCrossSigningTrust",
"checkCrossSigningPrivateKey",
"legacyDeviceVerification",
"prepareToEncrypt",
"isCrossSigningReady",
"bootstrapCrossSigning",
"getCryptoTrustCrossSignedDevices",
"setCryptoTrustCrossSignedDevices",
"countSessionsNeedingBackup",
]);
/**
* Get information about the encryption of an event
*
* @function module:client~MatrixClient#getEventEncryptionInfo
*
* @param {module:models/event.MatrixEvent} event event to be checked
*
* @return {object} An object with the fields:
* - encrypted: whether the event is encrypted (if not encrypted, some of the
* other properties may not be set)
* - senderKey: the sender's key
* - algorithm: the algorithm used to encrypt the event
* - authenticated: whether we can be sure that the owner of the senderKey
* sent the event
* - sender: the sender's device information, if available
* - mismatchedSender: if the event's ed25519 and curve25519 keys don't match
* (only meaningful if `sender` is set)
*/
/**
* Create a recovery key from a user-supplied passphrase.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#createRecoveryKeyFromPassphrase
* @param {string} password Passphrase string that can be entered by the user
* when restoring the backup as an alternative to entering the recovery key.
* Optional.
* @returns {Promise