Compare commits

..

42 Commits

Author SHA1 Message Date
RiotRobot c1b4f97372 v34.7.0-rc.0 2024-10-01 14:54:58 +00:00
Florian Duros 5f3b89990d Move out crypto/aes (#4431)
* Move `SecretEncryptedPayload` in `src/utils/@types`

* Move `encryptAES` to a dedicated file. Moved in a utils folder.

* Move `deriveKeys` to a dedicated file in order to share it

* Move `decryptAES` to a dedicated file. Moved in a utils folder.

* Move `calculateKeyCheck` to a dedicated file. Moved in a utils folder.

* Remove AES functions in `aes.ts` and export new ones for backward compatibility

* Update import to use new functions

* Add `src/utils` entrypoint in `README.md`

* - Rename `SecretEncryptedPayload` to `AESEncryptedSecretStoragePayload`.
- Move into `src/@types`

* Move `calculateKeyCheck` into `secret-storage.ts`.

* Move `deriveKeys` into `src/utils/internal` folder.

* - Rename `encryptAES` on `encryptAESSecretStorageItem`
- Change named export by default export

* - Rename `decryptAES` on `decryptAESSecretStorageItem`
- Change named export by default export

* Update documentation

* Update `decryptAESSecretStorageItem` doc

* Add lnk to spec for `calculateKeyCheck`

* Fix downstream tests
2024-10-01 13:52:59 +00:00
Richard van der Hoff 866fd6f4a3 Bump matrix-rust-sdk to 9.1.0 (#4435)
Mostly, to pick up https://github.com/matrix-org/matrix-rust-sdk/pull/3985
2024-09-30 16:46:33 +00:00
Valere 9ecb66e695 crypto: configure key sharing strategy based on DeviceIsolationMode (#4425)
* crypto: configure key sharing strategy based on deviceIsolationMode

fix eslint import error

cryptoMode was renamed to deviceIsolationMode

post rebase fix: Device Isolation mode name changes

* Fix outdated docs referring to old cryptomode

* code review: better comment for globalBlacklistUnverifiedDevices option

* RoomEncryptor: Use appropriate default for getBlacklistUnverifiedDevices

* do not provide a default value for DeviceIsolationMode for encryption

* Update src/rust-crypto/RoomEncryptor.ts

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-09-30 14:26:34 +00:00
Hugh Nimmo-Smith baa6d13506 RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys() (#4427)
* RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys()

* Clarify comment

* Feedback from code review

* Update src/matrixrtc/MatrixRTCSession.ts

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>

* Fix test

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
2024-09-27 15:55:07 +00:00
Hugh Nimmo-Smith 2d6230f199 Rotate RTC key when a new member joins (#4422) 2024-09-27 15:54:48 +00:00
Tulir Asokan 825d85f18d Update media event content types to include captions (#4403)
Signed-off-by: Tulir Asokan <tulir@maunium.net>
2024-09-26 16:06:59 +00:00
renovate[bot] d56fa197d0 Update dependency typescript to v5.6.2 (#4420)
* Update dependency typescript to v5.6.2

* Fix TS errors

* Update minimal version of TS to `5.4.2` since the code is not compliant with an older version.

* Review fixes

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
Co-authored-by: Florian Duros <florianduros@element.io>
2024-09-26 08:23:38 +00:00
Michael Telatynski f7229bfff0 Update OIDC registration types to match latest MSC2966 state (#4432)
* Update OIDC registration types to match latest MSC2966 state

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add comment

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-09-25 16:30:13 +00:00
Valere 538717c23e crypto: Replace cryptoMode with DeviceIsolationMode concept (#4429)
* crypto: Replace cryptoMode with DeviceIsolationMode concept

* use enum instead of string for the IsolationMode kind

* Code review - Cleaning, renaming

* review: unneeded @see in doc

* review: Rename IsolationMode with better names

* review: quick cleaning and doc
2024-09-25 13:33:02 +00:00
Richard van der Hoff 1a8ea3d685 Add CryptoApi.pinCurrentUserIdentity and UserIdentity.needsUserApproval (#4415)
* Implement `UserVerificationStatus.needsUserApproval`

Expose the `identityNeedsUserApproval` flag from the rust crypto crate.

* Add CryptoApi.pinCurrentUserIdentity

Expose `pinCurrentMasterKey` from the rust crypto api.

* Test data: add second cross-signing key for Bob

* Add tests for verification status
2024-09-24 16:38:18 +00:00
Florian Duros d0890d9450 Update sonarcloud action to use v3.3 matrix-org/sonarcloud-workflow-action (#4430) 2024-09-24 15:35:40 +00:00
RiotRobot 42ec17b977 Merge branch 'master' into develop 2024-09-24 12:37:47 +00:00
RiotRobot 82e9eefce6 v34.6.0 2024-09-24 12:37:19 +00:00
renovate[bot] f8208b1891 Update typedoc (#4419)
* Update typedoc

* Don't link a private method in tsdoc of a public method

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
2024-09-23 13:47:49 +00:00
Richard van der Hoff 092a59af66 Throw clearer error if createSecretStorageKey misbehaves (#4412)
https://github.com/element-hq/element-web/issues/27888 shows a slightly
mysterious error message, but the problem is `createSecretStorageKey`
returning the wrong thing.
2024-09-23 10:25:42 +00:00
renovate[bot] dbd7d26968 Update dependency @types/node to v18.19.50 (#4418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 15:26:50 +00:00
renovate[bot] 4fda9e8419 Update all non-major dependencies (#4417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 13:38:54 +00:00
RiotRobot a13e0389db v34.6.0-rc.0 2024-09-18 13:31:13 +00:00
Hubert Chathi dbb4828eda Add crypto mode setting for invisible crypto, and apply it to decrypting events (#4407)
Adds a global "crypto mode" setting to the crypto API (only works with Rust crypto), and changes the decryption settings based on that.
2024-09-18 12:53:07 +01:00
Hugh Nimmo-Smith 414ac9d8cc Don't share full key history for RTC per-participant encryption (#4406)
* Don't share full key history for RTC per-participant encryption

Also record stats for how many keys have been sent/received and age of those received

* Update src/matrixrtc/MatrixRTCSession.ts

Co-authored-by: Robin <robin@robin.town>

* Add comment about why we track total age of events

---------

Co-authored-by: Robin <robin@robin.town>
2024-09-18 08:57:26 +00:00
Johannes Marbach 30058a4bdc Add ESLint rule to ensure .ts extensions on imports (#4409)
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-09-18 07:40:01 +00:00
Florian Duros fab9cab3df Move crypto/key_passphrase.ts to crypto-api/key-passphrase.ts (#4401)
* Move `crypto/key_passphrase.ts` to `crypto-api/key-passphrase.ts`

* Re-export `crypto-api/key-passphrase` into `crypto/key_passphrase.ts`

* Add doc

* Deprecate `MatrixClient.keyBackupKeyFromPassword`

* Move `keyFromAuthData` to `common-crypto/key-passphrase.ts`

* Fix faulty import

* Keep `keyFromPassphrase` in old crypto

* - Rename `deriveKey` into `deriveRecoveryKeyFromPassphrase`
- Call `deriveRecoveryKeyFromPassphrase` into `RustCrypto.createRecoveryKeyFromPassphrase` instead of using `keyFromPassphrase`

* Remove alternative in `keyBackupKeyFromPassword` deprecation.

* Add tests for `keyFromAuthData`

* Deprecate `keyFromAuthData`

* Review changes
2024-09-17 13:05:47 +00:00
Johannes Marbach 53b599f8fe Export membership types (#4405)
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-09-14 10:44:51 +00:00
Timo 17f6cc733e Fix redact sending over in embedded (widget) mode (#4398) 2024-09-13 16:07:44 +00:00
Johannes Marbach c8403f39aa Fix build failure in node.js example (#4394)
* Fix node.js example

Relates to: element-hq/element-web#26922
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>

* Update examples/node/app.js

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

* Update examples/node/package.json

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

* Move imports to the top of the file

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-09-13 15:35:45 +00:00
Florian Duros 8cf5df73ee Move crypto/recoverykey.ts to crypto-api/recovery-key.ts (#4399)
* Move `crypto/recoverykey.ts` to `crypto-api/recovery-key.ts`

* Re-export `crypto-api/recovery-key` into `crypto/recoverykey`

* Add a bit of doc

* Deprecate `MatrixClient.isValidRecoveryKey` and `MatrixClient.keyBackupKeyFromRecoveryKey`

* Import `index.ts` directly

* Update `recovery-key.ts` doc

* Add tests for `decodeRecoveryKey`

* Move `recovery-key.spec.ts` file
2024-09-13 13:26:30 +00:00
Florian Duros febe27ddcc Replace matrix.org/matrix-react-sdk by element-hq/matrix-react-sdk (#4404) 2024-09-13 13:03:31 +00:00
Hubert Chathi 7987ce76ec Bump dependency on rust-sdk-crypto-wasm to v8.0.0 (#4396) 2024-09-13 10:22:33 +01:00
Richard van der Hoff 60cedf2fdb Improve documentation on {encrypt,decrypt}AES (#4397) 2024-09-11 14:13:12 +00:00
Robin ed44514974 Expose the event ID of a call membership (#4395)
This is in line with the other information we're already exposing, such as the event's sender and timestamp. We want this in order to play around with adding reactions to the membership event.
2024-09-10 20:15:07 +00:00
RiotRobot 9f8c1ee953 Merge branch 'master' into develop 2024-09-10 12:31:32 +00:00
Will Hunt e8128d34a1 MSC4133 - Extended profiles (#4391)
* Add MSC4133 functionality.

* Add MSC4133 capability.

* Tidy

* Add tests for extended profiles.

* improve docs

* undefined

* Add a prefix function to reduce reptitiveness

* Add a docstring
2024-09-09 12:06:38 +00:00
Richard van der Hoff ba7bd06295 Element-R: Mark unsupported MatrixClient methods as deprecated (#4389)
Slap a `@deprecated` tag on all the MatrixClient methods which no longer work
with rust crypto.
2024-09-05 13:37:08 +00:00
Richard van der Hoff e4db6008b8 Minor improvements to logging in device verification (#4390)
A grab-bag of small logging improvements in the Rust crypto wrapper.
2024-09-05 10:22:54 +00:00
Johannes Marbach 52f35409ec Stabilise MSC4156 (#4381)
* Stabilise MSC4156

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>

* Add deprecation comments

* Add minimum Matrix version

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-09-04 12:01:33 +00:00
renovate[bot] f50aab37c3 Update babel monorepo (#4387)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 21:29:21 +00:00
renovate[bot] df0f817f83 Update dependency typedoc-plugin-mdn-links to v3.2.10 (#4386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 21:20:01 +00:00
renovate[bot] 7efa6352f8 Update shogo82148/actions-upload-release-asset digest to aac270e (#4385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 20:32:50 +00:00
renovate[bot] f74614705e Update mheap/github-action-required-labels digest to d25134c (#4384)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 20:32:29 +00:00
renovate[bot] 169e8f8613 Update dependency @types/node to v18.19.47 (#4383)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 15:22:51 +00:00
renovate[bot] f2f77bd1f7 Update all non-major dependencies (#4382)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 13:51:09 +00:00
69 changed files with 2642 additions and 832 deletions
+9 -1
View File
@@ -1,5 +1,5 @@
module.exports = {
plugins: ["matrix-org", "import", "jsdoc"],
plugins: ["matrix-org", "import", "jsdoc", "node"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
parserOptions: {
project: ["./tsconfig.json"],
@@ -128,6 +128,14 @@ module.exports = {
// These need a bit more work before we can enable
// "jsdoc/check-param-names": "error",
// "jsdoc/check-indentation": "error",
// Ensure .ts extension on imports outside of tests
"node/file-extension-in-import": [
"error",
"always",
{
tryExtensions: [".ts"],
},
],
},
},
{
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@6d4fd50e333ee797e83ae380e6bc55b048baec41 # v1
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ env.VERSION }}.tar.gz.asc
@@ -29,13 +29,13 @@ runs:
- name: Upload asset signatures
if: inputs.gpg-fingerprint
uses: shogo82148/actions-upload-release-asset@6d4fd50e333ee797e83ae380e6bc55b048baec41 # v1
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@6d4fd50e333ee797e83ae380e6bc55b048baec41 # v1
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
@@ -21,14 +21,14 @@ concurrency:
jobs:
playwright:
name: Playwright
uses: matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop
uses: element-hq/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop
permissions:
actions: read
issues: read
pull-requests: read
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
react-sdk-repository: element-hq/matrix-react-sdk
# We only want to run the playwright tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume multiple concurrent runners.
skip: ${{ github.event_name != 'merge_group' }}
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
include:
- repo: element-hq/element-web
event: element-web-notify
- repo: matrix-org/matrix-react-sdk
- repo: element-hq/matrix-react-sdk
event: upstream-sdk-notify
runs-on: ubuntu-latest
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
name: Preview Changelog
runs-on: ubuntu-latest
steps:
- uses: mheap/github-action-required-labels@5847eef68201219cf0a4643ea7be61e77837bbce # v5
- uses: mheap/github-action-required-labels@d25134c992b943fb6ad00c25ea00eb5988c0a9dd # v5
if: github.event_name != 'merge_group'
with:
labels: |
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
with:
final: ${{ inputs.mode == 'final' }}
npm: ${{ inputs.npm }}
downstreams: '["matrix-org/matrix-react-sdk", "element-hq/element-web"]'
downstreams: '["element-hq/matrix-react-sdk", "element-hq/element-web"]'
docs:
name: Publish Documentation
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v3.2
uses: matrix-org/sonarcloud-workflow-action@v3.3
# 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:
+1 -1
View File
@@ -76,7 +76,7 @@ jobs:
matrix-react-sdk:
name: Downstream test matrix-react-sdk
if: github.event_name == 'merge_group'
uses: matrix-org/matrix-react-sdk/.github/workflows/tests.yml@develop
uses: element-hq/matrix-react-sdk/.github/workflows/tests.yml@develop
with:
disable_coverage: true
matrix-js-sdk-sha: ${{ github.sha }}
+16
View File
@@ -1,3 +1,19 @@
Changes in [34.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.6.0) (2024-09-24)
==================================================================================================
## 🦖 Deprecations
* Element-R: Mark unsupported MatrixClient methods as deprecated ([#4389](https://github.com/matrix-org/matrix-js-sdk/pull/4389)). Contributed by @richvdh.
## ✨ Features
* Add crypto mode setting for invisible crypto, and apply it to decrypting events ([#4407](https://github.com/matrix-org/matrix-js-sdk/pull/4407)). Contributed by @uhoreg.
* Don't share full key history for RTC per-participant encryption ([#4406](https://github.com/matrix-org/matrix-js-sdk/pull/4406)). Contributed by @hughns.
* Export membership types ([#4405](https://github.com/matrix-org/matrix-js-sdk/pull/4405)). Contributed by @Johennes.
* Fix sending redacts in embedded (widget) mode ([#4398](https://github.com/matrix-org/matrix-js-sdk/pull/4398)). Contributed by @toger5.
* Expose the event ID of a call membership ([#4395](https://github.com/matrix-org/matrix-js-sdk/pull/4395)). Contributed by @robintown.
* MSC4133 - Extended profiles ([#4391](https://github.com/matrix-org/matrix-js-sdk/pull/4391)). Contributed by @Half-Shot.
Changes in [34.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.5.0) (2024-09-10)
==================================================================================================
## 🦖 Deprecations
+1
View File
@@ -191,6 +191,7 @@ As well as the primary entry point (`matrix-js-sdk`), there are several other en
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
| `matrix-js-sdk/lib/utils/*.js` | A set of modules exporting standalone functions (and their types). |
## Examples
+16 -11
View File
@@ -1,9 +1,15 @@
import clc from "cli-color";
import fs from "fs";
import readline from "readline";
import sdk, { ClientEvent, EventType, MsgType, RoomEvent } from "matrix-js-sdk";
import { KnownMembership } from "matrix-js-sdk/lib/@types/membership.js";
var myHomeServer = "http://localhost:8008";
var myUserId = "@example:localhost";
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
var sdk = require("matrix-js-sdk");
var clc = require("cli-color");
var matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
baseUrl: myHomeServer,
accessToken: myAccessToken,
userId: myUserId,
});
@@ -15,7 +21,6 @@ var numMessagesToShow = 20;
// Reading from stdin
var CLEAR_CONSOLE = "\x1B[2J";
var readline = require("readline");
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
@@ -97,7 +102,7 @@ rl.on("line", function (line) {
})
.then(function (url) {
var content = {
msgtype: "m.file",
msgtype: MsgType.File,
body: filename,
url: JSON.parse(url).content_uri,
};
@@ -138,7 +143,7 @@ rl.on("line", function (line) {
// ==== END User input
// show the room list after syncing.
matrixClient.on("sync", function (state, prevState, data) {
matrixClient.on(ClientEvent.Sync, function (state, prevState, data) {
switch (state) {
case "PREPARED":
setRoomList();
@@ -149,7 +154,7 @@ matrixClient.on("sync", function (state, prevState, data) {
}
});
matrixClient.on("Room", function () {
matrixClient.on(ClientEvent.Room, function () {
setRoomList();
if (!viewingRoom) {
printRoomList();
@@ -158,7 +163,7 @@ matrixClient.on("Room", function () {
});
// print incoming messages.
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
if (toStartOfTimeline) {
return; // don't print paginated results
}
@@ -305,7 +310,7 @@ function printRoomInfo(room) {
print(eTypeHeader + sendHeader + contentHeader);
print(new Array(100).join("-"));
eventMap.keys().forEach(function (eventType) {
if (eventType === "m.room.member") {
if (eventType === EventType.RoomMember) {
return;
} // use /members instead.
var eventEventMap = eventMap.get(eventType);
@@ -343,7 +348,7 @@ function printLine(event) {
name = name.slice(0, maxNameWidth - 1) + "\u2026";
}
if (event.getType() === "m.room.message") {
if (event.getType() === EventType.RoomMessage) {
body = event.getContent().body;
} else if (event.isState()) {
var stateName = event.getType();
@@ -394,4 +399,4 @@ function fixWidth(str, len) {
return str;
}
matrixClient.startClient(numMessagesToShow); // messages for each room.
matrixClient.startClient({ initialSyncLimit: numMessagesToShow });
+2 -1
View File
@@ -5,8 +5,9 @@
"main": "app.js",
"author": "",
"license": "Apache 2.0",
"type": "module",
"dependencies": {
"cli-color": "^1.0.0",
"matrix-js-sdk": "^32.0.0"
"matrix-js-sdk": "^34.5.0"
}
}
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "34.5.0",
"version": "34.7.0-rc.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=20.0.0"
@@ -50,7 +50,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^7.0.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0",
"@matrix-org/olm": "3.2.15",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
@@ -100,10 +100,11 @@
"eslint-plugin-jest": "^28.0.0",
"eslint-plugin-jsdoc": "^50.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-tsdoc": "^0.3.0",
"eslint-plugin-unicorn": "^55.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "11.1.1",
"fetch-mock": "11.1.3",
"fetch-mock-jest": "^1.5.1",
"husky": "^9.0.0",
"jest": "^29.0.0",
@@ -121,7 +122,7 @@
"typedoc-plugin-coverage": "^3.0.0",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-missing-exports": "^3.0.0",
"typescript": "^5.3.3"
"typescript": "^5.4.2"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
+4 -4
View File
@@ -21,7 +21,7 @@ import { IDBFactory } from "fake-indexeddb";
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import { encryptAES } from "../../../src/crypto/aes";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
@@ -169,17 +169,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
const masterKey = await encryptAES(
const masterKey = await encryptAESSecretStorageItem(
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.master",
);
const selfSigningKey = await encryptAES(
const selfSigningKey = await encryptAESSecretStorageItem(
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.self_signing",
);
const userSigningKey = await encryptAES(
const userSigningKey = await encryptAESSecretStorageItem(
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.user_signing",
+151 -6
View File
@@ -83,9 +83,12 @@ import {
CrossSigningKey,
CryptoCallbacks,
DecryptionFailureCode,
DeviceIsolationMode,
EventShieldColour,
EventShieldReason,
KeyBackupInfo,
AllDevicesIsolationMode,
OnlySignedDevicesIsolationMode,
} from "../../../src/crypto-api";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { IKeyBackup } from "../../../src/crypto/backup";
@@ -746,6 +749,95 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
);
});
describe("IsolationMode decryption tests", () => {
newBackendOnly(
"OnlySigned mode - fails with an error when cross-signed sender is required but sender is not cross-signed",
async () => {
const decryptedEvent = await setUpTestAndDecrypt(new OnlySignedDevicesIsolationMode());
// It will error as an unknown device because we haven't fetched
// the sender's device keys.
expect(decryptedEvent.isDecryptionFailure()).toBe(true);
expect(decryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_SENDER_DEVICE);
},
);
newBackendOnly(
"NoIsolation mode - Decrypts with warning when cross-signed sender is required but sender is not cross-signed",
async () => {
const decryptedEvent = await setUpTestAndDecrypt(new AllDevicesIsolationMode(false));
expect(decryptedEvent.isDecryptionFailure()).toBe(false);
expect(await aliceClient.getCrypto()!.getEncryptionInfoForEvent(decryptedEvent)).toEqual({
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.UNKNOWN_DEVICE,
});
},
);
async function setUpTestAndDecrypt(isolationMode: DeviceIsolationMode): Promise<MatrixEvent> {
// This tests that a message will not be decrypted if the sender
// is not sufficiently trusted according to the selected crypto
// mode.
//
// This test is almost the same as the "Alice receives a megolm
// message" test, with the main difference that we set the
// crypto mode.
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
// Start by using Invisible crypto mode
aliceClient.getCrypto()!.setDeviceIsolationMode(isolationMode);
await startClientAndAwaitFirstSync();
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: keyReceiver.getDeviceKey(),
recipientEd25519Key: keyReceiver.getSigningKey(),
olmAccount: testOlmAccount,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
const messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {
[ROOM_ID]: { timeline: { events: [messageEncrypted] } },
},
},
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
// it probably won't be decrypted yet, because it takes a while to process the olm keys
return await testUtils.awaitDecryption(event);
}
});
it("Decryption fails with Unable to decrypt for other errors", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -3179,15 +3271,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
});
describe("Check if the cross signing keys are available for a user", () => {
describe("User identity", () => {
let keyResponder: E2EKeyResponder;
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
const keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
keyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
keyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
keyResponder.addKeyReceiver(BOB_TEST_USER_ID, keyReceiver);
keyResponder.addKeyReceiver(TEST_USER_ID, keyReceiver);
keyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA);
keyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA);
@@ -3203,6 +3293,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
.getCrypto()!
.userHasCrossSigningKeys(BOB_TEST_USER_ID, true);
expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(false);
});
it("Cross signing keys are available for a tracked user", async () => {
@@ -3213,11 +3309,60 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// Alice is the local user and should be tracked !
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(false);
});
it("Cross signing keys are not available for an unknown user", async () => {
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
expect(hasCrossSigningKeysForUser).toBe(false);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(false);
});
newBackendOnly("An unverified user changes identity", async () => {
// We have to be tracking Bob's keys, which means we need to share a room with him
syncResponder.sendOrQueueSyncResponse({
...getSyncResponse([BOB_TEST_USER_ID]),
device_lists: { changed: [BOB_TEST_USER_ID] },
});
await syncPromise(aliceClient);
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(BOB_TEST_USER_ID);
expect(hasCrossSigningKeysForUser).toBe(true);
// Bob changes his cross-signing keys
keyResponder.addCrossSigningData(testData.BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA);
syncResponder.sendOrQueueSyncResponse({
next_batch: "2",
device_lists: { changed: [BOB_TEST_USER_ID] },
});
await syncPromise(aliceClient);
await aliceClient.getCrypto()!.userHasCrossSigningKeys(BOB_TEST_USER_ID, true);
{
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(true);
}
// Pinning the new identity should clear the needsUserApproval flag.
await aliceClient.getCrypto()!.pinCurrentUserIdentity(BOB_TEST_USER_ID);
{
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.needsUserApproval).toBe(false);
}
});
});
+15 -5
View File
@@ -101,9 +101,8 @@ export const makeGeolocationPosition = ({
}: {
timestamp?: number;
coords: Partial<GeolocationCoordinates>;
}): GeolocationPosition => ({
timestamp: timestamp ?? 1647256791840,
coords: {
}): GeolocationPosition => {
const { toJSON, ...coordsJSON } = {
accuracy: 1,
latitude: 54.001927,
longitude: -8.253491,
@@ -112,5 +111,16 @@ export const makeGeolocationPosition = ({
heading: null,
speed: null,
...coords,
},
});
};
const posJSON = {
timestamp: timestamp ?? 1647256791840,
coords: {
toJSON: () => coordsJSON,
...coordsJSON,
},
};
return {
toJSON: () => posJSON,
...posJSON,
};
};
@@ -66,6 +66,7 @@ BOB_DATA = {
"TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"Deadmuledeadmuledeadmuledeadmule",
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Doyouspeakwhaaaaaaaaaaaaaaaaaale",
"ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"DoYouSpeakWhaaaaaaaaaaaaaaaaaale",
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Useruseruseruseruseruseruseruser",
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Selfselfselfselfselfselfselfself",
@@ -208,7 +209,7 @@ def build_test_data(user_data, prefix = "") -> str:
backup_recovery_key = export_recovery_key(user_data["B64_BACKUP_DECRYPTION_KEY"])
return f"""\
result = f"""\
export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}";
export const {prefix}TEST_ROOM_ID = "{user_data['TEST_ROOM_ID']}";
@@ -239,7 +240,7 @@ export const {prefix}USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(user_data), indent=4)
json.dumps(build_cross_signing_keys_data(user_data, user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]), indent=4)
};
/** Signed OTKs, returned by `POST /keys/claim` */
@@ -279,12 +280,20 @@ export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, ind
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
"""
alt_master_key = user_data.get("ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES")
if alt_master_key is not None:
result += f"""
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const {prefix}ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(user_data, alt_master_key), indent=4)
};
"""
def build_cross_signing_keys_data(user_data) -> dict:
return result
def build_cross_signing_keys_data(user_data, master_key_bytes) -> dict:
"""Build the signed cross-signing-keys data for return from /keys/query"""
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(master_key_bytes)
b64_master_public_key = encode_base64(
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
+47
View File
@@ -449,3 +449,50 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
"origin_server_ts": 1507753886000
};
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {
"@bob:xyz": {
"keys": {
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E"
},
"user_id": "@bob:xyz",
"usage": [
"master"
]
}
},
"self_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A": "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A"
},
"user_id": "@bob:xyz",
"usage": [
"self_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "eDZETBRUw9yW0WJnBZ7vxo12TW09Yb7/47qBPKZzPZzZEvs9M82dnAOtWUv00mcTdp2K9GpeFYDQJ6qLQgxaCA"
}
}
}
},
"user_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw": "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw"
},
"user_id": "@bob:xyz",
"usage": [
"user_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "Q1CbIXvp2BxBsu3F/eZ1ZpuR5rXIt0+FrrA/l6itskpW748xwMoIKxQRVQqs87kh7pCsWEoTy6FzIL8nV+P6BQ"
}
}
}
}
};
+7 -4
View File
@@ -147,10 +147,13 @@ export class MockRTCPeerConnection {
}
constructor() {
this.localDescription = {
const localDescriptionJSON = {
sdp: DUMMY_SDP,
type: "offer",
toJSON: function () {},
type: "offer" as RTCSdpType,
};
this.localDescription = {
toJSON: () => localDescriptionJSON,
...localDescriptionJSON,
};
this.readyToNegotiate = new Promise<void>((resolve) => {
@@ -265,7 +268,7 @@ export class MockRTCRtpTransceiver {
this.peerConn.needsNegotiation = true;
}
public setCodecPreferences = jest.fn<void, RTCRtpCodecCapability[]>();
public setCodecPreferences = jest.fn<void, RTCRtpCodec[]>();
}
export class MockMediaStreamTrack {
@@ -0,0 +1,38 @@
/*
* Copyright 2024 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 { keyFromAuthData } from "../../../src/common-crypto/key-passphrase.ts";
describe("key-passphrase", () => {
describe("keyFromAuthData", () => {
it("should throw an error if salt or iterations are missing", async () => {
// missing salt
expect(() => keyFromAuthData({ private_key_iterations: 5 }, "passphrase")).toThrow(
"Salt and/or iterations not found: this backup cannot be restored with a passphrase",
);
// missing iterations
expect(() => keyFromAuthData({ private_key_salt: "salt" }, "passphrase")).toThrow(
"Salt and/or iterations not found: this backup cannot be restored with a passphrase",
);
});
it("should derive key from auth data", async () => {
const key = await keyFromAuthData({ private_key_salt: "salt", private_key_iterations: 5 }, "passphrase");
expect(key).toBeDefined();
});
});
});
+44
View File
@@ -0,0 +1,44 @@
/*
* Copyright 2024 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 { decodeRecoveryKey, encodeRecoveryKey } from "../../../src/crypto-api";
describe("recovery key", () => {
describe("decodeRecoveryKey", () => {
it("should thrown an incorrect length error", () => {
const key = [0, 1];
const encodedKey = encodeRecoveryKey(key)!;
expect(() => decodeRecoveryKey(encodedKey)).toThrow("Incorrect length");
});
it("should thrown an incorrect parity", () => {
const key = Array.from({ length: 32 }, (_, i) => i);
let encodedKey = encodeRecoveryKey(key)!;
// Mutate the encoded key to have incorrect parity
encodedKey = encodedKey.replace("EsSz", "EsSZ");
expect(() => decodeRecoveryKey(encodedKey!)).toThrow("Incorrect parity");
});
it("should decode a valid encoded key", () => {
const key = Array.from({ length: 32 }, (_, i) => i);
const encodedKey = encodeRecoveryKey(key)!;
expect(decodeRecoveryKey(encodedKey)).toEqual(new Uint8Array(key));
});
});
});
+2 -2
View File
@@ -20,7 +20,7 @@ import { IObject } from "../../../src/crypto/olmlib";
import { MatrixEvent } from "../../../src/models/event";
import { TestClient } from "../../TestClient";
import { makeTestClients } from "./verification/util";
import { encryptAES } from "../../../src/crypto/aes";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from "../../../src/logger";
import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client";
@@ -612,7 +612,7 @@ describe("Secrets", function () {
type: "m.megolm_backup.v1",
content: {
encrypted: {
key_id: await encryptAES(
key_id: await encryptAESSecretStorageItem(
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
secretStorageKeys.key_id,
"m.megolm_backup.v1",
+118
View File
@@ -1029,6 +1029,124 @@ describe("MatrixClient", function () {
});
});
describe("extended profiles", () => {
const unstableMSC4133Prefix = `${ClientPrefix.Unstable}/uk.tcpip.msc4133`;
const userId = "@profile_user:example.org";
beforeEach(() => {
unstableFeatures["uk.tcpip.msc4133"] = true;
});
it("throws when unsupported by server", async () => {
unstableFeatures["uk.tcpip.msc4133"] = false;
const errorMessage = "Server does not support extended profiles";
await expect(client.doesServerSupportExtendedProfiles()).resolves.toEqual(false);
await expect(client.getExtendedProfile(userId)).rejects.toThrow(errorMessage);
await expect(client.getExtendedProfileProperty(userId, "test_key")).rejects.toThrow(errorMessage);
await expect(client.setExtendedProfileProperty("test_key", "foo")).rejects.toThrow(errorMessage);
await expect(client.deleteExtendedProfileProperty("test_key")).rejects.toThrow(errorMessage);
await expect(client.patchExtendedProfile({ test_key: "foo" })).rejects.toThrow(errorMessage);
await expect(client.setExtendedProfile({ test_key: "foo" })).rejects.toThrow(errorMessage);
});
it("can fetch a extended user profile", async () => {
const testProfile = {
test_key: "foo",
};
httpLookups = [
{
method: "GET",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(userId),
data: testProfile,
},
];
await expect(client.getExtendedProfile(userId)).resolves.toEqual(testProfile);
expect(httpLookups).toHaveLength(0);
});
it("can fetch a property from a extended user profile", async () => {
const testProfile = {
test_key: "foo",
};
httpLookups = [
{
method: "GET",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(userId) + "/test_key",
data: testProfile,
},
];
await expect(client.getExtendedProfileProperty(userId, "test_key")).resolves.toEqual("foo");
expect(httpLookups).toHaveLength(0);
});
it("can set a property in our extended profile", async () => {
httpLookups = [
{
method: "PUT",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(client.credentials.userId!) + "/test_key",
expectBody: {
test_key: "foo",
},
},
];
await expect(client.setExtendedProfileProperty("test_key", "foo")).resolves.toEqual(undefined);
expect(httpLookups).toHaveLength(0);
});
it("can delete a property in our extended profile", async () => {
httpLookups = [
{
method: "DELETE",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(client.credentials.userId!) + "/test_key",
},
];
await expect(client.deleteExtendedProfileProperty("test_key")).resolves.toEqual(undefined);
expect(httpLookups).toHaveLength(0);
});
it("can patch our extended profile", async () => {
const testProfile = {
test_key: "foo",
};
const patchedProfile = {
existing: "key",
test_key: "foo",
};
httpLookups = [
{
method: "PATCH",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(client.credentials.userId!),
data: patchedProfile,
expectBody: testProfile,
},
];
await expect(client.patchExtendedProfile(testProfile)).resolves.toEqual(patchedProfile);
});
it("can replace our extended profile", async () => {
const testProfile = {
test_key: "foo",
};
httpLookups = [
{
method: "PUT",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(client.credentials.userId!),
data: testProfile,
expectBody: testProfile,
},
];
await expect(client.setExtendedProfile(testProfile)).resolves.toEqual(undefined);
});
});
it("should create (unstable) file trees", async () => {
const userId = "@test:example.org";
const roomId = "!room:example.org";
+211 -51
View File
@@ -585,33 +585,47 @@ describe("MatrixRTCSession", () => {
it("creates a key when joining", () => {
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
const keys = sess?.getKeysForParticipant("@alice:example.org", "AAAAAAA");
expect(keys).toHaveLength(1);
const allKeys = sess!.getEncryptionKeys();
expect(allKeys).toBeTruthy();
expect(Array.from(allKeys)).toHaveLength(1);
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess?.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
expect.any(Uint8Array),
0,
"@alice:example.org:AAAAAAA",
);
});
it("sends keys when joining", async () => {
const eventSentPromise = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
jest.useFakeTimers();
try {
const eventSentPromise = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await eventSentPromise;
await eventSentPromise;
expect(sendEventMock).toHaveBeenCalledWith(expect.stringMatching(".*"), "io.element.call.encryption_keys", {
call_id: "",
device_id: "AAAAAAA",
keys: [
expect(sendEventMock).toHaveBeenCalledWith(
expect.stringMatching(".*"),
"io.element.call.encryption_keys",
{
index: 0,
key: expect.stringMatching(".*"),
call_id: "",
device_id: "AAAAAAA",
keys: [
{
index: 0,
key: expect.stringMatching(".*"),
},
],
sent_ts: Date.now(),
},
],
});
);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
} finally {
jest.useRealTimers();
}
});
it("does not send key if join called when already joined", () => {
@@ -619,10 +633,12 @@ describe("MatrixRTCSession", () => {
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(client.sendEvent).toHaveBeenCalledTimes(1);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(client.sendEvent).toHaveBeenCalledTimes(1);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
});
it("retries key sends", async () => {
@@ -651,6 +667,7 @@ describe("MatrixRTCSession", () => {
await eventSentPromise;
expect(sendEventMock).toHaveBeenCalledTimes(2);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
} finally {
jest.useRealTimers();
}
@@ -672,24 +689,27 @@ describe("MatrixRTCSession", () => {
expect(client.cancelPendingEvent).toHaveBeenCalledWith(eventSentinel);
});
it("Re-sends key if a new member joins", async () => {
it("Rotates key if a new member joins", async () => {
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const keysSentPromise1 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
const keysSentPromise1 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await keysSentPromise1;
const firstKeysPayload = await keysSentPromise1;
expect(firstKeysPayload.keys).toHaveLength(1);
expect(firstKeysPayload.keys[0].index).toEqual(0);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
sendEventMock.mockClear();
jest.advanceTimersByTime(10000);
const keysSentPromise2 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
const keysSentPromise2 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
const onMembershipsChanged = jest.fn();
@@ -704,9 +724,15 @@ describe("MatrixRTCSession", () => {
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
sess.onMembershipUpdate();
await keysSentPromise2;
jest.advanceTimersByTime(10000);
const secondKeysPayload = await keysSentPromise2;
expect(sendEventMock).toHaveBeenCalled();
expect(secondKeysPayload.keys).toHaveLength(1);
expect(secondKeysPayload.keys[0].index).toEqual(1);
expect(secondKeysPayload.keys[0].key).not.toEqual(firstKeysPayload.keys[0].key);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
} finally {
jest.useRealTimers();
}
@@ -747,14 +773,17 @@ describe("MatrixRTCSession", () => {
key: expect.stringMatching(".*"),
},
],
sent_ts: Date.now(),
},
);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
sendEventMock.mockClear();
// these should be a no-op:
sess.onMembershipUpdate();
expect(sendEventMock).toHaveBeenCalledTimes(0);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
} finally {
jest.useRealTimers();
}
@@ -796,8 +825,10 @@ describe("MatrixRTCSession", () => {
key: expect.stringMatching(".*"),
},
],
sent_ts: Date.now(),
},
);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
sendEventMock.mockClear();
@@ -832,8 +863,10 @@ describe("MatrixRTCSession", () => {
key: expect.stringMatching(".*"),
},
],
sent_ts: Date.now(),
},
);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
} finally {
jest.useRealTimers();
}
@@ -877,8 +910,10 @@ describe("MatrixRTCSession", () => {
key: expect.stringMatching(".*"),
},
],
sent_ts: Date.now(),
},
);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
sendEventMock.mockClear();
@@ -913,8 +948,10 @@ describe("MatrixRTCSession", () => {
key: expect.stringMatching(".*"),
},
],
sent_ts: Date.now(),
},
);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
} finally {
jest.useRealTimers();
}
@@ -946,6 +983,8 @@ describe("MatrixRTCSession", () => {
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
const firstKeysPayload = await keysSentPromise1;
expect(firstKeysPayload.keys).toHaveLength(1);
expect(firstKeysPayload.keys[0].index).toEqual(0);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
sendEventMock.mockClear();
@@ -962,8 +1001,10 @@ describe("MatrixRTCSession", () => {
const secondKeysPayload = await keysSentPromise2;
expect(secondKeysPayload.keys).toHaveLength(2);
expect(secondKeysPayload.keys).toHaveLength(1);
expect(secondKeysPayload.keys[0].index).toEqual(1);
expect(onMyEncryptionKeyChanged).toHaveBeenCalledTimes(2);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
} finally {
jest.useRealTimers();
}
@@ -984,6 +1025,7 @@ describe("MatrixRTCSession", () => {
await keysSentPromise1;
sendEventMock.mockClear();
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
@@ -1002,6 +1044,7 @@ describe("MatrixRTCSession", () => {
});
expect(sendEventMock).not.toHaveBeenCalled();
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
} finally {
jest.useRealTimers();
}
@@ -1164,9 +1207,17 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(1);
expect(bobKeys[0]).toEqual(Buffer.from("this is the key", "utf-8"));
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
0,
"@bob:example.org:bobsphone",
);
expect(sess!.statistics.counters.roomEventEncryptionKeysReceived).toEqual(1);
});
it("collects keys at non-zero indices", () => {
@@ -1188,13 +1239,17 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(5);
expect(bobKeys[0]).toBeFalsy();
expect(bobKeys[1]).toBeFalsy();
expect(bobKeys[2]).toBeFalsy();
expect(bobKeys[3]).toBeFalsy();
expect(bobKeys[4]).toEqual(Buffer.from("this is the key", "utf-8"));
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
4,
"@bob:example.org:bobsphone",
);
expect(sess!.statistics.counters.roomEventEncryptionKeysReceived).toEqual(1);
});
it("collects keys by merging", () => {
@@ -1216,9 +1271,17 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
let bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(1);
expect(bobKeys[0]).toEqual(Buffer.from("this is the key", "utf-8"));
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
0,
"@bob:example.org:bobsphone",
);
expect(sess!.statistics.counters.roomEventEncryptionKeysReceived).toEqual(1);
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
@@ -1236,9 +1299,21 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(5);
expect(bobKeys[4]).toEqual(Buffer.from("this is the key", "utf-8"));
encryptionKeyChangedListener.mockClear();
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(2);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
0,
"@bob:example.org:bobsphone",
);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
4,
"@bob:example.org:bobsphone",
);
expect(sess!.statistics.counters.roomEventEncryptionKeysReceived).toEqual(2);
});
it("ignores older keys at same index", () => {
@@ -1276,9 +1351,17 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(1000), // earlier timestamp than the newer key
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(1);
expect(bobKeys[0]).toEqual(Buffer.from("newer key", "utf-8"));
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("newer key", "utf-8"),
0,
"@bob:example.org:bobsphone",
);
expect(sess!.statistics.counters.roomEventEncryptionKeysReceived).toEqual(2);
});
it("key timestamps are treated as monotonic", () => {
@@ -1316,9 +1399,15 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(1000), // same timestamp as the first key
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(1);
expect(bobKeys[0]).toEqual(Buffer.from("second key", "utf-8"));
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("second key", "utf-8"),
0,
"@bob:example.org:bobsphone",
);
});
it("ignores keys event for the local participant", () => {
@@ -1340,7 +1429,78 @@ describe("MatrixRTCSession", () => {
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
const myKeys = sess.getKeysForParticipant(client.getUserId()!, client.getDeviceId()!)!;
expect(myKeys).toBeFalsy();
const encryptionKeyChangedListener = jest.fn();
sess!.on(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKeyChangedListener);
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(0);
expect(sess!.statistics.counters.roomEventEncryptionKeysReceived).toEqual(0);
});
it("tracks total age statistics for collected keys", () => {
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
// defaults to getTs()
jest.setSystemTime(1000);
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
getContent: jest.fn().mockReturnValue({
device_id: "bobsphone",
call_id: "",
keys: [
{
index: 0,
key: "dGhpcyBpcyB0aGUga2V5",
},
],
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
getTs: jest.fn().mockReturnValue(0),
} as unknown as MatrixEvent);
expect(sess!.statistics.totals.roomEventEncryptionKeysReceivedTotalAge).toEqual(1000);
jest.setSystemTime(2000);
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
getContent: jest.fn().mockReturnValue({
device_id: "bobsphone",
call_id: "",
keys: [
{
index: 0,
key: "dGhpcyBpcyB0aGUga2V5",
},
],
sent_ts: 0,
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
expect(sess!.statistics.totals.roomEventEncryptionKeysReceivedTotalAge).toEqual(3000);
jest.setSystemTime(3000);
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
getContent: jest.fn().mockReturnValue({
device_id: "bobsphone",
call_id: "",
keys: [
{
index: 0,
key: "dGhpcyBpcyB0aGUga2V5",
},
],
sent_ts: 1000,
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
expect(sess!.statistics.totals.roomEventEncryptionKeysReceivedTotalAge).toEqual(5000);
} finally {
jest.useRealTimers();
}
});
});
+112 -9
View File
@@ -17,6 +17,7 @@
*/
import {
CollectStrategy,
Curve25519PublicKey,
Ed25519PublicKey,
HistoryVisibility as RustHistoryVisibility,
@@ -31,6 +32,7 @@ import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
import { defer } from "../../../src/utils";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
import { KnownMembership } from "../../../src/@types/membership";
import { DeviceIsolationMode, AllDevicesIsolationMode, OnlySignedDevicesIsolationMode } from "../../../src/crypto-api";
describe("RoomEncryptor", () => {
describe("History Visibility", () => {
@@ -99,7 +101,7 @@ describe("RoomEncryptor", () => {
getEncryptionTargetMembers: jest.fn().mockReturnValue([mockRoomMember]),
shouldEncryptForInvitedMembers: jest.fn().mockReturnValue(true),
getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Invited),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(null),
} as unknown as Mocked<Room>;
roomEncryptor = new RoomEncryptor(
@@ -111,6 +113,8 @@ describe("RoomEncryptor", () => {
);
});
const defaultDevicesIsolationMode = new AllDevicesIsolationMode(false);
it("should ensure that there is only one shareRoomKey at a time", async () => {
const deferredShare = defer<void>();
const insideOlmShareRoom = defer<void>();
@@ -120,19 +124,19 @@ describe("RoomEncryptor", () => {
await deferredShare.promise;
});
roomEncryptor.prepareForEncryption(false);
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
await insideOlmShareRoom.promise;
// call several times more
roomEncryptor.prepareForEncryption(false);
roomEncryptor.encryptEvent(createMockEvent("Hello"), false);
roomEncryptor.prepareForEncryption(false);
roomEncryptor.encryptEvent(createMockEvent("World"), false);
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
roomEncryptor.encryptEvent(createMockEvent("Hello"), false, defaultDevicesIsolationMode);
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
roomEncryptor.encryptEvent(createMockEvent("World"), false, defaultDevicesIsolationMode);
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalledTimes(1);
deferredShare.resolve();
await roomEncryptor.prepareForEncryption(false);
await roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
// should have been called again
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalledTimes(6);
@@ -158,8 +162,16 @@ describe("RoomEncryptor", () => {
let firstMessageFinished: string | null = null;
const firstRequest = roomEncryptor.encryptEvent(createMockEvent("Hello"), false);
const secondRequest = roomEncryptor.encryptEvent(createMockEvent("Edit of Hello"), false);
const firstRequest = roomEncryptor.encryptEvent(
createMockEvent("Hello"),
false,
defaultDevicesIsolationMode,
);
const secondRequest = roomEncryptor.encryptEvent(
createMockEvent("Edit of Hello"),
false,
defaultDevicesIsolationMode,
);
firstRequest.then(() => {
if (firstMessageFinished === null) {
@@ -181,5 +193,96 @@ describe("RoomEncryptor", () => {
expect(firstMessageFinished).toBe("hello");
});
describe("DeviceIsolationMode", () => {
type TestCase = [
string,
{
mode: DeviceIsolationMode;
expectedStrategy: CollectStrategy;
globalBlacklistUnverifiedDevices: boolean;
},
];
const testCases: TestCase[] = [
[
"Share AllDevicesIsolationMode",
{
mode: new AllDevicesIsolationMode(false),
expectedStrategy: CollectStrategy.deviceBasedStrategy(false, false),
globalBlacklistUnverifiedDevices: false,
},
],
[
"Share AllDevicesIsolationMode - with blacklist unverified",
{
mode: new AllDevicesIsolationMode(false),
expectedStrategy: CollectStrategy.deviceBasedStrategy(true, false),
globalBlacklistUnverifiedDevices: true,
},
],
[
"Share OnlySigned - blacklist true",
{
mode: new OnlySignedDevicesIsolationMode(),
expectedStrategy: CollectStrategy.identityBasedStrategy(),
globalBlacklistUnverifiedDevices: true,
},
],
[
"Share OnlySigned",
{
mode: new OnlySignedDevicesIsolationMode(),
expectedStrategy: CollectStrategy.identityBasedStrategy(),
globalBlacklistUnverifiedDevices: false,
},
],
[
"Share AllDevicesIsolationMode - Verified user problems true",
{
mode: new AllDevicesIsolationMode(true),
expectedStrategy: CollectStrategy.deviceBasedStrategy(false, true),
globalBlacklistUnverifiedDevices: false,
},
],
[
'Share AllDevicesIsolationMode - with blacklist unverified - Verified user problems true"',
{
mode: new AllDevicesIsolationMode(true),
expectedStrategy: CollectStrategy.deviceBasedStrategy(true, true),
globalBlacklistUnverifiedDevices: true,
},
],
];
let capturedSettings: CollectStrategy | undefined = undefined;
beforeEach(() => {
capturedSettings = undefined;
mockOlmMachine.shareRoomKey.mockImplementationOnce(async (roomId, users, encryptionSettings) => {
capturedSettings = encryptionSettings.sharingStrategy;
});
});
it.each(testCases)(
"prepareForEncryption should properly set sharing strategy based on crypto mode: %s",
async (_, { mode, expectedStrategy, globalBlacklistUnverifiedDevices }) => {
await roomEncryptor.prepareForEncryption(globalBlacklistUnverifiedDevices, mode);
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalled();
expect(capturedSettings).toBeDefined();
expect(expectedStrategy.eq(capturedSettings!)).toBeTruthy();
},
);
it.each(testCases)(
"encryptEvent should properly set sharing strategy based on crypto mode: %s",
async (_, { mode, expectedStrategy, globalBlacklistUnverifiedDevices }) => {
await roomEncryptor.encryptEvent(createMockEvent("Hello"), globalBlacklistUnverifiedDevices, mode);
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalled();
expect(capturedSettings).toBeDefined();
expect(expectedStrategy.eq(capturedSettings!)).toBeTruthy();
},
);
});
});
});
+43 -4
View File
@@ -69,7 +69,7 @@ import { logger } from "../../../src/logger";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
import { ClientEvent, ClientEventHandlerMap } from "../../../src/client";
import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
import { encryptAES } from "../../../src/crypto/aes";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
const TEST_USER = "@alice:example.com";
@@ -425,7 +425,7 @@ describe("initRustCrypto", () => {
}, 10000);
async function encryptAndStoreSecretKey(type: string, key: Uint8Array, pickleKey: string, store: CryptoStore) {
const encryptedKey = await encryptAES(encodeBase64(key), Buffer.from(pickleKey), type);
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), Buffer.from(pickleKey), type);
store.storeSecretStorePrivateKey(undefined, type as keyof SecretStorePrivateKeys, encryptedKey);
}
@@ -1362,13 +1362,52 @@ describe("RustCrypto", () => {
});
it("returns a verified UserVerificationStatus when the UserIdentity is verified", async () => {
olmMachine.getIdentity.mockResolvedValue({ free: jest.fn(), isVerified: jest.fn().mockReturnValue(true) });
olmMachine.getIdentity.mockResolvedValue({
free: jest.fn(),
isVerified: jest.fn().mockReturnValue(true),
wasPreviouslyVerified: jest.fn().mockReturnValue(true),
});
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
expect(userVerificationStatus.isVerified()).toBeTruthy();
expect(userVerificationStatus.isTofu()).toBeFalsy();
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
expect(userVerificationStatus.wasCrossSigningVerified()).toBeFalsy();
expect(userVerificationStatus.wasCrossSigningVerified()).toBeTruthy();
});
});
describe("pinCurrentIdentity", () => {
let rustCrypto: RustCrypto;
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
beforeEach(() => {
olmMachine = {
getIdentity: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(
logger,
olmMachine,
{} as MatrixClient["http"],
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
});
it("throws an error for an unknown user", async () => {
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
"Cannot pin identity of unknown user",
);
});
it("throws an error for our own user", async () => {
const ownIdentity = new RustSdkCryptoJs.OwnUserIdentity();
olmMachine.getIdentity.mockResolvedValue(ownIdentity);
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
"Cannot pin identity of own user",
);
});
});
+1 -1
View File
@@ -25,8 +25,8 @@ import {
ServerSideSecretStorageImpl,
trimTrailingEquals,
} from "../../src/secret-storage";
import { calculateKeyCheck } from "../../src/crypto/aes";
import { randomString } from "../../src/randomstring";
import { calculateKeyCheck } from "../../src/calculateKeyCheck.ts";
describe("ServerSideSecretStorageImpl", function () {
describe(".addKey", function () {
@@ -0,0 +1,29 @@
/*
* Copyright 2024 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.
*/
/**
* An AES-encrypted secret storage payload.
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2-1
*/
export interface AESEncryptedSecretStoragePayload {
[key: string]: any; // extensible
/** the initialization vector in base64 */
iv: string;
/** the ciphertext in base64 */
ciphertext: string;
/** the HMAC in base64 */
mac: string;
}
+29 -36
View File
@@ -21,7 +21,7 @@ import { MsgType } from "../@types/event.ts";
*
* Used within `m.room.message` events that reference files, such as `m.file` and `m.image`.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#extensions-to-mroommessage-msgtypes
* @see https://spec.matrix.org/v1.11/client-server-api/#extensions-to-mroommessage-msgtypes
*/
export interface EncryptedFile {
/**
@@ -84,13 +84,13 @@ interface BaseInfo {
*
* Used within `m.room.message` events that reference files.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mfile
* @see https://spec.matrix.org/v1.11/client-server-api/#mfile
*/
export interface FileInfo extends BaseInfo {
/**
* Information on the encrypted thumbnail file, as specified in End-to-end encryption.
* Only present if the thumbnail is encrypted.
* @see https://spec.matrix.org/v1.9/client-server-api/#sending-encrypted-attachments
* @see https://spec.matrix.org/v1.11/client-server-api/#sending-encrypted-attachments
*/
thumbnail_file?: EncryptedFile;
/**
@@ -108,7 +108,7 @@ export interface FileInfo extends BaseInfo {
*
* Used within `m.room.message` events that reference images.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mimage
* @see https://spec.matrix.org/v1.11/client-server-api/#mimage
*/
export interface ImageInfo extends FileInfo, ThumbnailInfo {}
@@ -117,7 +117,7 @@ export interface ImageInfo extends FileInfo, ThumbnailInfo {}
*
* Used within `m.room.message` events that reference audio files.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#maudio
* @see https://spec.matrix.org/v1.11/client-server-api/#maudio
*/
export interface AudioInfo extends BaseInfo {
/**
@@ -131,7 +131,7 @@ export interface AudioInfo extends BaseInfo {
*
* Used within `m.room.message` events that reference video files.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mvideo
* @see https://spec.matrix.org/v1.11/client-server-api/#mvideo
*/
export interface VideoInfo extends AudioInfo, ImageInfo {
/**
@@ -148,30 +148,39 @@ export type MediaEventInfo = FileInfo | ImageInfo | AudioInfo | VideoInfo;
interface BaseContent {
/**
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
* @see https://spec.matrix.org/v1.9/client-server-api/#sending-encrypted-attachments
* @see https://spec.matrix.org/v1.11/client-server-api/#sending-encrypted-attachments
*/
file?: EncryptedFile;
/**
* Required if the file is unencrypted. The URL (typically mxc:// URI) to the file.
*/
url?: string;
}
/**
* Content format of media events with msgtype `m.file`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mfile
*/
export interface FileContent extends BaseContent {
/**
* A human-readable description of the file.
* This is recommended to be the filename of the original upload.
* If filename is not set or the value of both properties are identical,
* this is the filename of the original upload. Otherwise, this is a
* caption for the file.
*/
body: string;
/**
* The original filename of the uploaded file.
*/
filename?: string;
/**
* The format used in the `formatted_body`.
*/
format?: "org.matrix.custom.html";
/**
* The formatted version of the `body`, when it acts as a caption. This is required if `format` is specified.
*/
formatted_body?: string;
}
/**
* Content format of media events with msgtype `m.file`
*
* @see https://spec.matrix.org/v1.11/client-server-api/#mfile
*/
export interface FileContent extends BaseContent {
/**
* Information about the file referred to in url.
*/
@@ -185,15 +194,9 @@ export interface FileContent extends BaseContent {
/**
* Content format of media events with msgtype `m.image`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mimage
* @see https://spec.matrix.org/v1.11/client-server-api/#mimage
*/
export interface ImageContent extends BaseContent {
/**
* A textual representation of the image.
* This could be the alt text of the image, the filename of the image,
* or some kind of content description for accessibility e.g. image attachment.
*/
body: string;
/**
* Metadata about the image referred to in url.
*/
@@ -207,14 +210,9 @@ export interface ImageContent extends BaseContent {
/**
* Content format of media events with msgtype `m.audio`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#maudio
* @see https://spec.matrix.org/v1.11/client-server-api/#maudio
*/
export interface AudioContent extends BaseContent {
/**
* A description of the audio e.g. Bee Gees - Stayin Alive,
* or some kind of content description for accessibility e.g. audio attachment.
*/
body: string;
/**
* Metadata for the audio clip referred to in url.
*/
@@ -228,14 +226,9 @@ export interface AudioContent extends BaseContent {
/**
* Content format of media events with msgtype `m.video`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mvideo
* @see https://spec.matrix.org/v1.11/client-server-api/#mvideo
*/
export interface VideoContent extends BaseContent {
/**
* A description of the video e.g. Gangnam style,
* or some kind of content description for accessibility e.g. video attachment.
*/
body: string;
/**
* Metadata about the video clip referred to in url.
*/
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright 2024 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.
*/
// string of zeroes, for calculating the key check
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts";
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
/**
* Calculate the MAC for checking the key.
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2, steps 3 and 4.
*
* @param key - the key to use
* @param iv - The initialization vector as a base64-encoded string.
* If omitted, a random initialization vector will be created.
* @returns An object that contains, `mac` and `iv` properties.
*/
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<AESEncryptedSecretStoragePayload> {
return encryptAESSecretStorageItem(ZERO_STR, key, "", iv);
}
+258 -36
View File
@@ -85,8 +85,6 @@ import {
isCryptoAvailable,
} from "./crypto/index.ts";
import { DeviceInfo } from "./crypto/deviceinfo.ts";
import { decodeRecoveryKey } from "./crypto/recoverykey.ts";
import { keyFromAuthData } from "./crypto/key_passphrase.ts";
import { User, UserEvent, UserEventHandlerMap } from "./models/user.ts";
import { getHttpUriForMxc } from "./content-repo.ts";
import { SearchResult } from "./models/search-result.ts";
@@ -223,7 +221,13 @@ import { LocalNotificationSettings } from "./@types/local_notifications.ts";
import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature.ts";
import { BackupDecryptor, CryptoBackend } from "./common-crypto/CryptoBackend.ts";
import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants.ts";
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo, CryptoApi, ImportRoomKeysOpts } from "./crypto-api/index.ts";
import {
BootstrapCrossSigningOpts,
CrossSigningKeyInfo,
CryptoApi,
decodeRecoveryKey,
ImportRoomKeysOpts,
} from "./crypto-api/index.ts";
import { DeviceInfoMap } from "./crypto/DeviceList.ts";
import {
AddSecretStorageKeyOpts,
@@ -239,6 +243,7 @@ import { RoomMessageEventContent, StickerEventContent } from "./@types/events.ts
import { ImageInfo } from "./@types/media.ts";
import { Capabilities, ServerCapabilities } from "./serverCapabilities.ts";
import { sha256 } from "./digest.ts";
import { keyFromAuthData } from "./common-crypto/key-passphrase.ts";
export type Store = IStore;
@@ -544,6 +549,8 @@ export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_m
export const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140";
export const UNSTABLE_MSC4133_EXTENDED_PROFILES = "uk.tcpip.msc4133";
enum CrossSigningKeyType {
MasterKey = "master_key",
SelfSigningKey = "self_signing_key",
@@ -1590,6 +1597,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves to undefined if a device could not be dehydrated, or
* to the new device ID if the dehydration was successful.
* @returns Rejects: with an error response.
*
* @deprecated MSC2697 device dehydration is not supported for rust cryptography.
*/
public async rehydrateDevice(): Promise<string | undefined> {
if (this.crypto) {
@@ -1690,6 +1699,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param deviceDisplayName - The device display name for the
* dehydrated device.
* @returns A promise that resolves when the dehydrated device is stored.
*
* @deprecated Not supported for Rust Cryptography.
*/
public async setDehydrationKey(
key: Uint8Array,
@@ -1704,13 +1715,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
/**
* Creates a new dehydrated device (without queuing periodic dehydration)
* Creates a new MSC2967 dehydrated device (without queuing periodic dehydration)
* @param key - the dehydration key
* @param keyInfo - Information about the key. Primarily for
* information about how to generate the key from a passphrase.
* @param deviceDisplayName - The device display name for the
* dehydrated device.
* @returns the device id of the newly created dehydrated device
*
* @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.startDehydration}.
*/
public async createDehydratedDevice(
key: Uint8Array,
@@ -1725,6 +1738,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.crypto.dehydrationManager.dehydrateDevice();
}
/** @deprecated Not supported for Rust Cryptography. */
public async exportDevice(): Promise<IExportedDevice | undefined> {
if (!this.crypto) {
this.logger.warn("not exporting device if crypto is not enabled");
@@ -2121,6 +2135,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* It will return a Promise which will resolve when the crypto layer has been
* successfully initialised.
*
* @deprecated libolm is deprecated. Prefer {@link initRustCrypto}.
*/
public async initCrypto(): Promise<void> {
if (!isCryptoAvailable()) {
@@ -2238,6 +2254,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// importing rust-crypto will download the webassembly, so we delay it until we know it will be
// needed.
this.logger.debug("Downloading Rust crypto library");
// blocked on https://github.com/matrix-org/matrix-js-sdk/issues/4392 / https://github.com/babel/babel/issues/16750
// eslint-disable-next-line node/file-extension-in-import
const RustCrypto = await import("./rust-crypto");
const rustCrypto = await RustCrypto.initRustCrypto({
@@ -2314,7 +2332,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns base64-encoded ed25519 key. Null if crypto is
* disabled.
*
* @deprecated Prefer {@link CryptoApi.getOwnDeviceKeys}
* @deprecated Not supported for Rust Cryptography.Prefer {@link CryptoApi.getOwnDeviceKeys}
*/
public getDeviceEd25519Key(): string | null {
return this.crypto?.getDeviceEd25519Key() ?? null;
@@ -2326,7 +2344,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns base64-encoded curve25519 key. Null if crypto is
* disabled.
*
* @deprecated Use {@link CryptoApi.getOwnDeviceKeys}
* @deprecated Not supported for Rust Cryptography. Use {@link CryptoApi.getOwnDeviceKeys}
*/
public getDeviceCurve25519Key(): string | null {
return this.crypto?.getDeviceCurve25519Key() ?? null;
@@ -2347,7 +2365,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @returns A promise which resolves to a map userId-\>deviceId-\>`DeviceInfo`
*
* @deprecated Prefer {@link CryptoApi.getUserDeviceInfo}
* @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.getUserDeviceInfo}
*/
public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> {
if (!this.crypto) {
@@ -2362,7 +2380,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param userId - the user to list keys for.
*
* @returns list of devices
* @deprecated Prefer {@link CryptoApi.getUserDeviceInfo}
* @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.getUserDeviceInfo}
*/
public getStoredDevicesForUser(userId: string): DeviceInfo[] {
if (!this.crypto) {
@@ -2378,7 +2396,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param deviceId - unique identifier for the device
*
* @returns device or null
* @deprecated Prefer {@link CryptoApi.getUserDeviceInfo}
* @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.getUserDeviceInfo}
*/
public getStoredDevice(userId: string, deviceId: string): DeviceInfo | null {
if (!this.crypto) {
@@ -2428,6 +2446,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @remarks
* Fires {@link CryptoEvent.DeviceVerificationChanged}
*
* @deprecated Not supported for Rust Cryptography.
*/
public setDeviceBlocked(userId: string, deviceId: string, blocked = true): Promise<void> {
return this.setDeviceVerification(userId, deviceId, null, blocked, null);
@@ -2447,6 +2467,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @remarks
* Fires {@link CryptoEvent#DeviceVerificationChanged}
*
* @deprecated Not supported for Rust Cryptography.
*/
public setDeviceKnown(userId: string, deviceId: string, known = true): Promise<void> {
return this.setDeviceVerification(userId, deviceId, null, null, known);
@@ -2474,7 +2496,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns resolves to a VerificationRequest
* when the request has been sent to the other party.
*
* @deprecated Prefer {@link CryptoApi.requestVerificationDM}.
* @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.requestVerificationDM}.
*/
public requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
if (!this.crypto) {
@@ -2489,7 +2511,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param roomId - the room to use for verification
*
* @returns the VerificationRequest that is in progress, if any
* @deprecated Prefer {@link CryptoApi.findVerificationRequestDMInProgress}.
* @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.findVerificationRequestDMInProgress}.
*/
public findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined {
if (!this.cryptoBackend) {
@@ -2508,7 +2530,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param userId - the ID of the user to query
*
* @returns the VerificationRequests that are in progress
* @deprecated Prefer {@link CryptoApi.getVerificationRequestsToDeviceInProgress}.
* @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.getVerificationRequestsToDeviceInProgress}.
*/
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
if (!this.crypto) {
@@ -2527,7 +2549,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns resolves to a VerificationRequest
* when the request has been sent to the other party.
*
* @deprecated Prefer {@link CryptoApi#requestOwnUserVerification} or {@link CryptoApi#requestDeviceVerification}.
* @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi#requestOwnUserVerification} or {@link CryptoApi#requestDeviceVerification}.
*/
public requestVerification(userId: string, devices?: string[]): Promise<VerificationRequest> {
if (!this.crypto) {
@@ -2544,7 +2566,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param deviceId - the device to verify
*
* @returns a verification object
* @deprecated Use `requestVerification` instead.
* @deprecated Prefer {@link CryptoApi#requestOwnUserVerification} or {@link CryptoApi#requestDeviceVerification}.
*/
public beginKeyVerification(method: string, userId: string, deviceId: string): Verification<any, any> {
if (!this.crypto) {
@@ -2639,7 +2661,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* "master", "self_signing", or "user_signing". Defaults to "master".
*
* @returns the key ID
* @deprecated prefer {@link Crypto.CryptoApi#getCrossSigningKeyId}
* @deprecated Not supported for Rust Cryptography. prefer {@link Crypto.CryptoApi#getCrossSigningKeyId}
*/
public getCrossSigningId(type: CrossSigningKey | string = CrossSigningKey.Master): string | null {
if (!this.crypto) {
@@ -2656,7 +2678,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param userId - the user ID to get the cross-signing info for.
*
* @returns the cross signing information for the user.
* @deprecated Prefer {@link CryptoApi#userHasCrossSigningKeys}
* @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi#userHasCrossSigningKeys}
*/
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
if (!this.cryptoBackend) {
@@ -2705,6 +2727,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param deviceId - The ID of the device to check
*
* @returns true if the device is cross-signed
*
* @deprecated Not supported for Rust Cryptography.
*/
public checkIfOwnDeviceCrossSigned(deviceId: string): boolean {
if (!this.crypto) {
@@ -2734,6 +2758,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param privateKey - The private key
* @param expectedPublicKey - The public key
* @returns true if the key matches, otherwise false
*
* @deprecated Not supported for Rust Cryptography.
*/
public checkCrossSigningPrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
if (!this.crypto) {
@@ -2742,7 +2768,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.crypto.checkCrossSigningPrivateKey(privateKey, expectedPublicKey);
}
// deprecated: use requestVerification instead
/**
* @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi#requestDeviceVerification}.
*/
public legacyDeviceVerification(userId: string, deviceId: string, method: string): Promise<VerificationRequest> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
@@ -2859,6 +2887,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Counts the number of end to end session keys that are waiting to be backed up
* @returns Promise which resolves to the number of sessions requiring backup
*
* @deprecated Not supported for Rust Cryptography.
*/
public countSessionsNeedingBackup(): Promise<number> {
if (!this.crypto) {
@@ -3040,6 +3070,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param devices - the devices to request the secret from
*
* @returns the secret request object
* @deprecated Not supported for Rust Cryptography.
*/
public requestSecret(name: string, devices: string[]): ISecretRequest {
if (!this.crypto) {
@@ -3099,6 +3130,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Get e2e information on the device that sent an event
*
* @param event - event to be checked
* @deprecated Not supported for Rust Cryptography.
*/
public async getEventSenderDeviceInfo(event: MatrixEvent): Promise<DeviceInfo | null> {
if (!this.crypto) {
@@ -3128,6 +3160,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param event - The event to check for
*
* @returns A room key request, or null if there is none
*
* @deprecated Not supported for Rust Cryptography.
*/
public getOutgoingRoomKeyRequest(event: MatrixEvent): Promise<OutgoingRoomKeyRequest | null> {
if (!this.crypto) {
@@ -3152,6 +3186,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param event - event of which to cancel and resend the room
* key request.
* @returns A promise that will resolve when the key request is queued
*
* @deprecated Not supported for Rust Cryptography.
*/
public cancelAndResendEventRoomKeyRequest(event: MatrixEvent): Promise<void> {
if (!this.crypto) {
@@ -3217,6 +3253,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* resolves once the message has been encrypted and sent to the given
* userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }`
* of the successfully sent messages.
*
* @deprecated Not supported for Rust Cryptography.
*/
public encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
if (!this.crypto) {
@@ -3232,7 +3270,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param roomId - The ID of the room to discard the session for
*
* @deprecated Prefer {@link CryptoApi.forceDiscardSession | `CryptoApi.forceDiscardSession`}:
*
*/
public forceDiscardSession(roomId: string): void {
if (!this.cryptoBackend) {
@@ -3333,7 +3370,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* @param info - key backup info dict from getKeyBackupVersion()
*
* @deprecated Prefer {@link CryptoApi.isKeyBackupTrusted | `CryptoApi.isKeyBackupTrusted`}.
* @deprecated Not supported for Rust Cryptography. Prefer {@link CryptoApi.isKeyBackupTrusted | `CryptoApi.isKeyBackupTrusted`}.
*/
public isKeyBackupTrusted(info: IKeyBackupInfo): Promise<TrustInfo> {
if (!this.crypto) {
@@ -3347,7 +3384,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* the server, otherwise false. If we haven't completed a successful check
* of key backup status yet, returns null.
*
* @deprecated Prefer direct access to {@link Crypto.CryptoApi.getActiveSessionBackupVersion}:
* @deprecated Not supported for Rust Cryptography. Prefer direct access to {@link Crypto.CryptoApi.getActiveSessionBackupVersion}:
*
* ```javascript
* let enabled = (await client.getCrypto().getActiveSessionBackupVersion()) !== null;
@@ -3380,7 +3417,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Disable backing up of keys.
*
* @deprecated It should be unnecessary to disable key backup.
* @deprecated Not supported for Rust Cryptography. It should be unnecessary to disable key backup.
*/
public disableKeyBackup(): void {
if (!this.crypto) {
@@ -3401,7 +3438,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Object that can be passed to createKeyBackupVersion and
* additionally has a 'recovery_key' member with the user-facing recovery key string.
*
* @deprecated Use {@link Crypto.CryptoApi.resetKeyBackup | `CryptoApi.resetKeyBackup`}.
* @deprecated Not supported for Rust cryptography. Use {@link Crypto.CryptoApi.resetKeyBackup | `CryptoApi.resetKeyBackup`}.
*/
public async prepareKeyBackupVersion(
password?: string | Uint8Array | null,
@@ -3529,6 +3566,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param data - Object keys to send
* @returns a promise that will resolve when the keys
* are uploaded
*
* @deprecated Not supported for Rust Cryptography.
*/
public sendKeyBackup(
roomId: undefined,
@@ -3566,8 +3605,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Marks all group sessions as needing to be backed up and schedules them to
* upload in the background as soon as possible.
*
* (This is done automatically as part of {@link CryptoApi.resetKeyBackup},
* so there is probably no need to call this manually.)
* @deprecated Not supported for Rust Cryptography. This is done automatically as part of
* {@link CryptoApi.resetKeyBackup}, so there is probably no need to call this manually.
*/
public async scheduleAllGroupSessionsForBackup(): Promise<void> {
if (!this.crypto) {
@@ -3585,6 +3624,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* so there is probably no need to call this manually.)
*
* @returns Promise which resolves to the number of sessions requiring a backup.
* @deprecated Not supported for Rust Cryptography.
*/
public flagAllGroupSessionsForBackup(): Promise<number> {
if (!this.crypto) {
@@ -3594,6 +3634,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.crypto.backupManager.flagAllGroupSessionsForBackup();
}
/**
* Return true if recovery key is valid.
* Try to decode the recovery key and check if it's successful.
* @param recoveryKey
* @deprecated Use {@link decodeRecoveryKey} directly
*/
public isValidRecoveryKey(recoveryKey: string): boolean {
try {
decodeRecoveryKey(recoveryKey);
@@ -3612,6 +3658,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param password - Passphrase
* @param backupInfo - Backup metadata from `checkKeyBackup`
* @returns key backup key
* @deprecated Deriving a backup key from a passphrase is not part of the matrix spec. Instead, a random key is generated and stored/shared via 4S.
*/
public keyBackupKeyFromPassword(password: string, backupInfo: IKeyBackupInfo): Promise<Uint8Array> {
return keyFromAuthData(backupInfo.auth_data, password);
@@ -3625,6 +3672,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param recoveryKey - The recovery key
* @returns key backup key
* @deprecated Use {@link decodeRecoveryKey} directly
*/
public keyBackupKeyFromRecoveryKey(recoveryKey: string): Uint8Array {
return decodeRecoveryKey(recoveryKey);
@@ -4035,10 +4083,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public deleteKeysFromBackup(roomId: string, sessionId: undefined, version?: string): Promise<void>;
public deleteKeysFromBackup(roomId: string, sessionId: string, version?: string): Promise<void>;
public async deleteKeysFromBackup(roomId?: string, sessionId?: string, version?: string): Promise<void> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
const path = this.makeKeyBackupPath(roomId!, sessionId!, version);
await this.http.authedRequest(Method.Delete, path.path, path.queryData, undefined, { prefix: ClientPrefix.V3 });
}
@@ -4049,6 +4093,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param roomId - the room for which keys should be shared.
* @param userIds - a list of users to share with. The keys will be sent to
* all of the user's current devices.
*
* @deprecated Do not use this method. It does not work with the Rust crypto stack, and even with the legacy
* stack it introduces a security vulnerability.
*/
public async sendSharedHistoryKeys(roomId: string, userIds: string[]): Promise<void> {
if (!this.crypto) {
@@ -4292,13 +4339,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
signPromise = this.http.requestOtherUrl<IThirdPartySigned>(Method.Post, url);
}
let queryParams: QueryDict = {};
const queryParams: QueryDict = {};
if (opts.viaServers) {
// server_name has been deprecated in favour of via with Matrix >1.11 (MSC4156)
queryParams.server_name = opts.viaServers;
queryParams.via = opts.viaServers;
if (this.canSupport.get(Feature.MigrateServerNameToVia) === ServerSupport.Unstable) {
queryParams = replaceParam("via", "org.matrix.msc4156.via", queryParams);
}
}
const data: IJoinRequestBody = {};
@@ -4341,13 +4386,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const path = utils.encodeUri("/knock/$roomIdOrAlias", { $roomIdOrAlias: roomIdOrAlias });
let queryParams: QueryDict = {};
const queryParams: QueryDict = {};
if (opts.viaServers) {
// server_name has been deprecated in favour of via with Matrix >1.11 (MSC4156)
queryParams.server_name = opts.viaServers;
queryParams.via = opts.viaServers;
if (this.canSupport.get(Feature.MigrateServerNameToVia) === ServerSupport.Unstable) {
queryParams = replaceParam("via", "org.matrix.msc4156.via", queryParams);
}
}
const body: Record<string, string> = {};
@@ -7911,6 +7954,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* The app may wish to see if we have a key cached without
* triggering a user interaction.
*
* @deprecated Not supported for Rust Cryptography.
*/
public getCrossSigningCacheCallbacks(): ICacheCallbacks | undefined {
// XXX: Private member access
@@ -8778,6 +8823,183 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest(Method.Get, path);
}
/**
* Determine if the server supports extended profiles, as described by MSC4133.
*
* @returns `true` if supported, otherwise `false`
*/
public async doesServerSupportExtendedProfiles(): Promise<boolean> {
return this.doesServerSupportUnstableFeature(UNSTABLE_MSC4133_EXTENDED_PROFILES);
}
/**
* Get the prefix used for extended profile requests.
*
* @returns The prefix for use with `authedRequest`
*/
private async getExtendedProfileRequestPrefix(): Promise<string> {
if (await this.doesServerSupportUnstableFeature("uk.tcpip.msc4133.stable")) {
return ClientPrefix.V3;
}
return "/_matrix/client/unstable/uk.tcpip.msc4133";
}
/**
* Fetch a user's *extended* profile, which may include additonal keys.
*
* @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md
* @param userId The user ID to fetch the profile of.
* @returns A set of keys to property values.
*
* @throws An error if the server does not support MSC4133.
* @throws A M_NOT_FOUND error if the profile could not be found.
*/
public async getExtendedProfile(userId: string): Promise<Record<string, unknown>> {
if (!(await this.doesServerSupportExtendedProfiles())) {
throw new Error("Server does not support extended profiles");
}
return this.http.authedRequest(
Method.Get,
utils.encodeUri("/profile/$userId", { $userId: userId }),
undefined,
undefined,
{
prefix: await this.getExtendedProfileRequestPrefix(),
},
);
}
/**
* Fetch a specific key from the user's *extended* profile.
*
* @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md
* @param userId The user ID to fetch the profile of.
* @param key The key of the property to fetch.
* @returns The property value.
*
* @throws An error if the server does not support MSC4133.
* @throws A M_NOT_FOUND error if the key was not set OR the profile could not be found.
*/
public async getExtendedProfileProperty(userId: string, key: string): Promise<unknown> {
if (!(await this.doesServerSupportExtendedProfiles())) {
throw new Error("Server does not support extended profiles");
}
const profile = (await this.http.authedRequest(
Method.Get,
utils.encodeUri("/profile/$userId/$key", { $userId: userId, $key: key }),
undefined,
undefined,
{
prefix: await this.getExtendedProfileRequestPrefix(),
},
)) as Record<string, unknown>;
return profile[key];
}
/**
* Set a property on your *extended* profile.
*
* @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md
* @param key The key of the property to set.
* @param value The value to set on the propety.
*
* @throws An error if the server does not support MSC4133 OR the server disallows editing the user profile.
*/
public async setExtendedProfileProperty(key: string, value: unknown): Promise<void> {
if (!(await this.doesServerSupportExtendedProfiles())) {
throw new Error("Server does not support extended profiles");
}
const userId = this.getUserId();
await this.http.authedRequest(
Method.Put,
utils.encodeUri("/profile/$userId/$key", { $userId: userId, $key: key }),
undefined,
{ [key]: value },
{
prefix: await this.getExtendedProfileRequestPrefix(),
},
);
}
/**
* Delete a property on your *extended* profile.
*
* @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md
* @param key The key of the property to delete.
*
* @throws An error if the server does not support MSC4133 OR the server disallows editing the user profile.
*/
public async deleteExtendedProfileProperty(key: string): Promise<void> {
if (!(await this.doesServerSupportExtendedProfiles())) {
throw new Error("Server does not support extended profiles");
}
const userId = this.getUserId();
await this.http.authedRequest(
Method.Delete,
utils.encodeUri("/profile/$userId/$key", { $userId: userId, $key: key }),
undefined,
undefined,
{
prefix: await this.getExtendedProfileRequestPrefix(),
},
);
}
/**
* Update multiple properties on your *extended* profile. This will
* merge with any existing keys.
*
* @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md
* @param profile The profile object to merge with the existing profile.
* @returns The newly merged profile.
*
* @throws An error if the server does not support MSC4133 OR the server disallows editing the user profile.
*/
public async patchExtendedProfile(profile: Record<string, unknown>): Promise<Record<string, unknown>> {
if (!(await this.doesServerSupportExtendedProfiles())) {
throw new Error("Server does not support extended profiles");
}
const userId = this.getUserId();
return this.http.authedRequest(
Method.Patch,
utils.encodeUri("/profile/$userId", { $userId: userId }),
{},
profile,
{
prefix: await this.getExtendedProfileRequestPrefix(),
},
);
}
/**
* Set multiple properties on your *extended* profile. This will completely
* replace the existing profile, removing any unspecified keys.
*
* @see https://github.com/tcpipuk/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md
* @param profile The profile object to set.
*
* @throws An error if the server does not support MSC4133 OR the server disallows editing the user profile.
*/
public async setExtendedProfile(profile: Record<string, unknown>): Promise<void> {
if (!(await this.doesServerSupportExtendedProfiles())) {
throw new Error("Server does not support extended profiles");
}
const userId = this.getUserId();
await this.http.authedRequest(
Method.Put,
utils.encodeUri("/profile/$userId", { $userId: userId }),
{},
profile,
{
prefix: await this.getExtendedProfileRequestPrefix(),
},
);
}
/**
* @returns Promise which resolves to a list of the user's threepids.
* @returns Rejects: with an error response.
+43
View File
@@ -0,0 +1,43 @@
/*
* Copyright 2024 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 { deriveRecoveryKeyFromPassphrase } from "../crypto-api/index.ts";
/* eslint-disable camelcase */
interface IAuthData {
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}
/**
* Derive a backup key from a passphrase using the salt and iterations from the auth data.
* @param authData - The auth data containing the salt and iterations
* @param passphrase - The passphrase to derive the key from
* @deprecated Deriving a backup key from a passphrase is not part of the matrix spec. Instead, a random key is generated and stored/shared via 4S.
*/
export function keyFromAuthData(authData: IAuthData, passphrase: string): Promise<Uint8Array> {
if (!authData.private_key_salt || !authData.private_key_iterations) {
throw new Error("Salt and/or iterations not found: " + "this backup cannot be restored with a passphrase");
}
return deriveRecoveryKeyFromPassphrase(
passphrase,
authData.private_key_salt,
authData.private_key_iterations,
authData.private_key_bits,
);
}
+108 -1
View File
@@ -40,6 +40,11 @@ export interface CryptoApi {
*/
globalBlacklistUnverifiedDevices: boolean;
/**
* The {@link DeviceIsolationMode} mode to use.
*/
setDeviceIsolationMode(isolationMode: DeviceIsolationMode): void;
/**
* Return the current version of the crypto module.
* For example: `Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`.
@@ -190,6 +195,16 @@ export interface CryptoApi {
*/
getUserVerificationStatus(userId: string): Promise<UserVerificationStatus>;
/**
* "Pin" the current identity of the given user, accepting it as genuine.
*
* This is useful if the user has changed identity since we first saw them (leading to
* {@link UserVerificationStatus.needsUserApproval}), and we are now accepting their new identity.
*
* Throws an error if called on our own user ID, or on a user ID that we don't have an identity for.
*/
pinCurrentUserIdentity(userId: string): Promise<void>;
/**
* Get the verification status of a given device.
*
@@ -589,6 +604,23 @@ export enum DecryptionFailureCode {
*/
HISTORICAL_MESSAGE_USER_NOT_JOINED = "HISTORICAL_MESSAGE_USER_NOT_JOINED",
/**
* The sender's identity is not verified, but was previously verified.
*/
SENDER_IDENTITY_PREVIOUSLY_VERIFIED = "SENDER_IDENTITY_PREVIOUSLY_VERIFIED",
/**
* The sender device is not cross-signed. This will only be used if the
* device isolation mode is set to `OnlySignedDevicesIsolationMode`.
*/
UNSIGNED_SENDER_DEVICE = "UNSIGNED_SENDER_DEVICE",
/**
* We weren't able to link the message back to any known device. This will
* only be used if the device isolation mode is set to `OnlySignedDevicesIsolationMode`.
*/
UNKNOWN_SENDER_DEVICE = "UNKNOWN_SENDER_DEVICE",
/** Unknown or unclassified error. */
UNKNOWN_ERROR = "UNKNOWN_ERROR",
@@ -632,6 +664,59 @@ export enum DecryptionFailureCode {
UNKNOWN_ENCRYPTION_ALGORITHM = "UNKNOWN_ENCRYPTION_ALGORITHM",
}
/** Base {@link DeviceIsolationMode} kind. */
export enum DeviceIsolationModeKind {
AllDevicesIsolationMode,
OnlySignedDevicesIsolationMode,
}
/**
* A type of {@link DeviceIsolationMode}.
*
* Message encryption keys are shared with all devices in the room, except in case of
* verified user problems (see {@link errorOnVerifiedUserProblems}).
*
* Events from all senders are always decrypted (and should be decorated with message shields in case
* of authenticity warnings, see {@link EventEncryptionInfo}).
*/
export class AllDevicesIsolationMode {
public readonly kind = DeviceIsolationModeKind.AllDevicesIsolationMode;
/**
*
* @param errorOnVerifiedUserProblems - Behavior when sharing keys to remote devices.
*
* If set to `true`, sharing keys will fail (i.e. message sending will fail) with an error if:
* - The user was previously verified but is not anymore, or:
* - A verified user has some unverified devices (not cross-signed).
*
* If `false`, the keys will be distributed as usual. In this case, the client UX should display
* warnings to inform the user about problematic devices/users, and stop them hitting this case.
*/
public constructor(public readonly errorOnVerifiedUserProblems: boolean) {}
}
/**
* A type of {@link DeviceIsolationMode}.
*
* Message encryption keys are only shared with devices that have been cross-signed by their owner.
* Encryption will throw an error if a verified user replaces their identity.
*
* Events are decrypted only if they come from a cross-signed device. Other events will result in a decryption
* failure. (To access the failure reason, see {@link MatrixEvent.decryptionFailureReason}.)
*/
export class OnlySignedDevicesIsolationMode {
public readonly kind = DeviceIsolationModeKind.OnlySignedDevicesIsolationMode;
}
/**
* DeviceIsolationMode represents the mode of device isolation used when encrypting or decrypting messages.
* It can be one of two types: {@link AllDevicesIsolationMode} or {@link OnlySignedDevicesIsolationMode}.
*
* Only supported by rust Crypto.
*/
export type DeviceIsolationMode = AllDevicesIsolationMode | OnlySignedDevicesIsolationMode;
/**
* Options object for `CryptoApi.bootstrapCrossSigning`.
*/
@@ -650,11 +735,29 @@ export interface BootstrapCrossSigningOpts {
* Represents the ways in which we trust a user
*/
export class UserVerificationStatus {
/**
* Indicates if the identity has changed in a way that needs user approval.
*
* This happens if the identity has changed since we first saw it, *unless* the new identity has also been verified
* by our user (eg via an interactive verification).
*
* To rectify this, either:
*
* * Conduct a verification of the new identity via {@link CryptoApi.requestVerificationDM}.
* * Pin the new identity, via {@link CryptoApi.pinCurrentUserIdentity}.
*
* @returns true if the identity has changed in a way that needs user approval.
*/
public readonly needsUserApproval: boolean;
public constructor(
private readonly crossSigningVerified: boolean,
private readonly crossSigningVerifiedBefore: boolean,
private readonly tofu: boolean,
) {}
needsUserApproval: boolean = false,
) {
this.needsUserApproval = needsUserApproval;
}
/**
* @returns true if this user is verified via any means
@@ -680,6 +783,8 @@ export class UserVerificationStatus {
/**
* @returns true if this user's key is trusted on first use
*
* @deprecated No longer supported, with the Rust crypto stack.
*/
public isTofu(): boolean {
return this.tofu;
@@ -967,3 +1072,5 @@ export interface OwnDeviceKeys {
export * from "./verification.ts";
export * from "./keybackup.ts";
export * from "./recovery-key.ts";
export * from "./key-passphrase.ts";
+58
View File
@@ -0,0 +1,58 @@
/*
* Copyright 2024 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.
*/
const DEFAULT_BIT_SIZE = 256;
/**
* Derive a recovery key from a passphrase and salt using PBKDF2.
* @see https://spec.matrix.org/v1.11/client-server-api/#deriving-keys-from-passphrases
*
* @param passphrase - The passphrase to derive the key from
* @param salt - The salt to use in the derivation
* @param iterations - The number of iterations to use in the derivation
* @param numBits - The number of bits to derive
*/
export async function deriveRecoveryKeyFromPassphrase(
passphrase: string,
salt: string,
iterations: number,
numBits = DEFAULT_BIT_SIZE,
): Promise<Uint8Array> {
if (!globalThis.crypto.subtle || !TextEncoder) {
throw new Error("Password-based backup is not available on this platform");
}
const key = await globalThis.crypto.subtle.importKey(
"raw",
new TextEncoder().encode(passphrase),
{ name: "PBKDF2" },
false,
["deriveBits"],
);
const keybits = await globalThis.crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt: new TextEncoder().encode(salt),
iterations: iterations,
hash: "SHA-512",
},
key,
numBits,
);
return new Uint8Array(keybits);
}
+2 -2
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { ISigned } from "../@types/signed.ts";
import { IEncryptedPayload } from "../crypto/aes.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
export interface Curve25519AuthData {
public_key: string;
@@ -77,7 +77,7 @@ export interface Curve25519SessionData {
}
/* eslint-disable camelcase */
export interface KeyBackupSession<T = Curve25519SessionData | IEncryptedPayload> {
export interface KeyBackupSession<T = Curve25519SessionData | AESEncryptedSecretStoragePayload> {
first_message_index: number;
forwarded_count: number;
is_verified: boolean;
+69
View File
@@ -0,0 +1,69 @@
/*
* Copyright 2024 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 bs58 from "bs58";
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
const OLM_RECOVERY_KEY_PREFIX = [0x8b, 0x01];
const KEY_SIZE = 32;
/**
* Encode a recovery key using the Matrix {@link https://spec.matrix.org/v1.11/appendices/#cryptographic-key-representation | Cryptographic key representation}
* @param key
*/
export function encodeRecoveryKey(key: ArrayLike<number>): string | undefined {
const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
let parity = 0;
for (let i = 0; i < buf.length - 1; ++i) {
parity ^= buf[i];
}
buf[buf.length - 1] = parity;
const base58key = bs58.encode(buf);
return base58key.match(/.{1,4}/g)?.join(" ");
}
/**
* Decode a recovery key encoded with the Matrix {@link https://spec.matrix.org/v1.11/appendices/#cryptographic-key-representation | Cryptographic key representation} encoding.
* @param recoveryKey
*/
export function decodeRecoveryKey(recoveryKey: string): Uint8Array {
const result = bs58.decode(recoveryKey.replace(/ /g, ""));
let parity = 0;
for (const b of result) {
parity ^= b;
}
if (parity !== 0) {
throw new Error("Incorrect parity");
}
for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
throw new Error("Incorrect prefix");
}
}
if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE + 1) {
throw new Error("Incorrect length");
}
return Uint8Array.from(result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE));
}
+4 -3
View File
@@ -22,7 +22,6 @@ import type { PkSigning } from "@matrix-org/olm";
import { IObject, pkSign, pkVerify } from "./olmlib.ts";
import { logger } from "../logger.ts";
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
import { decryptAES, encryptAES } from "./aes.ts";
import { DeviceInfo } from "./deviceinfo.ts";
import { ISignedKey, MatrixClient } from "../client.ts";
import { OlmDevice } from "./OlmDevice.ts";
@@ -36,6 +35,8 @@ import {
UserVerificationStatus as UserTrustLevel,
} from "../crypto-api/index.ts";
import { decodeBase64, encodeBase64 } from "../base64.ts";
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
// backwards-compatibility re-exports
export { UserTrustLevel };
@@ -662,7 +663,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
if (key && key.ciphertext) {
const pickleKey = Buffer.from(olmDevice.pickleKey);
const decrypted = await decryptAES(key, pickleKey, type);
const decrypted = await decryptAESSecretStorageItem(key, pickleKey, type);
return decodeBase64(decrypted);
} else {
return key;
@@ -676,7 +677,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
throw new Error(`storeCrossSigningKeyCache expects Uint8Array, got ${key}`);
}
const pickleKey = Buffer.from(olmDevice.pickleKey);
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, type);
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), pickleKey, type);
return store.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
store.storeSecretStorePrivateKey(txn, type, encryptedKey);
});
+9 -142
View File
@@ -14,146 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { decodeBase64, encodeBase64 } from "../base64.ts";
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
// salt for HKDF, with 8 bytes of zeros
const zeroSalt = new Uint8Array(8);
export interface IEncryptedPayload {
[key: string]: any; // extensible
/** the initialization vector in base64 */
iv: string;
/** the ciphertext in base64 */
ciphertext: string;
/** the HMAC in base64 */
mac: string;
}
/**
* encrypt a string
*
* @param data - the plaintext to encrypt
* @param key - the encryption key to use
* @param name - the name of the secret
* @param ivStr - the initialization vector to use
*/
export async function encryptAES(
data: string,
key: Uint8Array,
name: string,
ivStr?: string,
): Promise<IEncryptedPayload> {
let iv: Uint8Array;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = new Uint8Array(16);
globalThis.crypto.getRandomValues(iv);
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
}
const [aesKey, hmacKey] = await deriveKeys(key, name);
const encodedData = new TextEncoder().encode(data);
const ciphertext = await globalThis.crypto.subtle.encrypt(
{
name: "AES-CTR",
counter: iv,
length: 64,
},
aesKey,
encodedData,
);
const hmac = await globalThis.crypto.subtle.sign({ name: "HMAC" }, hmacKey, ciphertext);
return {
iv: encodeBase64(iv),
ciphertext: encodeBase64(ciphertext),
mac: encodeBase64(hmac),
};
}
/**
* decrypt a string
*
* @param data - the encrypted data
* @param key - the encryption key to use
* @param name - the name of the secret
*/
export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
const [aesKey, hmacKey] = await deriveKeys(key, name);
const ciphertext = decodeBase64(data.ciphertext);
if (!(await globalThis.crypto.subtle.verify({ name: "HMAC" }, hmacKey, decodeBase64(data.mac), ciphertext))) {
throw new Error(`Error decrypting secret ${name}: bad MAC`);
}
const plaintext = await globalThis.crypto.subtle.decrypt(
{
name: "AES-CTR",
counter: decodeBase64(data.iv),
length: 64,
},
aesKey,
ciphertext,
);
return new TextDecoder().decode(new Uint8Array(plaintext));
}
async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
const hkdfkey = await globalThis.crypto.subtle.importKey("raw", key, { name: "HKDF" }, false, ["deriveBits"]);
const keybits = await globalThis.crypto.subtle.deriveBits(
{
name: "HKDF",
salt: zeroSalt,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
info: new TextEncoder().encode(name),
hash: "SHA-256",
},
hkdfkey,
512,
);
const aesKey = keybits.slice(0, 32);
const hmacKey = keybits.slice(32);
const aesProm = globalThis.crypto.subtle.importKey("raw", aesKey, { name: "AES-CTR" }, false, [
"encrypt",
"decrypt",
]);
const hmacProm = globalThis.crypto.subtle.importKey(
"raw",
hmacKey,
{
name: "HMAC",
hash: { name: "SHA-256" },
},
false,
["sign", "verify"],
);
return Promise.all([aesProm, hmacProm]);
}
// string of zeroes, for calculating the key check
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
/** Calculate the MAC for checking the key.
*
* @param key - the key to use
* @param iv - The initialization vector as a base64-encoded string.
* If omitted, a random initialization vector will be created.
* @returns An object that contains, `mac` and `iv` properties.
*/
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<IEncryptedPayload> {
return encryptAES(ZERO_STR, key, "", iv);
}
// Export for backwards compatibility
export type { AESEncryptedSecretStoragePayload as IEncryptedPayload };
// Export with new names instead of using `as` to not break react-sdk tests
export const encryptAES = encryptAESSecretStorageItem;
export const decryptAES = decryptAESSecretStorageItem;
export { calculateKeyCheck } from "../calculateKeyCheck.ts";
+12 -7
View File
@@ -27,8 +27,6 @@ import { DeviceTrustLevel } from "./CrossSigning.ts";
import { keyFromPassphrase } from "./key_passphrase.ts";
import { encodeUri, safeSet, sleep } from "../utils.ts";
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store.ts";
import { encodeRecoveryKey } from "./recoverykey.ts";
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes.ts";
import {
Curve25519SessionData,
IAes256AuthData,
@@ -41,6 +39,11 @@ import { CryptoEvent } from "./index.ts";
import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api/index.ts";
import { BackupTrustInfo } from "../crypto-api/keybackup.ts";
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
import { encodeRecoveryKey } from "../crypto-api/index.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
import { calculateKeyCheck } from "../calculateKeyCheck.ts";
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
@@ -94,7 +97,7 @@ interface BackupAlgorithmClass {
interface BackupAlgorithm {
untrusted: boolean;
encryptSession(data: Record<string, any>): Promise<Curve25519SessionData | IEncryptedPayload>;
encryptSession(data: Record<string, any>): Promise<Curve25519SessionData | AESEncryptedSecretStoragePayload>;
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]>;
authData: AuthData;
keyMatches(key: ArrayLike<number>): Promise<boolean>;
@@ -825,22 +828,24 @@ export class Aes256 implements BackupAlgorithm {
return false;
}
public encryptSession(data: Record<string, any>): Promise<IEncryptedPayload> {
public encryptSession(data: Record<string, any>): Promise<AESEncryptedSecretStoragePayload> {
const plainText: Record<string, any> = Object.assign({}, data);
delete plainText.session_id;
delete plainText.room_id;
delete plainText.first_known_index;
return encryptAES(JSON.stringify(plainText), this.key, data.session_id);
return encryptAESSecretStorageItem(JSON.stringify(plainText), this.key, data.session_id);
}
public async decryptSessions(
sessions: Record<string, IKeyBackupSession<IEncryptedPayload>>,
sessions: Record<string, IKeyBackupSession<AESEncryptedSecretStoragePayload>>,
): Promise<IMegolmSessionData[]> {
const keys: IMegolmSessionData[] = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
const decrypted = JSON.parse(await decryptAES(sessionData.session_data, this.key, sessionId));
const decrypted = JSON.parse(
await decryptAESSecretStorageItem(sessionData.session_data, this.key, sessionId),
);
decrypted.session_id = sessionId;
keys.push(decrypted);
} catch (e) {
+4 -3
View File
@@ -19,11 +19,12 @@ import anotherjson from "another-json";
import type { IDeviceKeys, IOneTimeKey } from "../@types/crypto.ts";
import { decodeBase64, encodeBase64 } from "../base64.ts";
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
import { decryptAES, encryptAES } from "./aes.ts";
import { logger } from "../logger.ts";
import { Crypto } from "./index.ts";
import { Method } from "../http-api/index.ts";
import { SecretStorageKeyDescription } from "../secret-storage.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
export interface IDehydratedDevice {
device_id: string; // eslint-disable-line camelcase
@@ -61,7 +62,7 @@ export class DehydrationManager {
if (result) {
const { key, keyInfo, deviceDisplayName, time } = result;
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
const decrypted = await decryptAESSecretStorageItem(key, pickleKey, DEHYDRATION_ALGORITHM);
this.key = decodeBase64(decrypted);
this.keyInfo = keyInfo;
this.deviceDisplayName = deviceDisplayName;
@@ -141,7 +142,7 @@ export class DehydrationManager {
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
// update the crypto store with the timestamp
const key = await encryptAES(encodeBase64(this.key!), pickleKey, DEHYDRATION_ALGORITHM);
const key = await encryptAESSecretStorageItem(encodeBase64(this.key!), pickleKey, DEHYDRATION_ALGORITHM);
await this.crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", {
keyInfo: this.keyInfo,
+29 -9
View File
@@ -42,13 +42,11 @@ import { VerificationBase } from "./verification/Base.ts";
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from "./verification/QRCode.ts";
import { SAS as SASVerification } from "./verification/SAS.ts";
import { keyFromPassphrase } from "./key_passphrase.ts";
import { decodeRecoveryKey, encodeRecoveryKey } from "./recoverykey.ts";
import { VerificationRequest } from "./verification/request/VerificationRequest.ts";
import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChannel.ts";
import { Request, ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel.ts";
import { IllegalMethod } from "./verification/IllegalMethod.ts";
import { KeySignatureUploadError } from "../errors.ts";
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes.ts";
import { DehydrationManager } from "./dehydration.ts";
import { BackupManager, LibOlmBackupDecryptor, backupTrustInfoFromLegacyTrustInfo } from "./backup.ts";
import { IStore } from "../store/index.ts";
@@ -89,8 +87,11 @@ import {
BootstrapCrossSigningOpts,
CrossSigningKeyInfo,
CrossSigningStatus,
decodeRecoveryKey,
DecryptionFailureCode,
DeviceIsolationMode,
DeviceVerificationStatus,
encodeRecoveryKey,
EventEncryptionInfo,
EventShieldColour,
EventShieldReason,
@@ -105,6 +106,10 @@ import { deviceInfoToDevice } from "./device-converter.ts";
import { ClientPrefix, MatrixError, Method } from "../http-api/index.ts";
import { decodeBase64, encodeBase64 } from "../base64.ts";
import { KnownMembership } from "../@types/membership.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
import { calculateKeyCheck } from "../calculateKeyCheck.ts";
/* re-exports for backwards compatibility */
export type {
@@ -647,6 +652,12 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
this.backupManager.checkAndStart();
}
/**
* Implementation of {@link Crypto.CryptoApi#setDeviceIsolationMode}.
*/
public setDeviceIsolationMode(isolationMode: DeviceIsolationMode): void {
throw new Error("Not supported");
}
/**
* Implementation of {@link Crypto.CryptoApi#getVersion}.
*/
@@ -1314,11 +1325,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @returns the key, if any, or null
*/
public async getSessionBackupPrivateKey(): Promise<Uint8Array | null> {
const encodedKey = await new Promise<Uint8Array | IEncryptedPayload | string | null>((resolve) => {
this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1");
});
});
const encodedKey = await new Promise<Uint8Array | AESEncryptedSecretStoragePayload | string | null>(
(resolve) => {
this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1");
});
},
);
let key: Uint8Array | null = null;
@@ -1329,7 +1342,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
if (encodedKey && typeof encodedKey === "object" && "ciphertext" in encodedKey) {
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
const decrypted = await decryptAES(encodedKey, pickleKey, "m.megolm_backup.v1");
const decrypted = await decryptAESSecretStorageItem(encodedKey, pickleKey, "m.megolm_backup.v1");
key = decodeBase64(decrypted);
}
return key;
@@ -1346,7 +1359,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
}
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, "m.megolm_backup.v1");
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), pickleKey, "m.megolm_backup.v1");
return this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", encryptedKey);
});
@@ -1600,6 +1613,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return this.checkUserTrust(userId);
}
/**
* Implementation of {@link Crypto.CryptoApi.pinCurrentUserIdentity}.
*/
public async pinCurrentUserIdentity(userId: string): Promise<void> {
throw new Error("not implemented");
}
/**
* Check whether a given device is trusted.
*
+10 -56
View File
@@ -15,74 +15,28 @@ limitations under the License.
*/
import { randomString } from "../randomstring.ts";
import { deriveRecoveryKeyFromPassphrase } from "../crypto-api/index.ts";
const DEFAULT_ITERATIONS = 500000;
const DEFAULT_BITSIZE = 256;
/* eslint-disable camelcase */
interface IAuthData {
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}
/* eslint-enable camelcase */
interface IKey {
key: Uint8Array;
salt: string;
iterations: number;
}
export function keyFromAuthData(authData: IAuthData, password: string): Promise<Uint8Array> {
if (!authData.private_key_salt || !authData.private_key_iterations) {
throw new Error("Salt and/or iterations not found: " + "this backup cannot be restored with a passphrase");
}
return deriveKey(
password,
authData.private_key_salt,
authData.private_key_iterations,
authData.private_key_bits || DEFAULT_BITSIZE,
);
}
export async function keyFromPassphrase(password: string): Promise<IKey> {
/**
* Generate a new recovery key, based on a passphrase.
* @param passphrase - The passphrase to generate the key from
*/
export async function keyFromPassphrase(passphrase: string): Promise<IKey> {
const salt = randomString(32);
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE);
const key = await deriveRecoveryKeyFromPassphrase(passphrase, salt, DEFAULT_ITERATIONS);
return { key, salt, iterations: DEFAULT_ITERATIONS };
}
export async function deriveKey(
password: string,
salt: string,
iterations: number,
numBits = DEFAULT_BITSIZE,
): Promise<Uint8Array> {
if (!globalThis.crypto.subtle || !TextEncoder) {
throw new Error("Password-based backup is not available on this platform");
}
const key = await globalThis.crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
{ name: "PBKDF2" },
false,
["deriveBits"],
);
const keybits = await globalThis.crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt: new TextEncoder().encode(salt),
iterations: iterations,
hash: "SHA-512",
},
key,
numBits,
);
return new Uint8Array(keybits);
}
// Re-export the key passphrase functions to avoid breaking changes
export { deriveRecoveryKeyFromPassphrase as deriveKey };
export { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
+2 -45
View File
@@ -14,48 +14,5 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import bs58 from "bs58";
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
const OLM_RECOVERY_KEY_PREFIX = [0x8b, 0x01];
const KEY_SIZE = 32;
export function encodeRecoveryKey(key: ArrayLike<number>): string | undefined {
const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
let parity = 0;
for (let i = 0; i < buf.length - 1; ++i) {
parity ^= buf[i];
}
buf[buf.length - 1] = parity;
const base58key = bs58.encode(buf);
return base58key.match(/.{1,4}/g)?.join(" ");
}
export function decodeRecoveryKey(recoveryKey: string): Uint8Array {
const result = bs58.decode(recoveryKey.replace(/ /g, ""));
let parity = 0;
for (const b of result) {
parity ^= b;
}
if (parity !== 0) {
throw new Error("Incorrect parity");
}
for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
throw new Error("Incorrect prefix");
}
}
if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE + 1) {
throw new Error("Incorrect length");
}
return Uint8Array.from(result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + KEY_SIZE));
}
// Re-export to avoid breaking changes
export * from "../crypto-api/recovery-key.ts";
+3 -3
View File
@@ -25,8 +25,8 @@ import { Logger } from "../../logger.ts";
import { InboundGroupSessionData } from "../OlmDevice.ts";
import { MatrixEvent } from "../../models/event.ts";
import { DehydrationManager } from "../dehydration.ts";
import { IEncryptedPayload } from "../aes.ts";
import { CrossSigningKeyInfo } from "../../crypto-api/index.ts";
import { AESEncryptedSecretStoragePayload } from "../../@types/AESEncryptedSecretStoragePayload.ts";
/**
* Internal module. Definitions for storage for the crypto module
@@ -35,11 +35,11 @@ import { CrossSigningKeyInfo } from "../../crypto-api/index.ts";
export interface SecretStorePrivateKeys {
"dehydration": {
keyInfo: DehydrationManager["keyInfo"];
key: IEncryptedPayload;
key: AESEncryptedSecretStoragePayload;
deviceDisplayName: string;
time: number;
} | null;
"m.megolm_backup.v1": IEncryptedPayload;
"m.megolm_backup.v1": AESEncryptedSecretStoragePayload;
}
/**
+8 -2
View File
@@ -291,11 +291,17 @@ export class RoomWidgetClient extends MatrixClient {
event: MatrixEvent,
delayOpts?: SendDelayedEventRequestOpts,
): Promise<ISendEventResponse | SendDelayedEventResponse> {
// We need to extend the content with the redacts parameter
// The js sdk uses event.redacts but the widget api uses event.content.redacts
// This will be converted back to event.redacts in the widget driver.
const content = event.event.redacts
? { ...event.getContent(), redacts: event.event.redacts }
: event.getContent();
if (delayOpts) {
// TODO: updatePendingEvent for delayed events?
const response = await this.widgetApi.sendRoomEvent(
event.getType(),
event.getContent(),
content,
room.roomId,
"delay" in delayOpts ? delayOpts.delay : undefined,
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
@@ -305,7 +311,7 @@ export class RoomWidgetClient extends MatrixClient {
let response: ISendEventFromWidgetResponseData;
try {
response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId);
response = await this.widgetApi.sendRoomEvent(event.getType(), content, room.roomId);
} catch (e) {
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
throw e;
-4
View File
@@ -33,7 +33,6 @@ export enum Feature {
AccountDataDeletion = "AccountDataDeletion",
RelationsRecursion = "RelationsRecursion",
IntentionalMentions = "IntentionalMentions",
MigrateServerNameToVia = "MigrateServerNameToVia",
}
type FeatureSupportCondition = {
@@ -66,9 +65,6 @@ const featureSupportResolver: Record<string, FeatureSupportCondition> = {
unstablePrefixes: ["org.matrix.msc3952_intentional_mentions"],
matrixVersion: "v1.7",
},
[Feature.MigrateServerNameToVia]: {
unstablePrefixes: ["org.matrix.msc4156"],
},
};
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
+1
View File
@@ -76,6 +76,7 @@ export * from "./@types/read_receipts.ts";
export * from "./@types/crypto.ts";
export * from "./@types/extensible_events.ts";
export * from "./@types/IIdentityServerProvider.ts";
export * from "./@types/membership.ts";
export * from "./models/room-summary.ts";
export * from "./models/event-status.ts";
export type { RoomSummary } from "./client.ts";
+4
View File
@@ -128,6 +128,10 @@ export class CallMembership {
return this.parentEvent.getSender();
}
public get eventId(): string | undefined {
return this.parentEvent.getId();
}
public get callId(): string {
return this.membershipData.call_id;
}
+90 -20
View File
@@ -144,6 +144,31 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
// if it looks like a membership has been updated.
private lastMembershipFingerprints: Set<string> | undefined;
private currentEncryptionKeyIndex = -1;
/**
* The statistics for this session.
*/
public statistics = {
counters: {
/**
* The number of times we have sent a room event containing encryption keys.
*/
roomEventEncryptionKeysSent: 0,
/**
* The number of times we have received a room event containing encryption keys.
*/
roomEventEncryptionKeysReceived: 0,
},
totals: {
/**
* The total age (in milliseconds) of all room events containing encryption keys that we have received.
* We track the total age so that we can later calculate the average age of all keys received.
*/
roomEventEncryptionKeysReceivedTotalAge: 0,
},
};
/**
* The callId (sessionId) of the call.
*
@@ -380,20 +405,40 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
}
/**
* Re-emit an EncryptionKeyChanged event for each tracked encryption key. This can be used to export
* the keys.
*/
public reemitEncryptionKeys(): void {
this.encryptionKeys.forEach((keys, participantId) => {
keys.forEach((key, index) => {
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, key.key, index, participantId);
});
});
}
/**
* Get the known encryption keys for a given participant device.
*
* @param userId the user ID of the participant
* @param deviceId the device ID of the participant
* @returns The encryption keys for the given participant, or undefined if they are not known.
*
* @deprecated This will be made private in a future release.
*/
public getKeysForParticipant(userId: string, deviceId: string): Array<Uint8Array> | undefined {
return this.getKeysForParticipantInternal(userId, deviceId);
}
private getKeysForParticipantInternal(userId: string, deviceId: string): Array<Uint8Array> | undefined {
return this.encryptionKeys.get(getParticipantId(userId, deviceId))?.map((entry) => entry.key);
}
/**
* A map of keys used to encrypt and decrypt (we are using a symmetric
* cipher) given participant's media. This also includes our own key
*
* @deprecated This will be made private in a future release.
*/
public getEncryptionKeys(): IterableIterator<[string, Array<Uint8Array>]> {
// the returned array doesn't contain the timestamps
@@ -409,7 +454,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
if (!userId) throw new Error("No userId!");
if (!deviceId) throw new Error("No deviceId!");
return (this.getKeysForParticipant(userId, deviceId)?.length ?? 0) % 16;
return (this.getKeysForParticipantInternal(userId, deviceId)?.length ?? 0) % 16;
}
/**
@@ -467,10 +512,16 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
const useKeyTimeout = setTimeout(() => {
this.setNewKeyTimeouts.delete(useKeyTimeout);
logger.info(`Delayed-emitting key changed event for ${participantId} idx ${encryptionKeyIndex}`);
if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) {
this.currentEncryptionKeyIndex = encryptionKeyIndex;
}
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId);
}, USE_KEY_DELAY);
this.setNewKeyTimeouts.add(useKeyTimeout);
} else {
if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) {
this.currentEncryptionKeyIndex = encryptionKeyIndex;
}
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId);
}
}
@@ -479,8 +530,9 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
* Generate a new sender key and add it at the next available index
* @param delayBeforeUse - If true, wait for a short period before setting the key for the
* media encryptor to use. If false, set the key immediately.
* @returns The index of the new key
*/
private makeNewSenderKey(delayBeforeUse = false): void {
private makeNewSenderKey(delayBeforeUse = false): number {
const userId = this.client.getUserId();
const deviceId = this.client.getDeviceId();
@@ -491,6 +543,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
const encryptionKeyIndex = this.getNewEncryptionKeyIndex();
logger.info("Generated new key at index " + encryptionKeyIndex);
this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, Date.now(), delayBeforeUse);
return encryptionKeyIndex;
}
/**
@@ -517,17 +570,17 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
/**
* Re-sends the encryption keys room event
*/
private sendEncryptionKeysEvent = async (): Promise<void> => {
private sendEncryptionKeysEvent = async (indexToSend?: number): Promise<void> => {
if (this.keysEventUpdateTimeout !== undefined) {
clearTimeout(this.keysEventUpdateTimeout);
this.keysEventUpdateTimeout = undefined;
}
this.lastEncryptionKeyUpdateRequest = Date.now();
logger.info("Sending encryption keys event");
if (!this.isJoined()) return;
logger.info(`Sending encryption keys event. indexToSend=${indexToSend}`);
const userId = this.client.getUserId();
const deviceId = this.client.getDeviceId();
@@ -541,20 +594,33 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
return;
}
if (typeof indexToSend !== "number" && this.currentEncryptionKeyIndex === -1) {
logger.warn("Tried to send encryption keys event but no current key index found!");
return;
}
const keyIndexToSend = indexToSend ?? this.currentEncryptionKeyIndex;
const keyToSend = myKeys[keyIndexToSend];
try {
await this.client.sendEvent(this.room.roomId, EventType.CallEncryptionKeysPrefix, {
keys: myKeys.map((key, index) => {
return {
index,
key: encodeUnpaddedBase64(key),
};
}),
const content: EncryptionKeysEventContent = {
keys: [
{
index: keyIndexToSend,
key: encodeUnpaddedBase64(keyToSend),
},
],
device_id: deviceId,
call_id: "",
} as EncryptionKeysEventContent);
sent_ts: Date.now(),
};
this.statistics.counters.roomEventEncryptionKeysSent += 1;
await this.client.sendEvent(this.room.roomId, EventType.CallEncryptionKeysPrefix, content);
logger.debug(
`Embedded-E2EE-LOG updateEncryptionKeyEvent participantId=${userId}:${deviceId} numSent=${myKeys.length}`,
`Embedded-E2EE-LOG updateEncryptionKeyEvent participantId=${userId}:${deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.currentEncryptionKeyIndex} keyIndexToSend=${keyIndexToSend}`,
this.encryptionKeys,
);
} catch (error) {
@@ -649,6 +715,10 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
return;
}
this.statistics.counters.roomEventEncryptionKeysReceived += 1;
const age = Date.now() - (typeof content.sent_ts === "number" ? content.sent_ts : event.getTs());
this.statistics.totals.roomEventEncryptionKeysReceivedTotalAge += age;
for (const key of content.keys) {
if (!key) {
logger.info("Ignoring false-y key in keys event");
@@ -674,7 +744,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
);
} else {
logger.debug(
`Embedded-E2EE-LOG onCallEncryption userId=${userId}:${deviceId} encryptionKeyIndex=${encryptionKeyIndex}`,
`Embedded-E2EE-LOG onCallEncryption userId=${userId}:${deviceId} encryptionKeyIndex=${encryptionKeyIndex} age=${age}ms`,
this.encryptionKeys,
);
this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, event.getTs());
@@ -726,8 +796,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
logger.debug(`Member(s) have left: queueing sender key rotation`);
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, MAKE_KEY_DELAY);
} else if (anyJoined) {
logger.debug(`New member(s) have joined: re-sending keys`);
this.requestKeyEventSend();
logger.debug(`New member(s) have joined: queueing sender key rotation`);
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, MAKE_KEY_DELAY);
} else if (oldFingerprints) {
// does it look like any of the members have updated their memberships?
const newFingerprints = this.lastMembershipFingerprints!;
@@ -738,7 +808,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
Array.from(oldFingerprints).some((x) => !newFingerprints.has(x)) ||
Array.from(newFingerprints).some((x) => !oldFingerprints.has(x));
if (candidateUpdates) {
logger.debug(`Member(s) have updated/reconnected: re-sending keys`);
logger.debug(`Member(s) have updated/reconnected: re-sending keys to everyone`);
this.requestKeyEventSend();
}
}
@@ -1030,9 +1100,9 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.makeNewKeyTimeout = undefined;
logger.info("Making new sender key for key rotation");
this.makeNewSenderKey(true);
const newKeyIndex = this.makeNewSenderKey(true);
// send immediately: if we're about to start sending with a new key, it's
// important we get it out to others as soon as we can.
this.sendEncryptionKeysEvent();
this.sendEncryptionKeysEvent(newKeyIndex);
};
}
+1
View File
@@ -23,6 +23,7 @@ export interface EncryptionKeysEventContent {
keys: EncryptionKeyEntry[];
device_id: string;
call_id: string;
sent_ts?: number;
}
export type CallNotifyType = "ring" | "notify";
+7
View File
@@ -0,0 +1,7 @@
/**
* The timezone the user is currently in. The value of this property should
* match a timezone provided in https://www.iana.org/time-zones.
*
* @see https://github.com/matrix-org/matrix-spec-proposals/blob/clokep/profile-tz/proposals/4175-profile-field-time-zone.md
*/
export const ProfileKeyMSC4175Timezone = "us.cloke.msc4175.tz";
+1 -1
View File
@@ -2096,7 +2096,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}
/**
* Calls {@link processPollEvent} for a list of events.
* Process a list of poll events.
*
* @param events - List of events
*/
+8 -5
View File
@@ -34,17 +34,20 @@ export type OidcRegistrationClientMetadata = {
policyUri: OidcRegistrationRequestBody["policy_uri"];
};
/**
* Request body for dynamic registration as defined by https://github.com/matrix-org/matrix-spec-proposals/pull/2966
*/
interface OidcRegistrationRequestBody {
client_name: string;
client_name?: string;
client_uri: string;
logo_uri?: string;
contacts: NonEmptyArray<string>;
tos_uri: string;
policy_uri: string;
contacts?: string[];
tos_uri?: string;
policy_uri?: string;
redirect_uris?: NonEmptyArray<string>;
response_types?: NonEmptyArray<string>;
grant_types?: NonEmptyArray<string>;
id_token_signed_response_alg: string;
id_token_signed_response_alg?: string;
token_endpoint_auth_method: string;
application_type: "web" | "native";
}
+3 -3
View File
@@ -140,11 +140,11 @@ export class CrossSigningIdentity {
// 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 resistant to forgotten password/passphrase.
logger.log("resetCrossSigning: exporting to secret storage");
logger.log("resetCrossSigning: exporting private keys to secret storage");
await this.exportCrossSigningKeysToStorage();
}
logger.log("resetCrossSigning: publishing keys to server");
logger.log("resetCrossSigning: publishing public keys to server");
for (const req of [
outgoingRequests.uploadKeysRequest,
outgoingRequests.uploadSigningKeysRequest,
@@ -466,8 +466,6 @@ export class PerSessionKeyBackupDownloader {
return null;
}
const authData = currentServerVersion.auth_data as Curve25519AuthData;
const backupKeys = await this.getBackupDecryptionKey();
if (!backupKeys?.decryptionKey) {
this.logger.debug(`Not checking key backup for session (no decryption key)`);
@@ -483,8 +481,9 @@ export class PerSessionKeyBackupDownloader {
return null;
}
const authData = currentServerVersion.auth_data as Curve25519AuthData;
if (authData.public_key != backupKeys.decryptionKey.megolmV1PublicKey.publicKeyBase64) {
this.logger.debug(`getBackupDecryptor key mismatch error`);
this.logger.debug(`Key backup on server does not match our decryption key`);
this.hasConfigurationProblem = true;
return null;
}
+45 -15
View File
@@ -36,6 +36,7 @@ import { HistoryVisibility } from "../@types/partials.ts";
import { OutgoingRequestsManager } from "./OutgoingRequestsManager.ts";
import { logDuration } from "../utils.ts";
import { KnownMembership } from "../@types/membership.ts";
import { DeviceIsolationMode, DeviceIsolationModeKind } from "../crypto-api/index.ts";
/**
* RoomEncryptor: responsible for encrypting messages to a given room
@@ -119,10 +120,15 @@ export class RoomEncryptor {
*
* This ensures that we have a megolm session ready to use and that we have shared its key with all the devices
* in the room.
*
* @param globalBlacklistUnverifiedDevices - When `true`, it will not send encrypted messages to unverified devices
* @param globalBlacklistUnverifiedDevices - When `true`, and `deviceIsolationMode` is `AllDevicesIsolationMode`,
* will not send encrypted messages to unverified devices.
* Ignored when `deviceIsolationMode` is `OnlySignedDevicesIsolationMode`.
* @param deviceIsolationMode - The device isolation mode. See {@link DeviceIsolationMode}.
*/
public async prepareForEncryption(globalBlacklistUnverifiedDevices: boolean): Promise<void> {
public async prepareForEncryption(
globalBlacklistUnverifiedDevices: boolean,
deviceIsolationMode: DeviceIsolationMode,
): Promise<void> {
// We consider a prepareForEncryption as an encryption promise as it will potentially share keys
// even if it doesn't send an event.
// Usually this is called when the user starts typing, so we want to make sure we have keys ready when the
@@ -130,7 +136,7 @@ export class RoomEncryptor {
// If `encryptEvent` is invoked before `prepareForEncryption` has completed, the `encryptEvent` call will wait for
// `prepareForEncryption` to complete before executing.
// The part where `encryptEvent` shares the room key will then usually be a no-op as it was already performed by `prepareForEncryption`.
await this.encryptEvent(null, globalBlacklistUnverifiedDevices);
await this.encryptEvent(null, globalBlacklistUnverifiedDevices, deviceIsolationMode);
}
/**
@@ -140,9 +146,16 @@ export class RoomEncryptor {
* then, if an event is provided, encrypt it using the session.
*
* @param event - Event to be encrypted, or null if only preparing for encryption (in which case we will pre-share the room key).
* @param globalBlacklistUnverifiedDevices - When `true`, it will not send encrypted messages to unverified devices
* @param globalBlacklistUnverifiedDevices - When `true`, and `deviceIsolationMode` is `AllDevicesIsolationMode`,
* will not send encrypted messages to unverified devices.
* Ignored when `deviceIsolationMode` is `OnlySignedDevicesIsolationMode`.
* @param deviceIsolationMode - The device isolation mode. See {@link DeviceIsolationMode}.
*/
public encryptEvent(event: MatrixEvent | null, globalBlacklistUnverifiedDevices: boolean): Promise<void> {
public encryptEvent(
event: MatrixEvent | null,
globalBlacklistUnverifiedDevices: boolean,
deviceIsolationMode: DeviceIsolationMode,
): Promise<void> {
const logger = new LogSpan(this.prefixedLogger, event ? (event.getTxnId() ?? "") : "prepareForEncryption");
// Ensure order of encryption to avoid message ordering issues, as the scheduler only ensures
// events order after they have been encrypted.
@@ -153,7 +166,7 @@ export class RoomEncryptor {
})
.then(async () => {
await logDuration(logger, "ensureEncryptionSession", async () => {
await this.ensureEncryptionSession(logger, globalBlacklistUnverifiedDevices);
await this.ensureEncryptionSession(logger, globalBlacklistUnverifiedDevices, deviceIsolationMode);
});
if (event) {
await logDuration(logger, "encryptEventInner", async () => {
@@ -173,9 +186,16 @@ export class RoomEncryptor {
* in the room.
*
* @param logger - a place to write diagnostics to
* @param globalBlacklistUnverifiedDevices - When `true`, it will not send encrypted messages to unverified devices
* @param globalBlacklistUnverifiedDevices - When `true`, and `deviceIsolationMode` is `AllDevicesIsolationMode`,
* will not send encrypted messages to unverified devices.
* Ignored when `deviceIsolationMode` is `OnlySignedDevicesIsolationMode`.
* @param deviceIsolationMode - The device isolation mode. See {@link DeviceIsolationMode}.
*/
private async ensureEncryptionSession(logger: LogSpan, globalBlacklistUnverifiedDevices: boolean): Promise<void> {
private async ensureEncryptionSession(
logger: LogSpan,
globalBlacklistUnverifiedDevices: boolean,
deviceIsolationMode: DeviceIsolationMode,
): Promise<void> {
if (this.encryptionSettings.algorithm !== "m.megolm.v1.aes-sha2") {
throw new Error(
`Cannot encrypt in ${this.room.roomId} for unsupported algorithm '${this.encryptionSettings.algorithm}'`,
@@ -251,12 +271,22 @@ export class RoomEncryptor {
rustEncryptionSettings.rotationPeriodMessages = BigInt(this.encryptionSettings.rotation_period_msgs);
}
// When this.room.getBlacklistUnverifiedDevices() === null, the global settings should be used
// See Room#getBlacklistUnverifiedDevices
if (this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices) {
rustEncryptionSettings.sharingStrategy = CollectStrategy.DeviceBasedStrategyOnlyTrustedDevices;
} else {
rustEncryptionSettings.sharingStrategy = CollectStrategy.DeviceBasedStrategyAllDevices;
switch (deviceIsolationMode.kind) {
case DeviceIsolationModeKind.AllDevicesIsolationMode:
{
// When this.room.getBlacklistUnverifiedDevices() === null, the global settings should be used
// See Room#getBlacklistUnverifiedDevices
const onlyAllowTrustedDevices =
this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices;
rustEncryptionSettings.sharingStrategy = CollectStrategy.deviceBasedStrategy(
onlyAllowTrustedDevices,
deviceIsolationMode.errorOnVerifiedUserProblems,
);
}
break;
case DeviceIsolationModeKind.OnlySignedDevicesIsolationMode:
rustEncryptionSettings.sharingStrategy = CollectStrategy.identityBasedStrategy();
break;
}
await logDuration(this.prefixedLogger, "shareRoomKey", async () => {
+2 -2
View File
@@ -33,10 +33,10 @@ import { encodeUri, logDuration } from "../utils.ts";
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts";
import { sleep } from "../utils.ts";
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
import { IEncryptedPayload } from "../crypto/aes.ts";
import { ImportRoomKeyProgressData, ImportRoomKeysOpts } from "../crypto-api/index.ts";
import { IKeyBackupInfo } from "../crypto/keybackup.ts";
import { IKeyBackup } from "../crypto/backup.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
/** Authentification of the backup info, depends on algorithm */
type AuthData = KeyBackupInfo["auth_data"];
@@ -622,7 +622,7 @@ export class RustBackupDecryptor implements BackupDecryptor {
* Implements {@link BackupDecryptor#decryptSessions}
*/
public async decryptSessions(
ciphertexts: Record<string, KeyBackupSession<Curve25519SessionData | IEncryptedPayload>>,
ciphertexts: Record<string, KeyBackupSession<Curve25519SessionData | AESEncryptedSecretStoragePayload>>,
): Promise<IMegolmSessionData[]> {
const keys: IMegolmSessionData[] = [];
for (const [sessionId, sessionData] of Object.entries(ciphertexts)) {
+3 -2
View File
@@ -19,7 +19,6 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { Logger } from "../logger.ts";
import { CryptoStore, MigrationState, SecretStorePrivateKeys } from "../crypto/store/base.ts";
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
import { decryptAES, IEncryptedPayload } from "../crypto/aes.ts";
import { IHttpOpts, MatrixHttpApi } from "../http-api/index.ts";
import { requestKeyBackupVersion } from "./backup.ts";
import { IRoomEncryption } from "../crypto/RoomList.ts";
@@ -28,6 +27,8 @@ import { RustCrypto } from "./rust-crypto.ts";
import { KeyBackupInfo } from "../crypto-api/keybackup.ts";
import { sleep } from "../utils.ts";
import { encodeBase64 } from "../base64.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
/**
* Determine if any data needs migrating from the legacy store, and do so.
@@ -421,7 +422,7 @@ async function getAndDecryptCachedSecretKey(
});
if (key && key.ciphertext && key.iv && key.mac) {
return await decryptAES(key as IEncryptedPayload, legacyPickleKey, name);
return await decryptAESSecretStorageItem(key as AESEncryptedSecretStoragePayload, legacyPickleKey, name);
} else if (key instanceof Uint8Array) {
// This is a legacy backward compatibility case where the key was stored in clear.
return encodeBase64(key);
+111 -19
View File
@@ -58,6 +58,11 @@ import {
OwnDeviceKeys,
UserVerificationStatus,
VerificationRequest,
encodeRecoveryKey,
deriveRecoveryKeyFromPassphrase,
DeviceIsolationMode,
AllDevicesIsolationMode,
DeviceIsolationModeKind,
} from "../crypto-api/index.ts";
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
@@ -65,8 +70,6 @@ import { Device, DeviceMap } from "../models/device.ts";
import { SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../secret-storage.ts";
import { CrossSigningIdentity } from "./CrossSigningIdentity.ts";
import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } from "./secret-storage.ts";
import { keyFromPassphrase } from "../crypto/key_passphrase.ts";
import { encodeRecoveryKey } from "../crypto/recoverykey.ts";
import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts";
import { EventType, MsgType } from "../@types/event.ts";
import { CryptoEvent } from "../crypto/index.ts";
@@ -100,7 +103,13 @@ interface ISignableObject {
* @internal
*/
export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEventMap> implements CryptoBackend {
/**
* The number of iterations to use when deriving a recovery key from a passphrase.
*/
private readonly RECOVERY_KEY_DERIVATION_ITERATIONS = 500000;
private _trustCrossSignedDevices = true;
private deviceIsolationMode: DeviceIsolationMode = new AllDevicesIsolationMode(false);
/** whether {@link stop} has been called */
private stopped = false;
@@ -239,7 +248,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
throw new Error(`Cannot encrypt event in unconfigured room ${roomId}`);
}
await encryptor.encryptEvent(event, this.globalBlacklistUnverifiedDevices);
await encryptor.encryptEvent(event, this.globalBlacklistUnverifiedDevices, this.deviceIsolationMode);
}
public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
@@ -252,7 +261,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
// through decryptEvent and hence get rid of this case.
throw new Error("to-device event was not decrypted in preprocessToDeviceMessages");
}
return await this.eventDecryptor.attemptEventDecryption(event);
return await this.eventDecryptor.attemptEventDecryption(event, this.deviceIsolationMode);
}
/**
@@ -317,20 +326,19 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
* Implementation of {@link CryptoBackend#getBackupDecryptor}.
*/
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
if (backupInfo.algorithm != "m.megolm_backup.v1.curve25519-aes-sha2") {
throw new Error(`getBackupDecryptor Unsupported algorithm ${backupInfo.algorithm}`);
if (!(privKey instanceof Uint8Array)) {
throw new Error(`getBackupDecryptor: expects Uint8Array`);
}
const authData = <Curve25519AuthData>backupInfo.auth_data;
if (!(privKey instanceof Uint8Array)) {
throw new Error(`getBackupDecryptor expects Uint8Array`);
if (backupInfo.algorithm != "m.megolm_backup.v1.curve25519-aes-sha2") {
throw new Error(`getBackupDecryptor: Unsupported algorithm ${backupInfo.algorithm}`);
}
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(encodeBase64(privKey));
const authData = <Curve25519AuthData>backupInfo.auth_data;
if (authData.public_key != backupDecryptionKey.megolmV1PublicKey.publicKeyBase64) {
throw new Error(`getBackupDecryptor key mismatch error`);
throw new Error(`getBackupDecryptor: key backup on server does not match the decryption key`);
}
return this.backupManager.createBackupDecryptor(backupDecryptionKey);
@@ -363,6 +371,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
return `Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`;
}
/**
* Implementation of {@link CryptoApi#setDeviceIsolationMode}.
*/
public setDeviceIsolationMode(isolationMode: DeviceIsolationMode): void {
this.deviceIsolationMode = isolationMode;
}
/**
* Implementation of {@link CryptoApi#isEncryptionEnabledInRoom}.
*/
@@ -385,7 +400,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
const encryptor = this.roomEncryptors[room.roomId];
if (encryptor) {
encryptor.prepareForEncryption(this.globalBlacklistUnverifiedDevices);
encryptor.prepareForEncryption(this.globalBlacklistUnverifiedDevices, this.deviceIsolationMode);
}
}
@@ -641,9 +656,31 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
if (userIdentity === undefined) {
return new UserVerificationStatus(false, false, false);
}
const verified = userIdentity.isVerified();
const wasVerified = userIdentity.wasPreviouslyVerified();
const needsUserApproval =
userIdentity instanceof RustSdkCryptoJs.UserIdentity ? userIdentity.identityNeedsUserApproval() : false;
userIdentity.free();
return new UserVerificationStatus(verified, false, false);
return new UserVerificationStatus(verified, wasVerified, false, needsUserApproval);
}
/**
* Implementation of {@link CryptoApi#pinCurrentUserIdentity}.
*/
public async pinCurrentUserIdentity(userId: string): Promise<void> {
const userIdentity: RustSdkCryptoJs.UserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(userId));
if (userIdentity === undefined) {
throw new Error("Cannot pin identity of unknown user");
}
if (userIdentity instanceof RustSdkCryptoJs.OwnUserIdentity) {
throw new Error("Cannot pin identity of own user");
}
await userIdentity.pinCurrentMasterKey();
}
/**
@@ -764,6 +801,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
// Create a new storage key and add it to secret storage
this.logger.info("bootstrapSecretStorage: creating new secret storage key");
const recoveryKey = await createSecretStorageKey();
if (!recoveryKey) {
throw new Error("createSecretStorageKey() callback did not return a secret storage key");
}
await this.addSecretStorageKeyToSecretStorage(recoveryKey);
}
@@ -880,17 +920,24 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
public async createRecoveryKeyFromPassphrase(password?: string): Promise<GeneratedSecretStorageKey> {
if (password) {
// Generate the key from the passphrase
const derivation = await keyFromPassphrase(password);
// first we generate a random salt
const salt = randomString(32);
// then we derive the key from the passphrase
const recoveryKey = await deriveRecoveryKeyFromPassphrase(
password,
salt,
this.RECOVERY_KEY_DERIVATION_ITERATIONS,
);
return {
keyInfo: {
passphrase: {
algorithm: "m.pbkdf2",
iterations: derivation.iterations,
salt: derivation.salt,
iterations: this.RECOVERY_KEY_DERIVATION_ITERATIONS,
salt,
},
},
privateKey: derivation.key,
encodedPrivateKey: encodeRecoveryKey(derivation.key),
privateKey: recoveryKey,
encodedPrivateKey: encodeRecoveryKey(recoveryKey),
};
} else {
// Using the navigator crypto API to generate the private key
@@ -1731,17 +1778,32 @@ class EventDecryptor {
private readonly perSessionBackupDownloader: PerSessionKeyBackupDownloader,
) {}
public async attemptEventDecryption(event: MatrixEvent): Promise<IEventDecryptionResult> {
public async attemptEventDecryption(
event: MatrixEvent,
isolationMode: DeviceIsolationMode,
): Promise<IEventDecryptionResult> {
// add the event to the pending list *before* attempting to decrypt.
// then, if the key turns up while decryption is in progress (and
// decryption fails), we will schedule a retry.
// (fixes https://github.com/vector-im/element-web/issues/5001)
this.addEventToPendingList(event);
let trustRequirement;
switch (isolationMode.kind) {
case DeviceIsolationModeKind.AllDevicesIsolationMode:
trustRequirement = RustSdkCryptoJs.TrustRequirement.Untrusted;
break;
case DeviceIsolationModeKind.OnlySignedDevicesIsolationMode:
trustRequirement = RustSdkCryptoJs.TrustRequirement.CrossSignedOrLegacy;
break;
}
try {
const res = (await this.olmMachine.decryptRoomEvent(
stringifyEvent(event),
new RustSdkCryptoJs.RoomId(event.getRoomId()!),
new RustSdkCryptoJs.DecryptionSettings(trustRequirement),
)) as RustSdkCryptoJs.DecryptedRoomEvent;
// Success. We can remove the event from the pending list, if
@@ -1849,6 +1911,36 @@ class EventDecryptor {
errorDetails,
);
case RustSdkCryptoJs.DecryptionErrorCode.SenderIdentityPreviouslyVerified:
// We're refusing to decrypt due to not trusting the sender,
// rather than failing to decrypt due to lack of keys, so we
// don't need to keep it on the pending list.
this.removeEventFromPendingList(event);
throw new DecryptionError(
DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED,
"The sender identity is unverified, but was previously verified.",
);
case RustSdkCryptoJs.DecryptionErrorCode.UnknownSenderDevice:
// We're refusing to decrypt due to not trusting the sender,
// rather than failing to decrypt due to lack of keys, so we
// don't need to keep it on the pending list.
this.removeEventFromPendingList(event);
throw new DecryptionError(
DecryptionFailureCode.UNKNOWN_SENDER_DEVICE,
"The sender device is not known.",
);
case RustSdkCryptoJs.DecryptionErrorCode.UnsignedSenderDevice:
// We're refusing to decrypt due to not trusting the sender,
// rather than failing to decrypt due to lack of keys, so we
// don't need to keep it on the pending list.
this.removeEventFromPendingList(event);
throw new DecryptionError(
DecryptionFailureCode.UNSIGNED_SENDER_DEVICE,
"The sender identity is not cross-signed.",
);
// We don't map MismatchedIdentityKeys for now, as there is no equivalent in legacy.
// Just put it on the `UNKNOWN_ERROR` bucket.
default:
+12 -9
View File
@@ -23,9 +23,12 @@ limitations under the License.
import { TypedEventEmitter } from "./models/typed-event-emitter.ts";
import { ClientEvent, ClientEventHandlerMap } from "./client.ts";
import { MatrixEvent } from "./models/event.ts";
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./crypto/aes.ts";
import { randomString } from "./randomstring.ts";
import { logger } from "./logger.ts";
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
import decryptAESSecretStorageItem from "./utils/decryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "./@types/AESEncryptedSecretStoragePayload.ts";
import { calculateKeyCheck } from "./crypto/aes.ts";
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
@@ -200,13 +203,13 @@ export interface SecretStorageCallbacks {
interface SecretInfo {
encrypted: {
[keyId: string]: IEncryptedPayload;
[keyId: string]: AESEncryptedSecretStoragePayload;
};
}
interface Decryptors {
encrypt: (plaintext: string) => Promise<IEncryptedPayload>;
decrypt: (ciphertext: IEncryptedPayload) => Promise<string>;
encrypt: (plaintext: string) => Promise<AESEncryptedSecretStoragePayload>;
decrypt: (ciphertext: AESEncryptedSecretStoragePayload) => Promise<string>;
}
/**
@@ -491,7 +494,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
* @param keys - The IDs of the keys to use to encrypt the secret, or null/undefined to use the default key.
*/
public async store(name: string, secret: string, keys?: string[] | null): Promise<void> {
const encrypted: Record<string, IEncryptedPayload> = {};
const encrypted: Record<string, AESEncryptedSecretStoragePayload> = {};
if (!keys) {
const defaultKeyId = await this.getDefaultKeyId();
@@ -638,11 +641,11 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const decryption = {
encrypt: function (secret: string): Promise<IEncryptedPayload> {
return encryptAES(secret, privateKey, name);
encrypt: function (secret: string): Promise<AESEncryptedSecretStoragePayload> {
return encryptAESSecretStorageItem(secret, privateKey, name);
},
decrypt: function (encInfo: IEncryptedPayload): Promise<string> {
return decryptAES(encInfo, privateKey, name);
decrypt: function (encInfo: AESEncryptedSecretStoragePayload): Promise<string> {
return decryptAESSecretStorageItem(encInfo, privateKey, name);
},
};
return [keyId, decryption];
+3
View File
@@ -38,6 +38,8 @@ export interface ISetDisplayNameCapability extends ICapability {}
export interface ISetAvatarUrlCapability extends ICapability {}
export interface IProfileFieldsCapability extends ICapability {}
export enum RoomVersionStability {
Stable = "stable",
Unstable = "unstable",
@@ -61,6 +63,7 @@ export interface Capabilities {
"org.matrix.msc3882.get_login_token"?: IGetLoginTokenCapability;
"m.set_displayname"?: ISetDisplayNameCapability;
"m.set_avatar_url"?: ISetAvatarUrlCapability;
"uk.tcpip.msc4133.profile_fields"?: IProfileFieldsCapability;
}
type CapabilitiesResponse = {
+1
View File
@@ -26,6 +26,7 @@ export * from "./@types/membership.ts";
export type * from "./@types/event.ts";
export type * from "./@types/events.ts";
export type * from "./@types/state_events.ts";
export type * from "./@types/AESEncryptedSecretStoragePayload.ts";
/** The different methods for device and user verification */
export enum VerificationMethod {
+54
View File
@@ -0,0 +1,54 @@
/*
* Copyright 2024 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 { decodeBase64 } from "../base64.ts";
import { deriveKeys } from "./internal/deriveKeys.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
/**
* Decrypt an AES-encrypted Secret Storage item.
*
* @param data - the encrypted data, returned by {@link utils/encryptAESSecretStorageItem.default | encryptAESSecretStorageItem}.
* @param key - the encryption key to use as an input to the HKDF function which is used to derive the AES key. Must
* be the same as provided to {@link utils/encryptAESSecretStorageItem.default | encryptAESSecretStorageItem}.
* @param name - the name of the secret. Also used as an input to the HKDF operation which is used to derive the AES
* key, so again must be the same as provided to {@link utils/encryptAESSecretStorageItem.default | encryptAESSecretStorageItem}.
*/
export default async function decryptAESSecretStorageItem(
data: AESEncryptedSecretStoragePayload,
key: Uint8Array,
name: string,
): Promise<string> {
const [aesKey, hmacKey] = await deriveKeys(key, name);
const ciphertext = decodeBase64(data.ciphertext);
if (!(await globalThis.crypto.subtle.verify({ name: "HMAC" }, hmacKey, decodeBase64(data.mac), ciphertext))) {
throw new Error(`Error decrypting secret ${name}: bad MAC`);
}
const plaintext = await globalThis.crypto.subtle.decrypt(
{
name: "AES-CTR",
counter: decodeBase64(data.iv),
length: 64,
},
aesKey,
ciphertext,
);
return new TextDecoder().decode(new Uint8Array(plaintext));
}
+73
View File
@@ -0,0 +1,73 @@
/*
* Copyright 2024 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 { decodeBase64, encodeBase64 } from "../base64.ts";
import { deriveKeys } from "./internal/deriveKeys.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
/**
* Encrypt a string as a secret storage item, using AES-CTR.
*
* @param data - the plaintext to encrypt
* @param key - the encryption key to use as an input to the HKDF function which is used to derive the AES key for
* encryption. Obviously, the same key must be provided when decrypting.
* @param name - the name of the secret. Used as an input to the HKDF operation which is used to derive the AES key,
* so again the same value must be provided when decrypting.
* @param ivStr - the base64-encoded initialization vector to use. If not supplied, a random one will be generated.
*
* @returns The encrypted result, including the ciphertext itself, the initialization vector (as supplied in `ivStr`,
* or generated), and an HMAC on the ciphertext all base64-encoded.
*/
export default async function encryptAESSecretStorageItem(
data: string,
key: Uint8Array,
name: string,
ivStr?: string,
): Promise<AESEncryptedSecretStoragePayload> {
let iv: Uint8Array;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = new Uint8Array(16);
globalThis.crypto.getRandomValues(iv);
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
}
const [aesKey, hmacKey] = await deriveKeys(key, name);
const encodedData = new TextEncoder().encode(data);
const ciphertext = await globalThis.crypto.subtle.encrypt(
{
name: "AES-CTR",
counter: iv,
length: 64,
},
aesKey,
encodedData,
);
const hmac = await globalThis.crypto.subtle.sign({ name: "HMAC" }, hmacKey, ciphertext);
return {
iv: encodeBase64(iv),
ciphertext: encodeBase64(ciphertext),
mac: encodeBase64(hmac),
};
}
+63
View File
@@ -0,0 +1,63 @@
/*
* Copyright 2024 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.
*/
// salt for HKDF, with 8 bytes of zeros
const zeroSalt = new Uint8Array(8);
/**
* Derive AES and HMAC keys from a master key.
*
* This is used for deriving secret storage keys: see https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2 (step 1).
*
* @param key
* @param name
*/
export async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
const hkdfkey = await globalThis.crypto.subtle.importKey("raw", key, { name: "HKDF" }, false, ["deriveBits"]);
const keybits = await globalThis.crypto.subtle.deriveBits(
{
name: "HKDF",
salt: zeroSalt,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
info: new TextEncoder().encode(name),
hash: "SHA-256",
},
hkdfkey,
512,
);
const aesKey = keybits.slice(0, 32);
const hmacKey = keybits.slice(32);
const aesProm = globalThis.crypto.subtle.importKey("raw", aesKey, { name: "AES-CTR" }, false, [
"encrypt",
"decrypt",
]);
const hmacProm = globalThis.crypto.subtle.importKey(
"raw",
hmacKey,
{
name: "HMAC",
hash: { name: "SHA-256" },
},
false,
["sign", "verify"],
);
return Promise.all([aesProm, hmacProm]);
}
+3 -3
View File
@@ -2024,7 +2024,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.sendVoipEvent(EventType.CallNegotiate, {
lifetime: CALL_TIMEOUT_MS,
description: this.peerConn!.localDescription?.toJSON(),
description: this.peerConn!.localDescription?.toJSON() as RTCSessionDescription,
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(true),
});
}
@@ -2152,9 +2152,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// clunky because TypeScript can't follow the types through if we use an expression as the key
if (this.state === CallState.CreateOffer) {
content.offer = this.peerConn!.localDescription?.toJSON();
content.offer = this.peerConn!.localDescription?.toJSON() as RTCSessionDescription;
} else {
content.description = this.peerConn!.localDescription?.toJSON();
content.description = this.peerConn!.localDescription?.toJSON() as RTCSessionDescription;
}
content.capabilities = {
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://typedoc.org/schema.json",
"plugin": ["typedoc-plugin-mdn-links", "typedoc-plugin-missing-exports", "typedoc-plugin-coverage"],
"coverageLabel": "TypeDoc",
"entryPoints": ["src/matrix.ts", "src/types.ts", "src/testing.ts"],
"entryPoints": ["src/matrix.ts", "src/types.ts", "src/testing.ts", "src/utils/*.ts"],
"excludeExternals": true,
"out": "_docs"
}
+576 -281
View File
File diff suppressed because it is too large Load Diff