Compare commits

...

47 Commits

Author SHA1 Message Date
RiotRobot 25e5d79cf6 v29.0.0 2023-10-10 09:13:04 +01:00
RiotRobot 6c8e3d0707 Prepare changelog for v29.0.0 2023-10-10 09:13:01 +01:00
RiotRobot 53a72df01b v29.0.0-rc.1 2023-10-03 11:40:01 +01:00
RiotRobot 75e710d93e Prepare changelog for v29.0.0-rc.1 2023-10-03 11:39:58 +01:00
RiotRobot 1457ab0cf4 Revert "v29.0.0-rc.1"
This reverts commit f01037fe0d.
2023-10-03 11:39:12 +01:00
RiotRobot 14aafb7977 Revert "Prepare changelog for v29.0.0-rc.1"
This reverts commit 2cda6655d7.
2023-10-03 11:39:03 +01:00
Andy Balaam 90d00b863f Merge pull request #3772 from matrix-org/andybalaam/manually-backport-remove-dist-from-package
Remove the dist script since we no longer have a browserify build
2023-10-03 11:34:08 +01:00
Andy Balaam 5f0ada9578 Remove the dist script since we no longer have a browserify build 2023-10-03 11:32:59 +01:00
RiotRobot f01037fe0d v29.0.0-rc.1 2023-10-03 11:08:34 +01:00
RiotRobot 2cda6655d7 Prepare changelog for v29.0.0-rc.1 2023-10-03 11:08:31 +01:00
Michael Telatynski 68317ac836 Remove browserify builds (#3759) 2023-10-03 10:23:11 +01:00
Michael Telatynski 5c45c980e9 Update cypress.yml 2023-10-03 08:40:40 +01:00
Damon (Toal-Rossi) Vestervand 66251e0855 Use globalThis instead of global (#3763)
Switches use of `global` to `globalThis`, which is better supported when building with modern build tools like Vite.

Refs #2903

Signed-off-by: Damon Vestervand <damon@beyondwork.ai>
Signed-off-by: Damon <damon@vestervand.net>
2023-10-02 12:04:05 +00:00
Richard van der Hoff ff53557957 Clean up integ tests for incoming user verification (#3758)
Move the tests into verification.spec.ts, enable for both stacks, and other cleanups.
2023-09-29 17:26:24 +01:00
Richard van der Hoff 126352afd5 Bump matrix-sdk-crypto-wasm to 1.3.0 (#3757) 2023-09-29 17:25:33 +01:00
Hugh Nimmo-Smith f33da83d90 Support for stable MSC3882 get_login_token (#3416)
* Support for stable MSC3882 get_login_token

* Make changes non-breaking by deprecation

* Update src/@types/auth.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update spec/integ/matrix-client-methods.spec.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Suggestions from review

* Update src/client.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Fix and test prefix behaviour

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2023-09-29 13:14:22 +00:00
Valere 74193ad057 Implement exportCrossSigningKeysToStorage (#3731)
* Implement exportCrossSigningKeysToStorage

* fix bootstrap cross signing

* Update src/rust-crypto/CrossSigningIdentity.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/rust-crypto/CrossSigningIdentity.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/rust-crypto/CrossSigningIdentity.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update spec/unit/rust-crypto/CrossSigningIdentity.spec.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* code review

* Update src/rust-crypto/CrossSigningIdentity.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-09-27 09:07:02 +00:00
Kerry c672cad1a1 remove IsUserMention and IsRoomMention from DEFAULT_OVERRIDE_RULES (#3752) 2023-09-26 21:33:41 +00:00
Andy Balaam d59bb240fa Merge pull request #3746 from matrix-org/valere/element-r/backup/restore_test
fix restoreKeyBackupWithSecretStorage for rust
2023-09-26 12:42:47 +01:00
RiotRobot 65d988734e Resetting package fields for development 2023-09-26 12:20:14 +01:00
RiotRobot 4a402f0bd7 Merge branch 'master' into develop 2023-09-26 12:20:05 +01:00
RiotRobot a491508543 v28.2.0 2023-09-26 12:07:30 +01:00
RiotRobot 0abba3e626 Prepare changelog for v28.2.0 2023-09-26 12:07:26 +01:00
Valere 9fed45e47c quick test if no crypto 2023-09-26 12:05:27 +02:00
Valere fe67a68c95 fix typo 2023-09-26 09:13:19 +02:00
valere 4d3d4028a0 Merge branch 'develop' into valere/element-r/backup/restore_test 2023-09-26 09:05:03 +02:00
Robin 8f901590ff Fix a case where joinRoom creates a duplicate Room object (#3747)
When calling MatrixClient.joinRoom with a room alias, the method would create a new Room object, even if you were already present in that room. This changes its behavior to no-op, as the doc comment promises.
2023-09-25 18:07:51 +00:00
Michael Telatynski d29b8520f7 Update cypress.yml 2023-09-25 17:17:26 +01:00
Richard van der Hoff 37e1fd9af5 Another attempt at fixing the Cypress job (#3749) 2023-09-25 15:58:38 +01:00
Valere 76dbc7500f Update spec/integ/crypto/crypto.spec.ts
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-09-25 16:34:25 +02:00
Valere 3664f8c3c2 Update spec/integ/crypto/crypto.spec.ts
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-09-25 16:34:17 +02:00
Valere d0a10497bb Update spec/integ/crypto/crypto.spec.ts
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-09-25 16:34:10 +02:00
David Baker 6385c9c0da Add membershipID to call memberships (#3745)
* Add membershipID to call memberships

This allows us to recognise easily when a membership is from some
previous sessions rather than our own and therefore ignore it
(see comment for more).

This was causing us to see existing, expired membership events and
bump the expiry on them rather than send a new membership. This might
have been okay if we bumped them enough to actually make them un-expired,
but it's a fresh session so semanticly we want to post a fresh membership
rather than resurrecting a previous, expired membership.

* Fix test types

* Fix tests

* Make test coverage happy

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-09-22 15:31:17 +00:00
valere cecac3152e Merge branch 'develop' into valere/element-r/backup/restore_test 2023-09-22 14:09:35 +02:00
Valere f6c99b1d25 fix restoreKeyBackupWithSecretStorage for rust 2023-09-22 13:28:05 +02:00
Richard van der Hoff 407ec4d67a Add github action to mark cypress tests skipped (#3744) 2023-09-22 09:22:00 +00:00
Valere 4947a0cb64 Implement isSecretStorageReady in rust (#3730)
* Implement isSecretStorageReady in rust

* refactor extract common code to check 4S access

* fix incomplete mocks

* code review

* Remove keyId para from secretStorageCanAccessSecrets

* use map instead of array

* code review
2023-09-21 16:55:41 +00:00
Richard van der Hoff f134d6db01 Fix the message for messages from unknown devices (#3743) 2023-09-21 08:34:34 +00:00
Malte Finsterwalder fde6cebc20 Stop keep alive, when sync was stoped (#3720)
* T-Defect: stop keep alive, when sync was stoped

Signed-off-by: Malte Finsterwalder <malte@holi.team>

* T-Defect: add tests for keep alive

Signed-off-by: Malte Finsterwalder <malte@holi.team>

* fix copyright year

Signed-off-by: Malte Finsterwalder <malte@holi.team>

* fix copyright

Signed-off-by: Malte Finsterwalder <malte@holi.team>

---------

Signed-off-by: Malte Finsterwalder <malte@holi.team>
Co-authored-by: Malte Finsterwalder <malte@holi.team>
2023-09-20 16:44:27 +00:00
Richard van der Hoff 425cf6b91e Element-R: use the pickleKey to encrypt the crypto store (#3732)
* Element-R: use the pickleKey to encrypt the crypto store

`pickleKey` is a passphrase set by the application for this express purpose.

* update tests

* fix tests, again
2023-09-20 11:35:32 +00:00
renovate[bot] a3e273d6f1 Update all non-major dependencies (#3735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-20 10:26:16 +00:00
renovate[bot] b1a3b264e5 Update SimenB/github-actions-cpu-cores action to v2 (#3741)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-20 10:25:40 +00:00
renovate[bot] 053643a8ba Update babel monorepo to v7.22.20 (#3737)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-19 19:38:40 +00:00
Richard van der Hoff d2ea149012 Undeprecate MatrixClient.getKeyBackupVersion (#3733)
... and add some better documentation while we're at it.
2023-09-19 17:33:41 +00:00
Richard van der Hoff 23d244520c Sort imports in MatrixClient (#3734)
My IDE keeps trying to do this, so let's just do it in its own PR
2023-09-19 17:32:58 +00:00
renovate[bot] 267b52099b Update definitelyTyped (#3736)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-19 16:18:45 +00:00
renovate[bot] 430fd5660a Update dependency @types/jest to v29.5.5 (#3738)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-19 16:18:08 +00:00
52 changed files with 1550 additions and 2169 deletions
+29 -1
View File
@@ -15,7 +15,12 @@ concurrency:
jobs:
cypress:
name: Cypress
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.79.0
# We only want to run the cypress tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume 4 concurrent runners.
if: github.event.workflow_run.event == 'merge_group'
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@66854039a33ed6cfe1fc635ff2daa8bb261c0b56
permissions:
actions: read
issues: read
@@ -29,3 +34,26 @@ jobs:
with:
react-sdk-repository: matrix-org/matrix-react-sdk
rust-crypto: true
# We want to make the cypress tests a required check for the merge queue.
#
# Unfortunately, github doesn't distinguish between "checks needed for branch
# protection" (ie, the things that must pass before the PR will even be added
# to the merge queue) and "checks needed in the merge queue". We just have to add
# the check to the branch protection list.
#
# Ergo, if we know we're not going to run the cypress tests, we need to add a
# passing status check manually.
mark_skipped:
if: github.event.workflow_run.event != 'merge_group'
permissions:
statuses: write
runs-on: ubuntu-latest
steps:
- uses: Sibz/github-status-action@650dd1a882a76dbbbc4576fb5974b8d22f29847f # v1.1.6
with:
authToken: "${{ secrets.GITHUB_TOKEN }}"
state: success
description: Cypress skipped
context: "${{ github.workflow }} / cypress"
sha: "${{ github.event.workflow_run.head_sha }}"
+3 -4
View File
@@ -1,11 +1,10 @@
name: Build downstream artifacts
on:
# We only want the Rust Crypto Cypress tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume 4 concurrent runners.
# Anyone working on Rust Crypto is able to run the tests locally if required.
merge_group:
types: [checks_requested]
pull_request: {}
# For now at least, we don't run this or the cypress-tests against pushes
# to develop or master.
#
@@ -21,7 +20,7 @@ concurrency:
jobs:
build-element-web:
name: Build element-web
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.79.0
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.80.1
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@5a85faf05d2ade2d5b6682bfe5359915d5159c6c # v2.2.1
uses: JS-DevTools/npm-publish@fe72237be0920f7a0cafd6a966c9b929c9466e9b # v2.2.2
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.5
uses: matrix-org/sonarcloud-workflow-action@v2.6
# workflow_run fails report against the develop commit always, we don't want that for PRs
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
with:
+2 -6
View File
@@ -17,7 +17,7 @@ jobs:
timeout-minutes: 10
strategy:
matrix:
specs: [browserify, integ, unit]
specs: [integ, unit]
node: [18, latest]
steps:
- name: Checkout code
@@ -32,13 +32,9 @@ jobs:
- name: Install dependencies
run: "yarn install"
- name: Build
if: matrix.specs == 'browserify'
run: "yarn build"
- name: Get number of CPU cores
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@410541432439795d30db6501fb1d8178eb41e502 # v1
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2
- name: Run tests
run: |
+19 -2
View File
@@ -1,5 +1,22 @@
Changes in [28.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.2.0-rc.1) (2023-09-19)
============================================================================================================
Changes in [29.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v29.0.0) (2023-10-10)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove browserify builds ([\#3759](https://github.com/matrix-org/matrix-js-sdk/pull/3759)).
## ✨ Features
* Export AutoDiscoveryError and fix type of ALL_ERRORS ([\#3768](https://github.com/matrix-org/matrix-js-sdk/pull/3768)).
* Support for stable MSC3882 get_login_token ([\#3416](https://github.com/matrix-org/matrix-js-sdk/pull/3416)). Contributed by @hughns.
* Remove IsUserMention and IsRoomMention from DEFAULT_OVERRIDE_RULES ([\#3752](https://github.com/matrix-org/matrix-js-sdk/pull/3752)). Contributed by @kerryarchibald.
## 🐛 Bug Fixes
* Fix a case where joinRoom creates a duplicate Room object ([\#3747](https://github.com/matrix-org/matrix-js-sdk/pull/3747)).
* Add membershipID to call memberships ([\#3745](https://github.com/matrix-org/matrix-js-sdk/pull/3745)).
* Fix the warning for messages from unsigned devices ([\#3743](https://github.com/matrix-org/matrix-js-sdk/pull/3743)).
* Stop keep alive, when sync was stoped ([\#3720](https://github.com/matrix-org/matrix-js-sdk/pull/3720)). Contributed by @finsterwalder.
Changes in [28.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.2.0) (2023-09-26)
==================================================================================================
## 🦖 Deprecations
* Implement `getEncryptionInfoForEvent` and deprecate `getEventEncryptionInfo` ([\#3693](https://github.com/matrix-org/matrix-js-sdk/pull/3693)).
+1 -16
View File
@@ -23,19 +23,7 @@ endpoints from before Matrix 1.1, for example.
## In a browser
### Note, the browserify build has been deprecated. Please use a bundler like webpack or vite instead.
Download the browser version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
`<script>` to your page. There will be a global variable `matrixcs`
attached to `window` through which you can access the SDK. See below for how to
include libolm to enable end-to-end-encryption.
The browser bundle supports recent versions of browsers. Typically this is ES2015
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
[browserlists](https://github.com/browserslist/browserslist).
Please check [the working browser example](examples/browser) for more information.
### Note, the browserify build has been removed. Please use a bundler like webpack or vite instead.
## In Node.js
@@ -375,9 +363,6 @@ To run tests (Jest):
$ yarn test
```
> **Note**
> The `sync-browserify.spec.ts` requires a browser build (`yarn build`) in order to pass
To run linting:
```
-10
View File
@@ -1,10 +0,0 @@
To try it out, **you must build the SDK first** and then host this folder:
```
$ yarn install
$ yarn build
$ cd examples/browser
$ python -m http.server 8003
```
Then visit `http://localhost:8003`.
-9
View File
@@ -1,9 +0,0 @@
console.log("Loading browser sdk");
var client = matrixcs.createClient({ baseUrl: "https://matrix.org" });
client.publicRooms().then(function (data) {
console.log("data %s [...]", JSON.stringify(data).substring(0, 100));
console.log("Congratulations! The SDK is working on the browser!");
var result = document.getElementById("result");
result.innerHTML = "<p>The SDK appears to be working correctly.</p>";
});
-17
View File
@@ -1,17 +0,0 @@
<html lang="en">
<head>
<title>Test</title>
<meta charset="utf-8" />
<link rel="icon" href="data:," />
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<body>
Sanity Testing (check the console) : This example is here to make sure that the SDK works inside a browser. It
simply does a GET /publicRooms on matrix.org
<br />
You should see a message confirming that the SDK works below:
<br />
<div id="result"></div>
</body>
</html>
-1
View File
@@ -1 +0,0 @@
../../../dist/browser-matrix.js
-2
View File
@@ -1,2 +0,0 @@
olm.js
olm.wasm
-1
View File
@@ -1 +0,0 @@
../../../dist/browser-matrix.js
@@ -1,60 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Test Crypto in Browser</title>
<script src="lib/olm.js"></script>
<script src="lib/matrix.js"></script>
</head>
<body>
<h1>Testing export/import of Olm devices in the browser</h1>
<ul>
<li>Make sure you built the current version of the Matrix JS SDK (<code>yarn build</code>)</li>
<li>
copy <code>olm.js</code> and <code>olm.wasm</code> from a recent release of Olm (was tested with version
3.1.4) in directory <code>lib/</code>
</li>
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
<li>
in the JS console, do:
<pre>
aliceMatrixClient = await newMatrixClient("alice-"+randomHex());
await aliceMatrixClient.exportDevice();
await aliceMatrixClient.getAccessToken();
</pre
>
</li>
<li>
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere (<strong
>not</strong
>
in a JS variable as it will be destroyed when you refresh the page)
</li>
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
<li>
Do the following, replacing <code>ALICE_ID</code>
with the user ID of Alice (you can find it in the exported data)
<pre>
bobMatrixClient = await newMatrixClient("bob-"+randomHex());
roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]);
await bobMatrixClient.sendTextMessage('Hi Alice!', roomId);
</pre
>
</li>
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
<li>
Now do the following, using the exported data and the access token you saved previously:
<pre>
aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN);
</pre
>
</li>
<li>You should see the message sent by Bob printed in the console.</li>
</ul>
<script src="olm-device-export-import.js"></script>
</body>
</html>
@@ -1,105 +0,0 @@
if (!Olm) {
console.error("global.Olm does not seem to be present." + " Did you forget to add olm in the lib/ directory?");
}
const BASE_URL = "http://localhost:8008";
const ROOM_CRYPTO_CONFIG = { algorithm: "m.megolm.v1.aes-sha2" };
const PASSWORD = "password";
// useful to create new usernames
window.randomHex = () => Math.floor(Math.random() * 10 ** 6).toString(16);
window.newMatrixClient = async function (username) {
const registrationClient = matrixcs.createClient(BASE_URL);
const userRegisterResult = await registrationClient.register(username, PASSWORD, null, { type: "m.login.dummy" });
const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL,
userId: userRegisterResult.user_id,
accessToken: userRegisterResult.access_token,
deviceId: userRegisterResult.device_id,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
});
extendMatrixClient(matrixClient);
await matrixClient.initCrypto();
await matrixClient.startClient();
return matrixClient;
};
window.importMatrixClient = async function (exportedDevice, accessToken) {
const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL,
deviceToImport: exportedDevice,
accessToken,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
});
extendMatrixClient(matrixClient);
await matrixClient.initCrypto();
await matrixClient.startClient();
return matrixClient;
};
function extendMatrixClient(matrixClient) {
// automatic join
matrixClient.on("RoomMember.membership", async (event, member) => {
if (member.membership === "invite" && member.userId === matrixClient.getUserId()) {
await matrixClient.joinRoom(member.roomId);
// setting up of room encryption seems to be triggered automatically
// but if we don't wait for it the first messages we send are unencrypted
await matrixClient.setRoomEncryption(member.roomId, { algorithm: "m.megolm.v1.aes-sha2" });
}
});
matrixClient.onDecryptedMessage = (message) => {
console.log("Got encrypted message: ", message);
};
matrixClient.on("Event.decrypted", (event) => {
if (event.getType() === "m.room.message") {
matrixClient.onDecryptedMessage(event.getContent().body);
} else {
console.log("decrypted an event of type", event.getType());
console.log(event);
}
});
matrixClient.createEncryptedRoom = async function (usersToInvite) {
const { room_id: roomId } = await this.createRoom({
visibility: "private",
invite: usersToInvite,
});
// matrixClient.setRoomEncryption() only updates local state
// but does not send anything to the server
// (see https://github.com/matrix-org/matrix-js-sdk/issues/905)
// so we do it ourselves with 'sendStateEvent'
await this.sendStateEvent(roomId, "m.room.encryption", ROOM_CRYPTO_CONFIG);
await this.setRoomEncryption(roomId, ROOM_CRYPTO_CONFIG);
// Marking all devices as verified
let room = this.getRoom(roomId);
let members = (await room.getEncryptionTargetMembers()).map((x) => x["userId"]);
let memberkeys = await this.downloadKeys(members);
for (const userId in memberkeys) {
for (const deviceId in memberkeys[userId]) {
await this.setDeviceVerified(userId, deviceId);
}
}
return roomId;
};
matrixClient.sendTextMessage = async function (message, roomId) {
return matrixClient.sendMessage(roomId, {
body: message,
msgtype: "m.text",
});
};
}
+4 -30
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "28.2.0-rc.1",
"version": "29.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=18.0.0"
@@ -8,14 +8,11 @@
"scripts": {
"prepublishOnly": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
"clean": "rimraf lib dist",
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
"clean": "rimraf lib",
"build": "yarn build:dev",
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdir dist && BROWSERIFYSWAP_ENV='no-rust-crypto' browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "typedoc",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
@@ -42,7 +39,6 @@
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
"dist",
"lib",
"src",
"git-revision.txt",
@@ -97,11 +93,9 @@
"allchange": "^1.0.6",
"babel-jest": "^29.0.0",
"babelify": "^10.0.0",
"browserify": "^17.0.0",
"browserify-swap": "^0.2.2",
"debug": "^4.3.4",
"domexception": "^4.0.0",
"eslint": "8.48.0",
"eslint": "8.49.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.5.1",
@@ -137,25 +131,5 @@
"outputName": "jest-sonar-report.xml",
"relativePaths": true
},
"browserify": {
"transform": [
"browserify-swap",
[
"babelify",
{
"sourceMaps": "inline",
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
]
}
]
]
},
"browserify-swap": {
"no-rust-crypto": {
"src/rust-crypto/index.ts$": "./src/rust-crypto/browserify-index.ts"
}
},
"typings": "./lib/index.d.ts"
}
-34
View File
@@ -1,34 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import type { default as BrowserMatrix } from "../../src/browser-index";
// stub for browser-matrix browserify tests
// @ts-ignore
global.XMLHttpRequest = jest.fn();
afterAll(() => {
// clean up XMLHttpRequest mock
// @ts-ignore
global.XMLHttpRequest = undefined;
});
// Akin to spec/setupTests.ts - but that won't affect the browser-matrix bundle
global.matrixcs = {
...global.matrixcs,
timeoutSignal: () => new AbortController().signal,
} as typeof BrowserMatrix;
-92
View File
@@ -1,92 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import "./setupTests"; // uses browser-matrix instead of the src
import type { MatrixClient } from "../../src";
const USER_ID = "@user:test.server";
const DEVICE_ID = "device_id";
const ACCESS_TOKEN = "access_token";
const ROOM_ID = "!room_id:server.test";
describe("Browserify Test", function () {
let client: MatrixClient;
let httpBackend: HttpBackend;
beforeEach(() => {
httpBackend = new HttpBackend();
client = new global.matrixcs.MatrixClient({
baseUrl: "http://test.server",
userId: USER_ID,
accessToken: ACCESS_TOKEN,
deviceId: DEVICE_ID,
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
});
afterEach(async () => {
client.stopClient();
client.http.abort();
httpBackend.verifyNoOutstandingRequests();
httpBackend.verifyNoOutstandingExpectation();
await httpBackend.stop();
});
it("Sync", async () => {
const event = {
type: "m.room.member",
room_id: ROOM_ID,
content: {
membership: "join",
name: "Displayname",
},
event_id: "$foobar",
};
const syncData = {
next_batch: "batch1",
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [event],
limited: false,
},
},
},
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, syncData);
const syncPromise = new Promise((r) => client.once(global.matrixcs.ClientEvent.Sync, r));
const unexpectedErrorFn = jest.fn();
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);
client.startClient();
await httpBackend.flushAllExpected();
await syncPromise;
expect(unexpectedErrorFn).not.toHaveBeenCalled();
}, 20000); // additional timeout as this test can take quite a while
});
+182 -460
View File
@@ -22,23 +22,22 @@ import { IDBFactory } from "fake-indexeddb";
import { MockResponse, MockResponseFunction } from "fetch-mock";
import Olm from "@matrix-org/olm";
import type { IDeviceKeys } from "../../../src/@types/crypto";
import * as testUtils from "../../test-utils/test-utils";
import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import * as testData from "../../test-utils/test-data";
import {
BOB_SIGNED_CROSS_SIGNING_KEYS_DATA,
BOB_SIGNED_TEST_DEVICE_DATA,
BOB_TEST_USER_ID,
SIGNED_CROSS_SIGNING_KEYS_DATA,
SIGNED_TEST_DEVICE_DATA,
TEST_ROOM_ID,
TEST_ROOM_ID as ROOM_ID,
TEST_USER_ID,
} from "../../test-utils/test-data";
import { TestClient } from "../../TestClient";
import { logger } from "../../../src/logger";
import {
Category,
ClientEvent,
createClient,
CryptoEvent,
IClaimOTKsResult,
@@ -46,7 +45,6 @@ import {
IDownloadKeyResult,
IEvent,
IndexedDBCryptoStore,
IRoomEvent,
IStartClientOpts,
MatrixClient,
MatrixEvent,
@@ -57,8 +55,7 @@ import {
RoomStateEvent,
} from "../../../src/matrix";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import * as testData from "../../test-utils/test-data";
import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
import { escapeRegExp } from "../../../src/utils";
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
@@ -68,10 +65,21 @@ import {
mockSetupCrossSigningRequests,
mockSetupMegolmBackupRequests,
} from "../../test-utils/mockEndpoints";
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
import { CrossSigningKey, CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { DecryptionError } from "../../../src/crypto/algorithms";
import { IKeyBackup } from "../../../src/crypto/backup";
import {
createOlmSession,
createOlmAccount,
encryptGroupSessionKey,
encryptMegolmEvent,
encryptMegolmEventRawPlainText,
encryptOlmEvent,
establishOlmSession,
getTestOlmAccountKeys,
} from "./olm-utils";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -80,197 +88,6 @@ afterEach(() => {
indexedDB = new IDBFactory();
});
// start an Olm session with a given recipient
async function createOlmSession(olmAccount: Olm.Account, recipientTestClient: IE2EKeyReceiver): Promise<Olm.Session> {
const keys = await recipientTestClient.awaitOneTimeKeyUpload();
const otkId = Object.keys(keys)[0];
const otk = keys[otkId];
const session = new global.Olm.Session();
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
return session;
}
// IToDeviceEvent isn't exported by src/sync-accumulator.ts
interface ToDeviceEvent {
content: IContent;
sender: string;
type: string;
}
/** encrypt an event with an existing olm session */
function encryptOlmEvent(opts: {
/** the sender's user id */
sender?: string;
/** the sender's curve25519 key */
senderKey: string;
/** the sender's ed25519 key */
senderSigningKey: string;
/** the olm session to use for encryption */
p2pSession: Olm.Session;
/** the recipient's user id */
recipient: string;
/** the recipient's curve25519 key */
recipientCurve25519Key: string;
/** the recipient's ed25519 key */
recipientEd25519Key: string;
/** the payload of the message */
plaincontent?: object;
/** the event type of the payload */
plaintype?: string;
}): ToDeviceEvent {
expect(opts.senderKey).toBeTruthy();
expect(opts.p2pSession).toBeTruthy();
expect(opts.recipient).toBeTruthy();
const plaintext = {
content: opts.plaincontent || {},
recipient: opts.recipient,
recipient_keys: {
ed25519: opts.recipientEd25519Key,
},
keys: {
ed25519: opts.senderSigningKey,
},
sender: opts.sender || "@bob:xyz",
type: opts.plaintype || "m.test",
};
return {
content: {
algorithm: "m.olm.v1.curve25519-aes-sha2",
ciphertext: {
[opts.recipientCurve25519Key]: opts.p2pSession.encrypt(JSON.stringify(plaintext)),
},
sender_key: opts.senderKey,
},
sender: opts.sender || "@bob:xyz",
type: "m.room.encrypted",
};
}
// encrypt an event with megolm
function encryptMegolmEvent(opts: {
senderKey: string;
groupSession: Olm.OutboundGroupSession;
plaintext?: Partial<IEvent>;
room_id?: string;
}): IEvent {
expect(opts.senderKey).toBeTruthy();
expect(opts.groupSession).toBeTruthy();
const plaintext = opts.plaintext || {};
if (!plaintext.content) {
plaintext.content = {
body: "42",
msgtype: "m.text",
};
}
if (!plaintext.type) {
plaintext.type = "m.room.message";
}
if (!plaintext.room_id) {
expect(opts.room_id).toBeTruthy();
plaintext.room_id = opts.room_id;
}
return encryptMegolmEventRawPlainText({
senderKey: opts.senderKey,
groupSession: opts.groupSession,
plaintext,
});
}
function encryptMegolmEventRawPlainText(opts: {
senderKey: string;
groupSession: Olm.OutboundGroupSession;
plaintext: Partial<IEvent>;
origin_server_ts?: number;
}): IEvent {
return {
event_id: "$test_megolm_event_" + Math.random(),
sender: opts.plaintext.sender ?? "@not_the_real_sender:example.com",
origin_server_ts: opts.plaintext.origin_server_ts ?? 1672944778000,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: opts.groupSession.encrypt(JSON.stringify(opts.plaintext)),
device_id: "testDevice",
sender_key: opts.senderKey,
session_id: opts.groupSession.session_id(),
},
type: "m.room.encrypted",
unsigned: {},
};
}
/** build an encrypted room_key event to share a group session, using an existing olm session */
function encryptGroupSessionKey(opts: {
/** recipient's user id */
recipient: string;
/** the recipient's curve25519 key */
recipientCurve25519Key: string;
/** the recipient's ed25519 key */
recipientEd25519Key: string;
/** sender's olm account */
olmAccount: Olm.Account;
/** sender's olm session with the recipient */
p2pSession: Olm.Session;
groupSession: Olm.OutboundGroupSession;
room_id?: string;
}): Partial<IEvent> {
const senderKeys = JSON.parse(opts.olmAccount.identity_keys());
return encryptOlmEvent({
senderKey: senderKeys.curve25519,
senderSigningKey: senderKeys.ed25519,
recipient: opts.recipient,
recipientCurve25519Key: opts.recipientCurve25519Key,
recipientEd25519Key: opts.recipientEd25519Key,
p2pSession: opts.p2pSession,
plaincontent: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: opts.room_id,
session_id: opts.groupSession.session_id(),
session_key: opts.groupSession.session_key(),
},
plaintype: "m.room_key",
});
}
/**
* Establish an Olm Session with the test user
*
* Waits for the test user to upload their keys, then sends a /sync response with a to-device message which will
* establish an Olm session.
*
* @param testClient - the MatrixClient under test, which we expect to upload account keys, and to make a
* /sync request which we will respond to.
* @param keyReceiver - an IE2EKeyReceiver which will intercept the /keys/upload request from the client under test
* @param syncResponder - an ISyncResponder which will intercept /sync requests from the client under test
* @param peerOlmAccount: an OlmAccount which will be used to initiate the Olm session.
*/
async function establishOlmSession(
testClient: MatrixClient,
keyReceiver: IE2EKeyReceiver,
syncResponder: ISyncResponder,
peerOlmAccount: Olm.Account,
): Promise<Olm.Session> {
const peerE2EKeys = JSON.parse(peerOlmAccount.identity_keys());
const p2pSession = await createOlmSession(peerOlmAccount, keyReceiver);
const olmEvent = encryptOlmEvent({
senderKey: peerE2EKeys.curve25519,
senderSigningKey: peerE2EKeys.ed25519,
recipient: testClient.getUserId()!,
recipientCurve25519Key: keyReceiver.getDeviceKey(),
recipientEd25519Key: keyReceiver.getSigningKey(),
p2pSession: p2pSession,
});
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
to_device: { events: [olmEvent] },
});
await syncPromise(testClient);
return p2pSession;
}
/**
* Expect that the client shares keys with the given recipient
*
@@ -459,20 +276,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
* @returns The fake query response
*/
function getTestKeysQueryResponse(userId: string): IDownloadKeyResult {
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
const testDeviceKeys: IDeviceKeys = {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "DEVICE_ID",
keys: {
"curve25519:DEVICE_ID": testE2eKeys.curve25519,
"ed25519:DEVICE_ID": testE2eKeys.ed25519,
},
user_id: userId,
};
const j = anotherjson.stringify(testDeviceKeys);
const sig = testOlmAccount.sign(j);
testDeviceKeys.signatures = { [userId]: { "ed25519:DEVICE_ID": sig } };
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, userId, "DEVICE_ID");
return {
device_keys: { [userId]: { DEVICE_ID: testDeviceKeys } },
failures: {},
@@ -550,9 +354,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
await initCrypto(aliceClient);
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
await Olm.init();
testOlmAccount = new Olm.Account();
testOlmAccount.create();
testOlmAccount = await createOlmAccount();
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
testSenderKey = testE2eKeys.curve25519;
},
@@ -2283,6 +2085,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
describe("Secret Storage and Key Backup", () => {
/**
* The account data events to be returned by the sync.
* Will be updated when fecthMock intercepts calls to PUT `/_matrix/client/v3/user/:userId/account_data/`.
* Will be used by `sendSyncResponseWithUpdatedAccountData`
*/
let accountDataEvents: Map<String, any>;
/**
* Create a fake secret storage key
* Async because `bootstrapSecretStorage` expect an async method
@@ -2294,11 +2103,35 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
beforeEach(async () => {
createSecretStorageKey.mockClear();
accountDataEvents = new Map();
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
});
function mockGetAccountData() {
fetchMock.get(
`path:/_matrix/client/v3/user/:userId/account_data/:type`,
(url) => {
const type = url.split("/").pop();
const existing = accountDataEvents.get(type!);
if (existing) {
// return it
return {
status: 200,
body: existing.content,
};
} else {
// 404
return {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
};
}
},
{ overwriteRoutes: true },
);
}
/**
* Create a mock to respond to the PUT request `/_matrix/client/v3/user/:userId/account_data/m.cross_signing.${key}`
* Resolved when the cross signing key is uploaded
@@ -2311,6 +2144,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
`express:/_matrix/client/v3/user/:userId/account_data/m.cross_signing.${key}`,
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
const type = url.split("/").pop();
// update account data for sync response
accountDataEvents.set(type!, content);
resolve(content.encrypted);
return {};
},
@@ -2319,32 +2155,24 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
}
/**
* Send in the sync response the provided `secretStorageKey` into the account_data field
* The key is set for the `m.secret_storage.default_key` and `m.secret_storage.key.${secretStorageKey}` events
* https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv3sync
* @param secretStorageKey
* Send in the sync response the current account data events, as stored by `accountDataEvents`.
*/
function sendSyncResponse(secretStorageKey: string) {
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: [
{
type: "m.secret_storage.default_key",
content: {
key: secretStorageKey,
},
},
// Needed for secretStorage.getKey or secretStorage.hasKey
{
type: `m.secret_storage.key.${secretStorageKey}`,
content: {
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
},
},
],
},
});
function sendSyncResponseWithUpdatedAccountData() {
try {
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: Array.from(accountDataEvents, ([type, content]) => ({
type: type,
content: content,
})),
},
});
} catch (err) {
// Might fail with "Cannot queue more than one /sync response" if called too often.
// It's ok if it fails here, the sync response is cumulative and will contain
// the latest account data.
}
}
/**
@@ -2359,10 +2187,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
fetchMock.put(
"express:/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)",
(url: string, options: RequestInit) => {
const type = url.split("/").pop();
const content = JSON.parse(options.body as string);
// update account data for sync response
accountDataEvents.set(type!, content);
if (content.key) {
resolve(content.key);
}
sendSyncResponseWithUpdatedAccountData();
return {};
},
{ overwriteRoutes: true },
@@ -2377,6 +2211,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
`express:/_matrix/client/v3/user/:userId/account_data/m.megolm_backup.v1`,
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
// update account data for sync response
accountDataEvents.set("m.megolm_backup.v1", content);
resolve(content.encrypted);
return {};
},
@@ -2385,6 +2221,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
}
function awaitAccountDataUpdate(type: string): Promise<void> {
return new Promise((resolve) => {
aliceClient.on(ClientEvent.AccountData, (ev: MatrixEvent): void => {
if (ev.getType() === type) {
resolve();
}
});
});
}
/**
* Add all mocks needed to setup cross-signing, key backup, 4S and then
* configure the account to have recovery.
@@ -2422,32 +2268,41 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
// Wait for the key to be uploaded in the account data
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
await awaitSecretStorageKeyStoredInAccountData();
// Wait for the cross signing keys to be uploaded
await Promise.all(setupPromises);
// wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// Return the newly created key in the sync response
sendSyncResponseWithUpdatedAccountData();
// Finally ensure backup is working
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
await backupStatusUpdate;
}
describe("bootstrapSecretStorage", () => {
// Doesn't work with legacy crypto, which will try to bootstrap even without private key, which is buggy.
newBackendOnly(
"should throw an error if we are unable to create a key because createSecretStorageKey is not set",
async () => {
await expect(
aliceClient.getCrypto()!.bootstrapSecretStorage({ setupNewSecretStorage: true }),
).rejects.toThrow("unable to create a new secret storage key, createSecretStorageKey is not set");
expect(await aliceClient.getCrypto()!.isSecretStorageReady()).toStrictEqual(false);
},
);
it("should create a new key", async () => {
it("Should create a 4S key", async () => {
mockGetAccountData();
const awaitAccountData = awaitAccountDataUpdate("m.secret_storage.default_key");
const bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
@@ -2456,11 +2311,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
sendSyncResponseWithUpdatedAccountData();
// Finally, wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// await account data updated before getting default key.
await awaitAccountData;
const defaultKeyId = await aliceClient.secretStorage.getDefaultKeyId();
// Check that the uploaded key in stored in the secret storage
expect(await aliceClient.secretStorage.hasKey(secretStorageKey)).toBeTruthy();
@@ -2468,29 +2326,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expect(defaultKeyId).toBe(secretStorageKey);
});
newBackendOnly(
"should do nothing if an AES key is already in the secret storage and setupNewSecretStorage is not set",
async () => {
const bootstrapPromise = aliceClient
.getCrypto()!
.bootstrapSecretStorage({ createSecretStorageKey });
it("should do nothing if an AES key is already in the secret storage and setupNewSecretStorage is not set", async () => {
const awaitAccountDataClientUpdate = awaitAccountDataUpdate("m.secret_storage.default_key");
// Wait for the key to be uploaded in the account data
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
const bootstrapPromise = aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
// Wait for the key to be uploaded in the account data
await awaitSecretStorageKeyStoredInAccountData();
// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// Return the newly created key in the sync response
sendSyncResponseWithUpdatedAccountData();
// Call again bootstrapSecretStorage
await aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;
// createSecretStorageKey should be called only on the first run of bootstrapSecretStorage
expect(createSecretStorageKey).toHaveBeenCalledTimes(1);
},
);
// On legacy crypto we need to wait for ClientEvent.AccountData before calling bootstrap again.
await awaitAccountDataClientUpdate;
// Call again bootstrapSecretStorage
await aliceClient.getCrypto()!.bootstrapSecretStorage({ createSecretStorageKey });
// createSecretStorageKey should be called only on the first run of bootstrapSecretStorage
expect(createSecretStorageKey).toHaveBeenCalledTimes(1);
});
it("should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage", async () => {
let bootstrapPromise = aliceClient
@@ -2498,10 +2356,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
// Wait for the key to be uploaded in the account data
let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
await awaitSecretStorageKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
sendSyncResponseWithUpdatedAccountData();
// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;
@@ -2512,10 +2370,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
// Wait for the key to be uploaded in the account data
secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
await awaitSecretStorageKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
sendSyncResponseWithUpdatedAccountData();
// Wait for bootstrapSecretStorage to finished
await bootstrapPromise;
@@ -2539,7 +2397,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
// Return the newly created key in the sync response
sendSyncResponse(secretStorageKey);
sendSyncResponseWithUpdatedAccountData();
// Wait for the cross signing keys to be uploaded
const [masterKey, userSigningKey, selfSigningKey] = await Promise.all([
@@ -2561,6 +2419,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const backupVersion = "abc";
await bootstrapSecurity(backupVersion);
expect(await aliceClient.getCrypto()!.isSecretStorageReady()).toStrictEqual(true);
// Expect a backup to be available and used
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
expect(activeBackup).toStrictEqual(backupVersion);
@@ -2576,6 +2436,57 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
describe("Manage Key Backup", () => {
beforeEach(async () => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it("Should be able to restore from 4S after bootstrap", async () => {
const backupVersion = "1";
await bootstrapSecurity(backupVersion);
const check = await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
// Import a new key that should be uploaded
const newKey = testData.MEGOLM_SESSION_DATA;
const awaitKeyUploaded = new Promise<IKeyBackup>((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
(url, request) => {
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
resolve(uploadPayload);
return {
status: 200,
body: {
count: 1,
etag: "abcdefg",
},
};
},
{
overwriteRoutes: true,
},
);
});
await aliceClient.getCrypto()!.importRoomKeys([newKey]);
// The backup loop waits a random amount of time to avoid different clients firing at the same time.
jest.runAllTimers();
const keyBackupData = await awaitKeyUploaded;
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", keyBackupData);
// should be able to restore from 4S
const importResult = await aliceClient.restoreKeyBackupWithSecretStorage(check!.backupInfo!);
expect(importResult.imported).toStrictEqual(1);
});
it("Reset key backup should create a new backup and update 4S", async () => {
// First set up 4S and key backup
const backupVersion = "1";
@@ -2596,7 +2507,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
"path:/_matrix/client/v3/room_keys/version",
{
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
},
{ overwriteRoutes: true },
);
@@ -2659,195 +2570,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
});
describe("Incoming verification in a DM", () => {
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
keyResponder.addKeyReceiver(TEST_USER_ID, keyReceiver);
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
});
afterEach(() => {
jest.useRealTimers();
});
/**
* Return a verification request event from Bob
* @see https://spec.matrix.org/v1.7/client-server-api/#mkeyverificationrequest
*/
function createVerificationRequestEvent(): IRoomEvent {
return {
content: {
body: "Verification request from Bob to Alice",
from_device: "BobDevice",
methods: ["m.sas.v1"],
msgtype: "m.key.verification.request",
to: aliceClient.getUserId()!,
},
event_id: "$143273582443PhrSn:example.org",
origin_server_ts: Date.now(),
room_id: TEST_ROOM_ID,
sender: "@bob:xyz",
type: "m.room.message",
unsigned: {
age: 1234,
},
};
}
/**
* Create a to-device event
* @param groupSession
* @param p2pSession
*/
function createToDeviceEvent(groupSession: Olm.OutboundGroupSession, p2pSession: Olm.Session): Partial<IEvent> {
return encryptGroupSessionKey({
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: keyReceiver.getDeviceKey(),
recipientEd25519Key: keyReceiver.getSigningKey(),
olmAccount: testOlmAccount,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
}
/**
* Create and encrypt a verification request event
* @param groupSession
*/
function createEncryptedMessage(groupSession: Olm.OutboundGroupSession): IEvent {
return encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
plaintext: createVerificationRequestEvent(),
});
}
newBackendOnly("Verification request from Bob to Alice", async () => {
// Tell alice she is sharing a room with bob
const syncResponse = getSyncResponse(["@bob:xyz"]);
// Add verification request from Bob to Alice in the DM between them
syncResponse.rooms[Category.Join][TEST_ROOM_ID].timeline.events.push(createVerificationRequestEvent());
syncResponder.sendOrQueueSyncResponse(syncResponse);
// Wait for the sync response to be processed
await syncPromise(aliceClient);
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// Expect to find the verification request received during the sync
expect(request?.roomId).toBe(TEST_ROOM_ID);
expect(request?.isSelfVerification).toBe(false);
expect(request?.otherUserId).toBe("@bob:xyz");
});
newBackendOnly("Verification request not found", async () => {
// Tell alice she is sharing a room with bob
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
// Wait for the sync response to be processed
await syncPromise(aliceClient);
// Expect to not find any verification request
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
expect(request).not.toBeDefined();
});
newBackendOnly("Process encrypted verification request", async () => {
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event, but don't send it yet
const toDeviceEvent = createToDeviceEvent(groupSession, p2pSession);
// Add verification request from Bob to Alice in the DM between them
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
rooms: { join: { [ROOM_ID]: { timeline: { events: [createEncryptedMessage(groupSession)] } } } },
});
// Wait for the sync response to be processed
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const matrixEvent = room.getLiveTimeline().getEvents()[0];
// wait for a first attempt at decryption: should fail
await testUtils.awaitDecryption(matrixEvent);
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
// Send the Bob's keys
syncResponder.sendOrQueueSyncResponse({
next_batch: 2,
to_device: {
events: [toDeviceEvent],
},
});
await syncPromise(aliceClient);
// Wait for the message to be decrypted
await testUtils.awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// Expect to find the verification request received during the sync
expect(request?.roomId).toBe(TEST_ROOM_ID);
expect(request?.isSelfVerification).toBe(false);
expect(request?.otherUserId).toBe("@bob:xyz");
});
newBackendOnly(
"If Bob keys are not received in the 5mins after the verification request, the request is ignored",
async () => {
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event, but don't send it yet
const toDeviceEvent = createToDeviceEvent(groupSession, p2pSession);
jest.useFakeTimers();
// Add verification request from Bob to Alice in the DM between them
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
rooms: { join: { [ROOM_ID]: { timeline: { events: [createEncryptedMessage(groupSession)] } } } },
});
// Wait for the sync response to be processed
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const matrixEvent = room.getLiveTimeline().getEvents()[0];
// wait for a first attempt at decryption: should fail
await testUtils.awaitDecryption(matrixEvent);
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
// Advance time by 5mins, the verification request should be ignored after that
jest.advanceTimersByTime(5 * 60 * 1000);
// Send the Bob's keys
syncResponder.sendOrQueueSyncResponse({
next_batch: 2,
to_device: {
events: [toDeviceEvent],
},
});
await syncPromise(aliceClient);
// Wait for the message to be decrypted
await testUtils.awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// the request should not be present
expect(request).not.toBeDefined();
},
);
});
describe("Check if the cross signing keys are available for a user", () => {
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
+5
View File
@@ -237,6 +237,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
expect(result.imported).toStrictEqual(1);
await awaitKeyCached;
// The key should be now cached
const afterCache = await aliceClient.restoreKeyBackupWithCache(undefined, undefined, check!.backupInfo!);
expect(afterCache.imported).toStrictEqual(1);
});
it("recover specific session from backup", async function () {
+255
View File
@@ -0,0 +1,255 @@
/*
Copyright 2016-2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Olm from "@matrix-org/olm";
import anotherjson from "another-json";
import { IContent, IDeviceKeys, IEvent, MatrixClient } from "../../../src";
import { IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { ISyncResponder } from "../../test-utils/SyncResponder";
import { syncPromise } from "../../test-utils/test-utils";
/**
* @module
*
* A set of utilities for creating Olm accounts and sessions, and encrypting/decrypting with Olm/Megolm.
*/
/** Create an Olm Account object */
export async function createOlmAccount(): Promise<Olm.Account> {
await Olm.init();
const testOlmAccount = new Olm.Account();
testOlmAccount.create();
return testOlmAccount;
}
/**
* Get the device keys for the test Olm Account
*
* @param olmAccount - Test olm account
* @param userId - The user ID to present the keys as belonging to
*/
export function getTestOlmAccountKeys(olmAccount: Olm.Account, userId: string, deviceId: string): IDeviceKeys {
const testE2eKeys = JSON.parse(olmAccount.identity_keys());
const testDeviceKeys: IDeviceKeys = {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: deviceId,
keys: {
[`curve25519:${deviceId}`]: testE2eKeys.curve25519,
[`ed25519:${deviceId}`]: testE2eKeys.ed25519,
},
user_id: userId,
};
const j = anotherjson.stringify(testDeviceKeys);
const sig = olmAccount.sign(j);
testDeviceKeys.signatures = { [userId]: { [`ed25519:${deviceId}`]: sig } };
return testDeviceKeys;
}
/** start an Olm session with a given recipient */
export async function createOlmSession(
olmAccount: Olm.Account,
recipientTestClient: IE2EKeyReceiver,
): Promise<Olm.Session> {
const keys = await recipientTestClient.awaitOneTimeKeyUpload();
const otkId = Object.keys(keys)[0];
const otk = keys[otkId];
const session = new global.Olm.Session();
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
return session;
}
// IToDeviceEvent isn't exported by src/sync-accumulator.ts
export interface ToDeviceEvent {
content: IContent;
sender: string;
type: string;
}
/** encrypt an event with an existing olm session */
export function encryptOlmEvent(opts: {
/** the sender's user id */
sender?: string;
/** the sender's curve25519 key */
senderKey: string;
/** the sender's ed25519 key */
senderSigningKey: string;
/** the olm session to use for encryption */
p2pSession: Olm.Session;
/** the recipient's user id */
recipient: string;
/** the recipient's curve25519 key */
recipientCurve25519Key: string;
/** the recipient's ed25519 key */
recipientEd25519Key: string;
/** the payload of the message */
plaincontent?: object;
/** the event type of the payload */
plaintype?: string;
}): ToDeviceEvent {
expect(opts.senderKey).toBeTruthy();
expect(opts.p2pSession).toBeTruthy();
expect(opts.recipient).toBeTruthy();
const plaintext = {
content: opts.plaincontent || {},
recipient: opts.recipient,
recipient_keys: {
ed25519: opts.recipientEd25519Key,
},
keys: {
ed25519: opts.senderSigningKey,
},
sender: opts.sender || "@bob:xyz",
type: opts.plaintype || "m.test",
};
return {
content: {
algorithm: "m.olm.v1.curve25519-aes-sha2",
ciphertext: {
[opts.recipientCurve25519Key]: opts.p2pSession.encrypt(JSON.stringify(plaintext)),
},
sender_key: opts.senderKey,
},
sender: opts.sender || "@bob:xyz",
type: "m.room.encrypted",
};
}
// encrypt an event with megolm
export function encryptMegolmEvent(opts: {
senderKey: string;
groupSession: Olm.OutboundGroupSession;
plaintext?: Partial<IEvent>;
room_id?: string;
}): IEvent {
expect(opts.senderKey).toBeTruthy();
expect(opts.groupSession).toBeTruthy();
const plaintext = opts.plaintext || {};
if (!plaintext.content) {
plaintext.content = {
body: "42",
msgtype: "m.text",
};
}
if (!plaintext.type) {
plaintext.type = "m.room.message";
}
if (!plaintext.room_id) {
expect(opts.room_id).toBeTruthy();
plaintext.room_id = opts.room_id;
}
return encryptMegolmEventRawPlainText({
senderKey: opts.senderKey,
groupSession: opts.groupSession,
plaintext,
});
}
export function encryptMegolmEventRawPlainText(opts: {
senderKey: string;
groupSession: Olm.OutboundGroupSession;
plaintext: Partial<IEvent>;
origin_server_ts?: number;
}): IEvent {
return {
event_id: "$test_megolm_event_" + Math.random(),
sender: opts.plaintext.sender ?? "@not_the_real_sender:example.com",
origin_server_ts: opts.plaintext.origin_server_ts ?? 1672944778000,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: opts.groupSession.encrypt(JSON.stringify(opts.plaintext)),
device_id: "testDevice",
sender_key: opts.senderKey,
session_id: opts.groupSession.session_id(),
},
type: "m.room.encrypted",
unsigned: {},
};
}
/** build an encrypted room_key event to share a group session, using an existing olm session */
export function encryptGroupSessionKey(opts: {
/** recipient's user id */
recipient: string;
/** the recipient's curve25519 key */
recipientCurve25519Key: string;
/** the recipient's ed25519 key */
recipientEd25519Key: string;
/** sender's olm account */
olmAccount: Olm.Account;
/** sender's olm session with the recipient */
p2pSession: Olm.Session;
groupSession: Olm.OutboundGroupSession;
room_id?: string;
}): ToDeviceEvent {
const senderKeys = JSON.parse(opts.olmAccount.identity_keys());
return encryptOlmEvent({
senderKey: senderKeys.curve25519,
senderSigningKey: senderKeys.ed25519,
recipient: opts.recipient,
recipientCurve25519Key: opts.recipientCurve25519Key,
recipientEd25519Key: opts.recipientEd25519Key,
p2pSession: opts.p2pSession,
plaincontent: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: opts.room_id,
session_id: opts.groupSession.session_id(),
session_key: opts.groupSession.session_key(),
},
plaintype: "m.room_key",
});
}
/**
* Establish an Olm Session with the test user
*
* Waits for the test user to upload their keys, then sends a /sync response with a to-device message which will
* establish an Olm session.
*
* @param testClient - the MatrixClient under test, which we expect to upload account keys, and to make a
* /sync request which we will respond to.
* @param keyReceiver - an IE2EKeyReceiver which will intercept the /keys/upload request from the client under test
* @param syncResponder - an ISyncResponder which will intercept /sync requests from the client under test
* @param peerOlmAccount: an OlmAccount which will be used to initiate the Olm session.
*/
export async function establishOlmSession(
testClient: MatrixClient,
keyReceiver: IE2EKeyReceiver,
syncResponder: ISyncResponder,
peerOlmAccount: Olm.Account,
): Promise<Olm.Session> {
const peerE2EKeys = JSON.parse(peerOlmAccount.identity_keys());
const p2pSession = await createOlmSession(peerOlmAccount, keyReceiver);
const olmEvent = encryptOlmEvent({
senderKey: peerE2EKeys.curve25519,
senderSigningKey: peerE2EKeys.ed25519,
recipient: testClient.getUserId()!,
recipientCurve25519Key: keyReceiver.getDeviceKey(),
recipientEd25519Key: keyReceiver.getSigningKey(),
p2pSession: p2pSession,
});
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
to_device: { events: [olmEvent] },
});
await syncPromise(testClient);
return p2pSession;
}
+21 -2
View File
@@ -41,7 +41,7 @@ describe("MatrixClient.initRustCrypto", () => {
await expect(() => unknownDeviceClient.initRustCrypto()).rejects.toThrow("unknown deviceId");
});
it("should create the indexed dbs", async () => {
it("should create the indexed db", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
@@ -53,7 +53,25 @@ describe("MatrixClient.initRustCrypto", () => {
await matrixClient.initRustCrypto();
// should have two dbs now
// should have an indexed db now
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
expect(databaseNames).toEqual(expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto"]));
});
it("should create the meta db if given a pickleKey", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
pickleKey: "testKey",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto();
// should have two indexed dbs now
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
expect(databaseNames).toEqual(
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
@@ -78,6 +96,7 @@ describe("MatrixClient.clearStores", () => {
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
pickleKey: "testKey",
});
await matrixClient.initRustCrypto();
+192 -2
View File
@@ -21,12 +21,14 @@ import { MockResponse } from "fetch-mock";
import fetchMock from "fetch-mock-jest";
import { IDBFactory } from "fake-indexeddb";
import { createHash } from "crypto";
import Olm from "@matrix-org/olm";
import {
createClient,
CryptoEvent,
IContent,
ICreateClientOpts,
IEvent,
MatrixClient,
MatrixEvent,
MatrixEventEvent,
@@ -42,9 +44,17 @@ import {
VerifierEvent,
} from "../../../src/crypto-api/verification";
import { escapeRegExp } from "../../../src/utils";
import { CRYPTO_BACKENDS, emitPromise, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import {
awaitDecryption,
CRYPTO_BACKENDS,
emitPromise,
getSyncResponse,
InitCrypto,
syncPromise,
} from "../../test-utils/test-utils";
import { SyncResponder } from "../../test-utils/SyncResponder";
import {
BOB_ONE_TIME_KEYS,
BOB_SIGNED_CROSS_SIGNING_KEYS_DATA,
BOB_SIGNED_TEST_DEVICE_DATA,
BOB_TEST_USER_ID,
@@ -55,11 +65,11 @@ import {
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
TEST_ROOM_ID,
TEST_USER_ID,
BOB_ONE_TIME_KEYS,
} from "../../test-utils/test-data";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { createOlmSession, encryptGroupSessionKey, encryptMegolmEvent, ToDeviceEvent } from "./olm-utils";
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
// to ensure that we don't end up with dangling timeouts.
@@ -897,6 +907,175 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
});
});
describe("Incoming verification in a DM", () => {
let testOlmAccount: Olm.Account;
beforeEach(async () => {
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
await Olm.init();
testOlmAccount = new Olm.Account();
testOlmAccount.create();
aliceClient = await startTestClient();
aliceClient.setGlobalErrorOnUnknownDevices(false);
syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID]));
await syncPromise(aliceClient);
});
/**
* Return a plaintext verification request event from Bob to Alice
* @see https://spec.matrix.org/v1.7/client-server-api/#mkeyverificationrequest
*/
function createVerificationRequestEvent(): IEvent {
return {
content: {
body: "Verification request from Bob to Alice",
from_device: "BobDevice",
methods: ["m.sas.v1"],
msgtype: "m.key.verification.request",
to: aliceClient.getUserId()!,
},
event_id: "$143273582443PhrSn:example.org",
origin_server_ts: Date.now(),
room_id: TEST_ROOM_ID,
sender: "@bob:xyz",
type: "m.room.message",
unsigned: {
age: 1234,
},
};
}
/**
* Create a to-device event from Bob to Alice, sharing the group session key
* @param groupSession - group session key to share
* @param p2pSession - test Olm session to encrypt the key with
*/
function encryptGroupSessionKeyForAlice(
groupSession: Olm.OutboundGroupSession,
p2pSession: Olm.Session,
): ToDeviceEvent {
return encryptGroupSessionKey({
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: e2eKeyReceiver.getDeviceKey(),
recipientEd25519Key: e2eKeyReceiver.getSigningKey(),
olmAccount: testOlmAccount,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: TEST_ROOM_ID,
});
}
/**
* Create and encrypt a verification request event
* @param groupSession
*/
function createEncryptedVerificationRequest(groupSession: Olm.OutboundGroupSession): IEvent {
const testOlmAccountKeys = JSON.parse(testOlmAccount.identity_keys());
return encryptMegolmEvent({
senderKey: testOlmAccountKeys.curve25519,
groupSession: groupSession,
room_id: TEST_ROOM_ID,
plaintext: createVerificationRequestEvent(),
});
}
it("Plaintext verification request from Bob to Alice", async () => {
// Add verification request from Bob to Alice in the DM between them
returnRoomMessageFromSync(TEST_ROOM_ID, createVerificationRequestEvent());
// Wait for the sync response to be processed
await syncPromise(aliceClient);
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// Expect to find the verification request received during the sync
expect(request?.roomId).toBe(TEST_ROOM_ID);
expect(request?.isSelfVerification).toBe(false);
expect(request?.otherUserId).toBe("@bob:xyz");
});
it("Verification request not found", async () => {
// Expect to not find any verification request
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
expect(request).not.toBeDefined();
});
it("Encrypted verification request from Bob to Alice", async () => {
const p2pSession = await createOlmSession(testOlmAccount, e2eKeyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event, but don't send it yet
const toDeviceEvent = encryptGroupSessionKeyForAlice(groupSession, p2pSession);
// Add verification request from Bob to Alice in the DM between them
returnRoomMessageFromSync(TEST_ROOM_ID, createEncryptedVerificationRequest(groupSession));
// Wait for the sync response to be processed
await syncPromise(aliceClient);
const room = aliceClient.getRoom(TEST_ROOM_ID)!;
const matrixEvent = room.getLiveTimeline().getEvents()[0];
// wait for a first attempt at decryption: should fail
await awaitDecryption(matrixEvent);
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
// Send Bob the room keys
returnToDeviceMessageFromSync(toDeviceEvent);
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
// Wait for the message to be decrypted
await awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// Expect to find the verification request received during the sync
expect(request?.roomId).toBe(TEST_ROOM_ID);
expect(request?.isSelfVerification).toBe(false);
expect(request?.otherUserId).toBe("@bob:xyz");
});
newBackendOnly(
"If the verification request is not decrypted within 5 minutes, the request is ignored",
async () => {
const p2pSession = await createOlmSession(testOlmAccount, e2eKeyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event, but don't send it yet
const toDeviceEvent = encryptGroupSessionKeyForAlice(groupSession, p2pSession);
// Add verification request from Bob to Alice in the DM between them
returnRoomMessageFromSync(TEST_ROOM_ID, createEncryptedVerificationRequest(groupSession));
// Wait for the sync response to be processed
await syncPromise(aliceClient);
const room = aliceClient.getRoom(TEST_ROOM_ID)!;
const matrixEvent = room.getLiveTimeline().getEvents()[0];
// wait for a first attempt at decryption: should fail
await awaitDecryption(matrixEvent);
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
// Advance time by 5mins, the verification request should be ignored after that
jest.advanceTimersByTime(5 * 60 * 1000);
// Send Bob the room keys
returnToDeviceMessageFromSync(toDeviceEvent);
// Wait for the message to be decrypted
await awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// the request should not be present
expect(request).not.toBeDefined();
},
);
});
async function startTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
const client = createClient({
baseUrl: TEST_HOMESERVER_URL,
@@ -927,6 +1106,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
ev.sender ??= TEST_USER_ID;
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
}
function returnRoomMessageFromSync(roomId: string, ev: IEvent): void {
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
rooms: {
join: {
[roomId]: { timeline: { events: [ev] } },
},
},
});
}
});
/**
+57 -11
View File
@@ -150,7 +150,7 @@ describe("MatrixClient", function () {
});
describe("joinRoom", function () {
it("should no-op if you've already joined a room", function () {
it("should no-op given the ID of a room you've already joined", async () => {
const roomId = "!foo:bar";
const room = new Room(roomId, client, userId);
client.fetchRoomEvent = () =>
@@ -168,8 +168,32 @@ describe("MatrixClient", function () {
]);
httpBackend.verifyNoOutstandingRequests();
store.storeRoom(room);
client.joinRoom(roomId);
const joinPromise = client.joinRoom(roomId);
httpBackend.verifyNoOutstandingRequests();
expect(await joinPromise).toBe(room);
});
it("should no-op given the alias of a room you've already joined", async () => {
const roomId = "!roomId:server";
const roomAlias = "#my-fancy-room:server";
const room = new Room(roomId, client, userId);
room.addLiveEvents([
utils.mkMembership({
user: userId,
room: roomId,
mship: "join",
event: true,
}),
]);
store.storeRoom(room);
// The method makes a request to resolve the alias
httpBackend.when("POST", "/join/" + encodeURIComponent(roomAlias)).respond(200, { room_id: roomId });
const joinPromise = client.joinRoom(roomAlias);
await httpBackend.flushAllExpected();
expect(await joinPromise).toBe(room);
});
it("should send request to inviteSignUrl if specified", async () => {
@@ -1180,44 +1204,61 @@ describe("MatrixClient", function () {
describe("requestLoginToken", () => {
it("should hit the expected API endpoint with UIA", async () => {
jest.spyOn(client.http, "getUrl");
httpBackend
.when("GET", "/capabilities")
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: true } } });
.respond(200, { capabilities: { "m.get_login_token": { enabled: true } } });
const response = {};
const uiaData = {};
const prom = client.requestLoginToken(uiaData);
httpBackend
.when("POST", "/unstable/org.matrix.msc3882/login/get_token", { auth: uiaData })
.respond(200, response);
httpBackend.when("POST", "/v1/login/get_token", { auth: uiaData }).respond(200, response);
await httpBackend.flush("");
expect(await prom).toStrictEqual(response);
expect(client.http.getUrl).toHaveLastReturnedWith(
expect.objectContaining({
href: "http://alice.localhost.test.server/_matrix/client/v1/login/get_token",
}),
);
});
it("should hit the expected API endpoint without UIA", async () => {
jest.spyOn(client.http, "getUrl");
httpBackend
.when("GET", "/capabilities")
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: true } } });
.respond(200, { capabilities: { "m.get_login_token": { enabled: true } } });
const response = { login_token: "xyz", expires_in_ms: 5000 };
const prom = client.requestLoginToken();
httpBackend.when("POST", "/unstable/org.matrix.msc3882/login/get_token", {}).respond(200, response);
httpBackend.when("POST", "/v1/login/get_token", {}).respond(200, response);
await httpBackend.flush("");
// check that expires_in has been populated for compatibility with r0
expect(await prom).toStrictEqual({ ...response, expires_in: 5 });
expect(client.http.getUrl).toHaveLastReturnedWith(
expect.objectContaining({
href: "http://alice.localhost.test.server/_matrix/client/v1/login/get_token",
}),
);
});
it("should hit the r1 endpoint when capability is disabled", async () => {
it("should still hit the stable endpoint when capability is disabled (but present)", async () => {
jest.spyOn(client.http, "getUrl");
httpBackend
.when("GET", "/capabilities")
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: false } } });
.respond(200, { capabilities: { "m.get_login_token": { enabled: false } } });
const response = { login_token: "xyz", expires_in_ms: 5000 };
const prom = client.requestLoginToken();
httpBackend.when("POST", "/unstable/org.matrix.msc3882/login/get_token", {}).respond(200, response);
httpBackend.when("POST", "/v1/login/get_token", {}).respond(200, response);
await httpBackend.flush("");
// check that expires_in has been populated for compatibility with r0
expect(await prom).toStrictEqual({ ...response, expires_in: 5 });
expect(client.http.getUrl).toHaveLastReturnedWith(
expect.objectContaining({
href: "http://alice.localhost.test.server/_matrix/client/v1/login/get_token",
}),
);
});
it("should hit the r0 endpoint for fallback", async () => {
jest.spyOn(client.http, "getUrl");
httpBackend.when("GET", "/capabilities").respond(200, {});
const response = { login_token: "xyz", expires_in: 5 };
const prom = client.requestLoginToken();
@@ -1225,6 +1266,11 @@ describe("MatrixClient", function () {
await httpBackend.flush("");
// check that expires_in has been populated for compatibility with r1
expect(await prom).toStrictEqual({ ...response, expires_in_ms: 5000 });
expect(client.http.getUrl).toHaveLastReturnedWith(
expect.objectContaining({
href: "http://alice.localhost.test.server/_matrix/client/unstable/org.matrix.msc3882/login/token",
}),
);
});
});
@@ -0,0 +1,162 @@
/*
Copyright 2023 Holi Moli GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import { MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
const makeQueryablePromise = <T = void>(promise: Promise<T>) => {
let resolved = false;
let rejected = false;
// Observe the promise, saving the fulfillment in a closure scope.
const newPromise = promise.then(
(value) => {
resolved = true;
return value;
},
(error) => {
rejected = true;
throw error;
},
);
const isFulfilled = () => {
return resolved || rejected;
};
const isResolved = () => {
return resolved;
};
const isRejected = () => {
return rejected;
};
return { promise: newPromise, isFulfilled, isResolved, isRejected };
};
const queryablePromise = <T = void>() => {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: any) => void;
const promise = makeQueryablePromise<T>(
new Promise<T>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
}),
);
return { resolve, reject, ...promise };
};
describe("MatrixClient syncing errors", () => {
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
const unknownTokenErrorData = {
status: 401,
body: {
errcode: "M_UNKNOWN_TOKEN",
error: "Invalid access token passed.",
soft_logout: false,
},
};
let client: MatrixClient | undefined;
beforeEach(() => {
client = createClient({
baseUrl: "http://tocal.test.server",
userId: selfUserId,
accessToken: selfAccessToken,
deviceId: "myDevice",
});
});
it("should retry, until errors are solved.", async () => {
jest.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
fetchMock
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
.getOnce("end:versions", 429) // second version check fails with 429 triggering another retry
.get("end:versions", {}) // further version checks succeed
.getOnce("end:pushrules/", 429) // first pushrules check fails starting retry
.get("end:pushrules/", {}) // further pushrules check succeed
.catch({}); // all other calls succeed
const syncEvents = Array.from({ length: 5 }, queryablePromise<SyncState>);
client!.on(ClientEvent.Sync, (state: SyncState, lastState: SyncState | null) => {
let i = 0;
for (; i < syncEvents.length && syncEvents[i].isFulfilled(); i++) {
// find index of first unfulfilled promise
}
syncEvents[i].resolve(state);
});
await client!.startClient();
expect(await syncEvents[0].promise).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[1].promise).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
});
it("should stop sync keep alive when client is stopped.", async () => {
jest.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
fetchMock
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
.get("end:versions", unknownTokenErrorData) // further version checks fails with 401
.get("end:pushrules/", 401) // fails with 401 without an error. This does happen in practice e.g. with Synapse
.post("end:logout", unknownTokenErrorData) // just to keep up a consistent scenario. Does not have a real effect for this testcase
.post("end:filter", 401); // just to keep up a consistent scenario. Does not have a real effect for this testcase
const firstSyncEvent = queryablePromise<SyncState>();
const secondSyncEvent = queryablePromise<SyncState>();
client!.on(ClientEvent.Sync, (state: SyncState, lastState: SyncState | null) => {
if (firstSyncEvent.isFulfilled()) secondSyncEvent.resolve(state);
firstSyncEvent.resolve(state);
});
await client!.startClient();
const logoutDone = queryablePromise();
client!
.logout(true)
.then(() => {
logoutDone.resolve();
})
.catch((e) => {
logoutDone.resolve();
});
const syntState = await firstSyncEvent.promise;
expect(syntState).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive
jest.useRealTimers(); // we need real timer for the setTimout below to work
const timeoutPromise = makeQueryablePromise(new Promise<void>((res) => setTimeout(res, 1)));
await Promise.race([secondSyncEvent.promise, timeoutPromise.promise]);
// when syncing stopped, then the secondSyncEvent will never happen and the promise will not be resolved,
/// so the timeoutPromise will be resolved instead
expect(timeoutPromise.isFulfilled()).toBe(true);
expect(secondSyncEvent.isFulfilled()).toBe(false);
await logoutDone.promise; // wait for the logout to finish to prevent processing and logging after the test is done.
});
});
+1 -1
View File
@@ -20,7 +20,7 @@ import { logger } from "../src/logger";
// try to load the olm library.
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
global.Olm = require("@matrix-org/olm");
globalThis.Olm = require("@matrix-org/olm");
logger.log("loaded libolm");
} catch (e) {
logger.warn("unable to run crypto tests: libolm not available", e);
+1 -1
View File
@@ -258,7 +258,7 @@ describe("Crypto", function () {
const event = await buildEncryptedEvent();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.UNVERIFIED_IDENTITY,
shieldReason: EventShieldReason.UNSIGNED_DEVICE,
});
});
+14
View File
@@ -215,6 +215,20 @@ describe("MegolmBackup", function () {
jest.spyOn(global, "setTimeout").mockRestore();
});
test("fail if crypto not enabled", async () => {
const client = makeTestClient(cryptoStore);
const data = {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: "1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
await expect(client.restoreKeyBackupWithSecretStorage(data)).rejects.toThrow(
"End-to-end encryption disabled",
);
});
it("automatically calls the key back up", function () {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
@@ -23,6 +23,7 @@ const membershipTemplate: CallMembershipData = {
application: "m.call",
device_id: "AAAAAAA",
expires: 5000,
membershipID: "bloop",
};
function makeMockEvent(originTs = 0): MatrixEvent {
@@ -26,6 +26,7 @@ const membershipTemplate: CallMembershipData = {
application: "m.call",
device_id: "AAAAAAA",
expires: 60 * 60 * 1000,
membershipID: "bloop",
};
const mockFocus = { type: "mock" };
@@ -56,6 +57,7 @@ describe("MatrixRTCSession", () => {
expect(sess?.memberships[0].scope).toEqual("m.room");
expect(sess?.memberships[0].application).toEqual("m.call");
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
expect(sess?.memberships[0].membershipID).toEqual("bloop");
expect(sess?.memberships[0].isExpired()).toEqual(false);
});
@@ -219,6 +221,7 @@ describe("MatrixRTCSession", () => {
device_id: "AAAAAAA",
expires: 3600000,
foci_active: [{ type: "mock" }],
membershipID: expect.stringMatching(".*"),
},
],
},
@@ -286,6 +289,7 @@ describe("MatrixRTCSession", () => {
expires: 3600000 * 2,
foci_active: [{ type: "mock" }],
created_ts: 1000,
membershipID: expect.stringMatching(".*"),
},
],
},
@@ -357,6 +361,7 @@ describe("MatrixRTCSession", () => {
device_id: "AAAAAAA",
expires: 3600000,
foci_active: [mockFocus],
membershipID: expect.stringMatching(".*"),
},
],
},
@@ -388,6 +393,7 @@ describe("MatrixRTCSession", () => {
device_id: "OTHERDEVICE",
expires: 3600000,
created_ts: 1000,
membershipID: expect.stringMatching(".*"),
},
{
application: "m.call",
@@ -396,6 +402,7 @@ describe("MatrixRTCSession", () => {
device_id: "AAAAAAA",
expires: 3600000,
foci_active: [mockFocus],
membershipID: expect.stringMatching(".*"),
},
],
},
@@ -26,6 +26,7 @@ const membershipTemplate: CallMembershipData = {
application: "m.call",
device_id: "AAAAAAA",
expires: 60 * 60 * 1000,
membershipID: "bloop",
};
describe("MatrixRTCSessionManager", () => {
+16 -16
View File
@@ -37,7 +37,7 @@ function makeMockClient(opts: {
userId: string;
deviceId: string;
deviceKey?: string;
msc3882Enabled: boolean;
getLoginTokenEnabled: boolean;
msc3882r0Only: boolean;
msc3886Enabled: boolean;
devices?: Record<string, Partial<DeviceInfo>>;
@@ -54,7 +54,7 @@ function makeMockClient(opts: {
getVersions() {
return {
unstable_features: {
"org.matrix.msc3882": opts.msc3882Enabled,
"org.matrix.msc3882": opts.getLoginTokenEnabled,
"org.matrix.msc3886": opts.msc3886Enabled,
},
};
@@ -64,8 +64,8 @@ function makeMockClient(opts: {
? {}
: {
capabilities: {
"org.matrix.msc3882.get_login_token": {
enabled: opts.msc3882Enabled,
"m.get_login_token": {
enabled: opts.getLoginTokenEnabled,
},
},
};
@@ -122,7 +122,7 @@ describe("Rendezvous", function () {
userId: "@alice:example.com",
deviceId: "DEVICEID",
msc3886Enabled: false,
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: true,
});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
@@ -180,10 +180,10 @@ describe("Rendezvous", function () {
});
async function testNoProtocols({
msc3882Enabled,
getLoginTokenEnabled,
msc3882r0Only,
}: {
msc3882Enabled: boolean;
getLoginTokenEnabled: boolean;
msc3882r0Only: boolean;
}) {
const aliceTransport = makeTransport("Alice");
@@ -198,7 +198,7 @@ describe("Rendezvous", function () {
userId: "alice",
deviceId: "ALICE",
msc3886Enabled: false,
msc3882Enabled,
getLoginTokenEnabled,
msc3882r0Only,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
@@ -241,11 +241,11 @@ describe("Rendezvous", function () {
}
it("no protocols - r0", async function () {
await testNoProtocols({ msc3882Enabled: false, msc3882r0Only: true });
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: true });
});
it("no protocols - r1", async function () {
await testNoProtocols({ msc3882Enabled: false, msc3882r0Only: false });
it("no protocols - stable", async function () {
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: false });
});
it("new device declines protocol with outcome unsupported", async function () {
@@ -260,7 +260,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
@@ -319,7 +319,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
@@ -378,7 +378,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
@@ -439,7 +439,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
@@ -508,7 +508,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
devices,
@@ -41,6 +41,7 @@ describe("CrossSigningIdentity", () => {
olmMachine = {
crossSigningStatus: jest.fn(),
bootstrapCrossSigning: jest.fn(),
exportCrossSigningKeys: jest.fn(),
close: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
@@ -50,6 +51,8 @@ describe("CrossSigningIdentity", () => {
secretStorage = {
get: jest.fn(),
hasKey: jest.fn(),
store: jest.fn(),
} as unknown as Mocked<ServerSideSecretStorage>;
crossSigning = new CrossSigningIdentity(olmMachine, outgoingRequestProcessor, secretStorage);
@@ -61,7 +64,8 @@ describe("CrossSigningIdentity", () => {
hasSelfSigning: true,
hasUserSigning: true,
});
// TODO: secret storage
// in secret storage
secretStorage.get.mockResolvedValue("base64-saved-in-storage");
await crossSigning.bootstrapCrossSigning({});
expect(olmMachine.bootstrapCrossSigning).not.toHaveBeenCalled();
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
@@ -73,6 +77,19 @@ describe("CrossSigningIdentity", () => {
expect(olmMachine.bootstrapCrossSigning).toHaveBeenCalledWith(true);
});
it("Shoud update 4S on reset if 4S is set up", async () => {
olmMachine.bootstrapCrossSigning.mockResolvedValue([]);
secretStorage.hasKey.mockResolvedValue(true);
olmMachine.exportCrossSigningKeys.mockResolvedValue({
masterKey: "base64_aaaaaaaaaa",
self_signing_key: "base64_bbbbbbbbbbb",
userSigningKey: "base64_cccccccc",
});
await crossSigning.bootstrapCrossSigning({ setupNewCrossSigning: true });
expect(olmMachine.bootstrapCrossSigning).toHaveBeenCalledWith(true);
expect(secretStorage.store).toHaveBeenCalledTimes(3);
});
it("should call bootstrapCrossSigning if we need new keys", async () => {
olmMachine.crossSigningStatus.mockResolvedValue({
hasMaster: false,
+57 -6
View File
@@ -54,6 +54,56 @@ const TEST_DEVICE_ID = "TEST_DEVICE";
afterEach(() => {
fetchMock.reset();
jest.restoreAllMocks();
});
describe("initRustCrypto", () => {
function makeTestOlmMachine(): Mocked<OlmMachine> {
return {
registerRoomKeyUpdatedCallback: jest.fn(),
registerUserIdentityUpdatedCallback: jest.fn(),
outgoingRequests: jest.fn(),
} as unknown as Mocked<OlmMachine>;
}
it("passes through the store params", async () => {
const testOlmMachine = makeTestOlmMachine();
jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine);
await initRustCrypto(
{} as MatrixClient["http"],
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
"storePrefix",
"storePassphrase",
);
expect(OlmMachine.initialize).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
"storePrefix",
"storePassphrase",
);
});
it("suppresses the storePassphrase if storePrefix is unset", async () => {
const testOlmMachine = makeTestOlmMachine();
jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine);
await initRustCrypto(
{} as MatrixClient["http"],
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
null,
"storePassphrase",
);
expect(OlmMachine.initialize).toHaveBeenCalledWith(expect.anything(), expect.anything(), undefined, undefined);
});
});
describe("RustCrypto", () => {
@@ -162,6 +212,7 @@ describe("RustCrypto", () => {
it("returns sensible values on a default client", async () => {
const secretStorage = {
isStored: jest.fn().mockResolvedValue(null),
getDefaultKeyId: jest.fn().mockResolvedValue("key"),
} as unknown as Mocked<ServerSideSecretStorage>;
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
@@ -182,6 +233,7 @@ describe("RustCrypto", () => {
it("throws if `stop` is called mid-call", async () => {
const secretStorage = {
isStored: jest.fn().mockResolvedValue(null),
getDefaultKeyId: jest.fn().mockResolvedValue(null),
} as unknown as Mocked<ServerSideSecretStorage>;
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
@@ -208,7 +260,10 @@ describe("RustCrypto", () => {
});
it("isSecretStorageReady", async () => {
const rustCrypto = await makeTestRustCrypto();
const mockSecretStorage = {
getDefaultKeyId: jest.fn().mockResolvedValue(null),
} as unknown as Mocked<ServerSideSecretStorage>;
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, mockSecretStorage);
await expect(rustCrypto.isSecretStorageReady()).resolves.toBe(false);
});
@@ -398,10 +453,6 @@ describe("RustCrypto", () => {
);
});
afterEach(() => {
jest.restoreAllMocks();
});
async function makeEncryptedEvent(): Promise<MatrixEvent> {
const encryptedEvent = mkEvent({
event: true,
@@ -812,5 +863,5 @@ async function makeTestRustCrypto(
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
cryptoCallbacks: CryptoCallbacks = {} as CryptoCallbacks,
): Promise<RustCrypto> {
return await initRustCrypto(http, userId, deviceId, secretStorage, cryptoCallbacks, null);
return await initRustCrypto(http, userId, deviceId, secretStorage, cryptoCallbacks, null, undefined);
}
+61 -2
View File
@@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { secretStorageContainsCrossSigningKeys } from "../../../src/rust-crypto/secret-storage";
import {
secretStorageCanAccessSecrets,
secretStorageContainsCrossSigningKeys,
} from "../../../src/rust-crypto/secret-storage";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
describe("secret-storage", () => {
@@ -22,6 +25,7 @@ describe("secret-storage", () => {
it("should return false when the master cross-signing key is not stored in secret storage", async () => {
const secretStorage = {
isStored: jest.fn().mockReturnValue(false),
getDefaultKeyId: jest.fn().mockResolvedValue("SFQ3TbqGOdaaRVfxHtNkn0tvhx0rVj9S"),
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
@@ -35,6 +39,7 @@ describe("secret-storage", () => {
if (type === "m.cross_signing.master") return { secretStorageKey: {} };
else return { secretStorageKey2: {} };
},
getDefaultKeyId: jest.fn().mockResolvedValue("SFQ3TbqGOdaaRVfxHtNkn0tvhx0rVj9S"),
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
@@ -51,19 +56,73 @@ describe("secret-storage", () => {
return { secretStorageKey2: {} };
}
},
getDefaultKeyId: jest.fn().mockResolvedValue("secretStorageKey"),
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeFalsy();
});
it("should return true when there is shared secret storage key between master, user signing and self signing keys", async () => {
it("should return true when master, user signing and self signing keys are all encrypted with default key", async () => {
const secretStorage = {
isStored: jest.fn().mockReturnValue({ secretStorageKey: {} }),
getDefaultKeyId: jest.fn().mockResolvedValue("secretStorageKey"),
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeTruthy();
});
it("should return false when master, user signing and self signing keys are all encrypted with a non-default key", async () => {
const secretStorage = {
isStored: jest.fn().mockResolvedValue({ defaultKey: {} }),
getDefaultKeyId: jest.fn().mockResolvedValue("anotherCommonKey"),
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeFalsy();
});
});
it("Check canAccessSecrets", async () => {
const secretStorage = {
isStored: jest.fn((secretName) => {
if (secretName == "secretA") {
return { aaaa: {} };
} else if (secretName == "secretB") {
return { bbbb: {} };
} else if (secretName == "secretC") {
return { cccc: {} };
} else if (secretName == "secretD") {
return { aaaa: {} };
} else if (secretName == "secretE") {
return { aaaa: {}, bbbb: {} };
} else {
null;
}
}),
getDefaultKeyId: jest.fn().mockResolvedValue("aaaa"),
} as unknown as ServerSideSecretStorage;
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretE"])).toStrictEqual(true);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA"])).toStrictEqual(true);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretC"])).toStrictEqual(false);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretD"])).toStrictEqual(true);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretC"])).toStrictEqual(false);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretC", "secretA"])).toStrictEqual(false);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretD", "secretB"])).toStrictEqual(
false,
);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretD", "Unknown"])).toStrictEqual(
false,
);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretD", "secretE"])).toStrictEqual(
true,
);
expect(
await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretC", "secretD", "secretE"]),
).toStrictEqual(false);
expect(await secretStorageCanAccessSecrets(secretStorage, [])).toStrictEqual(true);
});
});
+2 -4
View File
@@ -243,9 +243,7 @@ export interface LoginResponse {
}
/**
* The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
* `m.login.token` issuance request.
* Note that this is UNSTABLE and subject to breaking changes without notice.
* The result of a successful `m.login.token` issuance request as per https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv1loginget_token
*/
export interface LoginTokenPostResponse {
/**
@@ -255,7 +253,7 @@ export interface LoginTokenPostResponse {
/**
* Expiration in seconds.
*
* @deprecated this is only provided for compatibility with original revision of the MSC.
* @deprecated this is only provided for compatibility with original revision of [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882).
*/
expires_in: number;
/**
+4 -7
View File
@@ -24,15 +24,15 @@ declare global {
/* eslint-enable no-var */
}
if (global.__js_sdk_entrypoint) {
if (globalThis.__js_sdk_entrypoint) {
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
}
global.__js_sdk_entrypoint = true;
globalThis.__js_sdk_entrypoint = true;
// just *accessing* indexedDB throws an exception in firefox with indexeddb disabled.
let indexedDB: IDBFactory | undefined;
try {
indexedDB = global.indexedDB;
indexedDB = globalThis.indexedDB;
} catch (e) {}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
@@ -40,8 +40,5 @@ if (indexedDB) {
matrixcs.setCryptoStoreFactory(() => new matrixcs.IndexedDBCryptoStore(indexedDB!, "matrix-js-sdk:crypto"));
}
// We export 3 things to make browserify happy as well as downstream projects.
// It's awkward, but required.
export * from "./matrix";
export default matrixcs; // keep export for browserify package deps
global.matrixcs = matrixcs;
globalThis.matrixcs = matrixcs;
+96 -46
View File
@@ -55,34 +55,34 @@ import { IRoomEncryption, RoomList } from "./crypto/RoomList";
import { logger } from "./logger";
import { SERVICE_TYPES } from "./service-types";
import {
Body,
ClientPrefix,
FileType,
HttpApiEvent,
HttpApiEventHandlerMap,
Upload,
UploadOpts,
MatrixError,
MatrixHttpApi,
Method,
retryNetworkOperation,
ClientPrefix,
MediaPrefix,
HTTPError,
IdentityPrefix,
IHttpOpts,
FileType,
UploadResponse,
HTTPError,
IRequestOpts,
Body,
MatrixError,
MatrixHttpApi,
MediaPrefix,
Method,
retryNetworkOperation,
Upload,
UploadOpts,
UploadResponse,
} from "./http-api";
import {
Crypto,
CryptoEvent,
CryptoEventHandlerMap,
fixBackupKey,
ICryptoCallbacks,
ICheckOwnCrossSigningTrustOpts,
ICryptoCallbacks,
IRoomKeyRequestBody,
isCryptoAvailable,
VerificationMethod,
IRoomKeyRequestBody,
} from "./crypto";
import { DeviceInfo } from "./crypto/deviceinfo";
import { decodeRecoveryKey } from "./crypto/recoverykey";
@@ -110,7 +110,7 @@ import { VerificationRequest } from "./crypto/verification/request/VerificationR
import { VerificationBase as Verification } from "./crypto/verification/Base";
import * as ContentHelpers from "./content-helpers";
import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
import { Room, NotificationCountType, RoomEvent, RoomEventHandlerMap, RoomNameState } from "./models/room";
import { NotificationCountType, Room, RoomEvent, RoomEventHandlerMap, RoomNameState } from "./models/room";
import { RoomMemberEvent, RoomMemberEventHandlerMap } from "./models/room-member";
import { IPowerLevelsContent, RoomStateEvent, RoomStateEventHandlerMap } from "./models/room-state";
import {
@@ -119,8 +119,10 @@ import {
IContextResponse,
ICreateRoomOpts,
IEventSearchOpts,
IFilterResponse,
IGuestAccessOpts,
IJoinRoomOpts,
INotificationsResponse,
IPaginateOpts,
IPresenceOpts,
IRedactOpts,
@@ -129,15 +131,14 @@ import {
IRoomDirectoryOptions,
ISearchOpts,
ISendEventResponse,
INotificationsResponse,
IFilterResponse,
ITagsResponse,
IStatusResponse,
ITagsResponse,
KnockRoomOpts,
} from "./@types/requests";
import {
EventType,
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
MSC3912_RELATION_BASED_REDACTIONS_PROP,
MsgType,
PUSHER_ENABLED,
RelationType,
@@ -146,7 +147,6 @@ import {
UNSTABLE_MSC3088_ENABLED,
UNSTABLE_MSC3088_PURPOSE,
UNSTABLE_MSC3089_TREE_SUBTYPE,
MSC3912_RELATION_BASED_REDACTIONS_PROP,
} from "./@types/event";
import { IdServerUnbindResult, IImageInfo, JoinRule, Preset, Visibility } from "./@types/partials";
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper";
@@ -178,30 +178,30 @@ import {
} from "./@types/PushRules";
import { IThreepid } from "./@types/threepids";
import { CryptoStore, OutgoingRoomKeyRequest } from "./crypto/store/base";
import { GroupCall, IGroupCallDataChannelOptions, GroupCallIntent, GroupCallType } from "./webrtc/groupCall";
import { GroupCall, GroupCallIntent, GroupCallType, IGroupCallDataChannelOptions } from "./webrtc/groupCall";
import { MediaHandler } from "./webrtc/mediaHandler";
import {
LoginTokenPostResponse,
ILoginFlowsResponse,
IRefreshTokenResponse,
SSOAction,
LoginResponse,
LoginRequest,
LoginResponse,
LoginTokenPostResponse,
SSOAction,
} from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter";
import { MAIN_ROOM_TIMELINE, ReceiptType } from "./@types/read_receipts";
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
import { SlidingSyncSdk } from "./sliding-sync-sdk";
import {
determineFeatureSupport,
FeatureSupport,
Thread,
THREAD_RELATION_TYPE,
determineFeatureSupport,
ThreadFilterType,
threadFilterTypeToFilter,
} from "./models/thread";
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
import { UnstableValue } from "./NamespacedValue";
import { M_BEACON_INFO, MBeaconInfoEventContent } from "./@types/beacon";
import { NamespacedValue, UnstableValue } from "./NamespacedValue";
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
import { ToDeviceBatch } from "./models/ToDeviceMessage";
import { IgnoredInvites } from "./models/invites-ignorer";
@@ -343,7 +343,14 @@ export interface ICreateClientOpts {
deviceToImport?: IExportedDevice;
/**
* Key used to pickle olm objects or other sensitive data.
* Encryption key used for encrypting sensitive data (such as e2ee keys) in storage.
*
* This must be set to the same value every time the client is initialised for the same device.
*
* If unset, either a hardcoded key or no encryption at all is used, depending on the Crypto implementation.
*
* No particular requirement is placed on the key data (it is fed into an HKDF to generate the actual encryption
* keys).
*/
pickleKey?: string;
@@ -512,9 +519,22 @@ export interface IChangePasswordCapability extends ICapability {}
export interface IThreadsCapability extends ICapability {}
export interface IMSC3882GetLoginTokenCapability extends ICapability {}
export interface IGetLoginTokenCapability extends ICapability {}
export const UNSTABLE_MSC3882_CAPABILITY = new UnstableValue("m.get_login_token", "org.matrix.msc3882.get_login_token");
/**
* @deprecated use {@link IGetLoginTokenCapability} instead
*/
export type IMSC3882GetLoginTokenCapability = IGetLoginTokenCapability;
export const GET_LOGIN_TOKEN_CAPABILITY = new NamespacedValue(
"m.get_login_token",
"org.matrix.msc3882.get_login_token",
);
/**
* @deprecated use {@link GET_LOGIN_TOKEN_CAPABILITY} instead
*/
export const UNSTABLE_MSC3882_CAPABILITY = GET_LOGIN_TOKEN_CAPABILITY;
export const UNSTABLE_MSC2666_SHARED_ROOMS = "uk.half-shot.msc2666";
export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms";
@@ -529,8 +549,8 @@ export interface Capabilities {
"m.change_password"?: IChangePasswordCapability;
"m.room_versions"?: IRoomVersionsCapability;
"io.element.thread"?: IThreadsCapability;
[UNSTABLE_MSC3882_CAPABILITY.name]?: IMSC3882GetLoginTokenCapability;
[UNSTABLE_MSC3882_CAPABILITY.altName]?: IMSC3882GetLoginTokenCapability;
"m.get_login_token"?: IGetLoginTokenCapability;
"org.matrix.msc3882.get_login_token"?: IGetLoginTokenCapability;
}
/** @deprecated prefer {@link CrossSigningKeyInfo}. */
@@ -1193,7 +1213,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public store: Store;
public deviceId: string | null;
public credentials: { userId: string | null };
/**
* Encryption key used for encrypting sensitive data (such as e2ee keys) in storage.
*
* As supplied in the constructor via {@link IMatrixClientCreateOpts#pickleKey}.
*
* If unset, either a hardcoded key or no encryption at all is used, depending on the Crypto implementation.
*
* @deprecated this should be a private property.
*/
public pickleKey?: string;
public scheduler?: MatrixScheduler;
public clientRunning = false;
public timelineSupport = false;
@@ -1534,7 +1565,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.clientOpts.threadSupport = this.clientOpts.experimentalThreadSupport;
}
this.syncApi.sync();
this.syncApi.sync().catch((e) => logger.info("Sync startup aborted with an error:", e));
if (this.clientOpts.clientWellKnownPollPeriod !== undefined) {
this.clientWellKnownIntervalID = setInterval(() => {
@@ -2279,6 +2310,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.secretStorage,
this.cryptoCallbacks,
useIndexedDB ? RUST_SDK_STORE_PREFIX : null,
this.pickleKey,
);
rustCrypto.setSupportedVerificationMethods(this.verificationMethods);
@@ -3313,10 +3345,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
/**
* Get information about the current key backup.
* @returns Information object from API or null
* Get information about the current key backup from the server.
*
* @deprecated Prefer {@link CryptoApi.checkKeyBackupAndEnable}.
* Performs some basic validity checks on the shape of the result, and raises an error if it is not as expected.
*
* **Note**: there is no (supported) way to distinguish between "failure to talk to the server" and "another client
* uploaded a key backup version using an algorithm I don't understand.
*
* @returns Information object from API, or null if no backup is present on the server.
*/
public async getKeyBackupVersion(): Promise<IKeyBackupInfo | null> {
let res: IKeyBackupInfo;
@@ -3696,7 +3732,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
targetSessionId?: string,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult> {
if (!this.crypto) {
if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled");
}
const storedKey = await this.secretStorage.get("m.megolm_backup.v1");
@@ -4164,9 +4200,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
const room = this.getRoom(roomIdOrAlias);
if (room?.hasMembershipState(this.credentials.userId!, "join")) {
return Promise.resolve(room);
}
if (room?.hasMembershipState(this.credentials.userId!, "join")) return room;
let signPromise: Promise<IThirdPartySigned | void> = Promise.resolve();
@@ -4191,6 +4225,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const res = await this.http.authedRequest<{ room_id: string }>(Method.Post, path, queryString, data);
const roomId = res.room_id;
// In case we were originally given an alias, check the room cache again
// with the resolved ID - this method is supposed to no-op if we already
// were in the room, after all.
const resolvedRoom = this.getRoom(roomId);
if (resolvedRoom?.hasMembershipState(this.credentials.userId!, "join")) return resolvedRoom;
const syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
const syncRoom = syncApi.createRoom(roomId);
if (opts.syncRoom) {
@@ -7975,7 +8015,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Make a request for an `m.login.token` to be issued as per
* [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882).
* The server may require User-Interactive auth.
* Note that this is UNSTABLE and subject to breaking changes without notice.
*
* Compatibility with unstable implementations of MSC3882 is deprecated and will be removed in a future release.
*
* @param auth - Optional. Auth data to supply for User-Interactive auth.
* @returns Promise which resolves: On success, the token response
* or UIA auth data.
@@ -7983,10 +8025,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public async requestLoginToken(auth?: AuthDict): Promise<UIAResponse<LoginTokenPostResponse>> {
// use capabilities to determine which revision of the MSC is being used
const capabilities = await this.getCapabilities();
// use r1 endpoint if capability is exposed otherwise use old r0 endpoint
const endpoint = UNSTABLE_MSC3882_CAPABILITY.findIn(capabilities)
? "/org.matrix.msc3882/login/get_token" // r1 endpoint
: "/org.matrix.msc3882/login/token"; // r0 endpoint
let endpoint: string;
if (capabilities[GET_LOGIN_TOKEN_CAPABILITY.name]) {
// use the stable endpoint
endpoint = `${ClientPrefix.V1}/login/get_token`;
} else if (capabilities[GET_LOGIN_TOKEN_CAPABILITY.altName!]) {
// newer unstable r1 endpoint
endpoint = `${ClientPrefix.Unstable}/org.matrix.msc3882/login/get_token`;
} else {
// old unstable r0 endpoint
endpoint = `${ClientPrefix.Unstable}/org.matrix.msc3882/login/token`;
}
const body: UIARequest<{}> = { auth };
const res = await this.http.authedRequest<UIAResponse<LoginTokenPostResponse>>(
@@ -7994,10 +8044,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
endpoint,
undefined, // no query params
body,
{ prefix: ClientPrefix.Unstable },
{ prefix: "" },
);
// the representation of expires_in changed from revision 0 to revision 1 so we populate
// the representation of expires_in changed from unstable revision 0 to unstable revision 1 so we cross populate
if ("login_token" in res) {
if (typeof res.expires_in_ms === "number") {
res.expires_in = Math.floor(res.expires_in_ms / 1000);
-1
View File
@@ -480,7 +480,6 @@ export class BackupManager {
const delay = Math.random() * maxDelay;
await sleep(delay);
if (!this.clientRunning) {
logger.debug("Key backup send aborted, client stopped");
this.sendingBackups = false;
return;
}
+3 -3
View File
@@ -16,9 +16,9 @@ limitations under the License.
import { logger } from "../logger";
export let crypto = global.window?.crypto;
export let subtleCrypto = global.window?.crypto?.subtle ?? global.window?.crypto?.webkitSubtle;
export let TextEncoder = global.window?.TextEncoder;
export let crypto = globalThis.window?.crypto;
export let subtleCrypto = globalThis.window?.crypto?.subtle ?? global.window?.crypto?.webkitSubtle;
export let TextEncoder = globalThis.window?.TextEncoder;
/* eslint-disable @typescript-eslint/no-var-requires */
if (!crypto) {
+10 -2
View File
@@ -134,7 +134,7 @@ export const verificationMethods = {
export type VerificationMethod = keyof typeof verificationMethods | string;
export function isCryptoAvailable(): boolean {
return Boolean(global.Olm);
return Boolean(globalThis.Olm);
}
// minimum time between attempting to unwedge an Olm session, if we succeeded
@@ -2738,6 +2738,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const senderId = event.getSender();
if (!senderId || encryptionInfo.mismatchedSender) {
// something definitely wrong is going on here
// previously: E2EState.Warning -> E2ePadlockUnverified -> Red/"Encrypted by an unverified session"
return {
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.MISMATCHED_SENDER_KEY,
@@ -2750,11 +2752,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// shield, otherwise if the user isn't cross-signed then
// nothing's needed
if (!encryptionInfo.authenticated) {
// previously: E2EState.Unauthenticated -> E2ePadlockUnauthenticated -> Grey/"The authenticity of this encrypted message can't be guaranteed on this device."
return {
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
};
} else {
// previously: E2EState.Normal -> no icon
return { shieldColour: EventShieldColour.NONE, shieldReason: null };
}
}
@@ -2765,6 +2769,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
(await this.getDeviceVerificationStatus(senderId, encryptionInfo.sender.deviceId));
if (!eventSenderTrust) {
// previously: E2EState.Unknown -> E2ePadlockUnknown -> Grey/"Encrypted by a deleted session"
return {
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.UNKNOWN_DEVICE,
@@ -2772,19 +2777,22 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
if (!eventSenderTrust.isVerified()) {
// previously: E2EState.Warning -> E2ePadlockUnverified -> Red/"Encrypted by an unverified session"
return {
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.UNVERIFIED_IDENTITY,
shieldReason: EventShieldReason.UNSIGNED_DEVICE,
};
}
if (!encryptionInfo.authenticated) {
// previously: E2EState.Unauthenticated -> E2ePadlockUnauthenticated -> Grey/"The authenticity of this encrypted message can't be guaranteed on this device."
return {
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
};
}
// previously: E2EState.Verified -> no icon
return { shieldColour: EventShieldColour.NONE, shieldReason: null };
}
+5
View File
@@ -29,6 +29,7 @@ export interface CallMembershipData {
created_ts?: number;
expires: number;
foci_active?: Focus[];
membershipID: string;
}
export class CallMembership {
@@ -64,6 +65,10 @@ export class CallMembership {
return this.data.scope;
}
public get membershipID(): string {
return this.data.membershipID;
}
public createdTs(): number {
return this.data.created_ts ?? this.parentEvent.getTs();
}
+16 -1
View File
@@ -23,6 +23,7 @@ import { EventType } from "../@types/event";
import { CallMembership, CallMembershipData } from "./CallMembership";
import { Focus } from "./focus";
import { MatrixEvent } from "../matrix";
import { randomString } from "../randomstring";
const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000;
const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event
@@ -54,6 +55,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
// if we're not yet joined
private relativeExpiry: number | undefined;
// An identifier for our membership of the call. This will allow us to easily recognise
// whether a membership was sent by this session or is stale from some other time.
// It also forces our membership events to be unique, because otherwise we could try
// to overwrite a membership from a previous session but it would do nothing because the
// event content would be identical. We need the origin_server_ts to update though, so
// forcing unique content fixes this.
private membershipId: string | undefined;
private memberEventTimeout?: ReturnType<typeof setTimeout>;
private expiryTimeout?: ReturnType<typeof setTimeout>;
@@ -174,6 +183,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
logger.info(`Joining call session in room ${this.room.roomId}`);
this.activeFoci = activeFoci;
this.relativeExpiry = MEMBERSHIP_EXPIRY_TIME;
this.membershipId = randomString(5);
this.emit(MatrixRTCSessionEvent.JoinStateChanged, true);
// We don't wait for this, mostly because it may fail and schedule a retry, so this
// function returning doesn't really mean anything at all.
@@ -195,6 +205,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
logger.info(`Leaving call session in room ${this.room.roomId}`);
this.relativeExpiry = undefined;
this.activeFoci = undefined;
this.membershipId = undefined;
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
this.triggerCallMembershipEventUpdate();
}
@@ -249,6 +260,9 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
if (this.relativeExpiry === undefined) {
throw new Error("Tried to create our own membership event when we're not joined!");
}
if (this.membershipId === undefined) {
throw new Error("Tried to create our own membership event when we have no membership ID!");
}
const m: CallMembershipData = {
call_id: "",
@@ -257,6 +271,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
device_id: this.client.getDeviceId()!,
expires: this.relativeExpiry,
foci_active: this.activeFoci,
membershipID: this.membershipId,
};
if (prevMembership) m.created_ts = prevMembership.createdTs();
@@ -373,7 +388,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
const myPrevMembershipData = memberships.find((m) => m.device_id === localDeviceId);
let myPrevMembership;
try {
if (myCallMemberEvent && myPrevMembershipData) {
if (myCallMemberEvent && myPrevMembershipData && myPrevMembershipData.membershipID === this.membershipId) {
myPrevMembership = new CallMembership(myCallMemberEvent, myPrevMembershipData);
}
} catch (e) {
-30
View File
@@ -71,36 +71,6 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
],
actions: [PushRuleActionName.DontNotify],
},
{
rule_id: RuleId.IsUserMention,
default: true,
enabled: true,
conditions: [
{
kind: ConditionKind.EventPropertyContains,
key: "content.m\\.mentions.user_ids",
value: "", // The user ID is dynamically added in rewriteDefaultRules.
},
],
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight }],
},
{
rule_id: RuleId.IsRoomMention,
default: true,
enabled: true,
conditions: [
{
kind: ConditionKind.EventPropertyIs,
key: "content.m\\.mentions.room",
value: true,
},
{
kind: ConditionKind.SenderNotificationPermission,
key: "room",
},
],
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight }],
},
{
// For homeservers which don't support MSC3786 yet
rule_id: ".org.matrix.msc3786.rule.room.server_acl",
+4 -9
View File
@@ -17,12 +17,7 @@ limitations under the License.
import { UnstableValue } from "matrix-events-sdk";
import { RendezvousChannel, RendezvousFailureListener, RendezvousFailureReason, RendezvousIntent } from ".";
import {
ICrossSigningKey,
IMSC3882GetLoginTokenCapability,
MatrixClient,
UNSTABLE_MSC3882_CAPABILITY,
} from "../client";
import { ICrossSigningKey, IGetLoginTokenCapability, MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "../client";
import { CrossSigningInfo } from "../crypto/CrossSigning";
import { DeviceInfo } from "../crypto/deviceinfo";
import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature";
@@ -105,15 +100,15 @@ export class MSC3906Rendezvous {
logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`);
// in r1 of MSC3882 the availability is exposed as a capability
// in stable and unstable r1 the availability is exposed as a capability
const capabilities = await this.client.getCapabilities();
// in r0 of MSC3882 the availability is exposed as a feature flag
const features = await buildFeatureSupportMap(await this.client.getVersions());
const capability = UNSTABLE_MSC3882_CAPABILITY.findIn<IMSC3882GetLoginTokenCapability>(capabilities);
const capability = GET_LOGIN_TOKEN_CAPABILITY.findIn<IGetLoginTokenCapability>(capabilities);
// determine available protocols
if (!capability?.enabled && features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) {
logger.info("Server doesn't support MSC3882");
logger.info("Server doesn't support get_login_token");
await this.send({ type: PayloadType.Finish, outcome: Outcome.Unsupported });
await this.cancel(RendezvousFailureReason.HomeserverLacksSupport);
return undefined;
+60 -28
View File
@@ -65,34 +65,42 @@ export class CrossSigningIdentity {
privateKeysInSecretStorage,
});
if (!olmDeviceHasKeys && !privateKeysInSecretStorage) {
logger.log(
"bootStrapCrossSigning: Cross-signing private keys not found locally or in secret storage, creating new keys",
);
await this.resetCrossSigning(opts.authUploadDeviceSigningKeys);
} else if (olmDeviceHasKeys) {
logger.log("bootStrapCrossSigning: Olm device has private keys: exporting to secret storage");
await this.exportCrossSigningKeysToStorage();
} else if (privateKeysInSecretStorage) {
logger.log(
"bootStrapCrossSigning: Cross-signing private keys not found locally, but they are available " +
"in secret storage, reading storage and caching locally",
);
await this.olmMachine.importCrossSigningKeys(
masterKeyFromSecretStorage,
selfSigningKeyFromSecretStorage,
userSigningKeyFromSecretStorage,
);
if (olmDeviceHasKeys) {
if (!privateKeysInSecretStorage) {
// the device has the keys but they are not in 4S, so update it
logger.log("bootStrapCrossSigning: Olm device has private keys: exporting to secret storage");
await this.exportCrossSigningKeysToStorage();
} else {
logger.log("bootStrapCrossSigning: Olm device has private keys and they are saved in 4S, do nothing");
}
} /* (!olmDeviceHasKeys) */ else {
if (privateKeysInSecretStorage) {
// they are in 4S, so import from there
logger.log(
"bootStrapCrossSigning: Cross-signing private keys not found locally, but they are available " +
"in secret storage, reading storage and caching locally",
);
await this.olmMachine.importCrossSigningKeys(
masterKeyFromSecretStorage,
selfSigningKeyFromSecretStorage,
userSigningKeyFromSecretStorage,
);
// Get the current device
const device: RustSdkCryptoJs.Device = await this.olmMachine.getDevice(
this.olmMachine.userId,
this.olmMachine.deviceId,
);
// Get the current device
const device: RustSdkCryptoJs.Device = await this.olmMachine.getDevice(
this.olmMachine.userId,
this.olmMachine.deviceId,
);
// Sign the device with our cross-signing key and upload the signature
const request: RustSdkCryptoJs.SignatureUploadRequest = await device.verify();
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
// Sign the device with our cross-signing key and upload the signature
const request: RustSdkCryptoJs.SignatureUploadRequest = await device.verify();
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
} else {
logger.log(
"bootStrapCrossSigning: Cross-signing private keys not found locally or in secret storage, creating new keys",
);
await this.resetCrossSigning(opts.authUploadDeviceSigningKeys);
}
}
// TODO: we might previously have bootstrapped cross-signing but not completed uploading the keys to the
@@ -108,13 +116,21 @@ export class CrossSigningIdentity {
* * Upload the private keys to SSSS, if it is set up
*/
private async resetCrossSigning(authUploadDeviceSigningKeys?: UIAuthCallback<void>): Promise<void> {
// XXX: We must find a way to make this atomic, currently if the user does not remember his account password
// or 4S passphrase/key the process will fail in a bad state, with keys rotated but not uploaded or saved in 4S.
const outgoingRequests: Array<OutgoingRequest> = await this.olmMachine.bootstrapCrossSigning(true);
// If 4S is configured we need to udpate it.
if (await this.secretStorage.hasKey()) {
// Update 4S before uploading cross-signing keys, to stay consistent with legacy that asks
// 4S passphrase before asking for account password.
// Ultimately should be made atomic and resistent to forgotten password/passphrase.
await this.exportCrossSigningKeysToStorage();
}
logger.log("bootStrapCrossSigning: publishing keys to server");
for (const req of outgoingRequests) {
await this.outgoingRequestProcessor.makeOutgoingRequest(req, authUploadDeviceSigningKeys);
}
await this.exportCrossSigningKeysToStorage();
}
/**
@@ -123,6 +139,22 @@ export class CrossSigningIdentity {
* (If secret storage is *not* configured, we assume that the export will happen when it is set up)
*/
private async exportCrossSigningKeysToStorage(): Promise<void> {
// TODO
const exported: RustSdkCryptoJs.CrossSigningKeyExport | null = await this.olmMachine.exportCrossSigningKeys();
/* istanbul ignore else (this function is only called when we know the olm machine has keys) */
if (exported?.masterKey) {
this.secretStorage.store("m.cross_signing.master", exported.masterKey);
} else {
logger.error(`Cannot export MSK to secret storage, private key unknown`);
}
if (exported?.self_signing_key) {
this.secretStorage.store("m.cross_signing.self_signing", exported.self_signing_key);
} else {
logger.error(`Cannot export SSK to secret storage, private key unknown`);
}
if (exported?.userSigningKey) {
this.secretStorage.store("m.cross_signing.user_signing", exported.userSigningKey);
} else {
logger.error(`Cannot export USK to secret storage, private key unknown`);
}
}
}
-31
View File
@@ -1,31 +0,0 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* This file replaces rust-crypto/index.ts when the js-sdk is being built for browserify.
*
* It is a stub, so that we do not import the whole of the base64'ed wasm artifact into the browserify bundle.
* It deliberately does nothing except raise an exception.
*/
import { IHttpOpts, MatrixHttpApi } from "../http-api";
export async function initRustCrypto(
_http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
_userId: string,
_deviceId: string,
): Promise<Crypto> {
throw new Error("Rust crypto is not supported under browserify.");
}
+6 -2
View File
@@ -32,7 +32,10 @@ import { ICryptoCallbacks } from "../crypto";
* @param secretStorage - Interface to server-side secret storage.
* @param cryptoCallbacks - Crypto callbacks provided by the application
* @param storePrefix - the prefix to use on the indexeddbs created by rust-crypto.
* If unset, a memory store will be used.
* If `null`, a memory store will be used.
* @param storePassphrase - a passphrase to use to encrypt the indexeddbs created by rust-crypto.
* Ignored if `storePrefix` is null. If this is `undefined` (and `storePrefix` is not null), the indexeddbs
* will be unencrypted.
*
* @internal
*/
@@ -43,6 +46,7 @@ export async function initRustCrypto(
secretStorage: ServerSideSecretStorage,
cryptoCallbacks: ICryptoCallbacks,
storePrefix: string | null,
storePassphrase: string | undefined,
): Promise<RustCrypto> {
// initialise the rust matrix-sdk-crypto-wasm, if it hasn't already been done
await RustSdkCryptoJs.initAsync();
@@ -59,7 +63,7 @@ export async function initRustCrypto(
u,
d,
storePrefix ?? undefined,
(storePrefix && "test pass") ?? undefined,
(storePrefix && storePassphrase) ?? undefined,
);
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId, secretStorage, cryptoCallbacks);
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
+16 -2
View File
@@ -57,7 +57,7 @@ import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
import { Device, DeviceMap } from "../models/device";
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../secret-storage";
import { CrossSigningIdentity } from "./CrossSigningIdentity";
import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } from "./secret-storage";
import { keyFromPassphrase } from "../crypto/key_passphrase";
import { encodeRecoveryKey } from "../crypto/recoverykey";
import { crypto } from "../crypto/crypto";
@@ -623,7 +623,20 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
* Implementation of {@link CryptoApi#isSecretStorageReady}
*/
public async isSecretStorageReady(): Promise<boolean> {
return false;
// make sure that the cross-signing keys are stored
const secretsToCheck = [
"m.cross_signing.master",
"m.cross_signing.user_signing",
"m.cross_signing.self_signing",
];
// if key backup is active, we also need to check that the backup decryption key is stored
const keyBackupEnabled = (await this.backupManager.getActiveBackupVersion()) != null;
if (keyBackupEnabled) {
secretsToCheck.push("m.megolm_backup.v1");
}
return secretStorageCanAccessSecrets(this.secretStorage, secretsToCheck);
}
/**
@@ -1649,6 +1662,7 @@ function rustEncryptionInfoToJsEncryptionInfo(
if (shieldState.message === null) {
shieldReason = null;
} else if (shieldState.message === "Encrypted by an unverified user.") {
// this case isn't actually used with lax shield semantics.
shieldReason = EventShieldReason.UNVERIFIED_IDENTITY;
} else if (shieldState.message === "Encrypted by a device not verified by its owner.") {
shieldReason = EventShieldReason.UNSIGNED_DEVICE;
+33 -17
View File
@@ -17,7 +17,7 @@ limitations under the License.
import { ServerSideSecretStorage } from "../secret-storage";
/**
* Check that the private cross signing keys (master, self signing, user signing) are stored in the secret storage and encrypted with the same secret storage key.
* Check that the private cross signing keys (master, self signing, user signing) are stored in the secret storage and encrypted with the default secret storage key.
*
* @param secretStorage - The secret store using account data
* @returns True if the cross-signing keys are all stored and encrypted with the same secret storage key.
@@ -25,20 +25,36 @@ import { ServerSideSecretStorage } from "../secret-storage";
* @internal
*/
export async function secretStorageContainsCrossSigningKeys(secretStorage: ServerSideSecretStorage): Promise<boolean> {
// Check if the master cross-signing key is stored in secret storage
const secretStorageMasterKeys = await secretStorage.isStored("m.cross_signing.master");
// Master key not stored
if (!secretStorageMasterKeys) return false;
// Get the user signing keys stored into the secret storage
const secretStorageUserSigningKeys = (await secretStorage.isStored(`m.cross_signing.user_signing`)) || {};
// Get the self signing keys stored into the secret storage
const secretStorageSelfSigningKeys = (await secretStorage.isStored(`m.cross_signing.self_signing`)) || {};
// Check that one of the secret storage keys used to encrypt the master key was also used to encrypt the user-signing and self-signing keys
return Object.keys(secretStorageMasterKeys).some(
(secretStorageKey) =>
secretStorageUserSigningKeys[secretStorageKey] && secretStorageSelfSigningKeys[secretStorageKey],
);
return secretStorageCanAccessSecrets(secretStorage, [
"m.cross_signing.master",
"m.cross_signing.user_signing",
"m.cross_signing.self_signing",
]);
}
/**
*
* Check that the secret storage can access the given secrets using the default key.
*
* @param secretStorage - The secret store using account data
* @param secretNames - The secret names to check
* @returns True if all the given secrets are accessible and encrypted with the given key.
*
* @internal
*/
export async function secretStorageCanAccessSecrets(
secretStorage: ServerSideSecretStorage,
secretNames: string[],
): Promise<boolean> {
const defaultKeyId = await secretStorage.getDefaultKeyId();
if (!defaultKeyId) return false;
for (const secretName of secretNames) {
// check which keys this particular secret is encrypted with
const record = (await secretStorage.isStored(secretName)) || {};
// if it's not encrypted with the right key, there is no point continuing
if (!(defaultKeyId in record)) return false;
}
return true;
}
+11
View File
@@ -1604,6 +1604,17 @@ export class SyncApi {
* @param connDidFail - True if a connectivity failure has been detected. Optional.
*/
private pokeKeepAlive(connDidFail = false): void {
if (!this.running) {
// we are in a keepAlive, retrying to connect, but the syncronization
// was stopped, so we are stopping the retry.
clearTimeout(this.keepAliveTimer);
if (this.connectionReturnedDefer) {
this.connectionReturnedDefer.reject("SyncApi.stop() was called");
this.connectionReturnedDefer = undefined;
}
return;
}
const success = (): void => {
clearTimeout(this.keepAliveTimer);
if (this.connectionReturnedDefer) {
+173 -1092
View File
File diff suppressed because it is too large Load Diff