Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c1b4f97372 | |||
| 5f3b89990d | |||
| 866fd6f4a3 | |||
| 9ecb66e695 | |||
| baa6d13506 | |||
| 2d6230f199 | |||
| 825d85f18d | |||
| d56fa197d0 | |||
| f7229bfff0 | |||
| 538717c23e | |||
| 1a8ea3d685 | |||
| d0890d9450 | |||
| 42ec17b977 | |||
| 82e9eefce6 | |||
| f8208b1891 | |||
| 092a59af66 | |||
| dbd7d26968 | |||
| 4fda9e8419 | |||
| a13e0389db | |||
| dbb4828eda | |||
| 414ac9d8cc | |||
| 30058a4bdc | |||
| fab9cab3df | |||
| 53b599f8fe | |||
| 17f6cc733e | |||
| c8403f39aa | |||
| 8cf5df73ee | |||
| febe27ddcc | |||
| 7987ce76ec | |||
| 60cedf2fdb | |||
| ed44514974 | |||
| 9f8c1ee953 | |||
| e8128d34a1 | |||
| ba7bd06295 | |||
| e4db6008b8 | |||
| 52f35409ec | |||
| f50aab37c3 | |||
| df0f817f83 | |||
| 7efa6352f8 | |||
| f74614705e | |||
| 169e8f8613 | |||
| f2f77bd1f7 |
+9
-1
@@ -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' }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface EncryptionKeysEventContent {
|
||||
keys: EncryptionKeyEntry[];
|
||||
device_id: string;
|
||||
call_id: string;
|
||||
sent_ts?: number;
|
||||
}
|
||||
|
||||
export type CallNotifyType = "ring" | "notify";
|
||||
|
||||
@@ -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
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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];
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user