/*
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.
*/
"use strict";
const PushProcessor = require('./pushprocessor');
import {sleep} from './utils';
/**
* This is an internal module. See {@link MatrixClient} for the public class.
* @module client
*/
const EventEmitter = require("events").EventEmitter;
import Promise from 'bluebird';
const url = require('url');
const httpApi = require("./http-api");
const MatrixEvent = require("./models/event").MatrixEvent;
const EventStatus = require("./models/event").EventStatus;
const EventTimeline = require("./models/event-timeline");
const SearchResult = require("./models/search-result");
const StubStore = require("./store/stub");
const webRtcCall = require("./webrtc/call");
const utils = require("./utils");
const contentRepo = require("./content-repo");
const Filter = require("./filter");
const SyncApi = require("./sync");
const MatrixBaseApis = require("./base-apis");
const MatrixError = httpApi.MatrixError;
const ContentHelpers = require("./content-helpers");
const olmlib = require("./crypto/olmlib");
import ReEmitter from './ReEmitter';
import RoomList from './crypto/RoomList';
import logger from './logger';
import Crypto from './crypto';
import { isCryptoAvailable } from './crypto';
import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey';
import { keyFromPassphrase, keyFromAuthData } from './crypto/key_passphrase';
import { randomString } from './randomstring';
// Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings.
Promise.config({warnings: false});
const SCROLLBACK_DELAY_MS = 3000;
const CRYPTO_ENABLED = isCryptoAvailable();
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
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 {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/riot-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.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 {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 (required for cross-signing). Function to call when a cross-signing private key is needed.
* 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 (required for cross-signing). Called when new private keys
* for cross-signing need to be saved.
* 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 with an array of:
* [, ] or null if it cannot provide
* any of the keys.
* Args:
* {object} keys Information about the keys:
* {
* : {
* pubkey: {UInt8Array}
* }
* }
*
* @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} user_id (string) The user ID of the client requesting
* {string} device_id The device ID of the client requesting the secret.
* {string} request_id 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} device_trust: The trust status of the device requesting
* the secret as returned by {@link module:client~MatrixClient#checkDeviceTrust}.
*/
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.store = opts.store || new StubStore();
this.deviceId = opts.deviceId || null;
const userId = (opts.userId || null);
this.credentials = {
userId: userId,
};
this.scheduler = opts.scheduler;
if (this.scheduler) {
const self = this;
this.scheduler.setProcessFunction(function(eventToSend) {
const room = self.getRoom(eventToSend.getRoomId());
if (eventToSend.status !== EventStatus.SENDING) {
_updatePendingEventStatus(room, eventToSend,
EventStatus.SENDING);
}
return _sendEventHttpRequest(self, eventToSend);
});
}
this.clientRunning = false;
this.callList = {
// callId: MatrixCall
};
// 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 = webRtcCall.createNewMatrixCall(this);
this._supportsVoip = false;
if (call) {
setupCallEventHandler(this);
this._supportsVoip = true;
}
this._syncingRetry = null;
this._syncApi = null;
this._peekSync = null;
this._isGuest = false;
this._ongoingScrollbacks = {};
this.timelineSupport = Boolean(opts.timelineSupport);
this.urlPreviewCache = {};
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._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);
// Cache of the server's /versions response
// TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020
this._serverVersionsCache = null;
this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp }
// 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/riot-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/riot-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;
}
highlightCount += this.getPushActionsForEvent(
event,
).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);
/**
* 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;
};
/**
* 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();
};
/**
* 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 {module:client.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`);
}
// 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",
"deviceVerificationChanged",
"userVerificationChanged",
"crossSigning.keysChanged",
]);
logger.log("Crypto: initialising crypto object...");
await crypto.init();
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();
};
/**
* 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 {Promise} list of devices
*/
MatrixClient.prototype.getStoredDevicesForUser = async 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 {Promise} device or null
*/
MatrixClient.prototype.getStoredDevice = async 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
*
* @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
*
* @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
*
* @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
* @param {Array} methods array of verification methods to use. Defaults to
* all known methods
*
* @returns {Promise} resolves to a verifier
* when the request is accepted by the other user
*/
MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.requestVerificationDM(userId, roomId, methods);
};
/**
* Accept a key verification request from a DM.
*
* @param {module:models/event~MatrixEvent} event the verification request
* that is accepted
* @param {string} method the verification mmethod to use
*
* @returns {module:crypto/verification/Base} a verifier
*/
MatrixClient.prototype.acceptVerificationDM = function(event, method) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.acceptVerificationDM(event, method);
};
/**
* Request a key verification from another user.
*
* @param {string} userId the user to request verification with
* @param {Array} methods array of verification methods to use. Defaults to
* all known methods
* @param {Array} devices array of device IDs to send requests to. Defaults to
* all devices owned by the user
*
* @returns {Promise} resolves to a verifier
* when the request is accepted by the other user
*/
MatrixClient.prototype.requestVerification = function(userId, methods, devices) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.requestVerification(userId, methods, 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();
};
/**
* 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
};
}
}
/**
* Check whether we already have cross-signing keys for the current user.
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#doesCrossSigningHaveKeys
* @return {boolean} Whether we have keys.
*/
/**
* Generate new cross-signing keys.
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#resetCrossSigningKeys
* @param {object} authDict Auth data to supply for User-Interactive auth.
* @param {CrossSigningLevel} [level] the level of cross-signing to reset. New
* keys will be created for the given level and below. Defaults to
* regenerating all keys.
*/
/**
* 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}
*/
wrapCryptoFuncs(MatrixClient, [
"doesCrossSigningHaveKeys",
"resetCrossSigningKeys",
"getCrossSigningId",
"getStoredCrossSigningForUser",
"checkUserTrust",
"checkDeviceTrust",
"checkOwnCrossSigningTrust",
"checkPrivateKey",
]);
/**
* Check if the sender of an event is verified
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param {MatrixEvent} event event to be checked
*
* @returns {DeviceTrustLevel}
*/
MatrixClient.prototype.checkEventSenderTrust = async function(event) {
const device = await this.getEventSenderDeviceInfo(event);
if (!device) {
return 0;
}
return await this._crypto.checkDeviceTrust(event.getSender(), device.deviceId);
};
/**
* Add a key for encrypting secrets.
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#addSecretKey
* @param {string} algorithm the algorithm used by the key
* @param {object} opts the options for the algorithm. The properties used
* depend on the algorithm given. This object may be modified to pass
* information back about the key.
* @param {string} [keyName] the name of the key. If not given, a random
* name will be generated.
*
* @return {string} the name of the key
*/
/**
* Store an encrypted secret on the server
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#storeSecret
* @param {string} name The name of the secret
* @param {string} secret The secret contents.
* @param {Array} keys The IDs of the keys to use to encrypt the secret or null/undefined
* to use the default (will throw if no default key is set).
*/
/**
* Get a secret from storage.
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#getSecret
* @param {string} name the name of the secret
*
* @return {string} the contents of the secret
*/
/**
* Check if a secret is stored on the server.
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#isSecretStored
* @param {string} name the name of the secret
* @param {boolean} checkKey check if the secret is encrypted by a trusted
* key
*
* @return {boolean} whether or not the secret is stored
*/
/**
* Request a secret from another device.
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#requestSecret
* @param {string} name the name of the secret to request
* @param {string[]} devices the devices to request the secret from
*
* @return {string} the contents of the secret
*/
/**
* Get the current default key ID for encrypting secrets.
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#getDefaultSecretStorageKeyId
*
* @return {string} The default key ID or null if no default key ID is set
*/
/**
* Set the current default key ID for encrypting secrets.
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#setDefaultSecretStorageKeyId
* @param {string} keyId The new default key ID
*/
wrapCryptoFuncs(MatrixClient, [
"addSecretKey",
"storeSecret",
"getSecret",
"isSecretStored",
"requestSecret",
"getDefaultSecretStorageKeyId",
"setDefaultSecretStorageKeyId",
]);
/**
* Get e2e information on the device that sent an event
*
* @param {MatrixEvent} event event to be checked
*
* @return {Promise}
*/
MatrixClient.prototype.getEventSenderDeviceInfo = async function(event) {
if (!this._crypto) {
return null;
}
return this._crypto.getEventSenderDeviceInfo(event);
};
/**
* Check if the sender of an event is verified
*
* @param {MatrixEvent} event event to be checked
*
* @return {boolean} true if the sender of this event has been verified using
* {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}.
*/
MatrixClient.prototype.isEventSenderVerified = async function(event) {
const device = await this.getEventSenderDeviceInfo(event);
if (!device) {
return false;
}
return device.isVerified();
};
/**
* Cancel a room key request for this event if one is ongoing and resend the
* request.
* @param {MatrixEvent} event event of which to cancel and resend the room
* key request.
* @return {Promise} A promise that will resolve when the key request is queued
*/
MatrixClient.prototype.cancelAndResendEventRoomKeyRequest = function(event) {
return event.cancelAndResendKeyRequest(this._crypto, this.getUserId());
};
/**
* Enable end-to-end encryption for a room.
* @param {string} roomId The room ID to enable encryption in.
* @param {object} config The encryption config for the room.
* @return {Promise} A promise that will resolve when encryption is set up.
*/
MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
if (!this._crypto) {
throw new Error("End-to-End encryption disabled");
}
return this._crypto.setRoomEncryption(roomId, config);
};
/**
* Whether encryption is enabled for a room.
* @param {string} roomId the room id to query.
* @return {bool} whether encryption is enabled.
*/
MatrixClient.prototype.isRoomEncrypted = function(roomId) {
const room = this.getRoom(roomId);
if (!room) {
// we don't know about this room, so can't determine if it should be
// encrypted. Let's assume not.
return false;
}
// if there is an 'm.room.encryption' event in this room, it should be
// encrypted (independently of whether we actually support encryption)
const ev = room.currentState.getStateEvents("m.room.encryption", "");
if (ev) {
return true;
}
// we don't have an m.room.encrypted event, but that might be because
// the server is hiding it from us. Check the store to see if it was
// previously encrypted.
return this._roomList.isRoomEncrypted(roomId);
};
/**
* Forces the current outbound group session to be discarded such
* that another one will be created next time an event is sent.
*
* @param {string} roomId The ID of the room to discard the session for
*
* This should not normally be necessary.
*/
MatrixClient.prototype.forceDiscardSession = function(roomId) {
if (!this._crypto) {
throw new Error("End-to-End encryption disabled");
}
this._crypto.forceDiscardSession(roomId);
};
/**
* Get a list containing all of the room keys
*
* This should be encrypted before returning it to the user.
*
* @return {module:client.Promise} a promise which resolves to a list of
* session export objects
*/
MatrixClient.prototype.exportRoomKeys = function() {
if (!this._crypto) {
return Promise.reject(new Error("End-to-end encryption disabled"));
}
return this._crypto.exportRoomKeys();
};
/**
* Import a list of room keys previously exported by exportRoomKeys
*
* @param {Object[]} keys a list of session export objects
*
* @return {module:client.Promise} a promise which resolves when the keys
* have been imported
*/
MatrixClient.prototype.importRoomKeys = function(keys) {
if (!this._crypto) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.importRoomKeys(keys);
};
/**
* Force a re-check of the local key backup status against
* what's on the server.
*
* @returns {Object} Object with backup info (as returned by
* getKeyBackupVersion) in backupInfo and
* trust information (as returned by isKeyBackupTrusted)
* in trustInfo.
*/
MatrixClient.prototype.checkKeyBackup = function() {
return this._crypto.checkKeyBackup();
};
/**
* Get information about the current key backup.
* @returns {Promise} Information object from API or null
*/
MatrixClient.prototype.getKeyBackupVersion = function() {
return this._http.authedRequest(
undefined, "GET", "/room_keys/version", undefined, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
).then((res) => {
if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) {
const err = "Unknown backup algorithm: " + res.algorithm;
return Promise.reject(err);
} else if (!(typeof res.auth_data === "object")
|| !res.auth_data.public_key) {
const err = "Invalid backup data returned";
return Promise.reject(err);
} else {
return res;
}
}).catch((e) => {
if (e.errcode === 'M_NOT_FOUND') {
return null;
} else {
throw e;
}
});
};
/**
* @param {object} info key backup info dict from getKeyBackupVersion()
* @return {object} {
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
* sigs: [
* valid: [bool],
* device: [DeviceInfo],
* ]
* }
*/
MatrixClient.prototype.isKeyBackupTrusted = function(info) {
return this._crypto.isKeyBackupTrusted(info);
};
/**
* @returns {bool} true if the client is configured to back up keys to
* the server, otherwise false.
*/
MatrixClient.prototype.getKeyBackupEnabled = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return Boolean(this._crypto.backupKey);
};
/**
* Enable backing up of keys, using data previously returned from
* getKeyBackupVersion.
*
* @param {object} info Backup information object as returned by getKeyBackupVersion
*/
MatrixClient.prototype.enableKeyBackup = function(info) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
this._crypto.backupInfo = info;
if (this._crypto.backupKey) this._crypto.backupKey.free();
this._crypto.backupKey = new global.Olm.PkEncryption();
this._crypto.backupKey.set_recipient_key(info.auth_data.public_key);
this.emit('crypto.keyBackupStatus', true);
// There may be keys left over from a partially completed backup, so
// schedule a send to check.
this._crypto.scheduleKeyBackupSend();
};
/**
* Disable backing up of keys.
*/
MatrixClient.prototype.disableKeyBackup = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
this._crypto.backupInfo = null;
if (this._crypto.backupKey) this._crypto.backupKey.free();
this._crypto.backupKey = null;
this.emit('crypto.keyBackupStatus', false);
};
/**
* Set up the data required to create a new backup version. The backup version
* will not be created and enabled until createKeyBackupVersion is called.
*
* @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