1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

Merge branch 'develop' into feed

This commit is contained in:
Šimon Brandner
2021-04-03 08:35:05 +02:00
17 changed files with 508 additions and 59 deletions

View File

@@ -1,3 +1,26 @@
Changes in [9.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.10.0) (2021-03-29)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.10.0-rc.1...v9.10.0)
* No changes since rc.1
Changes in [9.10.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.10.0-rc.1) (2021-03-25)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0...v9.10.0-rc.1)
* Don't send m.call.hangup if m.call.invite wasn't sent either
[\#1647](https://github.com/matrix-org/matrix-js-sdk/pull/1647)
* docs: registerGuest()
[\#1641](https://github.com/matrix-org/matrix-js-sdk/pull/1641)
* Download device keys in chunks of 250
[\#1639](https://github.com/matrix-org/matrix-js-sdk/pull/1639)
* More VoIP connectivity fixes
[\#1646](https://github.com/matrix-org/matrix-js-sdk/pull/1646)
* Make selectDesktopCapturerSource param optional
[\#1644](https://github.com/matrix-org/matrix-js-sdk/pull/1644)
* Expose APIs needed for reworked cross-signing login flow
[\#1632](https://github.com/matrix-org/matrix-js-sdk/pull/1632)
Changes in [9.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.9.0) (2021-03-15)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0-rc.1...v9.9.0)

View File

@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "9.9.0",
"version": "9.10.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",

View File

@@ -51,6 +51,36 @@ const signedDeviceList = {
},
};
const signedDeviceList2 = {
"failures": {},
"device_keys": {
"@test2:sw1v.org": {
"QJVRHWAKGH": {
"signatures": {
"@test2:sw1v.org": {
"ed25519:QJVRHWAKGH":
"w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" +
"1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3",
},
},
"user_id": "@test2:sw1v.org",
"keys": {
"ed25519:QJVRHWAKGH":
"Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
"curve25519:QJVRHWAKGH":
"YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
},
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2",
],
"device_id": "QJVRHWAKGH",
"unsigned": {},
},
},
},
};
describe('DeviceList', function() {
let downloadSpy;
let cryptoStore;
@@ -69,7 +99,7 @@ describe('DeviceList', function() {
}
});
function createTestDeviceList() {
function createTestDeviceList(keyDownloadChunkSize = 250) {
const baseApis = {
downloadKeysForUsers: downloadSpy,
getUserId: () => '@test1:sw1v.org',
@@ -78,7 +108,7 @@ describe('DeviceList', function() {
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
deviceLists.push(dl);
return dl;
}
@@ -150,4 +180,30 @@ describe('DeviceList', function() {
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});
it("should download device keys in batches", function() {
const dl = createTestDeviceList(1);
dl.startTrackingDeviceList('@test1:sw1v.org');
dl.startTrackingDeviceList('@test2:sw1v.org');
const queryDefer1 = utils.defer();
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
const queryDefer2 = utils.defer();
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toBeCalledTimes(2);
expect(downloadSpy).toHaveBeenNthCalledWith(1, ['@test1:sw1v.org'], {});
expect(downloadSpy).toHaveBeenNthCalledWith(2, ['@test2:sw1v.org'], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
queryDefer2.resolve(utils.deepCopy(signedDeviceList2));
return prom1.then(() => {
const storedKeys1 = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
});
});
});

View File

@@ -193,7 +193,9 @@ describe("Cross Signing", function() {
const keyChangePromise = new Promise((resolve, reject) => {
alice.once("crossSigning.keysChanged", async (e) => {
resolve(e);
await alice.checkOwnCrossSigningTrust();
await alice.checkOwnCrossSigningTrust({
allowPrivateKeyRequests: true,
});
});
});

View File

@@ -282,4 +282,30 @@ describe("utils", function() {
expect(target.nonenumerableProp).toBe(undefined);
});
});
describe("chunkPromises", function() {
it("should execute promises in chunks", async function() {
let promiseCount = 0;
function fn1() {
return new Promise(async function(resolve, reject) {
await utils.sleep(1);
expect(promiseCount).toEqual(0);
++promiseCount;
resolve();
});
}
function fn2() {
return new Promise(function(resolve, reject) {
expect(promiseCount).toEqual(1);
++promiseCount;
resolve();
});
}
await utils.chunkPromises([fn1, fn2], 1);
expect(promiseCount).toEqual(2);
});
});
});

View File

@@ -246,10 +246,25 @@ MatrixBaseApis.prototype.register = function(
/**
* Register a guest account.
* This method returns the auth info needed to create a new authenticated client,
* Remember to call `setGuest(true)` on the (guest-)authenticated client, e.g:
* ```javascript
* const tmpClient = await sdk.createClient(MATRIX_INSTANCE);
* const { user_id, device_id, access_token } = tmpClient.registerGuest();
* const client = createClient({
* baseUrl: MATRIX_INSTANCE,
* accessToken: access_token,
* userId: user_id,
* deviceId: device_id,
* })
* client.setGuest(true);
* ```
*
* @param {Object=} opts Registration options
* @param {Object} opts.body JSON HTTP body to provide.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {Promise} Resolves: JSON object that contains:
* { user_id, device_id, access_token, home_server }
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.registerGuest = function(opts, callback) {

View File

@@ -2303,6 +2303,39 @@ MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, versio
);
};
/**
* Share shared-history decryption keys with the given users.
*
* @param {string} roomId the room for which keys should be shared.
* @param {array} userIds a list of users to share with. The keys will be sent to
* all of the user's current devices.
*/
MatrixClient.prototype.sendSharedHistoryKeys = async function(roomId, userIds) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
const roomEncryption = this._roomList.getRoomEncryption(roomId);
if (!roomEncryption) {
// unknown room, or unencrypted room
logger.error("Unknown room. Not sharing decryption keys");
return;
}
const deviceInfos = await this._crypto.downloadKeys(userIds);
const devicesByUser = {};
for (const [userId, devices] of Object.entries(deviceInfos)) {
devicesByUser[userId] = Object.values(devices);
}
const alg = this._crypto._getRoomDecryptor(roomId, roomEncryption.algorithm);
if (alg.sendSharedHistoryInboundSessions) {
await alg.sendSharedHistoryInboundSessions(devicesByUser);
} else {
logger.warning("Algorithm does not support sharing previous keys", roomEncryption.algorithm);
}
};
// Group ops
// =========
// Operations on groups that come down the sync stream (ie. ones the

View File

@@ -28,7 +28,7 @@ import {DeviceInfo} from './deviceinfo';
import {CrossSigningInfo} from './CrossSigning';
import * as olmlib from './olmlib';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
import {defer, sleep} from '../utils';
import {chunkPromises, defer, sleep} from '../utils';
/* State transition diagram for DeviceList._deviceTrackingStatus
@@ -62,7 +62,7 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
* @alias module:crypto/DeviceList
*/
export class DeviceList extends EventEmitter {
constructor(baseApis, cryptoStore, olmDevice) {
constructor(baseApis, cryptoStore, olmDevice, keyDownloadChunkSize = 250) {
super();
this._cryptoStore = cryptoStore;
@@ -98,6 +98,9 @@ export class DeviceList extends EventEmitter {
// userId -> promise
this._keyDownloadsInProgressByUser = {};
// Maximum number of user IDs per request to prevent server overload (#1619)
this._keyDownloadChunkSize = keyDownloadChunkSize;
// Set whenever changes are made other than setting the sync token
this._dirty = false;
@@ -780,13 +783,17 @@ class DeviceListUpdateSerialiser {
opts.token = this._syncToken;
}
this._baseApis.downloadKeysForUsers(
downloadUsers, opts,
).then(async (res) => {
const dk = res.device_keys || {};
const masterKeys = res.master_keys || {};
const ssks = res.self_signing_keys || {};
const usks = res.user_signing_keys || {};
const factories = [];
for (let i = 0; i < downloadUsers.length; i += this._deviceList._keyDownloadChunkSize) {
const userSlice = downloadUsers.slice(i, i + this._deviceList._keyDownloadChunkSize);
factories.push(() => this._baseApis.downloadKeysForUsers(userSlice, opts));
}
chunkPromises(factories, 3).then(async (responses) => {
const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {})));
const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {})));
const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
const usks = Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
// yield to other things that want to execute in between users, to
// avoid wedging the CPU

View File

@@ -1048,6 +1048,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
'readwrite', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
], (txn) => {
/* if we already have this session, consider updating it */
this._getInboundGroupSession(
@@ -1104,6 +1105,12 @@ OlmDevice.prototype.addInboundGroupSession = async function(
this._cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn,
);
if (!existingSession && extraSessionData.sharedHistory) {
this._cryptoStore.addSharedHistoryInboundGroupSession(
roomId, senderKey, sessionId, txn,
);
}
} finally {
session.free();
}
@@ -1383,6 +1390,7 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
"forwarding_curve25519_key_chain":
sessionData.forwardingCurve25519KeyChain || [],
"sender_claimed_ed25519_key": senderEd25519Key,
"shared_history": sessionData.sharedHistory || false,
};
},
);
@@ -1415,10 +1423,24 @@ OlmDevice.prototype.exportInboundGroupSession = function(
"session_key": session.export_session(messageIndex),
"forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
"first_known_index": session.first_known_index(),
"org.matrix.msc3061.shared_history": sessionData.sharedHistory || false,
};
});
};
OlmDevice.prototype.getSharedHistoryInboundGroupSessions = async function(roomId) {
let result;
await this._cryptoStore.doTxn(
'readonly', [
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
], (txn) => {
result = this._cryptoStore.getSharedHistoryInboundGroupSessions(roomId, txn);
},
logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"),
);
return result;
};
// Utilities
// =========

View File

@@ -36,11 +36,27 @@ import {
import {WITHHELD_MESSAGES} from '../OlmDevice';
// determine whether the key can be shared with invitees
function isRoomSharedHistory(room) {
const visibilityEvent = room.currentState &&
room.currentState.getStateEvents("m.room.history_visibility", "");
// NOTE: if the room visibility is unset, it would normally default to
// "world_readable".
// (https://spec.matrix.org/unstable/client-server-api/#server-behaviour-5)
// But we will be paranoid here, and treat it as a situation where the room
// is not shared-history
const visibility = visibilityEvent && visibilityEvent.getContent() &&
visibilityEvent.getContent().history_visibility;
return ["world_readable", "shared"].includes(visibility);
}
/**
* @private
* @constructor
*
* @param {string} sessionId
* @param {boolean} sharedHistory whether the session can be freely shared with
* other group members, according to the room history visibility settings
*
* @property {string} sessionId
* @property {Number} useCount number of times this session has been used
@@ -50,12 +66,13 @@ import {WITHHELD_MESSAGES} from '../OlmDevice';
* devices with which we have shared the session key
* userId -> {deviceId -> msgindex}
*/
function OutboundSessionInfo(sessionId) {
function OutboundSessionInfo(sessionId, sharedHistory = false) {
this.sessionId = sessionId;
this.useCount = 0;
this.creationTime = new Date().getTime();
this.sharedWithDevices = {};
this.blockedDevicesNotified = {};
this.sharedHistory = sharedHistory;
}
@@ -183,6 +200,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
/**
* @private
*
* @param {module:models/room} room
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
* @param {Object} blocked The devices that are blocked, indexed by user ID
* @param {boolean} [singleOlmCreationPhase] Only perform one round of olm
@@ -192,7 +210,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
* OutboundSessionInfo when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = async function(
devicesInRoom, blocked, singleOlmCreationPhase,
room, devicesInRoom, blocked, singleOlmCreationPhase,
) {
let session;
@@ -204,6 +222,13 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
const prepareSession = async (oldSession) => {
session = oldSession;
const sharedHistory = isRoomSharedHistory(room);
// history visibility changed
if (session && sharedHistory !== session.sharedHistory) {
session = null;
}
// need to make a brand new session?
if (session && session.needsRotation(this._sessionRotationPeriodMsgs,
this._sessionRotationPeriodMs)
@@ -219,7 +244,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
if (!session) {
logger.log(`Starting new megolm session for room ${this._roomId}`);
session = await this._prepareNewSession();
session = await this._prepareNewSession(sharedHistory);
logger.log(`Started new megolm session ${session.sessionId} ` +
`for room ${this._roomId}`);
this._outboundSessions[session.sessionId] = session;
@@ -250,11 +275,12 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
const payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
"algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": this._roomId,
"session_id": session.sessionId,
"session_key": key.key,
"chain_index": key.chain_index,
"org.matrix.msc3061.shared_history": sharedHistory,
},
};
const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
@@ -374,15 +400,18 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
/**
* @private
*
* @param {boolean} sharedHistory
*
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*/
MegolmEncryption.prototype._prepareNewSession = async function() {
MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) {
const sessionId = this._olmDevice.createOutboundGroupSession();
const key = this._olmDevice.getOutboundGroupSessionKey(sessionId);
await this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId,
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
key.key, {ed25519: this._olmDevice.deviceEd25519Key}, false,
{sharedHistory: sharedHistory},
);
// don't wait for it to complete
@@ -391,7 +420,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
sessionId, key.key,
);
return new OutboundSessionInfo(sessionId);
return new OutboundSessionInfo(sessionId, sharedHistory);
};
/**
@@ -672,14 +701,15 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
const payload = {
type: "m.forwarded_room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: sessionId,
session_key: key.key,
chain_index: key.chain_index,
sender_key: senderKey,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
"algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": this._roomId,
"session_id": sessionId,
"session_key": key.key,
"chain_index": key.chain_index,
"sender_key": senderKey,
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
"forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": key.shared_history || false,
},
};
@@ -901,7 +931,7 @@ MegolmEncryption.prototype.prepareToEncrypt = function(room) {
}
logger.debug(`Ensuring outbound session in ${this._roomId}`);
await this._ensureOutboundSession(devicesInRoom, blocked, true);
await this._ensureOutboundSession(room, devicesInRoom, blocked, true);
logger.debug(`Ready to encrypt events for ${this._roomId}`);
} catch (e) {
@@ -945,7 +975,7 @@ MegolmEncryption.prototype.encryptMessage = async function(room, eventType, cont
this._checkForUnknownDevices(devicesInRoom);
}
const session = await this._ensureOutboundSession(devicesInRoom, blocked);
const session = await this._ensureOutboundSession(room, devicesInRoom, blocked);
const payloadJson = {
room_id: this._roomId,
type: eventType,
@@ -1370,10 +1400,14 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
keysClaimed = event.getKeysClaimed();
}
const extraSessionData = {};
if (content["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = true;
}
return this._olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, sessionId,
content.session_key, keysClaimed,
exportFormat,
exportFormat, extraSessionData,
).then(() => {
// have another go at decrypting events sent with this session.
this._retryDecryption(senderKey, sessionId)
@@ -1573,14 +1607,15 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
return {
type: "m.forwarded_room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: senderKey,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
session_id: sessionId,
session_key: key.key,
chain_index: key.chain_index,
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
"algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": roomId,
"sender_key": senderKey,
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
"session_id": sessionId,
"session_key": key.key,
"chain_index": key.chain_index,
"forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": key.shared_history || false,
},
};
};
@@ -1594,6 +1629,13 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
* @param {string} [opts.source] where the key came from
*/
MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
const extraSessionData = {};
if (opts.untrusted) {
extraSessionData.untrusted = true;
}
if (session["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = true;
}
return this._olmDevice.addInboundGroupSession(
session.room_id,
session.sender_key,
@@ -1602,7 +1644,7 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
session.session_key,
session.sender_claimed_keys,
true,
opts.untrusted ? { untrusted: opts.untrusted } : {},
extraSessionData,
).then(() => {
if (opts.source !== "backup") {
// don't wait for it to complete
@@ -1681,6 +1723,80 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey)
return !this._pendingEvents[senderKey];
};
MegolmDecryption.prototype.sendSharedHistoryInboundSessions = async function(devicesByUser) {
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser,
);
logger.log("sendSharedHistoryInboundSessions to users", Object.keys(devicesByUser));
const sharedHistorySessions =
await this._olmDevice.getSharedHistoryInboundGroupSessions(
this._roomId,
);
logger.log("shared-history sessions", sharedHistorySessions);
for (const [senderKey, sessionId] of sharedHistorySessions) {
const payload = await this._buildKeyForwardingMessage(
this._roomId, senderKey, sessionId,
);
const promises = [];
const contentMap = {};
for (const [userId, devices] of Object.entries(devicesByUser)) {
contentMap[userId] = {};
for (const deviceInfo of devices) {
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
contentMap[userId][deviceInfo.deviceId] = encryptedContent;
promises.push(
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._userId,
this._deviceId,
this._olmDevice,
userId,
deviceInfo,
payload,
),
);
}
}
await Promise.all(promises);
// prune out any devices that encryptMessageForDevice could not encrypt for,
// in which case it will have just not added anything to the ciphertext object.
// There's no point sending messages to devices if we couldn't encrypt to them,
// since that's effectively a blank message.
for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) {
if (Object.keys(contentMap[userId][deviceId].ciphertext).length === 0) {
logger.log(
"No ciphertext for device " +
userId + ":" + deviceId + ": pruning",
);
delete contentMap[userId][deviceId];
}
}
// No devices left for that user? Strip that too.
if (Object.keys(contentMap[userId]).length === 0) {
logger.log("Pruned all devices for user " + userId);
delete contentMap[userId];
}
}
// Is there anything left?
if (Object.keys(contentMap).length === 0) {
logger.log("No users left to send to: aborting");
return;
}
await this._baseApis.sendToDevice("m.room.encrypted", contentMap);
}
};
registerAlgorithm(
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption,
);

View File

@@ -568,7 +568,9 @@ Crypto.prototype.bootstrapCrossSigning = async function({
"Cross-signing private keys not found locally, but they are available " +
"in secret storage, reading storage and caching locally",
);
await this.checkOwnCrossSigningTrust();
await this.checkOwnCrossSigningTrust({
allowPrivateKeyRequests: true,
});
}
// Assuming no app-supplied callback, default to storing new private keys in
@@ -1300,13 +1302,19 @@ Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) {
* 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.
*/
Crypto.prototype.checkOwnCrossSigningTrust = async function() {
Crypto.prototype.checkOwnCrossSigningTrust = async function({
allowPrivateKeyRequests = false,
} = {}) {
const userId = this._userId;
// Before proceeding, ensure our cross-signing public keys have been
// downloaded via the device list.
await this.downloadKeys([this._userId]);
// Also check which private keys are locally cached.
const crossSigningPrivateKeys =
await this._crossSigningInfo.getCrossSigningKeysFromCache();
// If we see an update to our own master key, check it against the master
// key we have and, if it matches, mark it as verified
@@ -1324,16 +1332,22 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
const masterChanged = this._crossSigningInfo.getId() !== seenPubkey;
if (masterChanged) {
logger.info("Got new master public key", seenPubkey);
}
if (
allowPrivateKeyRequests &&
(masterChanged || !crossSigningPrivateKeys.has("master"))
) {
logger.info("Attempting to retrieve cross-signing master private key");
let signing = null;
// It's important for control flow that we leave any errors alone for
// higher levels to handle so that e.g. cancelling access properly
// aborts any larger operation as well.
try {
const ret = await this._crossSigningInfo.getCrossSigningKey(
'master', seenPubkey,
);
signing = ret[1];
logger.info("Got cross-signing master private key");
} catch (e) {
logger.error("Cross-signing master private key not available", e);
} finally {
if (signing) signing.free();
}
@@ -1352,6 +1366,11 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
if (selfSigningChanged) {
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
}
if (
allowPrivateKeyRequests &&
(selfSigningChanged || !crossSigningPrivateKeys.has("self_signing"))
) {
logger.info("Attempting to retrieve cross-signing self-signing private key");
let signing = null;
try {
@@ -1360,8 +1379,6 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
);
signing = ret[1];
logger.info("Got cross-signing self-signing private key");
} catch (e) {
logger.error("Cross-signing self-signing private key not available", e);
} finally {
if (signing) signing.free();
}
@@ -1374,6 +1391,11 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
}
if (userSigningChanged) {
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
}
if (
allowPrivateKeyRequests &&
(userSigningChanged || !crossSigningPrivateKeys.has("user_signing"))
) {
logger.info("Attempting to retrieve cross-signing user-signing private key");
let signing = null;
try {
@@ -1382,8 +1404,6 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
);
signing = ret[1];
logger.info("Got cross-signing user-signing private key");
} catch (e) {
logger.error("Cross-signing user-signing private key not available", e);
} finally {
if (signing) signing.free();
}

View File

@@ -19,7 +19,7 @@ limitations under the License.
import {logger} from '../../logger';
import * as utils from "../../utils";
export const VERSION = 9;
export const VERSION = 10;
const PROFILE_TRANSACTIONS = false;
/**
@@ -759,6 +759,38 @@ export class Backend {
}));
}
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
if (!txn) {
txn = this._db.transaction(
"shared_history_inbound_group_sessions", "readwrite",
);
}
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
const req = objectStore.get([roomId]);
req.onsuccess = () => {
const {sessions} = req.result || {sessions: []};
sessions.push([senderKey, sessionId]);
objectStore.put({roomId, sessions});
};
}
getSharedHistoryInboundGroupSessions(roomId, txn) {
if (!txn) {
txn = this._db.transaction(
"shared_history_inbound_group_sessions", "readonly",
);
}
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
const req = objectStore.get([roomId]);
return new Promise((resolve, reject) => {
req.onsuccess = () => {
const {sessions} = req.result || {sessions: []};
resolve(sessions);
};
req.onerror = reject;
});
}
doTxn(mode, stores, func, log = logger) {
let startTime;
let description;
@@ -834,6 +866,11 @@ export function upgradeDatabase(db, oldVersion) {
keyPath: ["userId", "deviceId"],
});
}
if (oldVersion < 10) {
db.createObjectStore("shared_history_inbound_group_sessions", {
keyPath: ["roomId"],
});
}
// Expand as needed.
}

View File

@@ -582,6 +582,29 @@ export class IndexedDBCryptoStore {
return this._backend.markSessionsNeedingBackup(sessions, txn);
}
/**
* Add a shared-history group session for a room.
* @param {string} roomId The room that the key belongs to
* @param {string} senderKey The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {*} txn An active transaction. See doTxn(). (optional)
*/
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
this._backend.addSharedHistoryInboundGroupSession(
roomId, senderKey, sessionId, txn,
);
}
/**
* Get the shared-history group session for a room.
* @param {string} roomId The room that the key belongs to
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} Resolves to an array of [senderKey, sessionId]
*/
getSharedHistoryInboundGroupSessions(roomId, txn) {
return this._backend.getSharedHistoryInboundGroupSessions(roomId, txn);
}
/**
* Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may
@@ -614,6 +637,8 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
= 'inbound_group_sessions_withheld';
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS
= 'shared_history_inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';

View File

@@ -51,6 +51,8 @@ export class MemoryCryptoStore {
this._rooms = {};
// Set of {senderCurve25519Key+'/'+sessionId}
this._sessionsNeedingBackup = {};
// roomId -> array of [senderKey, sessionId]
this._sharedHistoryInboundGroupSessions = {};
}
/**
@@ -467,6 +469,16 @@ export class MemoryCryptoStore {
return Promise.resolve();
}
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId) {
const sessions = this._sharedHistoryInboundGroupSessions[roomId] || [];
sessions.push([senderKey, sessionId]);
this._sharedHistoryInboundGroupSessions[roomId] = sessions;
}
getSharedHistoryInboundGroupSessions(roomId) {
return Promise.resolve(this._sharedHistoryInboundGroupSessions[roomId] || []);
}
// Session key backups
doTxn(mode, stores, func) {

View File

@@ -745,6 +745,15 @@ export function promiseTry<T>(fn: () => T): Promise<T> {
return new Promise((resolve) => resolve(fn()));
}
// Creates and awaits all promises, running no more than `chunkSize` at the same time
export async function chunkPromises<T>(fns: (() => Promise<T>)[], chunkSize: number): Promise<T[]> {
const results: T[] = [];
for (let i = 0; i < fns.length; i += chunkSize) {
results.push(...(await Promise.all(fns.slice(i, i + chunkSize).map(fn => fn()))));
}
return results;
}
// We need to be able to access the Node.js crypto library from within the
// Matrix SDK without needing to `require("crypto")`, which will fail in
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need

View File

@@ -340,8 +340,8 @@ export class MatrixCall extends EventEmitter {
logger.debug("placeVoiceCall");
this.checkForErrorListener();
const constraints = getUserMediaContraints(ConstraintsType.Audio);
await this.placeCallWithConstraints(constraints);
this.type = CallType.Voice;
await this.placeCallWithConstraints(constraints);
}
/**
@@ -352,8 +352,8 @@ export class MatrixCall extends EventEmitter {
logger.debug("placeVideoCall");
this.checkForErrorListener();
const constraints = getUserMediaContraints(ConstraintsType.Video);
await this.placeCallWithConstraints(constraints);
this.type = CallType.Video;
await this.placeCallWithConstraints(constraints);
}
/**
@@ -595,6 +595,8 @@ export class MatrixCall extends EventEmitter {
logger.debug("Ending call " + this.callId);
this.terminate(CallParty.Local, reason, !suppressEvent);
// We don't want to send hangup here if we didn't even get to sending an invite
if (this.state === CallState.WaitLocalMedia) return;
const content = {};
// Continue to send no reason for user hangups temporarily, until
// clients understand the user_hangup reason (voip v1)
@@ -1373,7 +1375,10 @@ export class MatrixCall extends EventEmitter {
}
}
async transfer(targetUserId: string, targetRoomId?: string) {
/*
* Transfers this call to another user
*/
async transfer(targetUserId: string) {
// Fetch the target user's global profile info: their room avatar / displayname
// could be different in whatever room we shae with them.
const profileInfo = await this.client.getProfileInfo(targetUserId);
@@ -1390,9 +1395,49 @@ export class MatrixCall extends EventEmitter {
create_call: replacementId,
} as MCallReplacesEvent;
if (targetRoomId) body.target_room = targetRoomId;
await this.sendVoipEvent(EventType.CallReplaces, body);
return this.sendVoipEvent(EventType.CallReplaces, body);
await this.terminate(CallParty.Local, CallErrorCode.Replaced, true);
}
/*
* Transfers this call to the target call, effectively 'joining' the
* two calls (so the remote parties on each call are connected together).
*/
async transferToCall(transferTargetCall?: MatrixCall) {
const targetProfileInfo = await this.client.getProfileInfo(transferTargetCall.getOpponentMember().userId);
const transfereeProfileInfo = await this.client.getProfileInfo(this.getOpponentMember().userId);
const newCallId = genCallID();
const bodyToTransferTarget = {
// the replacements on each side have their own ID, and it's distinct from the
// ID of the new call (but we can use the same function to generate it)
replacement_id: genCallID(),
target_user: {
id: this.getOpponentMember().userId,
display_name: transfereeProfileInfo.display_name,
avatar_url: transfereeProfileInfo.avatar_url,
},
await_call: newCallId,
} as MCallReplacesEvent;
await transferTargetCall.sendVoipEvent(EventType.CallReplaces, bodyToTransferTarget);
const bodyToTransferee = {
replacement_id: genCallID(),
target_user: {
id: transferTargetCall.getOpponentMember().userId,
display_name: targetProfileInfo.display_name,
avatar_url: targetProfileInfo.avatar_url,
},
create_call: newCallId,
} as MCallReplacesEvent;
await this.sendVoipEvent(EventType.CallReplaces, bodyToTransferee);
await this.terminate(CallParty.Local, CallErrorCode.Replaced, true);
await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Replaced, true);
}
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean) {

View File

@@ -32,6 +32,7 @@ export interface MCallReplacesEvent {
replacement_id: string;
target_user: MCallReplacesTarget;
create_call: string;
await_call: string;
target_room: string;
}
/* eslint-enable camelcase */