You've already forked matrix-js-sdk
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:
23
CHANGELOG.md
23
CHANGELOG.md
@@ -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)
|
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)
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0-rc.1...v9.9.0)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-js-sdk",
|
"name": "matrix-js-sdk",
|
||||||
"version": "9.9.0",
|
"version": "9.10.0",
|
||||||
"description": "Matrix Client-Server SDK for Javascript",
|
"description": "Matrix Client-Server SDK for Javascript",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublishOnly": "yarn build",
|
"prepublishOnly": "yarn build",
|
||||||
|
|||||||
@@ -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() {
|
describe('DeviceList', function() {
|
||||||
let downloadSpy;
|
let downloadSpy;
|
||||||
let cryptoStore;
|
let cryptoStore;
|
||||||
@@ -69,7 +99,7 @@ describe('DeviceList', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function createTestDeviceList() {
|
function createTestDeviceList(keyDownloadChunkSize = 250) {
|
||||||
const baseApis = {
|
const baseApis = {
|
||||||
downloadKeysForUsers: downloadSpy,
|
downloadKeysForUsers: downloadSpy,
|
||||||
getUserId: () => '@test1:sw1v.org',
|
getUserId: () => '@test1:sw1v.org',
|
||||||
@@ -78,7 +108,7 @@ describe('DeviceList', function() {
|
|||||||
const mockOlm = {
|
const mockOlm = {
|
||||||
verifySignature: function(key, message, signature) {},
|
verifySignature: function(key, message, signature) {},
|
||||||
};
|
};
|
||||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
|
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
|
||||||
deviceLists.push(dl);
|
deviceLists.push(dl);
|
||||||
return dl;
|
return dl;
|
||||||
}
|
}
|
||||||
@@ -150,4 +180,30 @@ describe('DeviceList', function() {
|
|||||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -193,7 +193,9 @@ describe("Cross Signing", function() {
|
|||||||
const keyChangePromise = new Promise((resolve, reject) => {
|
const keyChangePromise = new Promise((resolve, reject) => {
|
||||||
alice.once("crossSigning.keysChanged", async (e) => {
|
alice.once("crossSigning.keysChanged", async (e) => {
|
||||||
resolve(e);
|
resolve(e);
|
||||||
await alice.checkOwnCrossSigningTrust();
|
await alice.checkOwnCrossSigningTrust({
|
||||||
|
allowPrivateKeyRequests: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -282,4 +282,30 @@ describe("utils", function() {
|
|||||||
expect(target.nonenumerableProp).toBe(undefined);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -246,10 +246,25 @@ MatrixBaseApis.prototype.register = function(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a guest account.
|
* 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 Registration options
|
||||||
* @param {Object} opts.body JSON HTTP body to provide.
|
* @param {Object} opts.body JSON HTTP body to provide.
|
||||||
* @param {module:client.callback} callback Optional.
|
* @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.
|
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||||
*/
|
*/
|
||||||
MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
|
MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
|
||||||
|
|||||||
@@ -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
|
// Group ops
|
||||||
// =========
|
// =========
|
||||||
// Operations on groups that come down the sync stream (ie. ones the
|
// Operations on groups that come down the sync stream (ie. ones the
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {DeviceInfo} from './deviceinfo';
|
|||||||
import {CrossSigningInfo} from './CrossSigning';
|
import {CrossSigningInfo} from './CrossSigning';
|
||||||
import * as olmlib from './olmlib';
|
import * as olmlib from './olmlib';
|
||||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||||
import {defer, sleep} from '../utils';
|
import {chunkPromises, defer, sleep} from '../utils';
|
||||||
|
|
||||||
|
|
||||||
/* State transition diagram for DeviceList._deviceTrackingStatus
|
/* State transition diagram for DeviceList._deviceTrackingStatus
|
||||||
@@ -62,7 +62,7 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
|
|||||||
* @alias module:crypto/DeviceList
|
* @alias module:crypto/DeviceList
|
||||||
*/
|
*/
|
||||||
export class DeviceList extends EventEmitter {
|
export class DeviceList extends EventEmitter {
|
||||||
constructor(baseApis, cryptoStore, olmDevice) {
|
constructor(baseApis, cryptoStore, olmDevice, keyDownloadChunkSize = 250) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._cryptoStore = cryptoStore;
|
this._cryptoStore = cryptoStore;
|
||||||
@@ -98,6 +98,9 @@ export class DeviceList extends EventEmitter {
|
|||||||
// userId -> promise
|
// userId -> promise
|
||||||
this._keyDownloadsInProgressByUser = {};
|
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
|
// Set whenever changes are made other than setting the sync token
|
||||||
this._dirty = false;
|
this._dirty = false;
|
||||||
|
|
||||||
@@ -780,13 +783,17 @@ class DeviceListUpdateSerialiser {
|
|||||||
opts.token = this._syncToken;
|
opts.token = this._syncToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._baseApis.downloadKeysForUsers(
|
const factories = [];
|
||||||
downloadUsers, opts,
|
for (let i = 0; i < downloadUsers.length; i += this._deviceList._keyDownloadChunkSize) {
|
||||||
).then(async (res) => {
|
const userSlice = downloadUsers.slice(i, i + this._deviceList._keyDownloadChunkSize);
|
||||||
const dk = res.device_keys || {};
|
factories.push(() => this._baseApis.downloadKeysForUsers(userSlice, opts));
|
||||||
const masterKeys = res.master_keys || {};
|
}
|
||||||
const ssks = res.self_signing_keys || {};
|
|
||||||
const usks = res.user_signing_keys || {};
|
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
|
// yield to other things that want to execute in between users, to
|
||||||
// avoid wedging the CPU
|
// avoid wedging the CPU
|
||||||
|
|||||||
@@ -1048,6 +1048,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
|||||||
'readwrite', [
|
'readwrite', [
|
||||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||||
|
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
|
||||||
], (txn) => {
|
], (txn) => {
|
||||||
/* if we already have this session, consider updating it */
|
/* if we already have this session, consider updating it */
|
||||||
this._getInboundGroupSession(
|
this._getInboundGroupSession(
|
||||||
@@ -1104,6 +1105,12 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
|||||||
this._cryptoStore.storeEndToEndInboundGroupSession(
|
this._cryptoStore.storeEndToEndInboundGroupSession(
|
||||||
senderKey, sessionId, sessionData, txn,
|
senderKey, sessionId, sessionData, txn,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!existingSession && extraSessionData.sharedHistory) {
|
||||||
|
this._cryptoStore.addSharedHistoryInboundGroupSession(
|
||||||
|
roomId, senderKey, sessionId, txn,
|
||||||
|
);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
session.free();
|
session.free();
|
||||||
}
|
}
|
||||||
@@ -1383,6 +1390,7 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
|||||||
"forwarding_curve25519_key_chain":
|
"forwarding_curve25519_key_chain":
|
||||||
sessionData.forwardingCurve25519KeyChain || [],
|
sessionData.forwardingCurve25519KeyChain || [],
|
||||||
"sender_claimed_ed25519_key": senderEd25519Key,
|
"sender_claimed_ed25519_key": senderEd25519Key,
|
||||||
|
"shared_history": sessionData.sharedHistory || false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1415,10 +1423,24 @@ OlmDevice.prototype.exportInboundGroupSession = function(
|
|||||||
"session_key": session.export_session(messageIndex),
|
"session_key": session.export_session(messageIndex),
|
||||||
"forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
|
"forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
|
||||||
"first_known_index": session.first_known_index(),
|
"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
|
// Utilities
|
||||||
// =========
|
// =========
|
||||||
|
|
||||||
|
|||||||
@@ -36,11 +36,27 @@ import {
|
|||||||
|
|
||||||
import {WITHHELD_MESSAGES} from '../OlmDevice';
|
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
|
* @private
|
||||||
* @constructor
|
* @constructor
|
||||||
*
|
*
|
||||||
* @param {string} sessionId
|
* @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 {string} sessionId
|
||||||
* @property {Number} useCount number of times this session has been used
|
* @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
|
* devices with which we have shared the session key
|
||||||
* userId -> {deviceId -> msgindex}
|
* userId -> {deviceId -> msgindex}
|
||||||
*/
|
*/
|
||||||
function OutboundSessionInfo(sessionId) {
|
function OutboundSessionInfo(sessionId, sharedHistory = false) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.useCount = 0;
|
this.useCount = 0;
|
||||||
this.creationTime = new Date().getTime();
|
this.creationTime = new Date().getTime();
|
||||||
this.sharedWithDevices = {};
|
this.sharedWithDevices = {};
|
||||||
this.blockedDevicesNotified = {};
|
this.blockedDevicesNotified = {};
|
||||||
|
this.sharedHistory = sharedHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -183,6 +200,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
|
|||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*
|
*
|
||||||
|
* @param {module:models/room} room
|
||||||
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
|
* @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 {Object} blocked The devices that are blocked, indexed by user ID
|
||||||
* @param {boolean} [singleOlmCreationPhase] Only perform one round of olm
|
* @param {boolean} [singleOlmCreationPhase] Only perform one round of olm
|
||||||
@@ -192,7 +210,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
|
|||||||
* OutboundSessionInfo when setup is complete.
|
* OutboundSessionInfo when setup is complete.
|
||||||
*/
|
*/
|
||||||
MegolmEncryption.prototype._ensureOutboundSession = async function(
|
MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||||
devicesInRoom, blocked, singleOlmCreationPhase,
|
room, devicesInRoom, blocked, singleOlmCreationPhase,
|
||||||
) {
|
) {
|
||||||
let session;
|
let session;
|
||||||
|
|
||||||
@@ -204,6 +222,13 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
|||||||
const prepareSession = async (oldSession) => {
|
const prepareSession = async (oldSession) => {
|
||||||
session = oldSession;
|
session = oldSession;
|
||||||
|
|
||||||
|
const sharedHistory = isRoomSharedHistory(room);
|
||||||
|
|
||||||
|
// history visibility changed
|
||||||
|
if (session && sharedHistory !== session.sharedHistory) {
|
||||||
|
session = null;
|
||||||
|
}
|
||||||
|
|
||||||
// need to make a brand new session?
|
// need to make a brand new session?
|
||||||
if (session && session.needsRotation(this._sessionRotationPeriodMsgs,
|
if (session && session.needsRotation(this._sessionRotationPeriodMsgs,
|
||||||
this._sessionRotationPeriodMs)
|
this._sessionRotationPeriodMs)
|
||||||
@@ -219,7 +244,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
|||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
logger.log(`Starting new megolm session for room ${this._roomId}`);
|
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} ` +
|
logger.log(`Started new megolm session ${session.sessionId} ` +
|
||||||
`for room ${this._roomId}`);
|
`for room ${this._roomId}`);
|
||||||
this._outboundSessions[session.sessionId] = session;
|
this._outboundSessions[session.sessionId] = session;
|
||||||
@@ -250,11 +275,12 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
|||||||
const payload = {
|
const payload = {
|
||||||
type: "m.room_key",
|
type: "m.room_key",
|
||||||
content: {
|
content: {
|
||||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||||
room_id: this._roomId,
|
"room_id": this._roomId,
|
||||||
session_id: session.sessionId,
|
"session_id": session.sessionId,
|
||||||
session_key: key.key,
|
"session_key": key.key,
|
||||||
chain_index: key.chain_index,
|
"chain_index": key.chain_index,
|
||||||
|
"org.matrix.msc3061.shared_history": sharedHistory,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
|
const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
|
||||||
@@ -374,15 +400,18 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
|||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*
|
*
|
||||||
|
* @param {boolean} sharedHistory
|
||||||
|
*
|
||||||
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||||
*/
|
*/
|
||||||
MegolmEncryption.prototype._prepareNewSession = async function() {
|
MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) {
|
||||||
const sessionId = this._olmDevice.createOutboundGroupSession();
|
const sessionId = this._olmDevice.createOutboundGroupSession();
|
||||||
const key = this._olmDevice.getOutboundGroupSessionKey(sessionId);
|
const key = this._olmDevice.getOutboundGroupSessionKey(sessionId);
|
||||||
|
|
||||||
await this._olmDevice.addInboundGroupSession(
|
await this._olmDevice.addInboundGroupSession(
|
||||||
this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId,
|
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
|
// don't wait for it to complete
|
||||||
@@ -391,7 +420,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
|
|||||||
sessionId, key.key,
|
sessionId, key.key,
|
||||||
);
|
);
|
||||||
|
|
||||||
return new OutboundSessionInfo(sessionId);
|
return new OutboundSessionInfo(sessionId, sharedHistory);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -672,14 +701,15 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
|
|||||||
const payload = {
|
const payload = {
|
||||||
type: "m.forwarded_room_key",
|
type: "m.forwarded_room_key",
|
||||||
content: {
|
content: {
|
||||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||||
room_id: this._roomId,
|
"room_id": this._roomId,
|
||||||
session_id: sessionId,
|
"session_id": sessionId,
|
||||||
session_key: key.key,
|
"session_key": key.key,
|
||||||
chain_index: key.chain_index,
|
"chain_index": key.chain_index,
|
||||||
sender_key: senderKey,
|
"sender_key": senderKey,
|
||||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||||
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
|
"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}`);
|
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}`);
|
logger.debug(`Ready to encrypt events for ${this._roomId}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -945,7 +975,7 @@ MegolmEncryption.prototype.encryptMessage = async function(room, eventType, cont
|
|||||||
this._checkForUnknownDevices(devicesInRoom);
|
this._checkForUnknownDevices(devicesInRoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await this._ensureOutboundSession(devicesInRoom, blocked);
|
const session = await this._ensureOutboundSession(room, devicesInRoom, blocked);
|
||||||
const payloadJson = {
|
const payloadJson = {
|
||||||
room_id: this._roomId,
|
room_id: this._roomId,
|
||||||
type: eventType,
|
type: eventType,
|
||||||
@@ -1370,10 +1400,14 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
|||||||
keysClaimed = event.getKeysClaimed();
|
keysClaimed = event.getKeysClaimed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extraSessionData = {};
|
||||||
|
if (content["org.matrix.msc3061.shared_history"]) {
|
||||||
|
extraSessionData.sharedHistory = true;
|
||||||
|
}
|
||||||
return this._olmDevice.addInboundGroupSession(
|
return this._olmDevice.addInboundGroupSession(
|
||||||
content.room_id, senderKey, forwardingKeyChain, sessionId,
|
content.room_id, senderKey, forwardingKeyChain, sessionId,
|
||||||
content.session_key, keysClaimed,
|
content.session_key, keysClaimed,
|
||||||
exportFormat,
|
exportFormat, extraSessionData,
|
||||||
).then(() => {
|
).then(() => {
|
||||||
// have another go at decrypting events sent with this session.
|
// have another go at decrypting events sent with this session.
|
||||||
this._retryDecryption(senderKey, sessionId)
|
this._retryDecryption(senderKey, sessionId)
|
||||||
@@ -1573,14 +1607,15 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
|
|||||||
return {
|
return {
|
||||||
type: "m.forwarded_room_key",
|
type: "m.forwarded_room_key",
|
||||||
content: {
|
content: {
|
||||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||||
room_id: roomId,
|
"room_id": roomId,
|
||||||
sender_key: senderKey,
|
"sender_key": senderKey,
|
||||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||||
session_id: sessionId,
|
"session_id": sessionId,
|
||||||
session_key: key.key,
|
"session_key": key.key,
|
||||||
chain_index: key.chain_index,
|
"chain_index": key.chain_index,
|
||||||
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
|
"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
|
* @param {string} [opts.source] where the key came from
|
||||||
*/
|
*/
|
||||||
MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
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(
|
return this._olmDevice.addInboundGroupSession(
|
||||||
session.room_id,
|
session.room_id,
|
||||||
session.sender_key,
|
session.sender_key,
|
||||||
@@ -1602,7 +1644,7 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
|||||||
session.session_key,
|
session.session_key,
|
||||||
session.sender_claimed_keys,
|
session.sender_claimed_keys,
|
||||||
true,
|
true,
|
||||||
opts.untrusted ? { untrusted: opts.untrusted } : {},
|
extraSessionData,
|
||||||
).then(() => {
|
).then(() => {
|
||||||
if (opts.source !== "backup") {
|
if (opts.source !== "backup") {
|
||||||
// don't wait for it to complete
|
// don't wait for it to complete
|
||||||
@@ -1681,6 +1723,80 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey)
|
|||||||
return !this._pendingEvents[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(
|
registerAlgorithm(
|
||||||
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption,
|
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -568,7 +568,9 @@ Crypto.prototype.bootstrapCrossSigning = async function({
|
|||||||
"Cross-signing private keys not found locally, but they are available " +
|
"Cross-signing private keys not found locally, but they are available " +
|
||||||
"in secret storage, reading storage and caching locally",
|
"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
|
// 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
|
* 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.
|
* 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;
|
const userId = this._userId;
|
||||||
|
|
||||||
// Before proceeding, ensure our cross-signing public keys have been
|
// Before proceeding, ensure our cross-signing public keys have been
|
||||||
// downloaded via the device list.
|
// downloaded via the device list.
|
||||||
await this.downloadKeys([this._userId]);
|
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
|
// 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
|
// 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;
|
const masterChanged = this._crossSigningInfo.getId() !== seenPubkey;
|
||||||
if (masterChanged) {
|
if (masterChanged) {
|
||||||
logger.info("Got new master public key", seenPubkey);
|
logger.info("Got new master public key", seenPubkey);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
allowPrivateKeyRequests &&
|
||||||
|
(masterChanged || !crossSigningPrivateKeys.has("master"))
|
||||||
|
) {
|
||||||
logger.info("Attempting to retrieve cross-signing master private key");
|
logger.info("Attempting to retrieve cross-signing master private key");
|
||||||
let signing = null;
|
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 {
|
try {
|
||||||
const ret = await this._crossSigningInfo.getCrossSigningKey(
|
const ret = await this._crossSigningInfo.getCrossSigningKey(
|
||||||
'master', seenPubkey,
|
'master', seenPubkey,
|
||||||
);
|
);
|
||||||
signing = ret[1];
|
signing = ret[1];
|
||||||
logger.info("Got cross-signing master private key");
|
logger.info("Got cross-signing master private key");
|
||||||
} catch (e) {
|
|
||||||
logger.error("Cross-signing master private key not available", e);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (signing) signing.free();
|
if (signing) signing.free();
|
||||||
}
|
}
|
||||||
@@ -1352,6 +1366,11 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
|||||||
|
|
||||||
if (selfSigningChanged) {
|
if (selfSigningChanged) {
|
||||||
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
|
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");
|
logger.info("Attempting to retrieve cross-signing self-signing private key");
|
||||||
let signing = null;
|
let signing = null;
|
||||||
try {
|
try {
|
||||||
@@ -1360,8 +1379,6 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
|||||||
);
|
);
|
||||||
signing = ret[1];
|
signing = ret[1];
|
||||||
logger.info("Got cross-signing self-signing private key");
|
logger.info("Got cross-signing self-signing private key");
|
||||||
} catch (e) {
|
|
||||||
logger.error("Cross-signing self-signing private key not available", e);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (signing) signing.free();
|
if (signing) signing.free();
|
||||||
}
|
}
|
||||||
@@ -1374,6 +1391,11 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
|||||||
}
|
}
|
||||||
if (userSigningChanged) {
|
if (userSigningChanged) {
|
||||||
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
|
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");
|
logger.info("Attempting to retrieve cross-signing user-signing private key");
|
||||||
let signing = null;
|
let signing = null;
|
||||||
try {
|
try {
|
||||||
@@ -1382,8 +1404,6 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
|||||||
);
|
);
|
||||||
signing = ret[1];
|
signing = ret[1];
|
||||||
logger.info("Got cross-signing user-signing private key");
|
logger.info("Got cross-signing user-signing private key");
|
||||||
} catch (e) {
|
|
||||||
logger.error("Cross-signing user-signing private key not available", e);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (signing) signing.free();
|
if (signing) signing.free();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ limitations under the License.
|
|||||||
import {logger} from '../../logger';
|
import {logger} from '../../logger';
|
||||||
import * as utils from "../../utils";
|
import * as utils from "../../utils";
|
||||||
|
|
||||||
export const VERSION = 9;
|
export const VERSION = 10;
|
||||||
const PROFILE_TRANSACTIONS = false;
|
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) {
|
doTxn(mode, stores, func, log = logger) {
|
||||||
let startTime;
|
let startTime;
|
||||||
let description;
|
let description;
|
||||||
@@ -834,6 +866,11 @@ export function upgradeDatabase(db, oldVersion) {
|
|||||||
keyPath: ["userId", "deviceId"],
|
keyPath: ["userId", "deviceId"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 10) {
|
||||||
|
db.createObjectStore("shared_history_inbound_group_sessions", {
|
||||||
|
keyPath: ["roomId"],
|
||||||
|
});
|
||||||
|
}
|
||||||
// Expand as needed.
|
// Expand as needed.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -582,6 +582,29 @@ export class IndexedDBCryptoStore {
|
|||||||
return this._backend.markSessionsNeedingBackup(sessions, txn);
|
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
|
* Perform a transaction on the crypto store. Any store methods
|
||||||
* that require a transaction (txn) object to be passed in may
|
* 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 = 'inbound_group_sessions';
|
||||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
|
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
|
||||||
= '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_DEVICE_DATA = 'device_data';
|
||||||
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
|
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
|
||||||
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
|
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export class MemoryCryptoStore {
|
|||||||
this._rooms = {};
|
this._rooms = {};
|
||||||
// Set of {senderCurve25519Key+'/'+sessionId}
|
// Set of {senderCurve25519Key+'/'+sessionId}
|
||||||
this._sessionsNeedingBackup = {};
|
this._sessionsNeedingBackup = {};
|
||||||
|
// roomId -> array of [senderKey, sessionId]
|
||||||
|
this._sharedHistoryInboundGroupSessions = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -467,6 +469,16 @@ export class MemoryCryptoStore {
|
|||||||
return Promise.resolve();
|
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
|
// Session key backups
|
||||||
|
|
||||||
doTxn(mode, stores, func) {
|
doTxn(mode, stores, func) {
|
||||||
|
|||||||
@@ -745,6 +745,15 @@ export function promiseTry<T>(fn: () => T): Promise<T> {
|
|||||||
return new Promise((resolve) => resolve(fn()));
|
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
|
// 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
|
// 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
|
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
|
||||||
|
|||||||
@@ -340,8 +340,8 @@ export class MatrixCall extends EventEmitter {
|
|||||||
logger.debug("placeVoiceCall");
|
logger.debug("placeVoiceCall");
|
||||||
this.checkForErrorListener();
|
this.checkForErrorListener();
|
||||||
const constraints = getUserMediaContraints(ConstraintsType.Audio);
|
const constraints = getUserMediaContraints(ConstraintsType.Audio);
|
||||||
await this.placeCallWithConstraints(constraints);
|
|
||||||
this.type = CallType.Voice;
|
this.type = CallType.Voice;
|
||||||
|
await this.placeCallWithConstraints(constraints);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -352,8 +352,8 @@ export class MatrixCall extends EventEmitter {
|
|||||||
logger.debug("placeVideoCall");
|
logger.debug("placeVideoCall");
|
||||||
this.checkForErrorListener();
|
this.checkForErrorListener();
|
||||||
const constraints = getUserMediaContraints(ConstraintsType.Video);
|
const constraints = getUserMediaContraints(ConstraintsType.Video);
|
||||||
await this.placeCallWithConstraints(constraints);
|
|
||||||
this.type = CallType.Video;
|
this.type = CallType.Video;
|
||||||
|
await this.placeCallWithConstraints(constraints);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -595,6 +595,8 @@ export class MatrixCall extends EventEmitter {
|
|||||||
|
|
||||||
logger.debug("Ending call " + this.callId);
|
logger.debug("Ending call " + this.callId);
|
||||||
this.terminate(CallParty.Local, reason, !suppressEvent);
|
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 = {};
|
const content = {};
|
||||||
// Continue to send no reason for user hangups temporarily, until
|
// Continue to send no reason for user hangups temporarily, until
|
||||||
// clients understand the user_hangup reason (voip v1)
|
// 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
|
// Fetch the target user's global profile info: their room avatar / displayname
|
||||||
// could be different in whatever room we shae with them.
|
// could be different in whatever room we shae with them.
|
||||||
const profileInfo = await this.client.getProfileInfo(targetUserId);
|
const profileInfo = await this.client.getProfileInfo(targetUserId);
|
||||||
@@ -1390,9 +1395,49 @@ export class MatrixCall extends EventEmitter {
|
|||||||
create_call: replacementId,
|
create_call: replacementId,
|
||||||
} as MCallReplacesEvent;
|
} 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) {
|
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface MCallReplacesEvent {
|
|||||||
replacement_id: string;
|
replacement_id: string;
|
||||||
target_user: MCallReplacesTarget;
|
target_user: MCallReplacesTarget;
|
||||||
create_call: string;
|
create_call: string;
|
||||||
|
await_call: string;
|
||||||
target_room: string;
|
target_room: string;
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|||||||
Reference in New Issue
Block a user