Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00f63db80f | |||
| 9bcb83a20a | |||
| dd8d8e5410 | |||
| 32b8ff8116 | |||
| 3ceadd512d | |||
| 8182180550 | |||
| aed74c5a72 | |||
| 8c259c53a6 | |||
| 4d59291538 | |||
| 80009a1b31 | |||
| 93e6c95953 | |||
| 27a5507cef | |||
| be06f6655e | |||
| 71152f33bf | |||
| bc8f67089c | |||
| e76f627fe3 | |||
| 45b1e73842 | |||
| f3eefd2f32 | |||
| f7c053216b | |||
| 9c7739f14f | |||
| 897afe153a | |||
| a929391dcd | |||
| 45c5ee9f65 | |||
| e56aaa16c7 | |||
| da0d3d791e | |||
| ed5eb670a1 | |||
| b7fcb6e4c1 |
@@ -22,7 +22,7 @@ runs:
|
||||
|
||||
- name: Upload tarball signature
|
||||
if: ${{ inputs.upload-url }}
|
||||
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # 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@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}.asc
|
||||
|
||||
- name: Upload assets
|
||||
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
deployments: write
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5
|
||||
- uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
|
||||
if: github.event_name != 'merge_group'
|
||||
with:
|
||||
labels: |
|
||||
|
||||
@@ -22,8 +22,8 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: "pnpm"
|
||||
|
||||
@@ -38,8 +38,8 @@ jobs:
|
||||
sparse-checkout: |
|
||||
scripts/release
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
@@ -130,8 +130,8 @@ jobs:
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: ${{ inputs.dir }}/package.json
|
||||
|
||||
@@ -27,9 +27,9 @@ jobs:
|
||||
ref: staging
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- name: 🔧 pnpm cache
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
@@ -52,8 +52,8 @@ jobs:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version: "lts/*"
|
||||
@@ -81,9 +81,9 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- name: 🔧 pnpm cache
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
@@ -113,4 +113,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
|
||||
|
||||
@@ -13,6 +13,10 @@ on:
|
||||
type: boolean
|
||||
required: false
|
||||
description: "Whether to combine multiple LCOV and sonar-report files in coverage artifact"
|
||||
version-pkg-json-dir:
|
||||
type: string
|
||||
default: "."
|
||||
description: "Relative path of the directory containing package.json with the `version` to use."
|
||||
permissions: {}
|
||||
jobs:
|
||||
sonarqube:
|
||||
@@ -44,7 +48,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
if: ${{ !inputs.sharded }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -52,7 +56,7 @@ jobs:
|
||||
name: coverage
|
||||
path: coverage
|
||||
- name: 📥 Download sharded artifacts
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
if: inputs.sharded
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -84,7 +88,7 @@ jobs:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
skip_coverage_label: Z-Skip-Coverage
|
||||
version_cmd: "cat package.json | jq -r .version"
|
||||
version_cmd: "cat ${{ inputs.version-pkg-json-dir }}/package.json | jq -r .version"
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
@@ -38,8 +38,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
@@ -58,8 +58,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
@@ -92,8 +92,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
run: "pnpm lint:workflows"
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
|
||||
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
@@ -115,8 +115,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
@@ -143,8 +143,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
@@ -165,8 +165,8 @@ jobs:
|
||||
repository: element-hq/element-web
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version: "lts/*"
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
sync-labels:
|
||||
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@dac99c67f08f8f2a079e885ffb682a2f39cd3960
|
||||
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@7f2f93fb9b52ece7a0998f60e64862aa203c1746
|
||||
with:
|
||||
LABELS: |
|
||||
element-hq/element-meta
|
||||
|
||||
@@ -26,10 +26,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
- name: Setup Node
|
||||
id: setupNode
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version: ${{ matrix.node }}
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
if: github.event_name == 'merge_group'
|
||||
if: false # disabled due to flakiness
|
||||
permissions: read-all # zizmor: ignore[excessive-permissions]
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
|
||||
@@ -6,6 +6,6 @@ on:
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
call-triage-labelled:
|
||||
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@2c9f55cbea90702aa61b9304afa9cbf940efa14f
|
||||
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@6339bcda15c71d209303b18a06a9b1c021220bf9
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "41.2.0",
|
||||
"version": "41.3.0-rc.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
@@ -106,7 +106,7 @@
|
||||
"fetch-mock": "^12.6.0",
|
||||
"happy-dom": "^20.1.0",
|
||||
"husky": "^9.0.0",
|
||||
"knip": "^5.0.0",
|
||||
"knip": "^6.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "3.8.1",
|
||||
@@ -114,12 +114,12 @@
|
||||
"typedoc-plugin-coverage": "^4.0.0",
|
||||
"typedoc-plugin-mdn-links": "^5.0.0",
|
||||
"typedoc-plugin-missing-exports": "^4.0.0",
|
||||
"typescript": "^5.4.2",
|
||||
"typescript": "^6.0.0",
|
||||
"vitest": "^4.0.17",
|
||||
"vitest-sonar-reporter": "^3.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"expect": "30.2.0"
|
||||
"expect": "30.3.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
@@ -131,5 +131,5 @@
|
||||
"eslint": "8"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017"
|
||||
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
|
||||
}
|
||||
|
||||
Generated
+813
-327
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,78 @@ import {
|
||||
|
||||
const debug = mkDebug("matrix-js-sdk:history-sharing");
|
||||
|
||||
interface TestClient {
|
||||
client: MatrixClient;
|
||||
userId: string;
|
||||
homeserverUrl: string;
|
||||
keyResponder: E2EKeyResponder;
|
||||
keyReceiver: E2EKeyReceiver;
|
||||
keyClaimResponder: E2EOTKClaimResponder;
|
||||
syncResponder: SyncResponder;
|
||||
}
|
||||
|
||||
// Add to this array to allow for more testing clients.
|
||||
const TEST_USER_IDS = [TEST_USER_ID, "@bob:xyz", "@charlie:zyx"];
|
||||
let activeClients: TestClient[] = [];
|
||||
|
||||
/**
|
||||
* Sets up a number of test clients.
|
||||
* @param n - The total number of clients.
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
async function setupClients(n: number, options = { setupNewCrossSigning: true }): Promise<TestClient[]> {
|
||||
if (n > TEST_USER_IDS.length) {
|
||||
throw new Error("Requested more clients than configured - add to TEST_USER_IDS");
|
||||
}
|
||||
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
const clients = Array.from({ length: n }).map((_, i) => {
|
||||
const userId = TEST_USER_IDS[i];
|
||||
const routePrefix = `${userId.split(":")[0].slice(1)}-`; // e.g. @alice:example.com -> alice-
|
||||
const homeserverUrl = `https://${routePrefix}server.com`; // e.g. @alice:example.com -> https://alice-homeserver.com
|
||||
|
||||
return {
|
||||
client: createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: userId,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "xzcvb",
|
||||
logger: new DebugLogger(mkDebug(`matrix-js-sdk:${userId}`)),
|
||||
}),
|
||||
userId,
|
||||
homeserverUrl,
|
||||
keyReceiver: new E2EKeyReceiver(homeserverUrl, routePrefix),
|
||||
keyResponder: new E2EKeyResponder(homeserverUrl),
|
||||
keyClaimResponder: new E2EOTKClaimResponder(homeserverUrl),
|
||||
syncResponder: new SyncResponder(homeserverUrl),
|
||||
};
|
||||
});
|
||||
|
||||
// Add all combinations of key receivers to key (claim) responders.
|
||||
for (const { keyResponder: lhsKeyResponder, keyClaimResponder: lhsKeyClaimResponder } of clients) {
|
||||
for (const { userId: lhsUserId, keyReceiver: rhsKeyReceiver, client: lhsClient } of clients) {
|
||||
lhsKeyResponder.addKeyReceiver(lhsUserId, rhsKeyReceiver);
|
||||
lhsKeyClaimResponder.addKeyReceiver(lhsUserId, lhsClient.deviceId!, rhsKeyReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
// Start all the clients.
|
||||
for (const { userId, homeserverUrl, client, syncResponder } of clients) {
|
||||
mockInitialApiRequests(homeserverUrl, userId);
|
||||
await client.initRustCrypto({ cryptoDatabasePrefix: userId });
|
||||
await client.startClient();
|
||||
await client.getCrypto()!.bootstrapCrossSigning({ setupNewCrossSigning: options.setupNewCrossSigning });
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
await syncPromise(client);
|
||||
}
|
||||
|
||||
activeClients = clients;
|
||||
return activeClients;
|
||||
}
|
||||
|
||||
// load the rust library. This can take a few seconds on a slow GH worker.
|
||||
beforeAll(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
@@ -58,16 +130,19 @@ beforeAll(async () => {
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
}, 10000);
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
|
||||
// Stop and clear the active clients
|
||||
activeClients.forEach(({ client }) => client.stopClient());
|
||||
await flushPromises();
|
||||
activeClients = [];
|
||||
});
|
||||
|
||||
const ROOM_ID = "!room:example.com";
|
||||
const ALICE_HOMESERVER_URL = "https://alice-server.com";
|
||||
const BOB_HOMESERVER_URL = "https://bob-server.com";
|
||||
|
||||
async function createAndInitClient(homeserverUrl: string, userId: string, setupNewCrossSigning = true) {
|
||||
mockInitialApiRequests(homeserverUrl, userId);
|
||||
@@ -87,60 +162,24 @@ async function createAndInitClient(homeserverUrl: string, userId: string, setupN
|
||||
}
|
||||
|
||||
describe("History Sharing", () => {
|
||||
let aliceClient: MatrixClient;
|
||||
let aliceSyncResponder: SyncResponder;
|
||||
let bobClient: MatrixClient;
|
||||
let bobSyncResponder: SyncResponder;
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
const aliceId = TEST_USER_ID;
|
||||
const bobId = "@bob:xyz";
|
||||
|
||||
const aliceKeyReceiver = new E2EKeyReceiver(ALICE_HOMESERVER_URL, "alice-");
|
||||
const aliceKeyResponder = new E2EKeyResponder(ALICE_HOMESERVER_URL);
|
||||
const aliceKeyClaimResponder = new E2EOTKClaimResponder(ALICE_HOMESERVER_URL);
|
||||
aliceSyncResponder = new SyncResponder(ALICE_HOMESERVER_URL);
|
||||
|
||||
const bobKeyReceiver = new E2EKeyReceiver(BOB_HOMESERVER_URL, "bob-");
|
||||
const bobKeyResponder = new E2EKeyResponder(BOB_HOMESERVER_URL);
|
||||
bobSyncResponder = new SyncResponder(BOB_HOMESERVER_URL);
|
||||
|
||||
aliceKeyResponder.addKeyReceiver(aliceId, aliceKeyReceiver);
|
||||
aliceKeyResponder.addKeyReceiver(bobId, bobKeyReceiver);
|
||||
bobKeyResponder.addKeyReceiver(aliceId, aliceKeyReceiver);
|
||||
bobKeyResponder.addKeyReceiver(bobId, bobKeyReceiver);
|
||||
|
||||
aliceClient = await createAndInitClient(ALICE_HOMESERVER_URL, aliceId);
|
||||
bobClient = await createAndInitClient(BOB_HOMESERVER_URL, bobId);
|
||||
|
||||
aliceKeyClaimResponder.addKeyReceiver(bobId, bobClient.deviceId!, bobKeyReceiver);
|
||||
|
||||
aliceSyncResponder.sendOrQueueSyncResponse({});
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
bobSyncResponder.sendOrQueueSyncResponse({});
|
||||
await syncPromise(bobClient);
|
||||
});
|
||||
|
||||
test("Room keys are successfully shared on invite", async () => {
|
||||
const [alice, bob] = await setupClients(2);
|
||||
const { homeserverUrl: aliceHomeserverUrl, client: aliceClient, syncResponder: aliceSyncResponder } = alice;
|
||||
const { client: bobClient, syncResponder: bobSyncResponder } = bob;
|
||||
|
||||
// Alice is in an encrypted room
|
||||
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], HistoryVisibility.Shared, ROOM_ID);
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and she sends an event
|
||||
const msgProm = expectSendRoomEvent(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
const msgProm = expectSendRoomEvent(aliceHomeserverUrl, "m.room.encrypted");
|
||||
await aliceClient.sendEvent(ROOM_ID, EventType.RoomMessage, { msgtype: MsgType.Text, body: "Hi!" });
|
||||
const sentMessage = await msgProm;
|
||||
debug(`Alice sent encrypted room event: ${JSON.stringify(sentMessage)}`);
|
||||
|
||||
// Alice invites Bob, and shares the room history with them.
|
||||
await assertInviteAndShareHistory(ROOM_ID);
|
||||
await assertInviteAndShareHistory(alice, bob, ROOM_ID);
|
||||
|
||||
// Bob receives, should be able to decrypt, the megolm message
|
||||
const event = await bobReceivesEvent(
|
||||
@@ -166,22 +205,26 @@ describe("History Sharing", () => {
|
||||
});
|
||||
|
||||
test("Room keys are imported correctly if invite is accepted before the bundle arrives", async () => {
|
||||
const [alice, bob] = await setupClients(2);
|
||||
const { homeserverUrl: aliceHomeserverUrl, client: aliceClient, syncResponder: aliceSyncResponder } = alice;
|
||||
const { homeserverUrl: bobHomeserverUrl, client: bobClient, syncResponder: bobSyncResponder } = bob;
|
||||
|
||||
// Alice is in an encrypted room
|
||||
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], HistoryVisibility.Shared, ROOM_ID);
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and she sends an event
|
||||
const msgProm = expectSendRoomEvent(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
const msgProm = expectSendRoomEvent(aliceHomeserverUrl, "m.room.encrypted");
|
||||
await aliceClient.sendEvent(ROOM_ID, EventType.RoomMessage, { msgtype: MsgType.Text, body: "Hello!" });
|
||||
const sentMessage = await msgProm;
|
||||
debug(`Alice sent encrypted room event: ${JSON.stringify(sentMessage)}`);
|
||||
|
||||
// Now, Alice invites Bob
|
||||
const uploadProm = expectUploadRequest();
|
||||
const toDeviceMessageProm = expectSendToDeviceMessage(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
const uploadProm = expectUploadRequest(alice);
|
||||
const toDeviceMessageProm = expectSendToDeviceMessage(aliceHomeserverUrl, "m.room.encrypted");
|
||||
// POST https://alice-server.com/_matrix/client/v3/rooms/!room%3Aexample.com/invite
|
||||
fetchMock.postOnce(`${ALICE_HOMESERVER_URL}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
|
||||
fetchMock.postOnce(`${aliceHomeserverUrl}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
|
||||
await aliceClient.invite(ROOM_ID, bobClient.getSafeUserId(), { shareEncryptedHistory: true });
|
||||
|
||||
const uploadedBlob = await uploadProm;
|
||||
@@ -201,7 +244,7 @@ describe("History Sharing", () => {
|
||||
const room = bobClient.getRoom(ROOM_ID);
|
||||
expect(room).toBeTruthy();
|
||||
expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
fetchMock.postOnce(`${bobHomeserverUrl}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
await bobClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
|
||||
@@ -223,7 +266,7 @@ describe("History Sharing", () => {
|
||||
expect(event.isDecryptionFailure()).toBeTruthy();
|
||||
|
||||
// Now the room key bundle message arrives
|
||||
fetchMock.getOnce(`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
fetchMock.getOnce(`begin:${bobHomeserverUrl}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
body: uploadedBlob,
|
||||
});
|
||||
bobSyncResponder.sendOrQueueSyncResponse({
|
||||
@@ -255,22 +298,26 @@ describe("History Sharing", () => {
|
||||
test("Room keys are not imported if the bundle arrives more than 24H after the invite is accepted", async () => {
|
||||
vitest.useFakeTimers();
|
||||
|
||||
const [alice, bob] = await setupClients(2);
|
||||
const { homeserverUrl: aliceHomeserverUrl, client: aliceClient, syncResponder: aliceSyncResponder } = alice;
|
||||
const { homeserverUrl: bobHomeserverUrl, client: bobClient, syncResponder: bobSyncResponder } = bob;
|
||||
|
||||
// Alice is in an encrypted room
|
||||
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], HistoryVisibility.Shared, ROOM_ID);
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and she sends an event
|
||||
const msgProm = expectSendRoomEvent(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
const msgProm = expectSendRoomEvent(aliceHomeserverUrl, "m.room.encrypted");
|
||||
await aliceClient.sendEvent(ROOM_ID, EventType.RoomMessage, { msgtype: MsgType.Text, body: "Hello!" });
|
||||
const sentMessage = await msgProm;
|
||||
debug(`Alice sent encrypted room event: ${JSON.stringify(sentMessage)}`);
|
||||
|
||||
// Now, Alice invites Bob
|
||||
const uploadProm = expectUploadRequest();
|
||||
const toDeviceMessageProm = expectSendToDeviceMessage(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
const uploadProm = expectUploadRequest(alice);
|
||||
const toDeviceMessageProm = expectSendToDeviceMessage(aliceHomeserverUrl, "m.room.encrypted");
|
||||
// POST https://alice-server.com/_matrix/client/v3/rooms/!room%3Aexample.com/invite
|
||||
fetchMock.postOnce(`${ALICE_HOMESERVER_URL}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
|
||||
fetchMock.postOnce(`${aliceHomeserverUrl}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
|
||||
await aliceClient.invite(ROOM_ID, bobClient.getSafeUserId(), { shareEncryptedHistory: true });
|
||||
|
||||
const uploadedBlob = await uploadProm;
|
||||
@@ -290,7 +337,7 @@ describe("History Sharing", () => {
|
||||
const room = bobClient.getRoom(ROOM_ID);
|
||||
expect(room).toBeTruthy();
|
||||
expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
fetchMock.postOnce(`${bobHomeserverUrl}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
await bobClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
|
||||
@@ -314,7 +361,7 @@ describe("History Sharing", () => {
|
||||
// 24 hours elapse before the room key bundle message arrives.
|
||||
vitest.advanceTimersByTime(24 * 60 * 60 * 1000 + 1);
|
||||
|
||||
fetchMock.getOnce(`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
fetchMock.getOnce(`begin:${bobHomeserverUrl}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
body: uploadedBlob,
|
||||
});
|
||||
bobSyncResponder.sendOrQueueSyncResponse({
|
||||
@@ -339,22 +386,26 @@ describe("History Sharing", () => {
|
||||
});
|
||||
|
||||
test("Room keys are not imported if we left and rejoined the room after accepting the invite", async () => {
|
||||
const [alice, bob] = await setupClients(2);
|
||||
const { homeserverUrl: aliceHomeserverUrl, client: aliceClient, syncResponder: aliceSyncResponder } = alice;
|
||||
const { homeserverUrl: bobHomeserverUrl, client: bobClient, syncResponder: bobSyncResponder } = bob;
|
||||
|
||||
// Alice is in an encrypted room
|
||||
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], HistoryVisibility.Shared, ROOM_ID);
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and she sends an event
|
||||
const msgProm = expectSendRoomEvent(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
const msgProm = expectSendRoomEvent(aliceHomeserverUrl, "m.room.encrypted");
|
||||
await aliceClient.sendEvent(ROOM_ID, EventType.RoomMessage, { msgtype: MsgType.Text, body: "Hello!" });
|
||||
const sentMessage = await msgProm;
|
||||
debug(`Alice sent encrypted room event: ${JSON.stringify(sentMessage)}`);
|
||||
|
||||
// Now, Alice invites Bob
|
||||
const uploadProm = expectUploadRequest();
|
||||
const toDeviceMessageProm = expectSendToDeviceMessage(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
const uploadProm = expectUploadRequest(alice);
|
||||
const toDeviceMessageProm = expectSendToDeviceMessage(aliceHomeserverUrl, "m.room.encrypted");
|
||||
// POST https://alice-server.com/_matrix/client/v3/rooms/!room%3Aexample.com/invite
|
||||
fetchMock.postOnce(`${ALICE_HOMESERVER_URL}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
|
||||
fetchMock.postOnce(`${aliceHomeserverUrl}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
|
||||
await aliceClient.invite(ROOM_ID, bobClient.getSafeUserId(), { shareEncryptedHistory: true });
|
||||
|
||||
const uploadedBlob = await uploadProm;
|
||||
@@ -374,7 +425,7 @@ describe("History Sharing", () => {
|
||||
const room = bobClient.getRoom(ROOM_ID);
|
||||
expect(room).toBeTruthy();
|
||||
expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
fetchMock.post(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
fetchMock.post(`${bobHomeserverUrl}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
await bobClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
|
||||
@@ -430,7 +481,7 @@ describe("History Sharing", () => {
|
||||
await bobClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
|
||||
|
||||
// Now the bundle arrives
|
||||
fetchMock.getOnce(`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
fetchMock.getOnce(`begin:${bobHomeserverUrl}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
body: uploadedBlob,
|
||||
});
|
||||
bobSyncResponder.sendOrQueueSyncResponse({
|
||||
@@ -453,6 +504,10 @@ describe("History Sharing", () => {
|
||||
});
|
||||
|
||||
test("Room keys are downloaded from key backup before inviting", async () => {
|
||||
const [alice, bob] = await setupClients(2);
|
||||
const { client: aliceClient, syncResponder: aliceSyncResponder } = alice;
|
||||
const { client: bobClient, syncResponder: bobSyncResponder } = bob;
|
||||
|
||||
// Set up backup, and ignore requests to send room key requests
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", SIGNED_BACKUP_DATA);
|
||||
fetchMock.get(
|
||||
@@ -475,7 +530,7 @@ describe("History Sharing", () => {
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Alice invites Bob, and shares the room history with them.
|
||||
await assertInviteAndShareHistory(TEST_ROOM_ID);
|
||||
await assertInviteAndShareHistory(alice, bob, TEST_ROOM_ID);
|
||||
|
||||
// Bob receives, and should be able to decrypt, the historical message
|
||||
const event = await bobReceivesEvent(aliceClient, bobClient, ENCRYPTED_EVENT as any, bobSyncResponder);
|
||||
@@ -489,21 +544,25 @@ describe("History Sharing", () => {
|
||||
});
|
||||
|
||||
test("Room keys are successfully imported, if the app is shut down while the import is in progress", async () => {
|
||||
const [alice, bob] = await setupClients(2);
|
||||
const { homeserverUrl: aliceHomeserverUrl, client: aliceClient, syncResponder: aliceSyncResponder } = alice;
|
||||
const { homeserverUrl: bobHomeserverUrl, client: bobClient, syncResponder: bobSyncResponder } = bob;
|
||||
|
||||
// Alice is in an encrypted room
|
||||
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], HistoryVisibility.Shared, ROOM_ID);
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and she sends an event
|
||||
const msgProm = expectSendRoomEvent(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
const msgProm = expectSendRoomEvent(aliceHomeserverUrl, "m.room.encrypted");
|
||||
await aliceClient.sendEvent(ROOM_ID, EventType.RoomMessage, { msgtype: MsgType.Text, body: "Hi!" });
|
||||
const sentMessage = await msgProm;
|
||||
debug(`Alice sent encrypted room event: ${JSON.stringify(sentMessage)}`);
|
||||
|
||||
// Alice invites Bob, and shares the room history with him.
|
||||
const uploadProm = expectUploadRequest();
|
||||
const toDeviceMessageProm = expectSendToDeviceMessage(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
fetchMock.postOnce(`${ALICE_HOMESERVER_URL}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
|
||||
const uploadProm = expectUploadRequest(alice);
|
||||
const toDeviceMessageProm = expectSendToDeviceMessage(aliceHomeserverUrl, "m.room.encrypted");
|
||||
fetchMock.postOnce(`${aliceHomeserverUrl}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
|
||||
await aliceClient.invite(ROOM_ID, bobClient.getSafeUserId(), { shareEncryptedHistory: true });
|
||||
const uploadedBlob = await uploadProm;
|
||||
const sentToDeviceRequest = await toDeviceMessageProm;
|
||||
@@ -530,13 +589,13 @@ describe("History Sharing", () => {
|
||||
expect(room).toBeTruthy();
|
||||
expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
|
||||
fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
fetchMock.postOnce(`${bobHomeserverUrl}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// Have the /download request block indefinitely
|
||||
const downloadStarted = Promise.withResolvers<void>();
|
||||
fetchMock.getOnce(`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`, () => {
|
||||
fetchMock.getOnce(`begin:${bobHomeserverUrl}/_matrix/client/v1/media/download/alice-server/here`, () => {
|
||||
downloadStarted.resolve();
|
||||
return new Promise(() => {});
|
||||
});
|
||||
@@ -549,15 +608,15 @@ describe("History Sharing", () => {
|
||||
bobSyncResponder.sendOrQueueSyncResponse({});
|
||||
await flushPromises();
|
||||
|
||||
fetchMock.getOnce(`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
fetchMock.getOnce(`begin:${bobHomeserverUrl}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
body: uploadedBlob,
|
||||
});
|
||||
bobClient = await createAndInitClient(BOB_HOMESERVER_URL, bobClient.getSafeUserId(), false);
|
||||
const bobNewClient = await createAndInitClient(bobHomeserverUrl, bobClient.getSafeUserId(), false);
|
||||
|
||||
// Now, Bob receives the megolm message, and can decrypt it
|
||||
const event = await bobReceivesEvent(
|
||||
aliceClient,
|
||||
bobClient,
|
||||
bobNewClient,
|
||||
mkEventCustom({
|
||||
type: "m.room.encrypted",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
@@ -567,24 +626,31 @@ describe("History Sharing", () => {
|
||||
}),
|
||||
bobSyncResponder,
|
||||
);
|
||||
expect(event.getId()).toEqual("$event_id");
|
||||
await event.getDecryptionPromise();
|
||||
expect(event.getId()).toEqual("$event_id");
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().body).toEqual("Hi!");
|
||||
expect(event.getKeyForwardingUser()).toEqual(aliceClient.getUserId());
|
||||
const encryptionInfo = await bobClient.getCrypto()!.getEncryptionInfoForEvent(event);
|
||||
const encryptionInfo = await bobNewClient.getCrypto()!.getEncryptionInfoForEvent(event);
|
||||
expect(encryptionInfo?.shieldColour).toEqual(EventShieldColour.GREY);
|
||||
expect(encryptionInfo?.shieldReason).toEqual(EventShieldReason.AUTHENTICITY_NOT_GUARANTEED);
|
||||
|
||||
// We need to stop Bob's new client manually, since it isn't tracked by `setupClients`.
|
||||
bobNewClient.stopClient();
|
||||
});
|
||||
|
||||
test("Room keys are not shared if the current history visibility is unshared", async () => {
|
||||
const [alice, bob] = await setupClients(2);
|
||||
const { homeserverUrl: aliceHomeserverUrl, client: aliceClient, syncResponder: aliceSyncResponder } = alice;
|
||||
const { homeserverUrl: bobHomeserverUrl, client: bobClient, syncResponder: bobSyncResponder } = bob;
|
||||
|
||||
// Alice is in an encrypted room
|
||||
let syncResponse = getSyncResponse([aliceClient.getSafeUserId()], HistoryVisibility.Shared, ROOM_ID);
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and she sends an event
|
||||
let msgProm = expectSendRoomEvent(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
let msgProm = expectSendRoomEvent(aliceHomeserverUrl, "m.room.encrypted");
|
||||
await aliceClient.sendEvent(ROOM_ID, EventType.RoomMessage, {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Sent when shared",
|
||||
@@ -598,7 +664,7 @@ describe("History Sharing", () => {
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
/// ... and sends a second message.
|
||||
msgProm = expectSendRoomEvent(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
msgProm = expectSendRoomEvent(aliceHomeserverUrl, "m.room.encrypted");
|
||||
await aliceClient.sendEvent(ROOM_ID, EventType.RoomMessage, {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Sent when invited",
|
||||
@@ -606,7 +672,7 @@ describe("History Sharing", () => {
|
||||
const secondMessage = await msgProm;
|
||||
debug(`Alice sent encrypted room event: ${JSON.stringify(secondMessage)}`);
|
||||
|
||||
fetchMock.postOnce(`${ALICE_HOMESERVER_URL}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
|
||||
fetchMock.postOnce(`${aliceHomeserverUrl}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
|
||||
await aliceClient.invite(ROOM_ID, bobClient.getSafeUserId(), { shareEncryptedHistory: true });
|
||||
|
||||
const inviteEvent = mkEventCustom({
|
||||
@@ -627,7 +693,7 @@ describe("History Sharing", () => {
|
||||
expect(room).toBeTruthy();
|
||||
expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
|
||||
fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
fetchMock.postOnce(`${bobHomeserverUrl}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
@@ -658,85 +724,393 @@ describe("History Sharing", () => {
|
||||
|
||||
// Assert alice never uploaded the key bundle ...
|
||||
expect(
|
||||
fetchMock.callHistory.called(new URL("/_matrix/media/v3/upload", ALICE_HOMESERVER_URL).toString()),
|
||||
fetchMock.callHistory.called(new URL("/_matrix/media/v3/upload", aliceHomeserverUrl).toString()),
|
||||
).toBeFalsy();
|
||||
// ... didn't send Bob the key bundle info ...
|
||||
expect(
|
||||
fetchMock.callHistory.called(
|
||||
new RegExp(
|
||||
`^${escapeRegExp(ALICE_HOMESERVER_URL)}/_matrix/client/v3/sendToDevice/${escapeRegExp("m.room.encrypted")}/`,
|
||||
`^${escapeRegExp(aliceHomeserverUrl)}/_matrix/client/v3/sendToDevice/${escapeRegExp("m.room.encrypted")}/`,
|
||||
),
|
||||
),
|
||||
).toBeFalsy();
|
||||
// ... and Bob didn't try to download the key bundle.
|
||||
expect(
|
||||
fetchMock.callHistory.called(
|
||||
`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`,
|
||||
`begin:${bobHomeserverUrl}/_matrix/client/v1/media/download/alice-server/here`,
|
||||
),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vitest.useRealTimers();
|
||||
bobClient.stopClient();
|
||||
aliceClient.stopClient();
|
||||
await flushPromises();
|
||||
});
|
||||
test.each([false, true])(
|
||||
"Room key is rotated after a member joins and leaves the room (gappy sync = %s)",
|
||||
async (gappySync) => {
|
||||
const [alice, bob, charlie] = await setupClients(3);
|
||||
const { homeserverUrl: aliceHomeserverUrl, client: aliceClient, syncResponder: aliceSyncResponder } = alice;
|
||||
const { homeserverUrl: bobHomeserverUrl, client: bobClient, syncResponder: bobSyncResponder } = bob;
|
||||
const {
|
||||
homeserverUrl: charlieHomeserverUrl,
|
||||
client: charlieClient,
|
||||
syncResponder: charlieSyncResponder,
|
||||
} = charlie;
|
||||
|
||||
/**
|
||||
* Helper function to automatically test that room history is shared on invite.
|
||||
* The function performs the following:
|
||||
*
|
||||
* 1. Sets up the relevant fetchMock and to-device event listeners for Alice.
|
||||
* 2. Alice invites Bob to the room.
|
||||
* 3. Checks the key bundle was uploaded and that the `m.room_key_bundle`
|
||||
* to-device message was sent.
|
||||
* 4. Sends the invite event to Bob and ensures it is processed correctly.
|
||||
* 5. Sets up the relevant fetchMock listeners for Bob.
|
||||
* 5. Simulates Bob joining the room and verifies that the room history is shared.
|
||||
*
|
||||
* @param roomId The ID of the room where the invite and history sharing will be tested.
|
||||
*/
|
||||
async function assertInviteAndShareHistory(roomId: string): Promise<void> {
|
||||
const uploadProm = expectUploadRequest();
|
||||
const toDeviceMessageProm = expectSendToDeviceMessage(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
fetchMock.postOnce(`${ALICE_HOMESERVER_URL}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/invite`, {});
|
||||
await aliceClient.invite(roomId, bobClient.getSafeUserId(), { shareEncryptedHistory: true });
|
||||
const uploadedBlob = await uploadProm;
|
||||
const sentToDeviceRequest = await toDeviceMessageProm;
|
||||
debug(`Alice sent encrypted to-device events: ${JSON.stringify(sentToDeviceRequest)}`);
|
||||
const bobToDeviceMessage = sentToDeviceRequest[bobClient.getSafeUserId()][bobClient.deviceId!];
|
||||
expect(bobToDeviceMessage).toBeDefined();
|
||||
// Alice and Bob are in an encrypted room
|
||||
let syncResponse = getSyncResponse(
|
||||
[aliceClient.getSafeUserId(), bobClient.getSafeUserId()],
|
||||
HistoryVisibility.Shared,
|
||||
ROOM_ID,
|
||||
);
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
bobSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
|
||||
const inviteEvent = mkInviteEvent(aliceClient, bobClient);
|
||||
bobSyncResponder.sendOrQueueSyncResponse({
|
||||
rooms: { invite: { [roomId]: { invite_state: { events: [inviteEvent] } } } },
|
||||
to_device: {
|
||||
await syncPromise(aliceClient);
|
||||
await syncPromise(bobClient);
|
||||
|
||||
// Bob sends a message M1, which both he and Alice receive.
|
||||
let msgProm = expectSendRoomEvent(bobHomeserverUrl, "m.room.encrypted");
|
||||
let toDeviceMessageProm = expectSendToDeviceMessage(bobHomeserverUrl, "m.room.encrypted");
|
||||
await bobClient.sendEvent(ROOM_ID, EventType.RoomMessage, {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Charlie should be able to read",
|
||||
});
|
||||
const bobEventM1Content = await msgProm;
|
||||
let sentToDeviceRequest = await toDeviceMessageProm;
|
||||
expect(sentToDeviceRequest).toBeDefined();
|
||||
let aliceToDeviceMessage = sentToDeviceRequest[aliceClient.getSafeUserId()][aliceClient.deviceId!];
|
||||
|
||||
// Alice receives the message down sync.
|
||||
syncResponse = getSyncResponse(
|
||||
[aliceClient.getSafeUserId(), bobClient.getSafeUserId()],
|
||||
HistoryVisibility.Shared,
|
||||
ROOM_ID,
|
||||
);
|
||||
syncResponse.rooms.join[ROOM_ID].timeline.events.push(
|
||||
mkEventCustom({
|
||||
type: "m.room.encrypted",
|
||||
sender: bobClient.getSafeUserId(),
|
||||
content: bobEventM1Content,
|
||||
event_id: "$event_id_m1",
|
||||
}) as any,
|
||||
);
|
||||
syncResponse.to_device = {
|
||||
events: [
|
||||
{
|
||||
type: "m.room.encrypted",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
content: bobToDeviceMessage,
|
||||
sender: bobClient.getSafeUserId(),
|
||||
content: aliceToDeviceMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await syncPromise(bobClient);
|
||||
};
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const room = bobClient.getRoom(roomId);
|
||||
expect(room).toBeTruthy();
|
||||
expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
// Alice checks she can read M1.
|
||||
const aliceRoom = aliceClient.getRoom(ROOM_ID);
|
||||
const aliceM1 = aliceRoom!.getLastLiveEvent()!;
|
||||
await aliceM1.getDecryptionPromise();
|
||||
expect(aliceM1.getType()).toEqual("m.room.message");
|
||||
expect(aliceM1.getContent().body).toEqual("Charlie should be able to read");
|
||||
|
||||
fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(roomId)}`, {
|
||||
room_id: roomId,
|
||||
});
|
||||
fetchMock.getOnce(`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
body: uploadedBlob,
|
||||
});
|
||||
await bobClient.joinRoom(roomId, { acceptSharedHistory: true });
|
||||
}
|
||||
// Alice invites and sends a key bundle to Charlie
|
||||
const uploadProm = expectUploadRequest(alice);
|
||||
toDeviceMessageProm = expectSendToDeviceMessage(aliceHomeserverUrl, "m.room.encrypted");
|
||||
fetchMock.postOnce(
|
||||
`${aliceHomeserverUrl}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`,
|
||||
{},
|
||||
);
|
||||
await aliceClient.invite(ROOM_ID, charlieClient.getSafeUserId(), { shareEncryptedHistory: true });
|
||||
const uploadedBlob = await uploadProm;
|
||||
sentToDeviceRequest = await toDeviceMessageProm;
|
||||
debug(`Alice sent encrypted to-device events: ${JSON.stringify(sentToDeviceRequest)}`);
|
||||
const charlieToDeviceMessage = sentToDeviceRequest[charlieClient.getSafeUserId()][charlieClient.deviceId!];
|
||||
expect(charlieToDeviceMessage).toBeDefined();
|
||||
|
||||
/// Charlie receives the invite ...
|
||||
const inviteEvent = mkInviteEvent(aliceClient, charlieClient);
|
||||
charlieSyncResponder.sendOrQueueSyncResponse({
|
||||
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [inviteEvent] } } } },
|
||||
to_device: {
|
||||
events: [
|
||||
{
|
||||
type: "m.room.encrypted",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
content: charlieToDeviceMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await syncPromise(charlieClient);
|
||||
|
||||
const charlieRoom = charlieClient.getRoom(ROOM_ID);
|
||||
expect(charlieRoom).toBeTruthy();
|
||||
expect(charlieRoom?.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
|
||||
// ... and subsequently joins.
|
||||
fetchMock.postOnce(`${charlieHomeserverUrl}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
fetchMock.getOnce(`begin:${charlieHomeserverUrl}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
body: uploadedBlob,
|
||||
});
|
||||
await charlieClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
|
||||
|
||||
// Charlie syncs to receive M1 and ensure he can read it.
|
||||
syncResponse = getSyncResponse(
|
||||
[aliceClient.getSafeUserId(), bobClient.getSafeUserId(), charlieClient.getSafeUserId()],
|
||||
HistoryVisibility.Shared,
|
||||
ROOM_ID,
|
||||
);
|
||||
syncResponse.rooms.join[ROOM_ID].timeline.events.push(
|
||||
mkEventCustom({
|
||||
type: "m.room.encrypted",
|
||||
sender: bobClient.getSafeUserId(),
|
||||
content: bobEventM1Content,
|
||||
event_id: "$event_id_m1",
|
||||
}) as any,
|
||||
);
|
||||
charlieSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(charlieClient);
|
||||
|
||||
const charlieEventM1 = charlieRoom!
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
.find((e) => e.getId() === "$event_id_m1");
|
||||
|
||||
await charlieEventM1!.getDecryptionPromise();
|
||||
expect(charlieEventM1!.getType()).toEqual("m.room.message");
|
||||
expect(charlieEventM1!.getContent().body).toEqual("Charlie should be able to read");
|
||||
|
||||
// Charlie then immediately leaves.
|
||||
const charlieSyncResponse = {
|
||||
next_batch: "1",
|
||||
rooms: {
|
||||
leave: {
|
||||
[ROOM_ID]: {
|
||||
state: { events: [] },
|
||||
timeline: {
|
||||
events: [
|
||||
mkEventCustom({
|
||||
content: { membership: KnownMembership.Leave },
|
||||
type: EventType.RoomMember,
|
||||
sender: charlieClient.getSafeUserId(),
|
||||
state_key: charlieClient.getSafeUserId(),
|
||||
}),
|
||||
],
|
||||
prev_batch: "",
|
||||
},
|
||||
account_data: { events: [] },
|
||||
},
|
||||
},
|
||||
invite: {},
|
||||
join: {},
|
||||
knock: {},
|
||||
},
|
||||
account_data: { events: [] },
|
||||
};
|
||||
charlieSyncResponder.sendOrQueueSyncResponse(charlieSyncResponse);
|
||||
await syncPromise(charlieClient);
|
||||
|
||||
syncResponse = {
|
||||
next_batch: "2",
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
if (gappySync) {
|
||||
// In case of a gappy sync, the timeline is limited and we only see the leave event.
|
||||
syncResponse.rooms.join[ROOM_ID].timeline.limited = true;
|
||||
syncResponse.rooms.join[ROOM_ID].state = {
|
||||
events: [
|
||||
mkEventCustom({
|
||||
content: { membership: KnownMembership.Leave },
|
||||
type: EventType.RoomMember,
|
||||
sender: charlieClient.getSafeUserId(),
|
||||
state_key: charlieClient.getSafeUserId(),
|
||||
}) as any,
|
||||
],
|
||||
};
|
||||
} else {
|
||||
syncResponse.rooms.join[ROOM_ID].timeline.events.push(
|
||||
mkEventCustom({
|
||||
content: { membership: KnownMembership.Join },
|
||||
type: EventType.RoomMember,
|
||||
sender: charlieClient.getSafeUserId(),
|
||||
state_key: charlieClient.getSafeUserId(),
|
||||
}) as any,
|
||||
mkEventCustom({
|
||||
content: { membership: KnownMembership.Leave },
|
||||
type: EventType.RoomMember,
|
||||
sender: charlieClient.getSafeUserId(),
|
||||
state_key: charlieClient.getSafeUserId(),
|
||||
}) as any,
|
||||
);
|
||||
}
|
||||
// Bob syncs to learn about Charlie's leaving (and joining if non-gappy).
|
||||
bobSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(bobClient);
|
||||
|
||||
// Bob then sends M2, sharing a new room key with Alice.
|
||||
msgProm = expectSendRoomEvent(bobHomeserverUrl, "m.room.encrypted");
|
||||
toDeviceMessageProm = expectSendToDeviceMessage(bobHomeserverUrl, "m.room.encrypted");
|
||||
await bobClient.sendEvent(ROOM_ID, EventType.RoomMessage, {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Charlie should not be able to read",
|
||||
});
|
||||
const bobEventM2Content = await msgProm;
|
||||
sentToDeviceRequest = await toDeviceMessageProm;
|
||||
expect(sentToDeviceRequest).toBeDefined();
|
||||
aliceToDeviceMessage = sentToDeviceRequest[aliceClient.getSafeUserId()][aliceClient.deviceId!];
|
||||
|
||||
// Charlie should not receive the room key
|
||||
expect(sentToDeviceRequest[charlieClient.getSafeUserId()]).toBeUndefined();
|
||||
|
||||
debug(`Bob sent encrypted room event: ${JSON.stringify(bobEventM2Content)}`);
|
||||
|
||||
// Sync the message to Alice along with the to-device message, and check she can decrypt it.
|
||||
syncResponse = {
|
||||
next_batch: "3",
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
mkEventCustom({
|
||||
type: "m.room.encrypted",
|
||||
sender: bobClient.getSafeUserId(),
|
||||
content: bobEventM2Content,
|
||||
event_id: "$event_id_m2",
|
||||
}) as any,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
to_device: {
|
||||
events: [
|
||||
{
|
||||
type: "m.room.encrypted",
|
||||
sender: bobClient.getSafeUserId(),
|
||||
content: aliceToDeviceMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const aliceEventM2 = aliceRoom!.getLastLiveEvent()!;
|
||||
await aliceEventM2.getDecryptionPromise();
|
||||
expect(aliceEventM2.getType()).toEqual("m.room.message");
|
||||
expect(aliceEventM2.getContent().body).toEqual("Charlie should not be able to read");
|
||||
|
||||
// Charlie rejoins the room by ID, receives M2, which he should not be able to decrypt.
|
||||
fetchMock.postOnce(`${charlieHomeserverUrl}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
await charlieClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
|
||||
syncResponse = {
|
||||
next_batch: "4",
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
mkEventCustom({
|
||||
type: "m.room.encrypted",
|
||||
sender: bobClient.getSafeUserId(),
|
||||
content: bobEventM2Content,
|
||||
event_id: "$event_id_m2",
|
||||
}) as any,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
charlieSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(charlieClient);
|
||||
|
||||
const events = charlieRoom!.getLiveTimeline().getEvents();
|
||||
expect(events.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const charlieM2 = charlieRoom!
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
.find((e) => e.getId() === "$event_id_m2");
|
||||
|
||||
await charlieM2!.getDecryptionPromise();
|
||||
expect(charlieM2!.isDecryptionFailure()).toBeTruthy();
|
||||
},
|
||||
60e3,
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
vitest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to automatically test that room history is shared on invite.
|
||||
* The function performs the following:
|
||||
*
|
||||
* 1. Sets up the relevant fetchMock and to-device event listeners for Alice.
|
||||
* 2. Alice invites Bob to the room.
|
||||
* 3. Checks the key bundle was uploaded and that the `m.room_key_bundle`
|
||||
* to-device message was sent.
|
||||
* 4. Sends the invite event to Bob and ensures it is processed correctly.
|
||||
* 5. Sets up the relevant fetchMock listeners for Bob.
|
||||
* 5. Simulates Bob joining the room and verifies that the room history is shared.
|
||||
*
|
||||
* @param roomId The ID of the room where the invite and history sharing will be tested.
|
||||
*/
|
||||
async function assertInviteAndShareHistory(alice: TestClient, bob: TestClient, roomId: string): Promise<void> {
|
||||
const { homeserverUrl: aliceHomeserverUrl, client: aliceClient } = alice;
|
||||
const { homeserverUrl: bobHomeserverUrl, client: bobClient, syncResponder: bobSyncResponder } = bob;
|
||||
|
||||
const uploadProm = expectUploadRequest(alice);
|
||||
const toDeviceMessageProm = expectSendToDeviceMessage(aliceHomeserverUrl, "m.room.encrypted");
|
||||
fetchMock.postOnce(`${aliceHomeserverUrl}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/invite`, {});
|
||||
await aliceClient.invite(roomId, bobClient.getSafeUserId(), { shareEncryptedHistory: true });
|
||||
const uploadedBlob = await uploadProm;
|
||||
const sentToDeviceRequest = await toDeviceMessageProm;
|
||||
debug(`Alice sent encrypted to-device events: ${JSON.stringify(sentToDeviceRequest)}`);
|
||||
const bobToDeviceMessage = sentToDeviceRequest[bobClient.getSafeUserId()][bobClient.deviceId!];
|
||||
expect(bobToDeviceMessage).toBeDefined();
|
||||
|
||||
const inviteEvent = mkInviteEvent(aliceClient, bobClient);
|
||||
bobSyncResponder.sendOrQueueSyncResponse({
|
||||
rooms: { invite: { [roomId]: { invite_state: { events: [inviteEvent] } } } },
|
||||
to_device: {
|
||||
events: [
|
||||
{
|
||||
type: "m.room.encrypted",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
content: bobToDeviceMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await syncPromise(bobClient);
|
||||
|
||||
const room = bobClient.getRoom(roomId);
|
||||
expect(room).toBeTruthy();
|
||||
expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
|
||||
fetchMock.postOnce(`${bobHomeserverUrl}/_matrix/client/v3/join/${encodeURIComponent(roomId)}`, {
|
||||
room_id: roomId,
|
||||
});
|
||||
fetchMock.getOnce(`begin:${bobHomeserverUrl}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
body: uploadedBlob,
|
||||
});
|
||||
await bobClient.joinRoom(roomId, { acceptSharedHistory: true });
|
||||
}
|
||||
|
||||
function mkInviteEvent(inviter: MatrixClient, recipient: MatrixClient): Partial<IRoomEvent> {
|
||||
return mkEventCustom({
|
||||
type: "m.room.member",
|
||||
@@ -763,11 +1137,11 @@ function expectSendRoomEvent(homeserverUrl: string, msgtype: string): Promise<IC
|
||||
}
|
||||
|
||||
/** Expect an upload to Alice's server. Returns a Promise that resolves when the upload is complete. */
|
||||
function expectUploadRequest(): Promise<Uint8Array> {
|
||||
function expectUploadRequest({ userId, homeserverUrl }: TestClient): Promise<Uint8Array> {
|
||||
return new Promise<Uint8Array>((resolve) => {
|
||||
fetchMock.postOnce(new URL("/_matrix/media/v3/upload", ALICE_HOMESERVER_URL).toString(), (callLog) => {
|
||||
fetchMock.postOnce(new URL("/_matrix/media/v3/upload", homeserverUrl).toString(), (callLog) => {
|
||||
const body = callLog.options.body as Uint8Array;
|
||||
debug(`Alice uploaded blob of length ${body.length}`);
|
||||
debug(`${userId} uploaded blob of length ${body.length}`);
|
||||
resolve(body);
|
||||
return { content_uri: "mxc://alice-server/here" };
|
||||
});
|
||||
|
||||
@@ -180,7 +180,7 @@ describe("oidc authorization", () => {
|
||||
describe("completeAuthorizationCodeGrant", () => {
|
||||
const homeserverUrl = "https://server.org/";
|
||||
const identityServerUrl = "https://id.org/";
|
||||
const nonce = "test-nonce";
|
||||
const nonce = "hRpB6pkE06";
|
||||
const redirectUri = baseUrl;
|
||||
const code = "auth_code_xyz";
|
||||
const validBearerTokenResponse = {
|
||||
|
||||
+2
-1
@@ -101,7 +101,7 @@ import {
|
||||
type RoomNameState,
|
||||
} from "./models/room.ts";
|
||||
import { RoomMemberEvent, type RoomMemberEventHandlerMap } from "./models/room-member.ts";
|
||||
import { type IPowerLevelsContent, type RoomStateEvent, type RoomStateEventHandlerMap } from "./models/room-state.ts";
|
||||
import { RoomStateEvent, type IPowerLevelsContent, type RoomStateEventHandlerMap } from "./models/room-state.ts";
|
||||
import {
|
||||
isSendDelayedEventRequestOpts,
|
||||
UpdateDelayedEventAction,
|
||||
@@ -2027,6 +2027,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
// attach the event listeners needed by RustCrypto
|
||||
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
|
||||
this.on(RoomStateEvent.Events, rustCrypto.onRoomStateEvent.bind(rustCrypto));
|
||||
this.on(ClientEvent.Event, (event) => {
|
||||
rustCrypto.onLiveEventFromSync(event);
|
||||
});
|
||||
|
||||
@@ -794,6 +794,9 @@ export enum DeviceIsolationModeKind {
|
||||
*
|
||||
* Events from all senders are always decrypted (and should be decorated with message shields in case
|
||||
* of authenticity warnings, see {@link EventEncryptionInfo}).
|
||||
*
|
||||
* `AllDevicesIsolationMode` is used in the legacy, non-'exclude insecure devices' mode in Element Web. It is not
|
||||
* recommended (see {@link https://github.com/matrix-org/matrix-spec-proposals/pull/4153 | MSC4153}).
|
||||
*/
|
||||
export class AllDevicesIsolationMode {
|
||||
public readonly kind = DeviceIsolationModeKind.AllDevicesIsolationMode;
|
||||
@@ -820,6 +823,9 @@ export class AllDevicesIsolationMode {
|
||||
*
|
||||
* 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}.)
|
||||
*
|
||||
* `OnlySignedDevicesIsolationMode` corresponds to the 'Exclude insecure devices' mode in Element Web, which is
|
||||
* recommended by {@link https://github.com/matrix-org/matrix-spec-proposals/pull/4153 | MSC4153}.
|
||||
*/
|
||||
export class OnlySignedDevicesIsolationMode {
|
||||
public readonly kind = DeviceIsolationModeKind.OnlySignedDevicesIsolationMode;
|
||||
|
||||
@@ -98,6 +98,7 @@ import { VerificationMethod } from "../types.ts";
|
||||
import { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
|
||||
import { type UIAuthCallback } from "../interactive-auth.ts";
|
||||
import { getHttpUriForMxc } from "../content-repo.ts";
|
||||
import { type RoomState } from "../matrix.ts";
|
||||
|
||||
const ALL_VERIFICATION_METHODS = [
|
||||
VerificationMethod.Sas,
|
||||
@@ -1955,6 +1956,43 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
enc.onRoomMembership(member);
|
||||
}
|
||||
|
||||
/**
|
||||
* Previously, it was sufficient to check if we need to rotate the room key
|
||||
* prior to sending a message. However, the history sharing feature
|
||||
* (MSC4268) breaks this logic:
|
||||
*
|
||||
* 1. Alice sends a message M1 in room X;
|
||||
* 2. Bob invites Charlie, who joins and immediately leaves the room;
|
||||
* 3. Alice sends another message M2 in room X.
|
||||
*
|
||||
* Under the old logic, Alice would not rotate her key after Charlie
|
||||
* leaves, resulting in M2 being encrypted with the same session as M1.
|
||||
* This would allow Charlie to decrypt M2 if he ever gains access to
|
||||
* the event.
|
||||
*
|
||||
* To counter this, we proactively discard any active outgoing Megolm
|
||||
* session when we see a `leave` event. Note that we have to do this in
|
||||
* `onRoomStateEvent` rather than `onRoomMembership`, because
|
||||
* `onRoomMembership` is only called when we see a *change* in membership.
|
||||
* In the case of a gappy sync, we might miss Charlie's invite and join,
|
||||
* and only see the final `leave` event (sohis membership goes from `leave`
|
||||
* to `leave`).
|
||||
*/
|
||||
public onRoomStateEvent(event: MatrixEvent, state: RoomState, prevEvent: MatrixEvent | null): void {
|
||||
if (event.getType() != EventType.RoomMember) {
|
||||
// Ignore all events that aren't member updates.
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
event.getStateKey()! !== this.olmMachine.userId.toString() &&
|
||||
event.getContent().membership === KnownMembership.Leave
|
||||
) {
|
||||
this.logger.info(`Rotating session for room ${event.getRoomId()} due to member leaving the room`);
|
||||
this.forceDiscardSession(event.getRoomId()!);
|
||||
}
|
||||
}
|
||||
|
||||
/** Callback for OlmMachine.registerRoomKeyUpdatedCallback
|
||||
*
|
||||
* Called by the rust-sdk whenever there is an update to (megolm) room keys. We
|
||||
|
||||
Reference in New Issue
Block a user