Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c669382e12 | |||
| 19251c427a | |||
| a7aec9e2a4 | |||
| 99d7622b42 | |||
| 8728619d2c | |||
| f6d51fdfb8 | |||
| 0ffaa8d617 | |||
| c7aee7cebd | |||
| 9ed6c99ec8 | |||
| 7d398d41d0 | |||
| 0af763252c | |||
| 1c9dbbbb19 | |||
| 2a688bdac8 | |||
| 3c53c818cb | |||
| 8e53cb324e | |||
| efbd454775 | |||
| 68c7273f56 | |||
| 8b56ff4eff | |||
| 8134eedd93 | |||
| a87ae770cc | |||
| 355da0f9a9 | |||
| 10bdd63762 | |||
| 69dc518c2c | |||
| fa1dddf06c | |||
| f683f4544a | |||
| bc5b587651 | |||
| 8083031029 | |||
| 7b8102a42c | |||
| dbe2f5e4db | |||
| f27791b7de | |||
| 2a78170395 | |||
| cfca1c7b06 | |||
| fb7a67025f | |||
| 8a6cd48b8e | |||
| e21d1f539d | |||
| f62049559c | |||
| 1fe9dd03a3 | |||
| c3283a7297 | |||
| f423164a1c | |||
| 75fe596e24 | |||
| c5eb290e66 | |||
| d3b2c8246d | |||
| c11796af4b | |||
| 8591815d66 | |||
| b82870adb2 | |||
| 3c5b304b6b | |||
| f656698061 | |||
| d4b4bc5031 | |||
| 9bcf33b6d3 | |||
| fa550e8f03 | |||
| 2d73564eba | |||
| 1bd80247fd | |||
| 1c194e8163 | |||
| aa2d0d9a08 | |||
| fd126b8563 | |||
| bc97e7a5ea | |||
| 3dece4f46b | |||
| be05452c70 | |||
| 583650cf7d | |||
| 505915528f | |||
| ace8a787b4 | |||
| ed2ea9ac8e | |||
| 8d09a4abe6 | |||
| 1da959ab02 | |||
| 997dd9b88a | |||
| ebe66bdd6e | |||
| 013fbb87a7 | |||
| 73764d23dc | |||
| 8f62703bf2 | |||
| 6dedae2e4d | |||
| d32131b2b8 | |||
| 0a790b2ae3 | |||
| ef1d5e3d76 | |||
| c525a19df5 | |||
| a987a31667 | |||
| 10329c3436 | |||
| 29f10bcd44 | |||
| 19fe9b8ac7 | |||
| 76da708352 | |||
| 145f01ff2d | |||
| 18b1e00875 | |||
| d021498fa9 | |||
| b83aa54661 | |||
| 429550ca3e | |||
| 2a6d8c2b1d | |||
| 01c9159830 | |||
| 3b3ed5159c | |||
| 636661dd45 | |||
| cc8e8434ec | |||
| 1b94b3c4de |
@@ -1,3 +1,90 @@
|
||||
Changes in [8.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.0.0) (2020-07-27)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.1.0...v8.0.0)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `RoomState` events changed to use a Map instead of an object, which changes the collection APIs available to access them.
|
||||
|
||||
All Changes
|
||||
---
|
||||
|
||||
* Properly support txnId
|
||||
[\#1424](https://github.com/matrix-org/matrix-js-sdk/pull/1424)
|
||||
* [BREAKING] Remove deprecated getIdenticonUri
|
||||
[\#1423](https://github.com/matrix-org/matrix-js-sdk/pull/1423)
|
||||
* Bump lodash from 4.17.15 to 4.17.19
|
||||
[\#1421](https://github.com/matrix-org/matrix-js-sdk/pull/1421)
|
||||
* [BREAKING] Convert RoomState's stored state map to a real map
|
||||
[\#1419](https://github.com/matrix-org/matrix-js-sdk/pull/1419)
|
||||
|
||||
Changes in [7.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.1.0) (2020-07-03)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.1.0-rc.1...v7.1.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [7.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.1.0-rc.1) (2020-07-01)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.0.0...v7.1.0-rc.1)
|
||||
|
||||
* Ask general crypto callbacks for 4S privkey if operation adapter doesn't
|
||||
have it yet
|
||||
[\#1414](https://github.com/matrix-org/matrix-js-sdk/pull/1414)
|
||||
* Fix ICreateClientOpts missing idBaseUrl
|
||||
[\#1413](https://github.com/matrix-org/matrix-js-sdk/pull/1413)
|
||||
* Increase max event listeners for rooms
|
||||
[\#1411](https://github.com/matrix-org/matrix-js-sdk/pull/1411)
|
||||
* Don't trust keys megolm received from backup for verifying the sender
|
||||
[\#1406](https://github.com/matrix-org/matrix-js-sdk/pull/1406)
|
||||
* Raise the last known account data / state event for an update
|
||||
[\#1410](https://github.com/matrix-org/matrix-js-sdk/pull/1410)
|
||||
* Isolate encryption bootstrap side-effects
|
||||
[\#1380](https://github.com/matrix-org/matrix-js-sdk/pull/1380)
|
||||
* Add method to get current in-flight to-device requests
|
||||
[\#1405](https://github.com/matrix-org/matrix-js-sdk/pull/1405)
|
||||
|
||||
Changes in [7.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.0.0) (2020-06-23)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.0.0-rc.1...v7.0.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [7.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.0.0-rc.1) (2020-06-17)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.2.2...v7.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* Presence lists were removed from the spec in r0.5.0, and the corresponding methods have now been removed here as well:
|
||||
* `getPresenceList`
|
||||
* `inviteToPresenceList`
|
||||
* `dropFromPresenceList`
|
||||
|
||||
All changes
|
||||
---
|
||||
|
||||
* Remove support for unspecced device-specific push rules
|
||||
[\#1404](https://github.com/matrix-org/matrix-js-sdk/pull/1404)
|
||||
* Use existing session id for fetching flows as to not get a new session
|
||||
[\#1403](https://github.com/matrix-org/matrix-js-sdk/pull/1403)
|
||||
* Upgrade deps
|
||||
[\#1400](https://github.com/matrix-org/matrix-js-sdk/pull/1400)
|
||||
* Bring back backup key format migration
|
||||
[\#1398](https://github.com/matrix-org/matrix-js-sdk/pull/1398)
|
||||
* Fix: more informative error message when we cant find a key to decrypt with
|
||||
[\#1313](https://github.com/matrix-org/matrix-js-sdk/pull/1313)
|
||||
* Add js-sdk mechanism for polling client well-known for config
|
||||
[\#1394](https://github.com/matrix-org/matrix-js-sdk/pull/1394)
|
||||
* Fix verification request timeouts to match spec
|
||||
[\#1388](https://github.com/matrix-org/matrix-js-sdk/pull/1388)
|
||||
* Drop presence list methods
|
||||
[\#1391](https://github.com/matrix-org/matrix-js-sdk/pull/1391)
|
||||
* Batch up URL previews to prevent excessive requests
|
||||
[\#1395](https://github.com/matrix-org/matrix-js-sdk/pull/1395)
|
||||
|
||||
Changes in [6.2.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.2.2) (2020-06-16)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.2.1...v6.2.2)
|
||||
|
||||
@@ -288,7 +288,7 @@ function printMemberList(room) {
|
||||
}
|
||||
|
||||
function printRoomInfo(room) {
|
||||
var eventDict = room.currentState.events;
|
||||
var eventMap = room.currentState.events;
|
||||
var eTypeHeader = " Event Type(state_key) ";
|
||||
var sendHeader = " Sender ";
|
||||
// pad content to 100
|
||||
@@ -300,14 +300,15 @@ function printRoomInfo(room) {
|
||||
var contentHeader = padSide + "Content" + padSide;
|
||||
print(eTypeHeader+sendHeader+contentHeader);
|
||||
print(new Array(100).join("-"));
|
||||
Object.keys(eventDict).forEach(function(eventType) {
|
||||
eventMap.keys().forEach(function(eventType) {
|
||||
if (eventType === "m.room.member") { return; } // use /members instead.
|
||||
Object.keys(eventDict[eventType]).forEach(function(stateKey) {
|
||||
var eventEventMap = eventMap.get(eventType);
|
||||
eventEventMap.keys().forEach(function(stateKey) {
|
||||
var typeAndKey = eventType + (
|
||||
stateKey.length > 0 ? "("+stateKey+")" : ""
|
||||
);
|
||||
var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
|
||||
var event = eventDict[eventType][stateKey];
|
||||
var event = eventEventMap.get(stateKey);
|
||||
var sendStr = fixWidth(event.getSender(), sendHeader.length);
|
||||
var contentStr = fixWidth(
|
||||
JSON.stringify(event.getContent()), contentHeader.length
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "6.2.2",
|
||||
"version": "8.0.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {getHttpUriForMxc, getIdenticonUri} from "../../src/content-repo";
|
||||
import {getHttpUriForMxc} from "../../src/content-repo";
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
const baseUrl = "https://my.home.server";
|
||||
@@ -56,31 +56,4 @@ describe("ContentRepo", function() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIdenticonUri", function() {
|
||||
it("should do nothing for null input", function() {
|
||||
expect(getIdenticonUri(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it("should set w/h by default to 96", function() {
|
||||
expect(getIdenticonUri(baseUrl, "foobar")).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
|
||||
"?width=96&height=96",
|
||||
);
|
||||
});
|
||||
|
||||
it("should be able to set custom w/h", function() {
|
||||
expect(getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
|
||||
it("should URL encode the identicon string", function() {
|
||||
expect(getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as olmlib from "../../src/crypto/olmlib";
|
||||
import {sleep} from "../../src/utils";
|
||||
import {EventEmitter} from "events";
|
||||
import {CRYPTO_ENABLED} from "../../src/client";
|
||||
import {DeviceInfo} from "../../src/crypto/deviceinfo";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -26,6 +27,66 @@ describe("Crypto", function() {
|
||||
expect(Crypto.getOlmVersion()[0]).toEqual(3);
|
||||
});
|
||||
|
||||
describe("encrypted events", function() {
|
||||
it("provides encryption information", async function() {
|
||||
const client = (new TestClient(
|
||||
"@alice:example.com", "deviceid",
|
||||
)).client;
|
||||
await client.initCrypto();
|
||||
|
||||
// unencrypted event
|
||||
const event = {
|
||||
getId: () => "$event_id",
|
||||
getSenderKey: () => null,
|
||||
getWireContent: () => {return {};},
|
||||
};
|
||||
|
||||
let encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeFalsy();
|
||||
|
||||
// unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
|
||||
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
event.getWireContent = () => {return {algorithm: olmlib.MEGOLM_ALGORITHM};};
|
||||
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
event.getClaimedEd25519Key =
|
||||
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
expect(encryptionInfo.authenticated).toBeFalsy();
|
||||
expect(encryptionInfo.sender).toBeFalsy();
|
||||
|
||||
// known sender, megolm key from backup
|
||||
event.getForwardingCurve25519KeyChain = () => [];
|
||||
event.isKeySourceUntrusted = () => true;
|
||||
const device = new DeviceInfo("FLIBBLE");
|
||||
device.keys["curve25519:FLIBBLE"] =
|
||||
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
device.keys["ed25519:FLIBBLE"] =
|
||||
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
client._crypto._deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
expect(encryptionInfo.authenticated).toBeFalsy();
|
||||
expect(encryptionInfo.sender).toBeTruthy();
|
||||
expect(encryptionInfo.mismatchedSender).toBeFalsy();
|
||||
|
||||
// known sender, trusted megolm key, but bad ed25519key
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
device.keys["ed25519:FLIBBLE"] =
|
||||
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
expect(encryptionInfo.authenticated).toBeTruthy();
|
||||
expect(encryptionInfo.sender).toBeTruthy();
|
||||
expect(encryptionInfo.mismatchedSender).toBeTruthy();
|
||||
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session management', function() {
|
||||
const otkResponse = {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {MockStorageApi} from "../../MockStorageApi";
|
||||
import * as testUtils from "../../test-utils";
|
||||
import {OlmDevice} from "../../../src/crypto/OlmDevice";
|
||||
import {Crypto} from "../../../src/crypto";
|
||||
import {resetCrossSigningKeys} from "./crypto-utils";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -332,7 +333,7 @@ describe("MegolmBackup", function() {
|
||||
client.on("crossSigning.getKey", function(e) {
|
||||
e.done(privateKeys[e.type]);
|
||||
});
|
||||
await client.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(client);
|
||||
let numCalls = 0;
|
||||
await new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
@@ -517,6 +518,7 @@ describe("MegolmBackup", function() {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
expect(res.untrusted).toBeTruthy(); // keys from backup are untrusted
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import anotherjson from 'another-json';
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {HttpResponse, setHttpResponses} from '../../test-utils';
|
||||
import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
|
||||
|
||||
async function makeTestClient(userInfo, options, keys) {
|
||||
if (!keys) keys = {};
|
||||
@@ -66,8 +67,13 @@ describe("Cross Signing", function() {
|
||||
);
|
||||
});
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.setAccountData = async () => {};
|
||||
alice.getAccountDataFromServer = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await alice.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -78,7 +84,7 @@ describe("Cross Signing", function() {
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's device key
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
@@ -273,7 +279,7 @@ describe("Cross Signing", function() {
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
@@ -363,7 +369,7 @@ describe("Cross Signing", function() {
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
|
||||
const selfSigningKey = new Uint8Array([
|
||||
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
|
||||
@@ -520,7 +526,7 @@ describe("Cross Signing", function() {
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
// (NOTE: device key is not signed by ssk)
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
@@ -588,7 +594,7 @@ describe("Cross Signing", function() {
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's keys
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
@@ -740,7 +746,7 @@ describe("Cross Signing", function() {
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
// set Bob's cross-signing key
|
||||
await bob.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(bob);
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: {
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
@@ -766,7 +772,7 @@ describe("Cross Signing", function() {
|
||||
let upgradePromise = new Promise((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
await upgradePromise;
|
||||
|
||||
const bobTrust = alice.checkUserTrust("@bob:example.com");
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {IndexedDBCryptoStore} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
|
||||
|
||||
// needs to be phased out and replaced with bootstrapSecretStorage,
|
||||
// but that is doing too much extra stuff for it to be an easy transition.
|
||||
export async function resetCrossSigningKeys(client, {
|
||||
level,
|
||||
authUploadDeviceSigningKeys = async func => await func(),
|
||||
} = {}) {
|
||||
const crypto = client._crypto;
|
||||
|
||||
const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys);
|
||||
try {
|
||||
await crypto._crossSigningInfo.resetKeys(level);
|
||||
await crypto._signObject(crypto._crossSigningInfo.keys.master);
|
||||
// write a copy locally so we know these are trusted keys
|
||||
await crypto._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
crypto._cryptoStore.storeCrossSigningKeys(
|
||||
txn, crypto._crossSigningInfo.keys);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// If anything failed here, revert the keys so we know to try again from the start
|
||||
// next time.
|
||||
crypto._crossSigningInfo.keys = oldKeys;
|
||||
throw e;
|
||||
}
|
||||
crypto._baseApis.emit("crossSigning.keysChanged", {});
|
||||
await crypto._afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
export async function createSecretStorageKey() {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const storagePublicKey = decryption.generate_key();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
decryption.free();
|
||||
return {
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
keyInfo: { pubkey: storagePublicKey },
|
||||
privateKey: storagePrivateKey,
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {MatrixEvent} from "../../../src/models/event";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {makeTestClients} from './verification/util';
|
||||
import {encryptAES} from "../../../src/crypto/aes";
|
||||
import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
|
||||
|
||||
import * as utils from "../../../src/utils";
|
||||
|
||||
@@ -190,7 +191,7 @@ describe("Secrets", function() {
|
||||
}),
|
||||
]);
|
||||
};
|
||||
alice.resetCrossSigningKeys();
|
||||
resetCrossSigningKeys(alice);
|
||||
|
||||
const newKeyId = await alice.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
@@ -325,7 +326,10 @@ describe("Secrets", function() {
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await bob.bootstrapSecretStorage();
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
const crossSigning = bob._crypto._crossSigningInfo;
|
||||
const secretStorage = bob._crypto._secretStorage;
|
||||
@@ -381,6 +385,7 @@ describe("Secrets", function() {
|
||||
keyInfo: { pubkey: storagePublicKey },
|
||||
privateKey: storagePrivateKey,
|
||||
}),
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
// Clear local cross-signing keys and read from secret storage
|
||||
@@ -389,7 +394,9 @@ describe("Secrets", function() {
|
||||
crossSigning.toStorage(),
|
||||
);
|
||||
crossSigning.keys = {};
|
||||
await bob.bootstrapSecretStorage();
|
||||
await bob.bootstrapSecretStorage({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
@@ -510,7 +517,9 @@ describe("Secrets", function() {
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage();
|
||||
await alice.bootstrapSecretStorage({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
|
||||
.toEqual({key: "key_id"});
|
||||
@@ -650,7 +659,9 @@ describe("Secrets", function() {
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage();
|
||||
await alice.bootstrapSecretStorage({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
const backupKey = alice.getAccountData("m.megolm_backup.v1")
|
||||
.getContent();
|
||||
|
||||
@@ -22,6 +22,7 @@ import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
|
||||
import {verificationMethods} from "../../../../src/crypto";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {resetCrossSigningKeys} from "../crypto-utils";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -288,12 +289,12 @@ describe("SAS verification", function() {
|
||||
);
|
||||
alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
|
||||
alice.httpBackend.flush(undefined, 2);
|
||||
await alice.client.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice.client);
|
||||
bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {});
|
||||
bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
|
||||
bob.httpBackend.flush(undefined, 2);
|
||||
|
||||
await bob.client.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(bob.client);
|
||||
|
||||
bob.client._crypto._deviceList.storeCrossSigningForUser(
|
||||
"@alice:example.com", {
|
||||
|
||||
@@ -119,6 +119,8 @@ async function distributeEvent(ownRequest, theirRequest, event) {
|
||||
await theirRequest.channel.handleEvent(event, theirRequest, true);
|
||||
}
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request unit tests", function() {
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
@@ -246,4 +248,38 @@ describe("verification request unit tests", function() {
|
||||
expect(bob1Request.done).toBe(true);
|
||||
expect(bob2Request.done).toBe(true);
|
||||
});
|
||||
|
||||
it("request times out after 10 minutes", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true,
|
||||
true, true);
|
||||
|
||||
expect(aliceRequest.cancelled).toBe(false);
|
||||
expect(aliceRequest._cancellingUserId).toBe(undefined);
|
||||
jest.advanceTimersByTime(10 * 60 * 1000);
|
||||
expect(aliceRequest._cancellingUserId).toBe(alice.getUserId());
|
||||
});
|
||||
|
||||
it("request times out 2 minutes after receipt", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"), new Map(), bob);
|
||||
|
||||
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
|
||||
|
||||
expect(bobRequest.cancelled).toBe(false);
|
||||
expect(bobRequest._cancellingUserId).toBe(undefined);
|
||||
jest.advanceTimersByTime(2 * 60 * 1000);
|
||||
expect(bobRequest._cancellingUserId).toBe(bob.getUserId());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,12 +33,6 @@ describe("RoomMember", function() {
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
"was no m.room.member event", function() {
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
|
||||
expect(url.indexOf("http")).toEqual(0); // don't care about form
|
||||
});
|
||||
|
||||
it("should return nothing if there is no m.room.member and allowDefault=false",
|
||||
function() {
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
|
||||
@@ -45,12 +45,6 @@ describe("Room", function() {
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
"was no m.room.avatar event", function() {
|
||||
const url = room.getAvatarUrl(hsUrl, 64, 64, "crop", true);
|
||||
expect(url.indexOf("http")).toEqual(0); // don't care about form
|
||||
});
|
||||
|
||||
it("should return nothing if there is no m.room.avatar and allowDefault=false",
|
||||
function() {
|
||||
const url = room.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
|
||||
+90
-78
@@ -54,6 +54,7 @@ import {randomString} from './randomstring';
|
||||
import {PushProcessor} from "./pushprocessor";
|
||||
import {encodeBase64, decodeBase64} from "./crypto/olmlib";
|
||||
import { User } from "./models/user";
|
||||
import {AutoDiscovery} from "./autodiscovery";
|
||||
|
||||
const SCROLLBACK_DELAY_MS = 3000;
|
||||
export const CRYPTO_ENABLED = isCryptoAvailable();
|
||||
@@ -329,7 +330,7 @@ export function MatrixClient(opts) {
|
||||
this._isGuest = false;
|
||||
this._ongoingScrollbacks = {};
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
this.urlPreviewCache = {};
|
||||
this.urlPreviewCache = {}; // key=preview key, value=Promise for preview (may be an error)
|
||||
this._notifTimelineSet = null;
|
||||
this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
|
||||
|
||||
@@ -968,6 +969,20 @@ MatrixClient.prototype.findVerificationRequestDMInProgress = function(roomId) {
|
||||
return this._crypto.findVerificationRequestDMInProgress(roomId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all to-device verification requests that are already in progress for the given user id
|
||||
*
|
||||
* @param {string} userId the ID of the user to query
|
||||
*
|
||||
* @returns {module:crypto/verification/request/VerificationRequest[]} the VerificationRequests that are in progress
|
||||
*/
|
||||
MatrixClient.prototype.getVerificationRequestsToDeviceInProgress = function(userId) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this._crypto.getVerificationRequestsToDeviceInProgress(userId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Request a key verification from another user.
|
||||
*
|
||||
@@ -1074,17 +1089,6 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -1154,7 +1158,6 @@ function wrapCryptoFuncs(MatrixClient, names) {
|
||||
* @param {module:models/room} room the room the event is in
|
||||
*/
|
||||
wrapCryptoFuncs(MatrixClient, [
|
||||
"resetCrossSigningKeys",
|
||||
"getCrossSigningId",
|
||||
"getStoredCrossSigningForUser",
|
||||
"checkUserTrust",
|
||||
@@ -1169,20 +1172,23 @@ wrapCryptoFuncs(MatrixClient, [
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check if the sender of an event is verified
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
* Get information about the encryption of an event
|
||||
*
|
||||
* @param {MatrixEvent} event event to be checked
|
||||
* @function module:client~MatrixClient#getEventEncryptionInfo
|
||||
*
|
||||
* @returns {DeviceTrustLevel}
|
||||
* @param {module:models/event.MatrixEvent} event event to be checked
|
||||
*
|
||||
* @return {object} An object with the fields:
|
||||
* - encrypted: whether the event is encrypted (if not encrypted, some of the
|
||||
* other properties may not be set)
|
||||
* - senderKey: the sender's key
|
||||
* - algorithm: the algorithm used to encrypt the event
|
||||
* - authenticated: whether we can be sure that the owner of the senderKey
|
||||
* sent the event
|
||||
* - sender: the sender's device information, if available
|
||||
* - mismatchedSender: if the event's ed25519 and curve25519 keys don't match
|
||||
* (only meaningful if `sender` is set)
|
||||
*/
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a recovery key from a user-supplied passphrase.
|
||||
@@ -1312,6 +1318,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
|
||||
*/
|
||||
|
||||
wrapCryptoFuncs(MatrixClient, [
|
||||
"getEventEncryptionInfo",
|
||||
"createRecoveryKeyFromPassphrase",
|
||||
"bootstrapSecretStorage",
|
||||
"addSecretStorageKey",
|
||||
@@ -1977,7 +1984,11 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
||||
}
|
||||
}
|
||||
|
||||
return this.importRoomKeys(keys, { progressCallback });
|
||||
return this.importRoomKeys(keys, {
|
||||
progressCallback,
|
||||
untrusted: true,
|
||||
source: "backup",
|
||||
});
|
||||
}).then(() => {
|
||||
return this._crypto.setTrustedBackupPubKey(backupPubKey);
|
||||
}).then(() => {
|
||||
@@ -2512,7 +2523,7 @@ MatrixClient.prototype._sendCompleteEvent = function(roomId, eventObject, txnId,
|
||||
const type = localEvent.getType();
|
||||
logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
|
||||
|
||||
localEvent._txnId = txnId;
|
||||
localEvent.setTxnId(txnId);
|
||||
localEvent.setStatus(EventStatus.SENDING);
|
||||
|
||||
// add this event immediately to the local store as 'sending'.
|
||||
@@ -2680,7 +2691,11 @@ function _updatePendingEventStatus(room, event, newStatus) {
|
||||
}
|
||||
|
||||
function _sendEventHttpRequest(client, event) {
|
||||
const txnId = event._txnId ? event._txnId : client.makeTxnId();
|
||||
let txnId = event.getTxnId();
|
||||
if (!txnId) {
|
||||
txnId = client.makeTxnId();
|
||||
event.setTxnId(txnId);
|
||||
}
|
||||
|
||||
const pathParams = {
|
||||
$roomId: event.getRoomId(),
|
||||
@@ -3001,25 +3016,32 @@ MatrixClient.prototype.setRoomReadMarkers = async function(
|
||||
* May return synthesized attributes if the URL lacked OG meta.
|
||||
*/
|
||||
MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
|
||||
// bucket the timestamp to the nearest minute to prevent excessive spam to the server
|
||||
// Surely 60-second accuracy is enough for anyone.
|
||||
ts = Math.floor(ts / 60000) * 60000;
|
||||
|
||||
const key = ts + "_" + url;
|
||||
const og = this.urlPreviewCache[key];
|
||||
if (og) {
|
||||
return Promise.resolve(og);
|
||||
|
||||
// If there's already a request in flight (or we've handled it), return that instead.
|
||||
const cachedPreview = this.urlPreviewCache[key];
|
||||
if (cachedPreview) {
|
||||
if (callback) {
|
||||
cachedPreview.then(callback).catch(callback);
|
||||
}
|
||||
return cachedPreview;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
return this._http.authedRequest(
|
||||
const resp = this._http.authedRequest(
|
||||
callback, "GET", "/preview_url", {
|
||||
url: url,
|
||||
ts: ts,
|
||||
}, undefined, {
|
||||
prefix: PREFIX_MEDIA_R0,
|
||||
},
|
||||
).then(function(response) {
|
||||
// TODO: expire cache occasionally
|
||||
self.urlPreviewCache[key] = response;
|
||||
return response;
|
||||
});
|
||||
);
|
||||
// TODO: Expire the URL preview cache sometimes
|
||||
this.urlPreviewCache[key] = resp;
|
||||
return resp;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -3521,46 +3543,6 @@ MatrixClient.prototype.setPresence = function(opts, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
function _presenceList(callback, client, opts, method) {
|
||||
const path = utils.encodeUri("/presence/list/$userId", {
|
||||
$userId: client.credentials.userId,
|
||||
});
|
||||
return client._http.authedRequest(callback, method, path, undefined, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve current user presence list.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getPresenceList = function(callback) {
|
||||
return _presenceList(callback, this, undefined, "GET");
|
||||
};
|
||||
|
||||
/**
|
||||
* Add users to the current user presence list.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @param {string[]} userIds
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.inviteToPresenceList = function(callback, userIds) {
|
||||
const opts = {"invite": userIds};
|
||||
return _presenceList(callback, this, opts, "POST");
|
||||
};
|
||||
|
||||
/**
|
||||
* Drop users from the current user presence list.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @param {string[]} userIds
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
**/
|
||||
MatrixClient.prototype.dropFromPresenceList = function(callback, userIds) {
|
||||
const opts = {"drop": userIds};
|
||||
return _presenceList(callback, this, opts, "POST");
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve older messages from the given room and put them in the timeline.
|
||||
@@ -4762,6 +4744,9 @@ MatrixClient.prototype.deactivateSynapseUser = function(userId) {
|
||||
* @param {Boolean=} opts.lazyLoadMembers True to not load all membership events during
|
||||
* initial sync but fetch them when needed by calling `loadOutOfBandMembers`
|
||||
* This will override the filter option at this moment.
|
||||
* @param {Number=} opts.clientWellKnownPollPeriod The number of seconds between polls
|
||||
* to /.well-known/matrix/client, undefined to disable. This should be in the order of hours.
|
||||
* Default: undefined.
|
||||
*/
|
||||
MatrixClient.prototype.startClient = async function(opts) {
|
||||
if (this.clientRunning) {
|
||||
@@ -4810,6 +4795,29 @@ MatrixClient.prototype.startClient = async function(opts) {
|
||||
this._clientOpts = opts;
|
||||
this._syncApi = new SyncApi(this, opts);
|
||||
this._syncApi.sync();
|
||||
|
||||
if (opts.clientWellKnownPollPeriod !== undefined) {
|
||||
this._clientWellKnownIntervalID =
|
||||
setInterval(() => {
|
||||
this._fetchClientWellKnown();
|
||||
}, 1000 * opts.clientWellKnownPollPeriod);
|
||||
this._fetchClientWellKnown();
|
||||
}
|
||||
};
|
||||
|
||||
MatrixClient.prototype._fetchClientWellKnown = async function() {
|
||||
try {
|
||||
this._clientWellKnown = await AutoDiscovery.getRawClientConfig(this.getDomain());
|
||||
this.emit("WellKnown.client", this._clientWellKnown);
|
||||
} catch (err) {
|
||||
logger.error("Failed to get client well-known", err);
|
||||
this._clientWellKnown = undefined;
|
||||
this.emit("WellKnown.error", err);
|
||||
}
|
||||
};
|
||||
|
||||
MatrixClient.prototype.getClientWellKnown = function() {
|
||||
return this._clientWellKnown;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -4851,6 +4859,9 @@ MatrixClient.prototype.stopClient = function() {
|
||||
this._peekSync.stopPeeking();
|
||||
}
|
||||
global.clearTimeout(this._checkTurnServersTimeoutID);
|
||||
if (this._clientWellKnownIntervalID !== undefined) {
|
||||
global.clearInterval(this._clientWellKnownIntervalID);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -5603,8 +5614,9 @@ MatrixClient.prototype.generateClientSecret = function() {
|
||||
* Fires whenever new user-scoped account_data is added.
|
||||
* @event module:client~MatrixClient#"accountData"
|
||||
* @param {MatrixEvent} event The event describing the account_data just added
|
||||
* @param {MatrixEvent} event The previous account data, if known.
|
||||
* @example
|
||||
* matrixClient.on("accountData", function(event){
|
||||
* matrixClient.on("accountData", function(event, oldEvent){
|
||||
* myAccountData[event.type] = event.content;
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -75,35 +75,3 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params))) + fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an identicon URL from an arbitrary string.
|
||||
* @param {string} baseUrl The base homeserver url which has a content repo.
|
||||
* @param {string} identiconString The string to create an identicon for.
|
||||
* @param {Number} width The desired width of the image in pixels. Default: 96.
|
||||
* @param {Number} height The desired height of the image in pixels. Default: 96.
|
||||
* @return {string} The complete URL to the identicon.
|
||||
* @deprecated This is no longer in the specification.
|
||||
*/
|
||||
export function getIdenticonUri(baseUrl, identiconString, width, height) {
|
||||
if (!identiconString) {
|
||||
return null;
|
||||
}
|
||||
if (!width) {
|
||||
width = 96;
|
||||
}
|
||||
if (!height) {
|
||||
height = 96;
|
||||
}
|
||||
const params = {
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
|
||||
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
|
||||
$ident: identiconString,
|
||||
});
|
||||
return baseUrl + path +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params)));
|
||||
}
|
||||
|
||||
@@ -178,12 +178,12 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* typically called in conjunction with the creation of new cross-signing
|
||||
* keys.
|
||||
*
|
||||
* @param {object} keys The keys to store
|
||||
* @param {Map} keys The keys to store
|
||||
* @param {SecretStorage} secretStorage The secret store using account data
|
||||
*/
|
||||
static async storeInSecretStorage(keys, secretStorage) {
|
||||
for (const type of Object.keys(keys)) {
|
||||
const encodedKey = encodeBase64(keys[type]);
|
||||
for (const [type, privateKey] of keys) {
|
||||
const encodedKey = encodeBase64(privateKey);
|
||||
await secretStorage.store(`m.cross_signing.${type}`, encodedKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
import {MatrixEvent} from "../models/event";
|
||||
import {EventEmitter} from "events";
|
||||
import {createCryptoStoreCacheCallbacks} from "./CrossSigning";
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import {
|
||||
PREFIX_UNSTABLE,
|
||||
} from "../http-api";
|
||||
|
||||
/**
|
||||
* Builds an EncryptionSetupOperation by calling any of the add.. methods.
|
||||
* Once done, `buildOperation()` can be called which allows to apply to operation.
|
||||
*
|
||||
* This is used as a helper by Crypto to keep track of all the network requests
|
||||
* and other side-effects of bootstrapping, so it can be applied in one go (and retried in the future)
|
||||
* Also keeps track of all the private keys created during bootstrapping, so we don't need to prompt for them
|
||||
* more than once.
|
||||
*/
|
||||
export class EncryptionSetupBuilder {
|
||||
/**
|
||||
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
|
||||
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
|
||||
*/
|
||||
constructor(accountData, delegateCryptoCallbacks) {
|
||||
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
|
||||
this.crossSigningCallbacks = new CrossSigningCallbacks();
|
||||
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
|
||||
|
||||
this._crossSigningKeys = null;
|
||||
this._keySignatures = null;
|
||||
this._keyBackupInfo = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new cross-signing public keys
|
||||
* @param {Object} auth auth dictionary needed to upload the new keys
|
||||
* @param {Object} keys the new keys
|
||||
*/
|
||||
addCrossSigningKeys(auth, keys) {
|
||||
this._crossSigningKeys = {auth, keys};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the key backup info to be updated on the server
|
||||
*
|
||||
* Used either to create a new key backup, or add signatures
|
||||
* from the new MSK.
|
||||
*
|
||||
* @param {Object} keyBackupInfo as received from/sent to the server
|
||||
*/
|
||||
addSessionBackup(keyBackupInfo) {
|
||||
this._keyBackupInfo = keyBackupInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the session backup private key to be updated in the local cache
|
||||
*
|
||||
* Used after fixing the format of the key
|
||||
*
|
||||
* @param {Uint8Array} privateKey
|
||||
*/
|
||||
addSessionBackupPrivateKeyToCache(privateKey) {
|
||||
this._sessionBackupPrivateKey = privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add signatures from a given user and device/x-sign key
|
||||
* Used to sign the new cross-signing key with the device key
|
||||
*
|
||||
* @param {String} userId
|
||||
* @param {String} deviceId
|
||||
* @param {String} signature
|
||||
*/
|
||||
addKeySignature(userId, deviceId, signature) {
|
||||
if (!this._keySignatures) {
|
||||
this._keySignatures = {};
|
||||
}
|
||||
const userSignatures = this._keySignatures[userId] || {};
|
||||
this._keySignatures[userId] = userSignatures;
|
||||
userSignatures[deviceId] = signature;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @param {Object} content
|
||||
* @return {Promise}
|
||||
*/
|
||||
setAccountData(type, content) {
|
||||
return this.accountDataClientAdapter.setAccountData(type, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* builds the operation containing all the parts that have been added to the builder
|
||||
* @return {EncryptionSetupOperation}
|
||||
*/
|
||||
buildOperation() {
|
||||
const accountData = this.accountDataClientAdapter._values;
|
||||
return new EncryptionSetupOperation(
|
||||
accountData,
|
||||
this._crossSigningKeys,
|
||||
this._keyBackupInfo,
|
||||
this._keySignatures,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the created keys locally.
|
||||
*
|
||||
* This does not yet store the operation in a way that it can be restored,
|
||||
* but that is the idea in the future.
|
||||
*
|
||||
* @param {Crypto} crypto
|
||||
* @return {Promise}
|
||||
*/
|
||||
async persist(crypto) {
|
||||
// store self_signing and user_signing private key in cache
|
||||
if (this._crossSigningKeys) {
|
||||
const cacheCallbacks = createCryptoStoreCacheCallbacks(
|
||||
crypto._cryptoStore, crypto._olmDevice);
|
||||
for (const type of ["self_signing", "user_signing"]) {
|
||||
// logger.log(`Cache ${type} cross-signing private key locally`);
|
||||
const privateKey = this.crossSigningCallbacks.privateKeys.get(type);
|
||||
await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey);
|
||||
}
|
||||
// store own cross-sign pubkeys as trusted
|
||||
await crypto._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
crypto._cryptoStore.storeCrossSigningKeys(
|
||||
txn, this._crossSigningKeys.keys);
|
||||
},
|
||||
);
|
||||
}
|
||||
// store session backup key in cache
|
||||
if (this._sessionBackupPrivateKey) {
|
||||
await crypto.storeSessionBackupPrivateKey(this._sessionBackupPrivateKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be created from EncryptionSetupBuilder, or
|
||||
* (in a follow-up PR, not implemented yet) restored from storage, to retry.
|
||||
*
|
||||
* It does not have knowledge of any private keys, unlike the builder.
|
||||
*/
|
||||
export class EncryptionSetupOperation {
|
||||
/**
|
||||
* @param {Map<String, Object>} accountData
|
||||
* @param {Object} crossSigningKeys
|
||||
* @param {Object} keyBackupInfo
|
||||
* @param {Object} keySignatures
|
||||
*/
|
||||
constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) {
|
||||
this._accountData = accountData;
|
||||
this._crossSigningKeys = crossSigningKeys;
|
||||
this._keyBackupInfo = keyBackupInfo;
|
||||
this._keySignatures = keySignatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the (remaining part of, in the future) operation by sending requests to the server.
|
||||
* @param {Crypto} crypto
|
||||
*/
|
||||
async apply(crypto) {
|
||||
const baseApis = crypto._baseApis;
|
||||
// set account data
|
||||
if (this._accountData) {
|
||||
for (const [type, content] of this._accountData) {
|
||||
await baseApis.setAccountData(type, content);
|
||||
}
|
||||
}
|
||||
// upload cross-signing keys
|
||||
if (this._crossSigningKeys) {
|
||||
const keys = {};
|
||||
for (const [name, key] of Object.entries(this._crossSigningKeys.keys)) {
|
||||
keys[name + "_key"] = key;
|
||||
}
|
||||
await baseApis.uploadDeviceSigningKeys(
|
||||
this._crossSigningKeys.auth,
|
||||
keys,
|
||||
);
|
||||
// pass the new keys to the main instance of our own CrossSigningInfo.
|
||||
crypto._crossSigningInfo.setKeys(this._crossSigningKeys.keys);
|
||||
}
|
||||
// upload first cross-signing signatures with the new key
|
||||
// (e.g. signing our own device)
|
||||
if (this._keySignatures) {
|
||||
await baseApis.uploadKeySignatures(this._keySignatures);
|
||||
}
|
||||
// need to create/update key backup info
|
||||
if (this._keyBackupInfo) {
|
||||
if (this._keyBackupInfo.version) {
|
||||
// session backup signature
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// Sign the backup with the cross signing key so the key backup can
|
||||
// be trusted via cross-signing.
|
||||
await baseApis._http.authedRequest(
|
||||
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
|
||||
undefined, {
|
||||
algorithm: this._keyBackupInfo.algorithm,
|
||||
auth_data: this._keyBackupInfo.auth_data,
|
||||
},
|
||||
{prefix: PREFIX_UNSTABLE},
|
||||
);
|
||||
} else {
|
||||
// add new key backup
|
||||
await baseApis._http.authedRequest(
|
||||
undefined, "POST", "/room_keys/version",
|
||||
undefined, this._keyBackupInfo,
|
||||
{prefix: PREFIX_UNSTABLE},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Catches account data set by SecretStorage during bootstrapping by
|
||||
* implementing the methods related to account data in MatrixClient
|
||||
*/
|
||||
class AccountDataClientAdapter extends EventEmitter {
|
||||
/**
|
||||
* @param {Object.<String, MatrixEvent>} accountData existing account data
|
||||
*/
|
||||
constructor(accountData) {
|
||||
super();
|
||||
this._existingValues = accountData;
|
||||
this._values = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @return {Promise<Object>} the content of the account data
|
||||
*/
|
||||
getAccountDataFromServer(type) {
|
||||
return Promise.resolve(this.getAccountData(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @return {Object} the content of the account data
|
||||
*/
|
||||
getAccountData(type) {
|
||||
const modifiedValue = this._values.get(type);
|
||||
if (modifiedValue) {
|
||||
return modifiedValue;
|
||||
}
|
||||
const existingValue = this._existingValues[type];
|
||||
if (existingValue) {
|
||||
return existingValue.getContent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @param {Object} content
|
||||
* @return {Promise}
|
||||
*/
|
||||
setAccountData(type, content) {
|
||||
const lastEvent = this._values.get(type);
|
||||
this._values.set(type, content);
|
||||
// ensure accountData is emitted on the next tick,
|
||||
// as SecretStorage listens for it while calling this method
|
||||
// and it seems to rely on this.
|
||||
return Promise.resolve().then(() => {
|
||||
const event = new MatrixEvent({type, content});
|
||||
this.emit("accountData", event, lastEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Catches the private cross-signing keys set during bootstrapping
|
||||
* by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks.
|
||||
* See CrossSigningInfo constructor
|
||||
*/
|
||||
class CrossSigningCallbacks {
|
||||
constructor() {
|
||||
this.privateKeys = new Map();
|
||||
}
|
||||
|
||||
// cache callbacks
|
||||
getCrossSigningKeyCache(type, expectedPublicKey) {
|
||||
return this.getCrossSigningKey(type, expectedPublicKey);
|
||||
}
|
||||
|
||||
storeCrossSigningKeyCache(type, key) {
|
||||
this.privateKeys.set(type, key);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// non-cache callbacks
|
||||
getCrossSigningKey(type, _expectedPubkey) {
|
||||
return Promise.resolve(this.privateKeys.get(type));
|
||||
}
|
||||
|
||||
saveCrossSigningKeys(privateKeys) {
|
||||
for (const [type, privateKey] of Object.entries(privateKeys)) {
|
||||
this.privateKeys.set(type, privateKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Catches the 4S private key set during bootstrapping by implementing
|
||||
* the SecretStorage crypto callbacks
|
||||
*/
|
||||
class SSSSCryptoCallbacks {
|
||||
constructor(delegateCryptoCallbacks) {
|
||||
this._privateKeys = new Map();
|
||||
this._delegateCryptoCallbacks = delegateCryptoCallbacks;
|
||||
}
|
||||
|
||||
async getSecretStorageKey({ keys }, name) {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
const privateKey = this._privateKeys.get(keyId);
|
||||
if (privateKey) {
|
||||
return [keyId, privateKey];
|
||||
}
|
||||
}
|
||||
// if we don't have the key cached yet, ask
|
||||
// for it to the general crypto callbacks and cache it
|
||||
if (this._delegateCryptoCallbacks) {
|
||||
const result = await this._delegateCryptoCallbacks.
|
||||
getSecretStorageKey({keys}, name);
|
||||
if (result) {
|
||||
const [keyId, privateKey] = result;
|
||||
this._privateKeys.set(keyId, privateKey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
addPrivateKey(keyId, privKey) {
|
||||
this._privateKeys.set(keyId, privKey);
|
||||
}
|
||||
}
|
||||
@@ -992,11 +992,12 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
||||
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
|
||||
* @param {boolean} exportFormat true if the megolm keys are in export format
|
||||
* (ie, they lack an ed25519 signature)
|
||||
* @param {Object} [extraSessionData={}] any other data to be include with the session
|
||||
*/
|
||||
OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
roomId, senderKey, forwardingCurve25519KeyChain,
|
||||
sessionId, sessionKey, keysClaimed,
|
||||
exportFormat,
|
||||
exportFormat, extraSessionData = {},
|
||||
) {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [
|
||||
@@ -1043,12 +1044,12 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
" with first index " + session.first_known_index(),
|
||||
);
|
||||
|
||||
const sessionData = {
|
||||
const sessionData = Object.assign({}, extraSessionData, {
|
||||
room_id: roomId,
|
||||
session: session.pickle(this._pickleKey),
|
||||
keysClaimed: keysClaimed,
|
||||
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
|
||||
};
|
||||
});
|
||||
|
||||
this._cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, sessionData, txn,
|
||||
@@ -1224,6 +1225,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
forwardingCurve25519KeyChain: (
|
||||
sessionData.forwardingCurve25519KeyChain || []
|
||||
),
|
||||
untrusted: sessionData.untrusted,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
@@ -292,6 +292,11 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(keys).length === 0) {
|
||||
throw new Error(`Could not decrypt ${name} because none of ` +
|
||||
`the keys it is encrypted with are for a supported algorithm`);
|
||||
}
|
||||
|
||||
let keyId;
|
||||
let decryption;
|
||||
try {
|
||||
|
||||
@@ -1201,6 +1201,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
senderCurve25519Key: res.senderKey,
|
||||
claimedEd25519Key: res.keysClaimed.ed25519,
|
||||
forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain,
|
||||
untrusted: res.untrusted,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1548,8 +1549,11 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:crypto/OlmDevice.MegolmSessionData} session
|
||||
* @param {object} [opts={}] options for the import
|
||||
* @param {boolean} [opts.untrusted] whether the key should be considered as untrusted
|
||||
* @param {string} [opts.source] where the key came from
|
||||
*/
|
||||
MegolmDecryption.prototype.importRoomKey = function(session) {
|
||||
MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
||||
return this._olmDevice.addInboundGroupSession(
|
||||
session.room_id,
|
||||
session.sender_key,
|
||||
@@ -1558,8 +1562,9 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
|
||||
session.session_key,
|
||||
session.sender_claimed_keys,
|
||||
true,
|
||||
opts.untrusted ? { untrusted: opts.untrusted } : {},
|
||||
).then(() => {
|
||||
if (this._crypto.backupInfo) {
|
||||
if (this._crypto.backupInfo && opts.source !== "backup") {
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
session.room_id,
|
||||
|
||||
+280
-255
@@ -34,11 +34,11 @@ import {DeviceInfo} from "./deviceinfo";
|
||||
import * as algorithms from "./algorithms";
|
||||
import {
|
||||
CrossSigningInfo,
|
||||
CrossSigningLevel,
|
||||
DeviceTrustLevel,
|
||||
UserTrustLevel,
|
||||
createCryptoStoreCacheCallbacks,
|
||||
} from './CrossSigning';
|
||||
import {EncryptionSetupBuilder} from "./EncryptionSetup";
|
||||
import {SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage} from './SecretStorage';
|
||||
import {OutgoingRoomKeyRequestManager} from './OutgoingRoomKeyRequestManager';
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
@@ -49,11 +49,10 @@ import {
|
||||
} from './verification/QRCode';
|
||||
import {SAS} from './verification/SAS';
|
||||
import {keyFromPassphrase} from './key_passphrase';
|
||||
import {encodeRecoveryKey} from './recoverykey';
|
||||
import {encodeRecoveryKey, decodeRecoveryKey} from './recoverykey';
|
||||
import {VerificationRequest} from "./verification/request/VerificationRequest";
|
||||
import {InRoomChannel, InRoomRequests} from "./verification/request/InRoomChannel";
|
||||
import {ToDeviceChannel, ToDeviceRequests} from "./verification/request/ToDeviceChannel";
|
||||
import * as httpApi from "../http-api";
|
||||
import {IllegalMethod} from "./verification/IllegalMethod";
|
||||
import {KeySignatureUploadError} from "../errors";
|
||||
import {decryptAES, encryptAES} from './aes';
|
||||
@@ -450,11 +449,11 @@ Crypto.prototype.isCrossSigningReady = async function() {
|
||||
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
|
||||
* algorithm is found
|
||||
*
|
||||
* @param {function} [opts.authUploadDeviceSigningKeys] Optional. Function
|
||||
* @param {function} opts.authUploadDeviceSigningKeys Function
|
||||
* called to await an interactive auth flow when uploading device signing keys.
|
||||
* Args:
|
||||
* {function} A function that makes the request requiring auth. Receives the
|
||||
* auth data as an object.
|
||||
* auth data as an object. Can be called multiple times, first with an empty authDict, to obtain the flows.
|
||||
* @param {function} [opts.createSecretStorageKey] Optional. Function
|
||||
* called to await a secret storage key creation flow.
|
||||
* Returns:
|
||||
@@ -474,6 +473,7 @@ Crypto.prototype.isCrossSigningReady = async function() {
|
||||
* {Promise} A promise which resolves to key creation data for
|
||||
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
|
||||
*/
|
||||
|
||||
Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
authUploadDeviceSigningKeys,
|
||||
createSecretStorageKey = async () => ({ }),
|
||||
@@ -483,42 +483,22 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
getKeyBackupPassphrase,
|
||||
} = {}) {
|
||||
logger.log("Bootstrapping Secure Secret Storage");
|
||||
|
||||
// Create cross-signing keys if they don't exist, as we want to sign the SSSS default
|
||||
// key with the cross-signing master key. The cross-signing master key is also used
|
||||
// to verify the signature on the SSSS default key when adding secrets, so we
|
||||
// effectively need it for both reading and writing secrets.
|
||||
const crossSigningPrivateKeys = {};
|
||||
|
||||
// If we happen to reset cross-signing keys here, then we want access to the
|
||||
// cross-signing private keys, but only for the scope of this method, so we
|
||||
// use temporary callbacks to weave them through the various APIs.
|
||||
const appCallbacks = Object.assign({}, this._baseApis._cryptoCallbacks);
|
||||
const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks;
|
||||
const builder = new EncryptionSetupBuilder(
|
||||
this._baseApis.store.accountData,
|
||||
delegateCryptoCallbacks,
|
||||
);
|
||||
const secretStorage = new SecretStorage(
|
||||
builder.accountDataClientAdapter,
|
||||
builder.ssssCryptoCallbacks);
|
||||
const crossSigningInfo = new CrossSigningInfo(
|
||||
this._userId,
|
||||
builder.crossSigningCallbacks,
|
||||
builder.crossSigningCallbacks);
|
||||
|
||||
// the ID of the new SSSS key, if we create one
|
||||
let newKeyId = null;
|
||||
|
||||
// cache SSSS keys so that we don't need to constantly pester the user about it
|
||||
const ssssKeys = {};
|
||||
|
||||
this._baseApis._cryptoCallbacks.getSecretStorageKey =
|
||||
async ({keys}, name) => {
|
||||
// if we already have a key that works, return it
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (ssssKeys[keyId]) {
|
||||
return [keyId, ssssKeys[keyId]];
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, prompt the user and cache it
|
||||
const key = await appCallbacks.getSecretStorageKey({keys}, name);
|
||||
if (key) {
|
||||
const [keyId, keyData] = key;
|
||||
ssssKeys[keyId] = keyData;
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
// create a new SSSS key and set it as default
|
||||
const createSSSS = async (opts, privateKey) => {
|
||||
opts = opts || {};
|
||||
@@ -526,28 +506,46 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
opts.key = privateKey;
|
||||
}
|
||||
|
||||
const keyId = await this.addSecretStorageKey(
|
||||
const keyId = await secretStorage.addKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
|
||||
);
|
||||
await this.setDefaultSecretStorageKeyId(keyId);
|
||||
|
||||
if (privateKey) {
|
||||
// cache the private key so that we can access it again
|
||||
ssssKeys[keyId] = privateKey;
|
||||
// make the private key available to encrypt 4S secrets
|
||||
builder.ssssCryptoCallbacks.addPrivateKey(keyId, privateKey);
|
||||
}
|
||||
|
||||
await secretStorage.setDefaultKeyId(keyId);
|
||||
return keyId;
|
||||
};
|
||||
|
||||
// reset the cross-signing keys
|
||||
const resetCrossSigning = async () => {
|
||||
this._baseApis._cryptoCallbacks.saveCrossSigningKeys =
|
||||
keys => Object.assign(crossSigningPrivateKeys, keys);
|
||||
this._baseApis._cryptoCallbacks.getCrossSigningKey =
|
||||
name => crossSigningPrivateKeys[name];
|
||||
await this.resetCrossSigningKeys(
|
||||
CrossSigningLevel.MASTER,
|
||||
{ authUploadDeviceSigningKeys },
|
||||
);
|
||||
crossSigningInfo.resetKeys();
|
||||
// sign master key with device key
|
||||
await this._signObject(crossSigningInfo.keys.master);
|
||||
|
||||
await authUploadDeviceSigningKeys(authDict => {
|
||||
if (authDict) {
|
||||
builder.addCrossSigningKeys(authDict, crossSigningInfo.keys);
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
// This callback also gets called to obtain the IUA flows,
|
||||
// so do a call to obtain those if we don't have the authDict yet
|
||||
// We should get called again at a later point with the authDict.
|
||||
return this._baseApis.uploadDeviceSigningKeys(null, {});
|
||||
}
|
||||
});
|
||||
|
||||
// cross-sign own device
|
||||
const device = this._deviceList.getStoredDevice(this._userId, this._deviceId);
|
||||
const deviceSignature = await crossSigningInfo.signDevice(this._userId, device);
|
||||
builder.addKeySignature(this._userId, this._deviceId, deviceSignature);
|
||||
|
||||
if (keyBackupInfo) {
|
||||
await crossSigningInfo.signObject(keyBackupInfo.auth_data, "master");
|
||||
builder.addSessionBackup(keyBackupInfo);
|
||||
}
|
||||
};
|
||||
|
||||
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
|
||||
@@ -557,186 +555,185 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
);
|
||||
if (key) {
|
||||
const keyData = key[1];
|
||||
ssssKeys[keyId] = keyData;
|
||||
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyData);
|
||||
const {iv, mac} = await SecretStorage._calculateKeyCheck(keyData);
|
||||
keyInfo.iv = iv;
|
||||
keyInfo.mac = mac;
|
||||
|
||||
await this._baseApis.setAccountData(
|
||||
await builder.setAccountData(
|
||||
`m.secret_storage.key.${keyId}`, keyInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const oldSSSSKey = await this.getSecretStorageKey();
|
||||
const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
|
||||
const decryptionKeys =
|
||||
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
|
||||
const inStorage = !setupNewSecretStorage && decryptionKeys;
|
||||
const oldSSSSKey = await this.getSecretStorageKey();
|
||||
const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
|
||||
const decryptionKeys =
|
||||
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
|
||||
const inStorage = !setupNewSecretStorage && decryptionKeys;
|
||||
|
||||
if (!inStorage && !keyBackupInfo) {
|
||||
// either we don't have anything, or we've been asked to restart
|
||||
// from scratch
|
||||
logger.log(
|
||||
"Cross-signing private keys not found in secret storage, " +
|
||||
"creating new keys",
|
||||
);
|
||||
if (!inStorage && !keyBackupInfo) {
|
||||
// either we don't have anything, or we've been asked to restart
|
||||
// from scratch
|
||||
logger.log(
|
||||
"Cross-signing private keys not found in secret storage, " +
|
||||
"creating new keys",
|
||||
);
|
||||
|
||||
await resetCrossSigning();
|
||||
await resetCrossSigning();
|
||||
|
||||
if (
|
||||
setupNewSecretStorage ||
|
||||
!oldKeyInfo ||
|
||||
oldKeyInfo.algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES
|
||||
) {
|
||||
// if we already have a usable default SSSS key and aren't resetting SSSS just use it.
|
||||
// otherwise, create a new one
|
||||
// Note: we leave the old SSSS key in place: there could be other secrets using it, in theory.
|
||||
// We could move them to the new key but a) that would mean we'd need to prompt for the old
|
||||
// passphrase, and b) it's not clear that would be the right thing to do anyway.
|
||||
const { keyInfo, privateKey } = await createSecretStorageKey();
|
||||
newKeyId = await createSSSS(keyInfo, privateKey);
|
||||
}
|
||||
if (
|
||||
setupNewSecretStorage ||
|
||||
!oldKeyInfo ||
|
||||
oldKeyInfo.algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES
|
||||
) {
|
||||
// if we already have a usable default SSSS key and aren't resetting SSSS just use it.
|
||||
// otherwise, create a new one
|
||||
// Note: we leave the old SSSS key in place: there could be other secrets using it, in theory.
|
||||
// We could move them to the new key but a) that would mean we'd need to prompt for the old
|
||||
// passphrase, and b) it's not clear that would be the right thing to do anyway.
|
||||
const { keyInfo, privateKey } = await createSecretStorageKey();
|
||||
newKeyId = await createSSSS(keyInfo, privateKey);
|
||||
}
|
||||
} else if (!inStorage && keyBackupInfo) {
|
||||
// we have an existing backup, but no SSSS
|
||||
|
||||
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
|
||||
}
|
||||
} else if (!inStorage && keyBackupInfo) {
|
||||
// we have an existing backup, but no SSSS
|
||||
logger.log("Secret storage default key not found, using key backup key");
|
||||
|
||||
logger.log("Secret storage default key not found, using key backup key");
|
||||
// if we have the backup key already cached, use it; otherwise use the
|
||||
// callback to prompt for the key
|
||||
const backupKey = await this.getSessionBackupPrivateKey() ||
|
||||
await getKeyBackupPassphrase();
|
||||
|
||||
// if we have the backup key already cached, use it; otherwise use the
|
||||
// callback to prompt for the key
|
||||
const backupKey = await this.getSessionBackupPrivateKey() ||
|
||||
await getKeyBackupPassphrase();
|
||||
// create new cross-signing keys
|
||||
await resetCrossSigning();
|
||||
|
||||
// create new cross-signing keys
|
||||
await resetCrossSigning();
|
||||
// create a new SSSS key and use the backup key as the new SSSS key
|
||||
const opts = {};
|
||||
|
||||
// create a new SSSS key and use the backup key as the new SSSS key
|
||||
const opts = {};
|
||||
|
||||
if (
|
||||
keyBackupInfo.auth_data.private_key_salt &&
|
||||
keyBackupInfo.auth_data.private_key_iterations
|
||||
) {
|
||||
opts.passphrase = {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: keyBackupInfo.auth_data.private_key_iterations,
|
||||
salt: keyBackupInfo.auth_data.private_key_salt,
|
||||
bits: 256,
|
||||
};
|
||||
}
|
||||
|
||||
newKeyId = await createSSSS(opts, backupKey);
|
||||
|
||||
// store the backup key in secret storage
|
||||
await this.storeSecret(
|
||||
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
|
||||
);
|
||||
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// Sign the backup with the cross signing key so the key backup can
|
||||
// be trusted via cross-signing.
|
||||
logger.log("Adding cross signing signature to key backup");
|
||||
await this._crossSigningInfo.signObject(
|
||||
keyBackupInfo.auth_data, "master",
|
||||
);
|
||||
await this._baseApis._http.authedRequest(
|
||||
undefined, "PUT", "/room_keys/version/" + keyBackupInfo.version,
|
||||
undefined, keyBackupInfo,
|
||||
{prefix: httpApi.PREFIX_UNSTABLE},
|
||||
);
|
||||
} else if (!this._crossSigningInfo.getId()) {
|
||||
// we have SSSS, but we don't know if the server's cross-signing
|
||||
// keys should be trusted
|
||||
logger.log("Cross-signing private keys found in secret storage");
|
||||
|
||||
// fetch the private keys and set up our local copy of the keys for
|
||||
// use
|
||||
await this.checkOwnCrossSigningTrust();
|
||||
|
||||
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
// make sure that the default key has the information needed to
|
||||
// check the passphrase
|
||||
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
|
||||
}
|
||||
} else {
|
||||
// we have SSSS and we cross-signing is already set up
|
||||
logger.log("Cross signing keys are present in secret storage");
|
||||
|
||||
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
// make sure that the default key has the information needed to
|
||||
// check the passphrase
|
||||
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
|
||||
}
|
||||
if (
|
||||
keyBackupInfo.auth_data.private_key_salt &&
|
||||
keyBackupInfo.auth_data.private_key_iterations
|
||||
) {
|
||||
opts.passphrase = {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: keyBackupInfo.auth_data.private_key_iterations,
|
||||
salt: keyBackupInfo.auth_data.private_key_salt,
|
||||
bits: 256,
|
||||
};
|
||||
}
|
||||
|
||||
// If cross-signing keys were reset, store them in Secure Secret Storage.
|
||||
// This is done in a separate step so we can ensure secret storage has its
|
||||
// own key first.
|
||||
// XXX: We need to think about how to re-do these steps if they fail.
|
||||
// See also https://github.com/vector-im/riot-web/issues/11635
|
||||
if (Object.keys(crossSigningPrivateKeys).length) {
|
||||
logger.log("Storing cross-signing private keys in secret storage");
|
||||
// Assuming no app-supplied callback, default to storing in SSSS.
|
||||
if (!appCallbacks.saveCrossSigningKeys) {
|
||||
await CrossSigningInfo.storeInSecretStorage(
|
||||
crossSigningPrivateKeys,
|
||||
this._secretStorage,
|
||||
);
|
||||
}
|
||||
}
|
||||
newKeyId = await createSSSS(opts, backupKey);
|
||||
|
||||
if (setupNewKeyBackup && !keyBackupInfo) {
|
||||
const info = await this._baseApis.prepareKeyBackupVersion(
|
||||
null /* random key */,
|
||||
{ secureSecretStorage: true },
|
||||
);
|
||||
await this._baseApis.createKeyBackupVersion(info);
|
||||
}
|
||||
// store the backup key in secret storage
|
||||
await secretStorage.store(
|
||||
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
|
||||
);
|
||||
|
||||
// Call `getCrossSigningKey` for side effect of caching private keys for
|
||||
// future gossiping to other devices if enabled via app level callbacks.
|
||||
if (this._crossSigningInfo._cacheCallbacks) {
|
||||
for (const type of ["self_signing", "user_signing"]) {
|
||||
logger.log(`Cache ${type} cross-signing private key locally`);
|
||||
await this._crossSigningInfo.getCrossSigningKey(type);
|
||||
}
|
||||
}
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// Sign the backup with the cross signing key so the key backup can
|
||||
// be trusted via cross-signing.
|
||||
logger.log("Adding cross signing signature to key backup");
|
||||
await crossSigningInfo.signObject(
|
||||
keyBackupInfo.auth_data, "master",
|
||||
);
|
||||
builder.addSessionBackup(keyBackupInfo);
|
||||
} else if (!this._crossSigningInfo.getId()) {
|
||||
// we have SSSS, but we don't know if the server's cross-signing
|
||||
// keys should be trusted
|
||||
logger.log("Cross-signing private keys found in secret storage");
|
||||
|
||||
// and likewise for the session backup key
|
||||
const sessionBackupKey = await this.getSecret('m.megolm_backup.v1');
|
||||
if (sessionBackupKey) {
|
||||
logger.info("Got session backup key from secret storage: caching");
|
||||
// fix up the backup key if it's in the wrong format, and replace
|
||||
// in secret storage
|
||||
const fixedBackupKey = fixBackupKey(sessionBackupKey);
|
||||
if (fixedBackupKey) {
|
||||
await this.storeSecret(
|
||||
"m.megolm_backup.v1", fixedBackupKey, [newKeyId || oldKeyId],
|
||||
);
|
||||
}
|
||||
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
|
||||
fixedBackupKey || sessionBackupKey,
|
||||
));
|
||||
await this.storeSessionBackupPrivateKey(decodedBackupKey);
|
||||
// TODO: take this use case out of bootstrapping
|
||||
// fetch the private keys and set up our local copy of the keys for
|
||||
// use
|
||||
//
|
||||
// so if some other device resets the cross-signing keys,
|
||||
// we mark them as untrusted from _onDeviceListUserCrossSigningUpdated
|
||||
// you can either fix this by hitting the verify this session which (might?) call this method,
|
||||
// or the reset button in the settings
|
||||
await this.checkOwnCrossSigningTrust();
|
||||
|
||||
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
// make sure that the default key has the information needed to
|
||||
// check the passphrase
|
||||
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
|
||||
}
|
||||
} finally {
|
||||
// Restore the original callbacks. NB. we must do this by manipulating
|
||||
// the same object since the CrossSigning class has a reference to the
|
||||
// object, so if we assign the object here then our callbacks will change
|
||||
// but the instances of the CrossSigning class will be left with our
|
||||
// random, otherwise dead closures.
|
||||
for (const cb of Object.keys(this._baseApis._cryptoCallbacks)) {
|
||||
delete this._baseApis._cryptoCallbacks[cb];
|
||||
} else {
|
||||
// we have SSSS and we cross-signing is already set up
|
||||
logger.log("Cross signing keys are present in secret storage");
|
||||
|
||||
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
// make sure that the default key has the information needed to
|
||||
// check the passphrase
|
||||
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
|
||||
}
|
||||
Object.assign(this._baseApis._cryptoCallbacks, appCallbacks);
|
||||
}
|
||||
|
||||
const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys;
|
||||
if (crossSigningPrivateKeys.size) {
|
||||
logger.log("Storing cross-signing private keys in secret storage");
|
||||
// Assuming no app-supplied callback, default to storing in SSSS.
|
||||
if (!this._baseApis._cryptoCallbacks.saveCrossSigningKeys) {
|
||||
// this is writing to in-memory account data in builder.accountDataClientAdapter
|
||||
// so won't fail
|
||||
await CrossSigningInfo.storeInSecretStorage(
|
||||
crossSigningPrivateKeys,
|
||||
secretStorage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (setupNewKeyBackup && !keyBackupInfo) {
|
||||
const info = await this._baseApis.prepareKeyBackupVersion(
|
||||
null /* random key */,
|
||||
// don't write to secret storage, as it will write to this._secretStorage.
|
||||
// Here, we want to capture all the side-effects of bootstrapping,
|
||||
// and want to write to the local secretStorage object
|
||||
{ secureSecretStorage: false },
|
||||
);
|
||||
// write the key ourselves to 4S
|
||||
const privateKey = decodeRecoveryKey(info.recovery_key);
|
||||
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
|
||||
|
||||
// create keyBackupInfo object to add to builder
|
||||
const data = {
|
||||
algorithm: info.algorithm,
|
||||
auth_data: info.auth_data,
|
||||
};
|
||||
// sign with cross-sign master key
|
||||
await crossSigningInfo.signObject(data.auth_data, "master");
|
||||
// sign with the device fingerprint
|
||||
await this._signObject(data.auth_data);
|
||||
|
||||
|
||||
builder.addSessionBackup(data);
|
||||
}
|
||||
|
||||
// and likewise for the session backup key
|
||||
const sessionBackupKey = await secretStorage.get('m.megolm_backup.v1');
|
||||
if (sessionBackupKey) {
|
||||
logger.info("Got session backup key from secret storage: caching");
|
||||
// fix up the backup key if it's in the wrong format, and replace
|
||||
// in secret storage
|
||||
const fixedBackupKey = fixBackupKey(sessionBackupKey);
|
||||
if (fixedBackupKey) {
|
||||
await secretStorage.store("m.megolm_backup.v1",
|
||||
fixedBackupKey, [newKeyId || oldKeyId],
|
||||
);
|
||||
}
|
||||
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
|
||||
fixedBackupKey || sessionBackupKey,
|
||||
));
|
||||
await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
|
||||
}
|
||||
|
||||
const operation = builder.buildOperation();
|
||||
await operation.apply(this);
|
||||
// this persists private keys and public keys as trusted,
|
||||
// only do this if apply succeeded for now as retry isn't in place yet
|
||||
await builder.persist(this);
|
||||
|
||||
logger.log("Secure Secret Storage ready");
|
||||
};
|
||||
|
||||
@@ -897,57 +894,6 @@ Crypto.prototype.checkCrossSigningPrivateKey = function(privateKey, expectedPubl
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate new cross-signing keys.
|
||||
*
|
||||
* @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.
|
||||
* @param {function} [opts.authUploadDeviceSigningKeys] Optional. Function
|
||||
* called to await an interactive auth flow when uploading device signing keys.
|
||||
* Args:
|
||||
* {function} A function that makes the request requiring auth. Receives the
|
||||
* auth data as an object.
|
||||
*/
|
||||
Crypto.prototype.resetCrossSigningKeys = async function(level, {
|
||||
authUploadDeviceSigningKeys = async func => await func(),
|
||||
} = {}) {
|
||||
logger.info(`Resetting cross-signing keys at level ${level}`);
|
||||
// Copy old keys (usually empty) in case we need to revert
|
||||
const oldKeys = Object.assign({}, this._crossSigningInfo.keys);
|
||||
try {
|
||||
await this._crossSigningInfo.resetKeys(level);
|
||||
await this._signObject(this._crossSigningInfo.keys.master);
|
||||
|
||||
// send keys to server first before storing as trusted locally
|
||||
// to ensure upload succeeds
|
||||
const keys = {};
|
||||
for (const [name, key] of Object.entries(this._crossSigningInfo.keys)) {
|
||||
keys[name + "_key"] = key;
|
||||
}
|
||||
await authUploadDeviceSigningKeys(async authDict => {
|
||||
await this._baseApis.uploadDeviceSigningKeys(authDict, keys);
|
||||
});
|
||||
|
||||
// write a copy locally so we know these are trusted keys
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// If anything failed here, revert the keys so we know to try again from the start
|
||||
// next time.
|
||||
logger.error("Resetting cross-signing keys failed, revert to previous keys", e);
|
||||
this._crossSigningInfo.keys = oldKeys;
|
||||
throw e;
|
||||
}
|
||||
this._baseApis.emit("crossSigning.keysChanged", {});
|
||||
await this._afterCrossSigningLocalKeyChange();
|
||||
logger.info("Cross-signing key reset complete");
|
||||
};
|
||||
|
||||
/**
|
||||
* Run various follow-up actions after cross-signing keys have changed locally
|
||||
* (either by resetting the keys for the account or by getting them from secret
|
||||
@@ -2090,6 +2036,10 @@ Crypto.prototype.findVerificationRequestDMInProgress = function(roomId) {
|
||||
return this._inRoomVerificationRequests.findRequestInProgress(roomId);
|
||||
};
|
||||
|
||||
Crypto.prototype.getVerificationRequestsToDeviceInProgress = function(userId) {
|
||||
return this._toDeviceVerificationRequests.getRequestsInProgress(userId);
|
||||
};
|
||||
|
||||
Crypto.prototype.requestVerificationDM = function(userId, roomId) {
|
||||
const existingRequest = this._inRoomVerificationRequests.
|
||||
findRequestInProgress(roomId);
|
||||
@@ -2238,11 +2188,16 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
|
||||
|
||||
const forwardingChain = event.getForwardingCurve25519KeyChain();
|
||||
if (forwardingChain.length > 0) {
|
||||
// we got this event from somewhere else
|
||||
// we got the key this event from somewhere else
|
||||
// TODO: check if we can trust the forwarders.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (event.isKeySourceUntrusted()) {
|
||||
// we got the key for this event from a source that we consider untrusted
|
||||
return null;
|
||||
}
|
||||
|
||||
// senderKey is the Curve25519 identity key of the device which the event
|
||||
// was sent from. In the case of Megolm, it's actually the Curve25519
|
||||
// identity key of the device which set up the Megolm session.
|
||||
@@ -2281,6 +2236,76 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
|
||||
return device;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get information about the encryption of an event
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event event to be checked
|
||||
*
|
||||
* @return {object} An object with the fields:
|
||||
* - encrypted: whether the event is encrypted (if not encrypted, some of the
|
||||
* other properties may not be set)
|
||||
* - senderKey: the sender's key
|
||||
* - algorithm: the algorithm used to encrypt the event
|
||||
* - authenticated: whether we can be sure that the owner of the senderKey
|
||||
* sent the event
|
||||
* - sender: the sender's device information, if available
|
||||
* - mismatchedSender: if the event's ed25519 and curve25519 keys don't match
|
||||
* (only meaningful if `sender` is set)
|
||||
*/
|
||||
Crypto.prototype.getEventEncryptionInfo = function(event) {
|
||||
const ret = {};
|
||||
|
||||
ret.senderKey = event.getSenderKey();
|
||||
ret.algorithm = event.getWireContent().algorithm;
|
||||
|
||||
if (!ret.senderKey || !ret.algorithm) {
|
||||
ret.encrypted = false;
|
||||
return ret;
|
||||
}
|
||||
ret.encrypted = true;
|
||||
|
||||
const forwardingChain = event.getForwardingCurve25519KeyChain();
|
||||
if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
|
||||
// we got the key this event from somewhere else
|
||||
// TODO: check if we can trust the forwarders.
|
||||
ret.authenticated = false;
|
||||
} else {
|
||||
ret.authenticated = true;
|
||||
}
|
||||
|
||||
// senderKey is the Curve25519 identity key of the device which the event
|
||||
// was sent from. In the case of Megolm, it's actually the Curve25519
|
||||
// identity key of the device which set up the Megolm session.
|
||||
|
||||
ret.sender = this._deviceList.getDeviceByIdentityKey(
|
||||
ret.algorithm, ret.senderKey,
|
||||
);
|
||||
|
||||
// so far so good, but now we need to check that the sender of this event
|
||||
// hadn't advertised someone else's Curve25519 key as their own. We do that
|
||||
// by checking the Ed25519 claimed by the event (or, in the case of megolm,
|
||||
// the event which set up the megolm session), to check that it matches the
|
||||
// fingerprint of the purported sending device.
|
||||
//
|
||||
// (see https://github.com/vector-im/vector-web/issues/2215)
|
||||
|
||||
const claimedKey = event.getClaimedEd25519Key();
|
||||
if (!claimedKey) {
|
||||
logger.warn("Event " + event.getId() + " claims no ed25519 key: " +
|
||||
"cannot verify sending device");
|
||||
ret.mismatchedSender = true;
|
||||
}
|
||||
|
||||
if (ret.sender && claimedKey !== ret.sender.getFingerprint()) {
|
||||
logger.warn(
|
||||
"Event " + event.getId() + " claims ed25519 key " + claimedKey +
|
||||
"but sender device has key " + ret.sender.getFingerprint());
|
||||
ret.mismatchedSender = true;
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces the current outbound group session to be discarded such
|
||||
* that another one will be created next time an event is sent.
|
||||
@@ -2525,7 +2550,7 @@ Crypto.prototype.importRoomKeys = function(keys, opts = {}) {
|
||||
}
|
||||
|
||||
const alg = this._getRoomDecryptor(key.room_id, key.algorithm);
|
||||
return alg.importRoomKey(key).finally((r) => {
|
||||
return alg.importRoomKey(key, opts).finally((r) => {
|
||||
successes++;
|
||||
if (opts.progressCallback) { updateProgress(); }
|
||||
});
|
||||
|
||||
@@ -356,4 +356,12 @@ export class ToDeviceRequests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRequestsInProgress(userId) {
|
||||
const requestsByTxnId = this._requestsByUserId.get(userId);
|
||||
if (requestsByTxnId) {
|
||||
return Array.from(requestsByTxnId.values()).filter(r => r.pending);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,15 +25,17 @@ import {
|
||||
} from "../Error";
|
||||
import {QRCodeData, SCAN_QR_CODE_METHOD} from "../QRCode";
|
||||
|
||||
// the recommended amount of time before a verification request
|
||||
// should be (automatically) cancelled without user interaction
|
||||
// and ignored.
|
||||
const VERIFICATION_REQUEST_TIMEOUT = 10 * 60 * 1000; //10m
|
||||
// How long after the event's timestamp that the request times out
|
||||
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
// How long after we receive the event that the request times out
|
||||
const TIMEOUT_FROM_EVENT_RECEIPT = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
// to avoid almost expired verification notifications
|
||||
// from showing a notification and almost immediately
|
||||
// disappearing, also ignore verification requests that
|
||||
// are this amount of time away from expiring.
|
||||
const VERIFICATION_REQUEST_MARGIN = 3 * 1000; //3s
|
||||
const VERIFICATION_REQUEST_MARGIN = 3 * 1000; // 3 seconds
|
||||
|
||||
|
||||
export const EVENT_PREFIX = "m.key.verification.";
|
||||
@@ -80,6 +82,9 @@ export class VerificationRequest extends EventEmitter {
|
||||
// cross-signing identity reset between the .ready and .start event
|
||||
// and signing the wrong key after .start
|
||||
this._qrCodeData = null;
|
||||
|
||||
// The timestamp when we received the request event from the other side
|
||||
this._requestReceivedAt = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,12 +170,26 @@ export class VerificationRequest extends EventEmitter {
|
||||
return this._chosenMethod;
|
||||
}
|
||||
|
||||
calculateEventTimeout(event) {
|
||||
let effectiveExpiresAt = this.channel.getTimestamp(event)
|
||||
+ TIMEOUT_FROM_EVENT_TS;
|
||||
|
||||
if (this._requestReceivedAt && !this.initiatedByMe &&
|
||||
this.phase <= PHASE_REQUESTED
|
||||
) {
|
||||
const expiresAtByReceipt = this._requestReceivedAt
|
||||
+ TIMEOUT_FROM_EVENT_RECEIPT;
|
||||
effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt);
|
||||
}
|
||||
|
||||
return Math.max(0, effectiveExpiresAt - Date.now());
|
||||
}
|
||||
|
||||
/** The current remaining amount of ms before the request should be automatically cancelled */
|
||||
get timeout() {
|
||||
const requestEvent = this._getEventByEither(REQUEST_TYPE);
|
||||
if (requestEvent) {
|
||||
const elapsed = Date.now() - this.channel.getTimestamp(requestEvent);
|
||||
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
|
||||
return this.calculateEventTimeout(requestEvent);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -735,7 +754,7 @@ export class VerificationRequest extends EventEmitter {
|
||||
|
||||
_setupTimeout(phase) {
|
||||
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
|
||||
phase === PHASE_REQUESTED && this.initiatedByMe;
|
||||
phase === PHASE_REQUESTED;
|
||||
|
||||
if (shouldTimeout) {
|
||||
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
|
||||
@@ -754,7 +773,17 @@ export class VerificationRequest extends EventEmitter {
|
||||
|
||||
_cancelOnTimeout = () => {
|
||||
try {
|
||||
this.cancel({reason: "Other party didn't accept in time", code: "m.timeout"});
|
||||
if (this.initiatedByMe) {
|
||||
this.cancel({
|
||||
reason: "Other party didn't accept in time",
|
||||
code: "m.timeout",
|
||||
});
|
||||
} else {
|
||||
this.cancel({
|
||||
reason: "User didn't accept in time",
|
||||
code: "m.timeout",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error while cancelling verification request", err);
|
||||
}
|
||||
@@ -791,16 +820,8 @@ export class VerificationRequest extends EventEmitter {
|
||||
if (!isLiveEvent) {
|
||||
this._observeOnly = true;
|
||||
}
|
||||
// a timestamp is not provided on all to_device events
|
||||
const timestamp = this.channel.getTimestamp(event);
|
||||
if (Number.isFinite(timestamp)) {
|
||||
const elapsed = Date.now() - timestamp;
|
||||
// don't allow interaction on old requests
|
||||
if (elapsed > (VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN) ||
|
||||
elapsed < -(VERIFICATION_REQUEST_TIMEOUT / 2)
|
||||
) {
|
||||
this._observeOnly = true;
|
||||
}
|
||||
if (this.calculateEventTimeout(event) < VERIFICATION_REQUEST_MARGIN) {
|
||||
this._observeOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,6 +840,8 @@ export class VerificationRequest extends EventEmitter {
|
||||
this._eventsByThem.delete(type);
|
||||
}
|
||||
}
|
||||
// also remember when we received the request event
|
||||
this._requestReceivedAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -113,9 +113,9 @@ export function setCryptoStoreFactory(fac) {
|
||||
cryptoStoreFactory = fac;
|
||||
}
|
||||
|
||||
|
||||
interface ICreateClientOpts {
|
||||
baseUrl: string;
|
||||
idBaseUrl?: string;
|
||||
store?: Store;
|
||||
cryptoStore?: CryptoStore;
|
||||
scheduler?: MatrixScheduler;
|
||||
|
||||
+28
-1
@@ -144,6 +144,10 @@ export const MatrixEvent = function(
|
||||
*/
|
||||
this._forwardingCurve25519KeyChain = [];
|
||||
|
||||
/* where the decryption key is untrusted
|
||||
*/
|
||||
this._untrusted = null;
|
||||
|
||||
/* if we have a process decrypting this event, a Promise which resolves
|
||||
* when it is finished. Normally null.
|
||||
*/
|
||||
@@ -160,12 +164,16 @@ export const MatrixEvent = function(
|
||||
* so it can be easily accessed from the timeline.
|
||||
*/
|
||||
this.verificationRequest = null;
|
||||
|
||||
/* The txnId with which this event was sent if it was during this session,
|
||||
allows for a unique ID which does not change when the event comes back down sync.
|
||||
*/
|
||||
this._txnId = null;
|
||||
};
|
||||
utils.inherits(MatrixEvent, EventEmitter);
|
||||
|
||||
|
||||
utils.extend(MatrixEvent.prototype, {
|
||||
|
||||
/**
|
||||
* Get the event_id for this event.
|
||||
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
|
||||
@@ -599,6 +607,7 @@ utils.extend(MatrixEvent.prototype, {
|
||||
decryptionResult.claimedEd25519Key || null;
|
||||
this._forwardingCurve25519KeyChain =
|
||||
decryptionResult.forwardingCurve25519KeyChain || [];
|
||||
this._untrusted = decryptionResult.untrusted || false;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -689,6 +698,16 @@ utils.extend(MatrixEvent.prototype, {
|
||||
return this._forwardingCurve25519KeyChain;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the decryption key was obtained from an untrusted source. If so,
|
||||
* we cannot verify the authenticity of the message.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isKeySourceUntrusted: function() {
|
||||
return this._untrusted;
|
||||
},
|
||||
|
||||
getUnsigned: function() {
|
||||
return this.event.unsigned || {};
|
||||
},
|
||||
@@ -1070,6 +1089,14 @@ utils.extend(MatrixEvent.prototype, {
|
||||
setVerificationRequest: function(request) {
|
||||
this.verificationRequest = request;
|
||||
},
|
||||
|
||||
setTxnId(txnId) {
|
||||
this._txnId = txnId;
|
||||
},
|
||||
|
||||
getTxnId() {
|
||||
return this._txnId;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {EventEmitter} from "events";
|
||||
import {getHttpUriForMxc, getIdenticonUri} from "../content-repo";
|
||||
import {getHttpUriForMxc} from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
|
||||
/**
|
||||
@@ -274,10 +274,6 @@ RoomMember.prototype.getAvatarUrl =
|
||||
);
|
||||
if (httpUrl) {
|
||||
return httpUrl;
|
||||
} else if (allowDefault) {
|
||||
return getIdenticonUri(
|
||||
baseUrl, this.userId, width, height,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
+23
-16
@@ -68,9 +68,7 @@ export function RoomState(roomId, oobMemberFlags = undefined) {
|
||||
this.members = {
|
||||
// userId: RoomMember
|
||||
};
|
||||
this.events = {
|
||||
// eventType: { stateKey: MatrixEvent }
|
||||
};
|
||||
this.events = new Map(); // Map<eventType, Map<stateKey, MatrixEvent>>
|
||||
this.paginationToken = null;
|
||||
|
||||
this._sentinels = {
|
||||
@@ -211,14 +209,14 @@ RoomState.prototype.getSentinelMember = function(userId) {
|
||||
* <code>undefined</code>, else a single event (or null if no match found).
|
||||
*/
|
||||
RoomState.prototype.getStateEvents = function(eventType, stateKey) {
|
||||
if (!this.events[eventType]) {
|
||||
if (!this.events.has(eventType)) {
|
||||
// no match
|
||||
return stateKey === undefined ? [] : null;
|
||||
}
|
||||
if (stateKey === undefined) { // return all values
|
||||
return utils.values(this.events[eventType]);
|
||||
return Array.from(this.events.get(eventType).values());
|
||||
}
|
||||
const event = this.events[eventType][stateKey];
|
||||
const event = this.events.get(eventType).get(stateKey);
|
||||
return event ? event : null;
|
||||
};
|
||||
|
||||
@@ -238,9 +236,8 @@ RoomState.prototype.clone = function() {
|
||||
const status = this._oobMemberFlags.status;
|
||||
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
|
||||
|
||||
Object.values(this.events).forEach((eventsByStateKey) => {
|
||||
const eventsForType = Object.values(eventsByStateKey);
|
||||
copy.setStateEvents(eventsForType);
|
||||
Array.from(this.events.values()).forEach((eventsByStateKey) => {
|
||||
copy.setStateEvents(Array.from(eventsByStateKey.values()));
|
||||
});
|
||||
|
||||
// Ugly hack: see above
|
||||
@@ -276,8 +273,8 @@ RoomState.prototype.clone = function() {
|
||||
*/
|
||||
RoomState.prototype.setUnknownStateEvents = function(events) {
|
||||
const unknownStateEvents = events.filter((event) => {
|
||||
return this.events[event.getType()] === undefined ||
|
||||
this.events[event.getType()][event.getStateKey()] === undefined;
|
||||
return !this.events.has(event.getType()) ||
|
||||
!this.events.get(event.getType()).has(event.getStateKey());
|
||||
});
|
||||
|
||||
this.setStateEvents(unknownStateEvents);
|
||||
@@ -306,6 +303,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastStateEvent = self._getStateEventMatching(event);
|
||||
self._setStateEvent(event);
|
||||
if (event.getType() === "m.room.member") {
|
||||
_updateDisplayNameCache(
|
||||
@@ -313,7 +311,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
);
|
||||
_updateThirdPartyTokenCache(self, event);
|
||||
}
|
||||
self.emit("RoomState.events", event, self);
|
||||
self.emit("RoomState.events", event, self, lastStateEvent);
|
||||
});
|
||||
|
||||
// update higher level data structures. This needs to be done AFTER the
|
||||
@@ -385,10 +383,15 @@ RoomState.prototype._getOrCreateMember = function(userId, event) {
|
||||
};
|
||||
|
||||
RoomState.prototype._setStateEvent = function(event) {
|
||||
if (this.events[event.getType()] === undefined) {
|
||||
this.events[event.getType()] = {};
|
||||
if (!this.events.has(event.getType())) {
|
||||
this.events.set(event.getType(), new Map());
|
||||
}
|
||||
this.events[event.getType()][event.getStateKey()] = event;
|
||||
this.events.get(event.getType()).set(event.getStateKey(), event);
|
||||
};
|
||||
|
||||
RoomState.prototype._getStateEventMatching = function(event) {
|
||||
if (!this.events.has(event.getType())) return null;
|
||||
return this.events.get(event.getType()).get(event.getStateKey());
|
||||
};
|
||||
|
||||
RoomState.prototype._updateMember = function(member) {
|
||||
@@ -769,8 +772,12 @@ function _updateDisplayNameCache(roomState, userId, displayName) {
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomState} state The room state whose RoomState.events dictionary
|
||||
* was updated.
|
||||
* @param {MatrixEvent} prevEvent The event being replaced by the new state, if
|
||||
* known. Note that this can differ from `getPrevContent()` on the new state event
|
||||
* as this is the store's view of the last state, not the previous state provided
|
||||
* by the server.
|
||||
* @example
|
||||
* matrixClient.on("RoomState.events", function(event, state){
|
||||
* matrixClient.on("RoomState.events", function(event, state, prevEvent){
|
||||
* var newStateEvent = event;
|
||||
* });
|
||||
*/
|
||||
|
||||
+10
-7
@@ -23,7 +23,7 @@ limitations under the License.
|
||||
import {EventEmitter} from "events";
|
||||
import {EventTimelineSet} from "./event-timeline-set";
|
||||
import {EventTimeline} from "./event-timeline";
|
||||
import {getHttpUriForMxc, getIdenticonUri} from "../content-repo";
|
||||
import {getHttpUriForMxc} from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import {EventStatus, MatrixEvent} from "./event";
|
||||
import {RoomMember} from "./room-member";
|
||||
@@ -122,6 +122,10 @@ export function Room(roomId, client, myUserId, opts) {
|
||||
opts = opts || {};
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
||||
|
||||
// In some cases, we add listeners for every displayed Matrix event, so it's
|
||||
// common to have quite a few more than the default limit.
|
||||
this.setMaxListeners(100);
|
||||
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
|
||||
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
|
||||
@@ -814,10 +818,6 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
|
||||
return getHttpUriForMxc(
|
||||
baseUrl, mainUrl, width, height, resizeMethod,
|
||||
);
|
||||
} else if (allowDefault) {
|
||||
return getIdenticonUri(
|
||||
baseUrl, this.roomId, width, height,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1793,8 +1793,9 @@ Room.prototype.addAccountData = function(events) {
|
||||
if (event.getType() === "m.tag") {
|
||||
this.addTags(event);
|
||||
}
|
||||
const lastEvent = this.accountData[event.getType()];
|
||||
this.accountData[event.getType()] = event;
|
||||
this.emit("Room.accountData", event, this);
|
||||
this.emit("Room.accountData", event, this, lastEvent);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1991,8 +1992,10 @@ function memberNamesToRoomName(names, count = (names.length + 1)) {
|
||||
* @event module:client~MatrixClient#"Room.accountData"
|
||||
* @param {event} event The account_data event
|
||||
* @param {Room} room The room whose account_data was updated.
|
||||
* @param {MatrixEvent} prevEvent The event being replaced by
|
||||
* the new account data, if known.
|
||||
* @example
|
||||
* matrixClient.on("Room.accountData", function(event, room){
|
||||
* matrixClient.on("Room.accountData", function(event, room, oldEvent){
|
||||
* if (event.getType() === "m.room.colorscheme") {
|
||||
* applyColorScheme(event.getContents());
|
||||
* }
|
||||
|
||||
+9
-27
@@ -84,12 +84,15 @@ export function PushProcessor(client) {
|
||||
// $glob: RegExp,
|
||||
};
|
||||
|
||||
const matchingRuleFromKindSet = (ev, kindset, device) => {
|
||||
const matchingRuleFromKindSet = (ev, kindset) => {
|
||||
for (let ruleKindIndex = 0;
|
||||
ruleKindIndex < RULEKINDS_IN_ORDER.length;
|
||||
++ruleKindIndex) {
|
||||
const kind = RULEKINDS_IN_ORDER[ruleKindIndex];
|
||||
const ruleset = kindset[kind];
|
||||
if (!ruleset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
|
||||
const rule = ruleset[ruleIndex];
|
||||
@@ -97,7 +100,7 @@ export function PushProcessor(client) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawrule = templateRuleToRaw(kind, rule, device);
|
||||
const rawrule = templateRuleToRaw(kind, rule);
|
||||
if (!rawrule) {
|
||||
continue;
|
||||
}
|
||||
@@ -111,7 +114,7 @@ export function PushProcessor(client) {
|
||||
return null;
|
||||
};
|
||||
|
||||
const templateRuleToRaw = function(kind, tprule, device) {
|
||||
const templateRuleToRaw = function(kind, tprule) {
|
||||
const rawrule = {
|
||||
'rule_id': tprule.rule_id,
|
||||
'actions': tprule.actions,
|
||||
@@ -153,19 +156,12 @@ export function PushProcessor(client) {
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (device) {
|
||||
rawrule.conditions.push({
|
||||
'kind': 'device',
|
||||
'profile_tag': device,
|
||||
});
|
||||
}
|
||||
return rawrule;
|
||||
};
|
||||
|
||||
const eventFulfillsCondition = function(cond, ev) {
|
||||
const condition_functions = {
|
||||
"event_match": eventFulfillsEventMatchCondition,
|
||||
"device": eventFulfillsDeviceCondition,
|
||||
"contains_display_name": eventFulfillsDisplayNameCondition,
|
||||
"room_member_count": eventFulfillsRoomMemberCountCondition,
|
||||
"sender_notification_permission": eventFulfillsSenderNotifPermCondition,
|
||||
@@ -257,10 +253,6 @@ export function PushProcessor(client) {
|
||||
return content.body.search(pat) > -1;
|
||||
};
|
||||
|
||||
const eventFulfillsDeviceCondition = function(cond, ev) {
|
||||
return false; // XXX: Allow a profile tag to be set for the web client instance
|
||||
};
|
||||
|
||||
const eventFulfillsEventMatchCondition = function(cond, ev) {
|
||||
if (!cond.key) {
|
||||
return false;
|
||||
@@ -325,23 +317,13 @@ export function PushProcessor(client) {
|
||||
};
|
||||
|
||||
const matchingRuleForEventWithRulesets = function(ev, rulesets) {
|
||||
if (!rulesets || !rulesets.device) {
|
||||
if (!rulesets) {
|
||||
return null;
|
||||
}
|
||||
if (ev.getSender() == client.credentials.userId) {
|
||||
if (ev.getSender() === client.credentials.userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allDevNames = Object.keys(rulesets.device);
|
||||
for (let i = 0; i < allDevNames.length; ++i) {
|
||||
const devname = allDevNames[i];
|
||||
const devrules = rulesets.device[devname];
|
||||
|
||||
const matchingRule = matchingRuleFromKindSet(devrules, devname);
|
||||
if (matchingRule) {
|
||||
return matchingRule;
|
||||
}
|
||||
}
|
||||
return matchingRuleFromKindSet(ev, rulesets.global);
|
||||
};
|
||||
|
||||
@@ -392,7 +374,7 @@ export function PushProcessor(client) {
|
||||
* @return {object} The push rule, or null if no such rule was found
|
||||
*/
|
||||
this.getPushRuleById = function(ruleId) {
|
||||
for (const scope of ['device', 'global']) {
|
||||
for (const scope of ['global']) {
|
||||
if (client.pushRules[scope] === undefined) continue;
|
||||
|
||||
for (const kind of RULEKINDS_IN_ORDER) {
|
||||
|
||||
+7
-2
@@ -1023,18 +1023,23 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
// handle non-room account_data
|
||||
if (data.account_data && utils.isArray(data.account_data.events)) {
|
||||
const events = data.account_data.events.map(client.getEventMapper());
|
||||
const prevEventsMap = events.reduce((m, c) => {
|
||||
m[c.getId()] = client.store.getAccountData(c.getType());
|
||||
return m;
|
||||
}, {});
|
||||
client.store.storeAccountDataEvents(events);
|
||||
events.forEach(
|
||||
function(accountDataEvent) {
|
||||
// Honour push rules that come down the sync stream but also
|
||||
// honour push rules that were previously cached. Base rules
|
||||
// will be updated when we recieve push rules via getPushRules
|
||||
// will be updated when we receive push rules via getPushRules
|
||||
// (see SyncApi.prototype.sync) before syncing over the network.
|
||||
if (accountDataEvent.getType() === 'm.push_rules') {
|
||||
const rules = accountDataEvent.getContent();
|
||||
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
|
||||
}
|
||||
client.emit("accountData", accountDataEvent);
|
||||
const prevEvent = prevEventsMap[accountDataEvent.getId()];
|
||||
client.emit("accountData", accountDataEvent, prevEvent);
|
||||
return accountDataEvent;
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user