Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d5b6138ae | |||
| 4f63b47134 | |||
| c89f220e52 | |||
| 9675a1584d | |||
| c81199b9d5 | |||
| 6bdb087883 | |||
| c4f00895b1 | |||
| f8c3973efd | |||
| 0c0775c0bf | |||
| 70edf0f34d | |||
| b46b31563e | |||
| 8007bc5fe8 | |||
| d178fbf9cd | |||
| f81036346f | |||
| 1a364c93c3 | |||
| 1cd6fe7775 | |||
| a8b3369dd0 | |||
| 99600e87f1 | |||
| 7cf59d64e6 | |||
| 5967c670d8 | |||
| 2fe35fed13 | |||
| 2d1308c733 | |||
| 11348f9532 | |||
| 869576747c | |||
| 35ea144bca | |||
| 5bf29ef543 | |||
| 99b3cf2279 | |||
| 5e2acb558b | |||
| 19494e093b | |||
| ab217bdc35 | |||
| c4d32a3292 | |||
| 8e01b654bc | |||
| dc406ee2e8 | |||
| be8b769542 |
@@ -22,10 +22,14 @@ version-resolver:
|
||||
exclude-labels:
|
||||
- "T-Task"
|
||||
- "X-Reverted"
|
||||
- "backport staging"
|
||||
exclude-contributors:
|
||||
- "RiotRobot"
|
||||
template: |
|
||||
$CHANGES
|
||||
#no-changes-template: ""
|
||||
prerelease: true
|
||||
prerelease-identifier: rc
|
||||
include-pre-releases: false
|
||||
stable-ref: master
|
||||
staging-ref: staging
|
||||
|
||||
@@ -16,7 +16,7 @@ concurrency:
|
||||
jobs:
|
||||
build-element-web:
|
||||
name: Build element-web
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.88.0
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.90.0
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
name: Release Drafter
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
include-changes:
|
||||
description: Project to include changelog entries from in this release.
|
||||
type: string
|
||||
required: false
|
||||
concurrency: release-drafter-action
|
||||
jobs:
|
||||
draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: staging
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
|
||||
id: draft-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
disable-autolabeler: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
path: .action-repo
|
||||
sparse-checkout: |
|
||||
.github/actions
|
||||
scripts/release
|
||||
|
||||
- name: Ingest upstream changes
|
||||
if: inputs.include-changes
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.release.outputs.id }}
|
||||
DEPENDENCY: ${{ inputs.include-changes }}
|
||||
VERSION: ${{ steps.draft-release.outputs.tag_name }}
|
||||
with:
|
||||
retries: 3
|
||||
script: |
|
||||
const { RELEASE_ID: releaseId, DEPENDENCY, VERSION } = process.env;
|
||||
const { owner, repo } = context.repo;
|
||||
const script = require("./.action-repo/scripts/release/merge-release-notes.js");
|
||||
|
||||
let deps = [];
|
||||
if (DEPENDENCY.includes("/")) {
|
||||
deps.push(DEPENDENCY.replace("$VERSION", VERSION))
|
||||
} else {
|
||||
const fromVersion = JSON.parse(await exec.exec("git show origin/master:package.json")).dependencies[DEPENDENCY];
|
||||
const toVersion = require("./package.json").dependencies[DEPENDENCY];
|
||||
|
||||
if (toVersion.endsWith("#develop")) {
|
||||
core.warning(`${DEPENDENCY} will be kept at ${fromVersion}`, { title: "Develop dependency found" });
|
||||
} else {
|
||||
deps.push([DEPENDENCY, fromVersion, toVersion]);
|
||||
}
|
||||
}
|
||||
|
||||
if (deps.length) {
|
||||
const notes = await script({
|
||||
github,
|
||||
releaseId,
|
||||
dependencies: deps,
|
||||
});
|
||||
|
||||
await github.rest.repos.updateRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: releaseId,
|
||||
body: notes,
|
||||
});
|
||||
}
|
||||
@@ -2,20 +2,8 @@ name: Release Drafter
|
||||
on:
|
||||
push:
|
||||
branches: [staging]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
previous-version:
|
||||
description: What release to use as a base for release note purposes
|
||||
required: false
|
||||
type: string
|
||||
workflow_dispatch: {}
|
||||
concurrency: ${{ github.workflow }}
|
||||
jobs:
|
||||
draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@e64b19c4c46173209ed9f2e5a2f4ca7de89a0e86 # v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
disable-autolabeler: true
|
||||
previous-version: ${{ inputs.previous-version }}
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
|
||||
|
||||
@@ -20,10 +20,8 @@ on:
|
||||
description: Publish to npm
|
||||
type: boolean
|
||||
default: false
|
||||
dependencies:
|
||||
description: |
|
||||
List of dependencies to update in `npm-dep=version` format.
|
||||
`version` can be `"current"` to leave it at the current version.
|
||||
downstreams:
|
||||
description: List of github projects (owner/repo) which should have their dependency bumped to the newly released version (in JSON string array string syntax)
|
||||
type: string
|
||||
required: false
|
||||
include-changes:
|
||||
@@ -88,17 +86,11 @@ jobs:
|
||||
id: prepare
|
||||
run: |
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
{
|
||||
echo "RELEASE_NOTES<<EOF"
|
||||
echo "$BODY"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_ENV
|
||||
|
||||
HAS_DIST=0
|
||||
jq -e .scripts.dist package.json >/dev/null 2>&1 && HAS_DIST=1
|
||||
echo "has-dist-script=$HAS_DIST" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
BODY: ${{ steps.release.outputs.body }}
|
||||
VERSION: ${{ steps.release.outputs.tag_name }}
|
||||
|
||||
- name: Finalise version
|
||||
@@ -132,76 +124,23 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: Update dependencies
|
||||
id: update-dependencies
|
||||
if: inputs.dependencies
|
||||
run: |
|
||||
UPDATED=()
|
||||
while IFS= read -r DEPENDENCY; do
|
||||
[ -z "$DEPENDENCY" ] && continue
|
||||
IFS="=" read -r PACKAGE UPDATE_VERSION <<< "$DEPENDENCY"
|
||||
|
||||
CURRENT_VERSION=$(cat package.json | jq -r .dependencies[\"$PACKAGE\"])
|
||||
echo "Current $PACKAGE version is $CURRENT_VERSION"
|
||||
|
||||
if [ "$CURRENT_VERSION" == "null" ]
|
||||
then
|
||||
echo "Unable to find $PACKAGE in package.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$UPDATE_VERSION" == "current" ] || [ "$UPDATE_VERSION" == "$CURRENT_VERSION" ]
|
||||
then
|
||||
echo "Not updating dependency $PACKAGE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Upgrading $PACKAGE to $UPDATE_VERSION..."
|
||||
yarn upgrade "$PACKAGE@$UPDATE_VERSION" --exact
|
||||
git add -u
|
||||
git commit -m "Upgrade $PACKAGE to $UPDATE_VERSION"
|
||||
UPDATED+=("$PACKAGE")
|
||||
done <<< "$DEPENDENCIES"
|
||||
|
||||
JSON=$(jq --compact-output --null-input '$ARGS.positional' --args -- "${UPDATED[@]}")
|
||||
echo "updated=$JSON" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
DEPENDENCIES: ${{ inputs.dependencies }}
|
||||
|
||||
- name: Prevent develop dependencies
|
||||
if: inputs.dependencies
|
||||
- name: Handle develop dependencies
|
||||
run: |
|
||||
ret=0
|
||||
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
|
||||
if [ "$ret" -eq 0 ]; then
|
||||
echo "package.json contains develop dependencies. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
|
||||
IFS=" "
|
||||
PACKAGE=${dep[0]}
|
||||
VERSION=${dep[1]}
|
||||
|
||||
echo "::warning title=Develop dependency found::$DEPENDENCY will be kept at $VERSION"
|
||||
yarn upgrade "$PACKAGE@$VERSION" --exact
|
||||
git add -u
|
||||
git commit -m "Keep $PACKAGE at $VERSION"
|
||||
done
|
||||
|
||||
- name: Bump package.json version
|
||||
run: yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
|
||||
- name: Ingest upstream changes
|
||||
if: |
|
||||
inputs.include-changes &&
|
||||
(!inputs.dependencies || contains(fromJSON(steps.update-dependencies.outputs.updated), inputs.include-changes))
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.release.outputs.id }}
|
||||
DEPENDENCY: ${{ inputs.include-changes }}
|
||||
with:
|
||||
retries: 3
|
||||
script: |
|
||||
const { RELEASE_ID: releaseId, DEPENDENCY, VERSION } = process.env;
|
||||
const { owner, repo } = context.repo;
|
||||
const script = require("./.action-repo/scripts/release/merge-release-notes.js");
|
||||
const notes = await script({
|
||||
github,
|
||||
releaseId,
|
||||
dependencies: [DEPENDENCY.replace("$VERSION", VERSION)],
|
||||
});
|
||||
core.exportVariable("RELEASE_NOTES", notes);
|
||||
|
||||
- name: Add to CHANGELOG.md
|
||||
if: inputs.final
|
||||
run: |
|
||||
@@ -219,6 +158,8 @@ jobs:
|
||||
cat CHANGELOG.md.old >> CHANGELOG.md
|
||||
rm CHANGELOG.md.old
|
||||
git add CHANGELOG.md
|
||||
env:
|
||||
RELEASE_NOTES: ${{ steps.release.outputs.body }}
|
||||
|
||||
- name: Run pre-release script to update package.json fields
|
||||
run: |
|
||||
@@ -335,15 +276,16 @@ jobs:
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
update-labels:
|
||||
name: Advance release blocker labels
|
||||
post-release:
|
||||
name: Post release steps
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: repository
|
||||
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: garganshu/github-label-updater@3770d15ebfed2fe2cb06a241047bc340f774a7d1 # v1.0.0
|
||||
- name: Advance release blocker labels
|
||||
uses: garganshu/github-label-updater@3770d15ebfed2fe2cb06a241047bc340f774a7d1 # v1.0.0
|
||||
with:
|
||||
owner: ${{ github.repository_owner }}
|
||||
repo: ${{ steps.repository.outputs.REPO }}
|
||||
@@ -351,3 +293,39 @@ jobs:
|
||||
filter-labels: X-Upcoming-Release-Blocker
|
||||
remove-labels: X-Upcoming-Release-Blocker
|
||||
add-labels: X-Release-Blocker
|
||||
|
||||
- name: Wait for master->develop gitflow merge
|
||||
if: inputs.final
|
||||
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||
with:
|
||||
ref: master
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
check-name: merge
|
||||
allowed-conclusions: success
|
||||
|
||||
bump-downstreams:
|
||||
name: Update npm dependency in downstream projects
|
||||
needs: npm
|
||||
runs-on: ubuntu-latest
|
||||
if: inputs.downstreams
|
||||
strategy:
|
||||
matrix:
|
||||
repo: ${{ fromJSON(inputs.downstreams) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- name: Bump dependency
|
||||
env:
|
||||
DEPENDENCY: ${{ needs.npm.outputs.id }}
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
yarn upgrade "$DEPENDENCY" --exact
|
||||
git add package.json yarn.lock
|
||||
git commit -am"Upgrade dependency to $DEPENDENCY"
|
||||
git push origin staging
|
||||
|
||||
@@ -8,6 +8,8 @@ jobs:
|
||||
npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
id: ${{ steps.npm-publish.outputs.id }}
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -28,6 +28,7 @@ jobs:
|
||||
with:
|
||||
final: ${{ inputs.mode == 'final' }}
|
||||
npm: ${{ inputs.npm }}
|
||||
downstreams: '["matrix-org/matrix-react-sdk", "element-hq/element-web"]'
|
||||
|
||||
docs:
|
||||
name: Publish Documentation
|
||||
|
||||
@@ -90,6 +90,7 @@ Changes in [30.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v30
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
* Refactor & make base64 functions browser-safe ([\#3818](https://github.com/matrix-org/matrix-js-sdk/pull/3818)).
|
||||
* `IndexedDBStore.startup()` must be called after using it on `sdk.createClient` now.
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate `MatrixEvent.toJSON` ([\#3801](https://github.com/matrix-org/matrix-js-sdk/pull/3801)).
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
|
||||
# Deep dive
|
||||
|
||||
- [Release Process](release.md)
|
||||
- [Storage notes](storage-notes.md)
|
||||
- [Unverified devices](warning-on-unverified-devices.md)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Release Process
|
||||
|
||||
## Hotfix and off-cycle releases
|
||||
|
||||
1. Prepare the `staging` branch by using the backport automation and manually merging
|
||||
2. Go to [Releasing](#Releasing)
|
||||
|
||||
## Release candidates
|
||||
|
||||
1. Prepare the `staging` branch by running the [branch cut automation](https://github.com/vector-im/element-web/actions/workflows/release_prepare.yml)
|
||||
2. Go to [Releasing](#Releasing)
|
||||
|
||||
## Releasing
|
||||
|
||||
1. Open the [Releases page](https://github.com/matrix-org/matrix-js-sdk/releases) and inspect the draft release there
|
||||
2. Make any modifications to the release notes and tag/version as required
|
||||
3. Run [workflow](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release.yml) with the type set appropriately
|
||||
|
||||
## Artifacts
|
||||
|
||||
Releasing the Matrix JS SDK has just two artifacts:
|
||||
|
||||
- Package published to [npm](https://github.com/matrix-org/matrix-js-sdk)
|
||||
- Docs published to [Github Pages](https://matrix-org.github.io/matrix-js-sdk/)
|
||||
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "31.2.0",
|
||||
"version": "31.3.0-rc.3",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
"prepack": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"clean": "rimraf lib",
|
||||
"build": "yarn build:dev",
|
||||
@@ -52,7 +52,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^4.0.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^4.3.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
@@ -93,7 +93,7 @@
|
||||
"@types/uuid": "9",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"allchange": "^1.0.6",
|
||||
"allchange": "^1.3.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"domexception": "^4.0.0",
|
||||
@@ -117,7 +117,7 @@
|
||||
"jest-mock": "^29.0.0",
|
||||
"lint-staged": "^15.0.2",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "3.1.1",
|
||||
"prettier": "3.2.4",
|
||||
"rimraf": "^5.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.24.0",
|
||||
|
||||
@@ -2,6 +2,31 @@
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
// Dependency can be the name of an entry in package.json, in which case the owner, repo & version will be looked up in its own package.json
|
||||
// Or it can be a string in the form owner/repo@tag
|
||||
// Or it can be a tuple of dependency, from version, to version, in which case a list of releases in that range (to inclusive) will be returned
|
||||
async function getReleases(github, dependency) {
|
||||
if (Array.isArray(dependency)) {
|
||||
const [dep, fromVersion, toVersion] = dependency;
|
||||
const upstreamPackageJson = getDependencyPackageJson(dep);
|
||||
const [owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
|
||||
|
||||
const response = await github.rest.repos.listReleases({
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
});
|
||||
const releases = response.data.filter((release) => !release.draft && !release.prerelease);
|
||||
|
||||
const fromVersionIndex = releases.findIndex((release) => release.tag_name === `v${fromVersion}`);
|
||||
const toVersionIndex = releases.findIndex((release) => release.tag_name === `v${toVersion}`);
|
||||
|
||||
return releases.slice(toVersionIndex, fromVersionIndex);
|
||||
}
|
||||
|
||||
return [await getRelease(github, dependency)];
|
||||
}
|
||||
|
||||
async function getRelease(github, dependency) {
|
||||
let owner;
|
||||
let repo;
|
||||
@@ -11,7 +36,7 @@ async function getRelease(github, dependency) {
|
||||
repo = dependency.split("/")[1].split("@")[0];
|
||||
tag = dependency.split("@")[1];
|
||||
} else {
|
||||
const upstreamPackageJson = JSON.parse(fs.readFileSync(`./node_modules/${dependency}/package.json`, "utf8"));
|
||||
const upstreamPackageJson = getDependencyPackageJson(dependency);
|
||||
[owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
|
||||
tag = `v${upstreamPackageJson.version}`;
|
||||
}
|
||||
@@ -24,25 +49,45 @@ async function getRelease(github, dependency) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function getDependencyPackageJson(dependency) {
|
||||
return JSON.parse(fs.readFileSync(`./node_modules/${dependency}/package.json`, "utf8"));
|
||||
}
|
||||
|
||||
const HEADING_PREFIX = "## ";
|
||||
|
||||
const categories = [
|
||||
"🔒 SECURITY FIXES",
|
||||
"🚨 BREAKING CHANGESd",
|
||||
"🦖 Deprecations",
|
||||
"✨ Features",
|
||||
"🐛 Bug Fixes",
|
||||
"🧰 Maintenance",
|
||||
];
|
||||
|
||||
const parseReleaseNotes = (body, sections) => {
|
||||
let heading = null;
|
||||
for (const line of body.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith(HEADING_PREFIX)) {
|
||||
heading = trimmed.slice(HEADING_PREFIX.length);
|
||||
if (!categories.includes(heading)) heading = null;
|
||||
continue;
|
||||
}
|
||||
if (heading && trimmed) {
|
||||
sections[heading].push(trimmed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const main = async ({ github, releaseId, dependencies }) => {
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [owner, repo] = GITHUB_REPOSITORY.split("/");
|
||||
|
||||
const sections = new Map();
|
||||
let heading = null;
|
||||
const sections = Object.fromEntries(categories.map((cat) => [cat, []]));
|
||||
for (const dependency of dependencies) {
|
||||
const release = await getRelease(github, dependency);
|
||||
for (const line of release.body.split("\n")) {
|
||||
if (line.startsWith(HEADING_PREFIX)) {
|
||||
heading = line.trim();
|
||||
sections.set(heading, []);
|
||||
continue;
|
||||
}
|
||||
if (heading && line) {
|
||||
sections.get(heading).push(line.trim());
|
||||
}
|
||||
const releases = await getReleases(github, dependency);
|
||||
for (const release of releases) {
|
||||
parseReleaseNotes(release.body, sections);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,36 +97,22 @@ const main = async ({ github, releaseId, dependencies }) => {
|
||||
release_id: releaseId,
|
||||
});
|
||||
|
||||
const headings = ["🚨 BREAKING CHANGES", "🦖 Deprecations", "✨ Features", "🐛 Bug Fixes", "🧰 Maintenance"].map(
|
||||
(h) => HEADING_PREFIX + h,
|
||||
);
|
||||
const intro = release.body.split(HEADING_PREFIX, 2)[0].trim();
|
||||
|
||||
heading = null;
|
||||
const output = [];
|
||||
for (const line of [...release.body.split("\n"), null]) {
|
||||
if (line === null || line.startsWith(HEADING_PREFIX)) {
|
||||
// If we have a heading, and it's not the first in the list of pending headings, output the section.
|
||||
// If we're processing the last line (null) then output all remaining sections.
|
||||
while (headings.length > 0 && (line === null || (heading && headings[0] !== heading))) {
|
||||
const heading = headings.shift();
|
||||
if (sections.has(heading)) {
|
||||
output.push(heading);
|
||||
output.push(...sections.get(heading));
|
||||
}
|
||||
}
|
||||
|
||||
if (heading && sections.has(heading)) {
|
||||
const lastIsBlank = !output.at(-1)?.trim();
|
||||
if (lastIsBlank) output.pop();
|
||||
output.push(...sections.get(heading));
|
||||
if (lastIsBlank) output.push("");
|
||||
}
|
||||
heading = line;
|
||||
}
|
||||
output.push(line);
|
||||
let output = "";
|
||||
if (intro) {
|
||||
output = intro + "\n\n";
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
for (const section in sections) {
|
||||
const lines = sections[section];
|
||||
if (!lines.length) continue;
|
||||
output += HEADING_PREFIX + section + "\n\n";
|
||||
output += lines.join("\n");
|
||||
output += "\n\n";
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
// This is just for testing locally
|
||||
|
||||
@@ -398,7 +398,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
|
||||
describe("crossSignDevice", () => {
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// make sure that there is another device which we can sign
|
||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
getSyncResponse,
|
||||
InitCrypto,
|
||||
mkEventCustom,
|
||||
mkMembershipCustom,
|
||||
syncPromise,
|
||||
} from "../../test-utils/test-utils";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
BOB_TEST_USER_ID,
|
||||
SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
SIGNED_TEST_DEVICE_DATA,
|
||||
TEST_ROOM_ID,
|
||||
TEST_ROOM_ID as ROOM_ID,
|
||||
TEST_USER_ID,
|
||||
} from "../../test-utils/test-data";
|
||||
@@ -230,9 +232,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */
|
||||
let keyReceiver: E2EKeyReceiver;
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let keyResponder: E2EKeyResponder;
|
||||
|
||||
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||
let syncResponder: ISyncResponder;
|
||||
|
||||
@@ -368,6 +367,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "xzcvb",
|
||||
cryptoCallbacks: createCryptoCallbacks(),
|
||||
logger: logger.getChild("aliceClient"),
|
||||
});
|
||||
|
||||
/* set up listeners for /keys/upload and /sync */
|
||||
@@ -701,7 +701,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
it("prepareToEncrypt", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
@@ -732,7 +732,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
it("Alice sends a megolm message with GlobalErrorOnUnknownDevices=false", async () => {
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
@@ -760,7 +760,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
it("We should start a new megolm session after forceDiscardSession", async () => {
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
@@ -1063,8 +1063,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
await startClientAndAwaitFirstSync();
|
||||
const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount);
|
||||
|
||||
// We need to fake the timers to advance the time
|
||||
jest.useFakeTimers();
|
||||
// We need to fake the timers to advance the time, but the wasm bindings of matrix-sdk-crypto rely on a
|
||||
// working `queueMicrotask`
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
const syncResponse = getSyncResponse(["@bob:xyz"]);
|
||||
|
||||
@@ -2069,7 +2070,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
it("Sending an event initiates a member list sync", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
@@ -2092,7 +2093,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
it("loading the membership list inhibits a later load", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
@@ -2189,7 +2190,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
describe("key upload request", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -2389,8 +2391,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(devicesInfo.get(user)?.size).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Get devices from tacked users", async () => {
|
||||
jest.useFakeTimers();
|
||||
it("Get devices from tracked users", async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
@@ -2745,7 +2748,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
|
||||
describe("Manage Key Backup", () => {
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -2899,7 +2903,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
|
||||
keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
||||
const 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);
|
||||
@@ -2935,4 +2939,180 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(hasCrossSigningKeysForUser).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/** Guards against downgrade attacks from servers hiding or manipulating the crypto settings. */
|
||||
describe("Persistent encryption settings", () => {
|
||||
let persistentStoreClient: MatrixClient;
|
||||
let client2: MatrixClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
const homeserverurl = "https://alice-server.com";
|
||||
const userId = "@alice:localhost";
|
||||
|
||||
const keyResponder = new E2EKeyResponder(homeserverurl);
|
||||
keyResponder.addKeyReceiver(userId, keyReceiver);
|
||||
|
||||
// For legacy crypto, these tests only work properly with a proper (indexeddb-based) CryptoStore, so
|
||||
// rather than using the existing `aliceClient`, create a new client. Once we drop legacy crypto, we can
|
||||
// just use `aliceClient` here.
|
||||
persistentStoreClient = await makeNewClient(homeserverurl, userId, "persistentStoreClient");
|
||||
await persistentStoreClient.startClient({});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
persistentStoreClient.stopClient();
|
||||
client2?.stopClient();
|
||||
});
|
||||
|
||||
test("Sending a message in a room where the server is hiding the state event does not send a plaintext event", async () => {
|
||||
// Alice is in an encrypted room
|
||||
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2" });
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState]));
|
||||
await syncPromise(persistentStoreClient);
|
||||
|
||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||
await Promise.all([persistentStoreClient.sendTextMessage(ROOM_ID, "test"), expectEncryptedSendMessage()]);
|
||||
|
||||
// We now replace the client, and allow the new one to resync, *without* the encryption event.
|
||||
client2 = await replaceClient(persistentStoreClient);
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([]));
|
||||
await client2.startClient({});
|
||||
await syncPromise(client2);
|
||||
logger.log(client2.getUserId() + ": restarted");
|
||||
|
||||
await expectSendMessageToFail(client2);
|
||||
});
|
||||
|
||||
test("Changes to the rotation period should be ignored", async () => {
|
||||
// Alice is in an encrypted room, where the rotation period is set to 2 messages
|
||||
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 });
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState]));
|
||||
await syncPromise(persistentStoreClient);
|
||||
|
||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||
const [, msg1Content] = await Promise.all([
|
||||
persistentStoreClient.sendTextMessage(ROOM_ID, "test1"),
|
||||
expectEncryptedSendMessage(),
|
||||
]);
|
||||
|
||||
// Replace the state with one which bumps the rotation period. This should be ignored, though it's not
|
||||
// clear that is correct behaviour (see https://github.com/element-hq/element-meta/issues/69)
|
||||
const encryptionState2 = mkEncryptionEvent({
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
rotation_period_msgs: 100,
|
||||
});
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: "1",
|
||||
rooms: { join: { [TEST_ROOM_ID]: { timeline: { events: [encryptionState2], prev_batch: "" } } } },
|
||||
});
|
||||
await syncPromise(persistentStoreClient);
|
||||
|
||||
// Send two more messages. The first should use the same megolm session as the first; the second should
|
||||
// use a different one.
|
||||
const [, msg2Content] = await Promise.all([
|
||||
persistentStoreClient.sendTextMessage(ROOM_ID, "test2"),
|
||||
expectEncryptedSendMessage(),
|
||||
]);
|
||||
expect(msg2Content.session_id).toEqual(msg1Content.session_id);
|
||||
const [, msg3Content] = await Promise.all([
|
||||
persistentStoreClient.sendTextMessage(ROOM_ID, "test3"),
|
||||
expectEncryptedSendMessage(),
|
||||
]);
|
||||
expect(msg3Content.session_id).not.toEqual(msg1Content.session_id);
|
||||
});
|
||||
|
||||
test("Changes to the rotation period should be ignored after a client restart", async () => {
|
||||
// Alice is in an encrypted room, where the rotation period is set to 2 messages
|
||||
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 });
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState]));
|
||||
await syncPromise(persistentStoreClient);
|
||||
|
||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||
await Promise.all([persistentStoreClient.sendTextMessage(ROOM_ID, "test1"), expectEncryptedSendMessage()]);
|
||||
|
||||
// We now replace the client, and allow the new one to resync with a *different* encryption event.
|
||||
client2 = await replaceClient(persistentStoreClient);
|
||||
const encryptionState2 = mkEncryptionEvent({
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
rotation_period_msgs: 100,
|
||||
});
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponseWithState([encryptionState2]));
|
||||
await client2.startClient({});
|
||||
await syncPromise(client2);
|
||||
logger.log(client2.getUserId() + ": restarted");
|
||||
|
||||
// Now send another message, which should (for now) be rejected.
|
||||
await expectSendMessageToFail(client2);
|
||||
});
|
||||
|
||||
/** Shut down `oldClient`, and build a new MatrixClient for the same user. */
|
||||
async function replaceClient(oldClient: MatrixClient) {
|
||||
oldClient.stopClient();
|
||||
syncResponder.sendOrQueueSyncResponse({}); // flush pending request from old client
|
||||
return makeNewClient(oldClient.getHomeserverUrl(), oldClient.getSafeUserId(), "client2");
|
||||
}
|
||||
|
||||
async function makeNewClient(
|
||||
homeserverUrl: string,
|
||||
userId: string,
|
||||
loggerPrefix: string,
|
||||
): Promise<MatrixClient> {
|
||||
const client = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: userId,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "xzcvb",
|
||||
cryptoCallbacks: createCryptoCallbacks(),
|
||||
logger: logger.getChild(loggerPrefix),
|
||||
|
||||
// For legacy crypto, these tests only work with a proper persistent cryptoStore.
|
||||
cryptoStore: new IndexedDBCryptoStore(indexedDB, "test"),
|
||||
});
|
||||
await initCrypto(client);
|
||||
mockInitialApiRequests(client.getHomeserverUrl());
|
||||
return client;
|
||||
}
|
||||
|
||||
function mkEncryptionEvent(content: Object) {
|
||||
return mkEventCustom({
|
||||
sender: persistentStoreClient.getSafeUserId(),
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
content: content,
|
||||
});
|
||||
}
|
||||
|
||||
/** Sync response which includes `TEST_ROOM_ID`, where alice is a member
|
||||
*
|
||||
* @param stateEvents - Additional state events for the test room
|
||||
*/
|
||||
function getSyncResponseWithState(stateEvents: Array<Object>) {
|
||||
const roomResponse = {
|
||||
state: {
|
||||
events: [
|
||||
mkMembershipCustom({ membership: "join", sender: persistentStoreClient.getSafeUserId() }),
|
||||
...stateEvents,
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
events: [],
|
||||
prev_batch: "",
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
next_batch: "1",
|
||||
rooms: { join: { [TEST_ROOM_ID]: roomResponse } },
|
||||
};
|
||||
}
|
||||
|
||||
/** Send a message with the given client, and check that it is not sent in plaintext */
|
||||
async function expectSendMessageToFail(aliceClient2: MatrixClient) {
|
||||
// The precise failure mode here is somewhat up for debate (https://github.com/element-hq/element-meta/issues/69).
|
||||
// For now, the attempt to send is rejected with an exception. The text is different between old and new stacks.
|
||||
await expect(aliceClient2.sendTextMessage(ROOM_ID, "test")).rejects.toThrow(
|
||||
/unconfigured room !room:id|Room !room:id was previously configured to use encryption/,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,7 +129,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
|
||||
@@ -136,6 +136,8 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
|
||||
expect(await matrixClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual("7");
|
||||
|
||||
expect(await matrixClient.getCrypto()!.isEncryptionEnabledInRoom("!CWLUCoEWXSFyTCOtfL:matrix.org")).toBe(true);
|
||||
|
||||
// check the progress callback
|
||||
expect(progressListener.mock.calls.length).toBeGreaterThan(50);
|
||||
|
||||
|
||||
@@ -85,7 +85,8 @@ import { encodeBase64 } from "../../../src/base64";
|
||||
|
||||
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
|
||||
// to ensure that we don't end up with dangling timeouts.
|
||||
jest.useFakeTimers();
|
||||
// But the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
beforeAll(async () => {
|
||||
// we use the libolm primitives in the test, so init the Olm library
|
||||
@@ -743,6 +744,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
expect(toDeviceMessage.code).toEqual("m.user");
|
||||
expect(request.phase).toEqual(VerificationPhase.Cancelled);
|
||||
expect(request.cancellationCode).toEqual("m.user");
|
||||
expect(request.cancellingUserId).toEqual("@alice:localhost");
|
||||
});
|
||||
|
||||
it("can cancel during the SAS phase", async () => {
|
||||
@@ -1285,7 +1288,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
@@ -1309,7 +1312,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
@@ -1334,7 +1337,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
@@ -1355,7 +1358,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers();
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
|
||||
@@ -24,11 +24,21 @@ import { KeyBackupInfo } from "../../src/crypto-api";
|
||||
* @param homeserverUrl - the homeserver url for the client under test
|
||||
*/
|
||||
export function mockInitialApiRequests(homeserverUrl: string) {
|
||||
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.postOnce(new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), {
|
||||
filter_id: "fid",
|
||||
});
|
||||
fetchMock.getOnce(
|
||||
new URL("/_matrix/client/versions", homeserverUrl).toString(),
|
||||
{ versions: ["v1.1"] },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.getOnce(
|
||||
new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(),
|
||||
{},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.postOnce(
|
||||
new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(),
|
||||
{ filter_id: "fid" },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,21 @@ describe("ContentRepo", function () {
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow redirects when requested on download URLs", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, false, true)).toEqual(
|
||||
baseUrl + "/_matrix/media/v3/download/server.name/resourceid?allow_redirect=true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow redirects when requested on thumbnail URLs", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 32, "scale", false, true)).toEqual(
|
||||
baseUrl +
|
||||
"/_matrix/media/v3/thumbnail/server.name/resourceid?width=32&height=32&method=scale&allow_redirect=true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the empty string for null input", function () {
|
||||
expect(getHttpUriForMxc(null as any, "")).toEqual("");
|
||||
});
|
||||
|
||||
@@ -189,10 +189,12 @@ describe("SAS verification", function () {
|
||||
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = async (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map.get(alice.client.getUserId()!)?.get(alice.client.deviceId!)
|
||||
?.message_authentication_code;
|
||||
keyAgreement = map.get(alice.client.getUserId()!)?.get(alice.client.deviceId!)
|
||||
?.key_agreement_protocol;
|
||||
macMethod = map
|
||||
.get(alice.client.getUserId()!)
|
||||
?.get(alice.client.deviceId!)?.message_authentication_code;
|
||||
keyAgreement = map
|
||||
.get(alice.client.getUserId()!)
|
||||
?.get(alice.client.deviceId!)?.key_agreement_protocol;
|
||||
}
|
||||
return origSendToDevice(type, map);
|
||||
};
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
RuleId,
|
||||
IPushRule,
|
||||
ConditionKind,
|
||||
getHttpUriForMxc,
|
||||
} from "../../src";
|
||||
import { supportsMatrixCall } from "../../src/webrtc/call";
|
||||
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||
@@ -64,7 +65,7 @@ import {
|
||||
PolicyScope,
|
||||
} from "../../src/models/invites-ignorer";
|
||||
import { IOlmDevice } from "../../src/crypto/algorithms/megolm";
|
||||
import { QueryDict } from "../../src/utils";
|
||||
import { defer, QueryDict } from "../../src/utils";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import * as featureUtils from "../../src/feature";
|
||||
import { StubStore } from "../../src/store/stub";
|
||||
@@ -369,6 +370,21 @@ describe("MatrixClient", function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
describe("mxcUrlToHttp", () => {
|
||||
it("should call getHttpUriForMxc", () => {
|
||||
const mxc = "mxc://server/example";
|
||||
expect(client.mxcUrlToHttp(mxc)).toBe(getHttpUriForMxc(client.baseUrl, mxc));
|
||||
expect(client.mxcUrlToHttp(mxc, 32)).toBe(getHttpUriForMxc(client.baseUrl, mxc, 32));
|
||||
expect(client.mxcUrlToHttp(mxc, 32, 46)).toBe(getHttpUriForMxc(client.baseUrl, mxc, 32, 46));
|
||||
expect(client.mxcUrlToHttp(mxc, 32, 46, "scale")).toBe(
|
||||
getHttpUriForMxc(client.baseUrl, mxc, 32, 46, "scale"),
|
||||
);
|
||||
expect(client.mxcUrlToHttp(mxc, 32, 46, "scale", false, true)).toBe(
|
||||
getHttpUriForMxc(client.baseUrl, mxc, 32, 46, "scale", false, true),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timestampToEvent", () => {
|
||||
const roomId = "!room:server.org";
|
||||
const eventId = "$eventId:example.org";
|
||||
@@ -1434,25 +1450,11 @@ describe("MatrixClient", function () {
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
updatePendingEvent: (event: MatrixEvent, status: EventStatus) => event.setStatus(status),
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
});
|
||||
} else if (eventType === EventType.RoomEncryption) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({ content: {} });
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
} as Room["currentState"],
|
||||
hasEncryptionStateEvent: jest.fn().mockReturnValue(true),
|
||||
} as unknown as Room;
|
||||
|
||||
let mockCrypto: Mocked<Crypto>;
|
||||
|
||||
let event: MatrixEvent;
|
||||
beforeEach(async () => {
|
||||
event = new MatrixEvent({
|
||||
@@ -1467,11 +1469,12 @@ describe("MatrixClient", function () {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
client.crypto = client["cryptoBackend"] = {
|
||||
// mock crypto
|
||||
encryptEvent: () => new Promise(() => {}),
|
||||
mockCrypto = {
|
||||
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true),
|
||||
encryptEvent: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
} as unknown as Crypto;
|
||||
} as unknown as Mocked<Crypto>;
|
||||
client.crypto = client["cryptoBackend"] = mockCrypto;
|
||||
});
|
||||
|
||||
function assertCancelled() {
|
||||
@@ -1488,12 +1491,21 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
it("should cancel an event which is encrypting", async () => {
|
||||
const encryptEventDefer = defer();
|
||||
mockCrypto.encryptEvent.mockReturnValue(encryptEventDefer.promise);
|
||||
|
||||
const statusPromise = testUtils.emitPromise(event, "Event.status");
|
||||
// @ts-ignore protected method access
|
||||
client.encryptAndSendEvent(mockRoom, event);
|
||||
await testUtils.emitPromise(event, "Event.status");
|
||||
const encryptAndSendPromise = client.encryptAndSendEvent(mockRoom, event);
|
||||
await statusPromise;
|
||||
expect(event.status).toBe(EventStatus.ENCRYPTING);
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
|
||||
// now let the encryption complete, and check that the message is not sent.
|
||||
encryptEventDefer.resolve();
|
||||
await encryptAndSendPromise;
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should cancel an event which is not sent", () => {
|
||||
|
||||
@@ -34,9 +34,12 @@ function makeMockEvent(originTs = 0): MatrixEvent {
|
||||
}
|
||||
|
||||
describe("CallMembership", () => {
|
||||
it("rejects membership with no expiry", () => {
|
||||
it("rejects membership with no expiry and no expires_ts", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: undefined }));
|
||||
new CallMembership(
|
||||
makeMockEvent(),
|
||||
Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: undefined }),
|
||||
);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
@@ -57,6 +60,16 @@ describe("CallMembership", () => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
|
||||
}).toThrow();
|
||||
});
|
||||
it("rejects with malformatted expires_ts", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires_ts: "string" }));
|
||||
}).toThrow();
|
||||
});
|
||||
it("rejects with malformatted expires", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: "string" }));
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("uses event timestamp if no created_ts", () => {
|
||||
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
|
||||
@@ -71,11 +84,19 @@ describe("CallMembership", () => {
|
||||
expect(membership.createdTs()).toEqual(67890);
|
||||
});
|
||||
|
||||
it("computes absolute expiry time", () => {
|
||||
it("computes absolute expiry time based on expires", () => {
|
||||
const membership = new CallMembership(makeMockEvent(1000), membershipTemplate);
|
||||
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
|
||||
});
|
||||
|
||||
it("computes absolute expiry time based on expires_ts", () => {
|
||||
const membership = new CallMembership(
|
||||
makeMockEvent(1000),
|
||||
Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: 6000 }),
|
||||
);
|
||||
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
|
||||
});
|
||||
|
||||
it("considers memberships unexpired if local age low enough", () => {
|
||||
const fakeEvent = makeMockEvent(1000);
|
||||
fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000);
|
||||
|
||||
@@ -214,8 +214,8 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
|
||||
it("sends a membership event when joining a call", () => {
|
||||
jest.useFakeTimers();
|
||||
sess!.joinRoomSession([mockFocus]);
|
||||
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
mockRoom!.roomId,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
@@ -227,6 +227,7 @@ describe("MatrixRTCSession", () => {
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 3600000,
|
||||
expires_ts: Date.now() + 3600000,
|
||||
foci_active: [{ type: "mock" }],
|
||||
membershipID: expect.stringMatching(".*"),
|
||||
},
|
||||
@@ -234,6 +235,7 @@ describe("MatrixRTCSession", () => {
|
||||
},
|
||||
"@alice:example.org",
|
||||
);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("does nothing if join called when already joined", () => {
|
||||
@@ -291,6 +293,7 @@ describe("MatrixRTCSession", () => {
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 3600000 * 2,
|
||||
expires_ts: 1000 + 3600000 * 2,
|
||||
foci_active: [{ type: "mock" }],
|
||||
created_ts: 1000,
|
||||
membershipID: expect.stringMatching(".*"),
|
||||
@@ -510,7 +513,7 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Does not emits if no membership changes", () => {
|
||||
it("Does not emit if no membership changes", () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
|
||||
@@ -591,6 +594,7 @@ describe("MatrixRTCSession", () => {
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 3600000,
|
||||
expires_ts: Date.now() + 3600000,
|
||||
foci_active: [mockFocus],
|
||||
membershipID: expect.stringMatching(".*"),
|
||||
},
|
||||
@@ -605,7 +609,7 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
it("fills in created_ts for other memberships on update", () => {
|
||||
client.sendStateEvent = jest.fn();
|
||||
|
||||
jest.useFakeTimers();
|
||||
const mockRoom = makeMockRoom([
|
||||
Object.assign({}, membershipTemplate, {
|
||||
device_id: "OTHERDEVICE",
|
||||
@@ -635,6 +639,7 @@ describe("MatrixRTCSession", () => {
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 3600000,
|
||||
expires_ts: Date.now() + 3600000,
|
||||
foci_active: [mockFocus],
|
||||
membershipID: expect.stringMatching(".*"),
|
||||
},
|
||||
@@ -642,6 +647,7 @@ describe("MatrixRTCSession", () => {
|
||||
},
|
||||
"@alice:example.org",
|
||||
);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("collects keys from encryption events", () => {
|
||||
|
||||
@@ -265,6 +265,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
|
||||
});
|
||||
const mockRoom = {
|
||||
updatePendingEvent: jest.fn(),
|
||||
hasEncryptionStateEvent: jest.fn().mockReturnValue(false),
|
||||
} as unknown as Room;
|
||||
client.resendEvent(dummyEvent, mockRoom);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
MSC3903ECDHPayload,
|
||||
MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel,
|
||||
} from "../../../src/rendezvous/channels";
|
||||
import { MatrixClient } from "../../../src";
|
||||
import { Device, MatrixClient } from "../../../src";
|
||||
import {
|
||||
MSC3886SimpleHttpRendezvousTransport,
|
||||
MSC3886SimpleHttpRendezvousTransportDetails,
|
||||
@@ -31,16 +31,57 @@ import {
|
||||
import { DummyTransport } from "./DummyTransport";
|
||||
import { decodeBase64 } from "../../../src/base64";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { CrossSigningKey, OwnDeviceKeys } from "../../../src/crypto-api";
|
||||
|
||||
type UserID = string;
|
||||
type DeviceID = string;
|
||||
type Fingerprint = string;
|
||||
type SimpleDeviceMap = Record<UserID, Record<DeviceID, Fingerprint>>;
|
||||
|
||||
function mockDevice(userId: UserID, deviceId: DeviceID, fingerprint: Fingerprint): Device {
|
||||
return {
|
||||
deviceId,
|
||||
userId,
|
||||
getFingerprint: () => fingerprint,
|
||||
} as unknown as Device;
|
||||
}
|
||||
|
||||
function mockDeviceMap(
|
||||
userId: UserID,
|
||||
deviceId: DeviceID,
|
||||
deviceKey?: Fingerprint,
|
||||
otherDevices: SimpleDeviceMap = {},
|
||||
): Map<string, Map<string, Device>> {
|
||||
const deviceMap: Map<string, Map<string, Device>> = new Map();
|
||||
|
||||
const myDevices: Map<string, Device> = new Map();
|
||||
if (deviceKey) {
|
||||
myDevices.set(deviceId, mockDevice(userId, deviceId, deviceKey));
|
||||
}
|
||||
deviceMap.set(userId, myDevices);
|
||||
|
||||
for (const u in otherDevices) {
|
||||
let userDevices = deviceMap.get(u);
|
||||
if (!userDevices) {
|
||||
userDevices = new Map();
|
||||
deviceMap.set(u, userDevices);
|
||||
}
|
||||
for (const d in otherDevices[u]) {
|
||||
userDevices.set(d, mockDevice(u, d, otherDevices[u][d]));
|
||||
}
|
||||
}
|
||||
|
||||
return deviceMap;
|
||||
}
|
||||
|
||||
function makeMockClient(opts: {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
deviceKey?: string;
|
||||
userId: UserID;
|
||||
deviceId: DeviceID;
|
||||
deviceKey?: Fingerprint;
|
||||
getLoginTokenEnabled: boolean;
|
||||
msc3882r0Only: boolean;
|
||||
msc3886Enabled: boolean;
|
||||
devices?: Record<string, Partial<DeviceInfo>>;
|
||||
devices?: SimpleDeviceMap;
|
||||
verificationFunction?: (
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
@@ -48,50 +89,77 @@ function makeMockClient(opts: {
|
||||
blocked: boolean,
|
||||
known: boolean,
|
||||
) => void;
|
||||
crossSigningIds?: Record<string, string>;
|
||||
}): MatrixClient {
|
||||
return {
|
||||
getVersions() {
|
||||
return {
|
||||
unstable_features: {
|
||||
"org.matrix.msc3882": opts.getLoginTokenEnabled,
|
||||
"org.matrix.msc3886": opts.msc3886Enabled,
|
||||
},
|
||||
};
|
||||
},
|
||||
getCapabilities() {
|
||||
return opts.msc3882r0Only
|
||||
? {}
|
||||
: {
|
||||
capabilities: {
|
||||
"m.get_login_token": {
|
||||
enabled: opts.getLoginTokenEnabled,
|
||||
crossSigningIds?: Partial<Record<CrossSigningKey, string>>;
|
||||
}): [MatrixClient, Map<string, Map<string, Device>>] {
|
||||
const deviceMap = mockDeviceMap(opts.userId, opts.deviceId, opts.deviceKey, opts.devices);
|
||||
return [
|
||||
{
|
||||
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
|
||||
if (feature === "org.matrix.msc3886") {
|
||||
return opts.msc3886Enabled;
|
||||
} else if (feature === "org.matrix.msc3882") {
|
||||
return opts.getLoginTokenEnabled;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
getVersions() {
|
||||
return {
|
||||
unstable_features: {
|
||||
"org.matrix.msc3882": opts.getLoginTokenEnabled,
|
||||
"org.matrix.msc3886": opts.msc3886Enabled,
|
||||
},
|
||||
};
|
||||
},
|
||||
getCapabilities() {
|
||||
return opts.msc3882r0Only
|
||||
? {}
|
||||
: {
|
||||
capabilities: {
|
||||
"m.get_login_token": {
|
||||
enabled: opts.getLoginTokenEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
getDeviceId() {
|
||||
return opts.deviceId;
|
||||
},
|
||||
getDeviceEd25519Key() {
|
||||
return opts.deviceKey;
|
||||
},
|
||||
baseUrl: "https://example.com",
|
||||
crypto: {
|
||||
getStoredDevice(userId: string, deviceId: string) {
|
||||
return opts.devices?.[deviceId] ?? null;
|
||||
};
|
||||
},
|
||||
setDeviceVerification: opts.verificationFunction,
|
||||
crossSigningInfo: {
|
||||
getId(key: string) {
|
||||
return opts.crossSigningIds?.[key];
|
||||
},
|
||||
getUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
},
|
||||
} as unknown as MatrixClient;
|
||||
getSafeUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
getDeviceId() {
|
||||
return opts.deviceId;
|
||||
},
|
||||
baseUrl: "https://example.com",
|
||||
getCrypto() {
|
||||
return {
|
||||
getUserDeviceInfo(
|
||||
[userId]: string[],
|
||||
downloadUncached?: boolean,
|
||||
): Promise<Map<string, Map<string, Device>>> {
|
||||
return Promise.resolve(deviceMap);
|
||||
},
|
||||
getCrossSigningKeyId(key: CrossSigningKey): string | null {
|
||||
return opts.crossSigningIds?.[key] ?? null;
|
||||
},
|
||||
setDeviceVerified(userId: string, deviceId: string, verified: boolean): Promise<void> {
|
||||
return Promise.resolve();
|
||||
},
|
||||
crossSignDevice(deviceId: string): Promise<void> {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getOwnDeviceKeys(): Promise<OwnDeviceKeys> {
|
||||
return Promise.resolve({
|
||||
ed25519: opts.deviceKey!,
|
||||
curve25519: "aaaa",
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
} as unknown as MatrixClient,
|
||||
deviceMap,
|
||||
];
|
||||
}
|
||||
|
||||
function makeTransport(name: string, uri = "https://test.rz/123456") {
|
||||
@@ -106,6 +174,7 @@ describe("Rendezvous", function () {
|
||||
let httpBackend: MockHttpBackend;
|
||||
let fetchFn: typeof global.fetch;
|
||||
let transports: DummyTransport<any, MSC3903ECDHPayload>[];
|
||||
const userId: UserID = "@user:example.com";
|
||||
|
||||
beforeEach(function () {
|
||||
httpBackend = new MockHttpBackend();
|
||||
@@ -118,9 +187,9 @@ describe("Rendezvous", function () {
|
||||
});
|
||||
|
||||
it("generate and cancel", async function () {
|
||||
const alice = makeMockClient({
|
||||
userId: "@alice:example.com",
|
||||
deviceId: "DEVICEID",
|
||||
const [alice] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
msc3886Enabled: false,
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: true,
|
||||
@@ -194,8 +263,8 @@ describe("Rendezvous", function () {
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
const [alice] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
msc3886Enabled: false,
|
||||
getLoginTokenEnabled,
|
||||
@@ -257,8 +326,8 @@ describe("Rendezvous", function () {
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
const [alice] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
@@ -316,8 +385,8 @@ describe("Rendezvous", function () {
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
const [alice] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
@@ -375,7 +444,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
const [alice] = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
@@ -436,7 +505,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
const [alice] = makeMockClient({
|
||||
userId: "alice",
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
@@ -495,7 +564,7 @@ describe("Rendezvous", function () {
|
||||
await bobCompleteProm;
|
||||
});
|
||||
|
||||
async function completeLogin(devices: Record<string, Partial<DeviceInfo>>) {
|
||||
async function completeLogin(devices: SimpleDeviceMap) {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
@@ -505,8 +574,8 @@ describe("Rendezvous", function () {
|
||||
// alice is already signs in and generates a code
|
||||
const aliceOnFailure = jest.fn();
|
||||
const aliceVerification = jest.fn();
|
||||
const alice = makeMockClient({
|
||||
userId: "alice",
|
||||
const [alice, deviceMap] = makeMockClient({
|
||||
userId,
|
||||
deviceId: "ALICE",
|
||||
getLoginTokenEnabled: true,
|
||||
msc3882r0Only: false,
|
||||
@@ -575,13 +644,14 @@ describe("Rendezvous", function () {
|
||||
aliceRz,
|
||||
bobTransport,
|
||||
bobEcdh,
|
||||
deviceMap,
|
||||
};
|
||||
}
|
||||
|
||||
it("approve on existing device + verification", async function () {
|
||||
const { bobEcdh, aliceRz } = await completeLogin({
|
||||
BOB: {
|
||||
getFingerprint: () => "bbbb",
|
||||
[userId]: {
|
||||
BOB: "bbbb",
|
||||
},
|
||||
});
|
||||
const verifyProm = aliceRz.verifyNewDeviceOnExistingDevice();
|
||||
@@ -607,33 +677,29 @@ describe("Rendezvous", function () {
|
||||
});
|
||||
|
||||
it("device appears online within timeout", async function () {
|
||||
const devices: Record<string, Partial<DeviceInfo>> = {};
|
||||
const { aliceRz } = await completeLogin(devices);
|
||||
// device appears after 1 second
|
||||
const devices: SimpleDeviceMap = {};
|
||||
const { aliceRz, deviceMap } = await completeLogin(devices);
|
||||
// device appears before the timeout
|
||||
setTimeout(() => {
|
||||
devices.BOB = {
|
||||
getFingerprint: () => "bbbb",
|
||||
};
|
||||
deviceMap.get(userId)!.set("BOB", mockDevice(userId, "BOB", "bbbb"));
|
||||
}, 1000);
|
||||
await aliceRz.verifyNewDeviceOnExistingDevice(2000);
|
||||
});
|
||||
|
||||
it("device appears online after timeout", async function () {
|
||||
const devices: Record<string, Partial<DeviceInfo>> = {};
|
||||
const { aliceRz } = await completeLogin(devices);
|
||||
// device appears after 1 second
|
||||
const devices: SimpleDeviceMap = {};
|
||||
const { aliceRz, deviceMap } = await completeLogin(devices);
|
||||
// device appears after the timeout
|
||||
setTimeout(() => {
|
||||
devices.BOB = {
|
||||
getFingerprint: () => "bbbb",
|
||||
};
|
||||
deviceMap.get(userId)!.set("BOB", mockDevice(userId, "BOB", "bbbb"));
|
||||
}, 1500);
|
||||
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("mismatched device key", async function () {
|
||||
const { aliceRz } = await completeLogin({
|
||||
BOB: {
|
||||
getFingerprint: () => "XXXX",
|
||||
[userId]: {
|
||||
BOB: "XXXX",
|
||||
},
|
||||
});
|
||||
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
|
||||
|
||||
@@ -1460,7 +1460,7 @@ describe("Room", function () {
|
||||
it("should reset the unread count when our non-synthetic receipt points to the latest event", () => {
|
||||
// Given a room with 2 events, and an unread count set.
|
||||
room.client.isInitialSyncComplete = jest.fn().mockReturnValue(true);
|
||||
room.timeline = [event1, event2];
|
||||
jest.spyOn(room, "timeline", "get").mockReturnValue([event1, event2]);
|
||||
room.setUnread(NotificationCountType.Total, 45);
|
||||
room.setUnread(NotificationCountType.Highlight, 57);
|
||||
// Sanity check:
|
||||
@@ -1479,7 +1479,7 @@ describe("Room", function () {
|
||||
it("should not reset the unread count when someone else's receipt points to the latest event", () => {
|
||||
// Given a room with 2 events, and an unread count set.
|
||||
room.client.isInitialSyncComplete = jest.fn().mockReturnValue(true);
|
||||
room.timeline = [event1, event2];
|
||||
jest.spyOn(room, "timeline", "get").mockReturnValue([event1, event2]);
|
||||
room.setUnread(NotificationCountType.Total, 45);
|
||||
room.setUnread(NotificationCountType.Highlight, 57);
|
||||
// Sanity check:
|
||||
@@ -1498,7 +1498,7 @@ describe("Room", function () {
|
||||
it("should not reset the unread count when our non-synthetic receipt points to an earlier event", () => {
|
||||
// Given a room with 2 events, and an unread count set.
|
||||
room.client.isInitialSyncComplete = jest.fn().mockReturnValue(true);
|
||||
room.timeline = [event1, event2];
|
||||
jest.spyOn(room, "timeline", "get").mockReturnValue([event1, event2]);
|
||||
room.setUnread(NotificationCountType.Total, 45);
|
||||
room.setUnread(NotificationCountType.Highlight, 57);
|
||||
// Sanity check:
|
||||
@@ -1517,7 +1517,7 @@ describe("Room", function () {
|
||||
it("should not reset the unread count when our a synthetic receipt points to the latest event", () => {
|
||||
// Given a room with 2 events, and an unread count set.
|
||||
room.client.isInitialSyncComplete = jest.fn().mockReturnValue(true);
|
||||
room.timeline = [event1, event2];
|
||||
jest.spyOn(room, "timeline", "get").mockReturnValue([event1, event2]);
|
||||
room.setUnread(NotificationCountType.Total, 45);
|
||||
room.setUnread(NotificationCountType.Highlight, 57);
|
||||
// Sanity check:
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
} from "../../../src";
|
||||
import { mkEvent } from "../../test-utils/test-utils";
|
||||
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
|
||||
import { IEventDecryptionResult } from "../../../src/@types/crypto";
|
||||
import { IEventDecryptionResult, IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import {
|
||||
AccountDataClient,
|
||||
@@ -75,6 +75,12 @@ import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/b
|
||||
const TEST_USER = "@alice:example.com";
|
||||
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||
|
||||
beforeAll(async () => {
|
||||
// Load the WASM upfront, before any of the tests. This can take some time, and doing it here means that it gets
|
||||
// a separate timeout.
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
}, 15000);
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
jest.restoreAllMocks();
|
||||
@@ -88,6 +94,7 @@ describe("initRustCrypto", () => {
|
||||
getSecretsFromInbox: jest.fn().mockResolvedValue([]),
|
||||
deleteSecretsFromInbox: jest.fn(),
|
||||
registerReceiveSecretCallback: jest.fn(),
|
||||
registerDevicesUpdatedCallback: jest.fn(),
|
||||
outgoingRequests: jest.fn(),
|
||||
isBackupEnabled: jest.fn().mockResolvedValue(false),
|
||||
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),
|
||||
@@ -161,6 +168,21 @@ describe("initRustCrypto", () => {
|
||||
});
|
||||
|
||||
describe("libolm migration", () => {
|
||||
let mockStore: RustSdkCryptoJs.StoreHandle;
|
||||
|
||||
beforeEach(() => {
|
||||
// Stub out a bunch of stuff in the Rust library
|
||||
mockStore = { free: jest.fn() } as unknown as StoreHandle;
|
||||
jest.spyOn(StoreHandle, "open").mockResolvedValue(mockStore);
|
||||
|
||||
jest.spyOn(Migration, "migrateBaseData").mockResolvedValue(undefined);
|
||||
jest.spyOn(Migration, "migrateOlmSessions").mockResolvedValue(undefined);
|
||||
jest.spyOn(Migration, "migrateMegolmSessions").mockResolvedValue(undefined);
|
||||
|
||||
const testOlmMachine = makeTestOlmMachine();
|
||||
jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine);
|
||||
});
|
||||
|
||||
it("migrates data from a legacy crypto store", async () => {
|
||||
const PICKLE_KEY = "pickle1234";
|
||||
const legacyStore = new MemoryCryptoStore();
|
||||
@@ -180,17 +202,6 @@ describe("initRustCrypto", () => {
|
||||
createMegolmSessions(legacyStore, nDevices, nSessionsPerDevice);
|
||||
await legacyStore.markSessionsNeedingBackup([{ senderKey: pad43("device5"), sessionId: "session5" }]);
|
||||
|
||||
// Stub out a bunch of stuff in the Rust library
|
||||
const mockStore = { free: jest.fn() } as unknown as StoreHandle;
|
||||
jest.spyOn(StoreHandle, "open").mockResolvedValue(mockStore);
|
||||
|
||||
jest.spyOn(Migration, "migrateBaseData").mockResolvedValue(undefined);
|
||||
jest.spyOn(Migration, "migrateOlmSessions").mockResolvedValue(undefined);
|
||||
jest.spyOn(Migration, "migrateMegolmSessions").mockResolvedValue(undefined);
|
||||
|
||||
const testOlmMachine = makeTestOlmMachine();
|
||||
jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", { version: "45" });
|
||||
|
||||
function legacyMigrationProgressListener(progress: number, total: number): void {
|
||||
@@ -269,12 +280,59 @@ describe("initRustCrypto", () => {
|
||||
expect(session.senderKey).toEqual(pad43(`device${i}`));
|
||||
expect(session.pickle).toEqual("sessionPickle");
|
||||
expect(session.roomId!.toString()).toEqual("!room:id");
|
||||
expect(session.senderSigningKey).toEqual("sender_signing_key");
|
||||
|
||||
// only one of the sessions needs backing up
|
||||
expect(session.backedUp).toEqual(i !== 5 || j !== 5);
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
it("handles megolm sessions with no `keysClaimed`", async () => {
|
||||
const legacyStore = new MemoryCryptoStore();
|
||||
legacyStore.storeAccount({}, "not a real account");
|
||||
|
||||
legacyStore.storeEndToEndInboundGroupSession(
|
||||
pad43(`device1`),
|
||||
`session1`,
|
||||
{
|
||||
forwardingCurve25519KeyChain: [],
|
||||
room_id: "!room:id",
|
||||
session: "sessionPickle",
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
const PICKLE_KEY = "pickle1234";
|
||||
await initRustCrypto({
|
||||
logger,
|
||||
http: makeMatrixHttpApi(),
|
||||
userId: TEST_USER,
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
secretStorage: {} as ServerSideSecretStorage,
|
||||
cryptoCallbacks: {} as CryptoCallbacks,
|
||||
storePrefix: "storePrefix",
|
||||
storePassphrase: "storePassphrase",
|
||||
legacyCryptoStore: legacyStore,
|
||||
legacyPickleKey: PICKLE_KEY,
|
||||
});
|
||||
|
||||
expect(Migration.migrateMegolmSessions).toHaveBeenCalledTimes(1);
|
||||
expect(Migration.migrateMegolmSessions).toHaveBeenCalledWith(
|
||||
expect.any(Array),
|
||||
new Uint8Array(Buffer.from(PICKLE_KEY)),
|
||||
mockStore,
|
||||
);
|
||||
const megolmSessions: PickledInboundGroupSession[] = mocked(Migration.migrateMegolmSessions).mock
|
||||
.calls[0][0];
|
||||
expect(megolmSessions.length).toEqual(1);
|
||||
const session = megolmSessions[0];
|
||||
expect(session.senderKey).toEqual(pad43(`device1`));
|
||||
expect(session.pickle).toEqual("sessionPickle");
|
||||
expect(session.roomId!.toString()).toEqual("!room:id");
|
||||
expect(session.senderSigningKey).toBe(undefined);
|
||||
}, 10000);
|
||||
|
||||
async function encryptAndStoreSecretKey(type: string, key: Uint8Array, pickleKey: string, store: CryptoStore) {
|
||||
const encryptedKey = await encryptAES(encodeBase64(key), Buffer.from(pickleKey), type);
|
||||
store.storeSecretStorePrivateKey(undefined, type as keyof SecretStorePrivateKeys, encryptedKey);
|
||||
@@ -639,6 +697,58 @@ describe("RustCrypto", () => {
|
||||
await awaitCallToMakeOutgoingRequest();
|
||||
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should encode outgoing requests properly", async () => {
|
||||
// we need a real OlmMachine, so replace the one created by beforeEach
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
|
||||
|
||||
const outgoingRequestProcessor = {} as unknown as OutgoingRequestProcessor;
|
||||
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
|
||||
const outgoingRequestsManager = new OutgoingRequestsManager(logger, olmMachine, outgoingRequestProcessor);
|
||||
rustCrypto["outgoingRequestsManager"] = outgoingRequestsManager;
|
||||
|
||||
// The second time we do a /keys/upload, the `device_keys` property
|
||||
// should be absent from the request body
|
||||
// cf. https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/issues/57
|
||||
//
|
||||
// On the first upload, we pretend that there are no OTKs, so it will
|
||||
// try to upload more keys
|
||||
let keysUploadCount = 0;
|
||||
let deviceKeys: object;
|
||||
let deviceKeysAbsent = false;
|
||||
outgoingRequestProcessor.makeOutgoingRequest = jest.fn(async (request, uiaCallback?) => {
|
||||
let resp: any = {};
|
||||
if (request instanceof RustSdkCryptoJs.KeysUploadRequest) {
|
||||
if (keysUploadCount == 0) {
|
||||
deviceKeys = JSON.parse(request.body).device_keys;
|
||||
resp = { one_time_key_counts: { signed_curve25519: 0 } };
|
||||
} else {
|
||||
deviceKeysAbsent = !("device_keys" in JSON.parse(request.body));
|
||||
resp = { one_time_key_counts: { signed_curve25519: 50 } };
|
||||
}
|
||||
keysUploadCount++;
|
||||
} else if (request instanceof RustSdkCryptoJs.KeysQueryRequest) {
|
||||
resp = {
|
||||
device_keys: {
|
||||
[TEST_USER]: {
|
||||
[TEST_DEVICE_ID]: deviceKeys,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (request instanceof RustSdkCryptoJs.UploadSigningKeysRequest) {
|
||||
// SigningKeysUploadRequest does not implement OutgoingRequest and does not need to be marked as sent.
|
||||
return;
|
||||
}
|
||||
if (request.id) {
|
||||
olmMachine.markRequestAsSent(request.id, request.type, JSON.stringify(resp));
|
||||
}
|
||||
});
|
||||
await outgoingRequestsManager.doProcessOutgoingRequests();
|
||||
await outgoingRequestsManager.doProcessOutgoingRequests();
|
||||
|
||||
expect(deviceKeysAbsent).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".getEventEncryptionInfo", () => {
|
||||
@@ -997,7 +1107,8 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
|
||||
it("should wait for a keys/query before returning devices", async () => {
|
||||
jest.useFakeTimers();
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", {
|
||||
@@ -1022,6 +1133,33 @@ describe("RustCrypto", () => {
|
||||
rustCrypto.stop();
|
||||
});
|
||||
|
||||
it("should emit events on device changes", async () => {
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", {
|
||||
device_keys: {
|
||||
[testData.TEST_USER_ID]: {
|
||||
[testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), testData.TEST_USER_ID);
|
||||
const willUpdateCallback = jest.fn();
|
||||
rustCrypto.on(CryptoEvent.WillUpdateDevices, willUpdateCallback);
|
||||
const devicesUpdatedCallback = jest.fn();
|
||||
rustCrypto.on(CryptoEvent.DevicesUpdated, devicesUpdatedCallback);
|
||||
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
// wait for the devices to be updated
|
||||
await rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]);
|
||||
expect(willUpdateCallback).toHaveBeenCalledWith([testData.TEST_USER_ID], false);
|
||||
expect(devicesUpdatedCallback).toHaveBeenCalledWith([testData.TEST_USER_ID], false);
|
||||
rustCrypto.stop();
|
||||
});
|
||||
|
||||
describe("requestDeviceVerification", () => {
|
||||
it("throws an error if the device is unknown", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
@@ -1202,6 +1340,34 @@ describe("RustCrypto", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores invalid keys when restoring from backup", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
|
||||
|
||||
await olmMachine.enableBackupV1(
|
||||
(testData.SIGNED_BACKUP_DATA.auth_data as Curve25519AuthData).public_key,
|
||||
testData.SIGNED_BACKUP_DATA.version!,
|
||||
);
|
||||
|
||||
const backup = Array.from(testData.MEGOLM_SESSION_DATA_ARRAY);
|
||||
// in addition to correct keys, we restore an invalid key
|
||||
backup.push({ room_id: "!roomid", session_id: "sessionid" } as IMegolmSessionData);
|
||||
const progressCallback = jest.fn();
|
||||
await rustCrypto.importBackedUpRoomKeys(backup, { progressCallback });
|
||||
expect(progressCallback).toHaveBeenCalledWith({
|
||||
total: 3,
|
||||
successes: 0,
|
||||
stage: "load_keys",
|
||||
failures: 1,
|
||||
});
|
||||
expect(progressCallback).toHaveBeenCalledWith({
|
||||
total: 3,
|
||||
successes: 1,
|
||||
stage: "load_keys",
|
||||
failures: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,8 +17,19 @@ limitations under the License.
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import { Mocked } from "jest-mock";
|
||||
|
||||
import { isVerificationEvent, RustVerificationRequest } from "../../../src/rust-crypto/verification";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import {
|
||||
isVerificationEvent,
|
||||
RustVerificationRequest,
|
||||
verificationMethodIdentifierToMethod,
|
||||
} from "../../../src/rust-crypto/verification";
|
||||
import {
|
||||
ShowSasCallbacks,
|
||||
VerificationRequestEvent,
|
||||
Verifier,
|
||||
VerifierEvent,
|
||||
} from "../../../src/crypto-api/verification";
|
||||
import { OutgoingRequest, OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import { IDeviceKeys } from "../../../src/@types/crypto";
|
||||
import { EventType, MatrixEvent, MsgType } from "../../../src";
|
||||
|
||||
describe("VerificationRequest", () => {
|
||||
@@ -91,6 +102,354 @@ describe("VerificationRequest", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("can verify with SAS", async () => {
|
||||
const aliceUserId = "@alice:example.org";
|
||||
const aliceDeviceId = "ABCDEFG";
|
||||
const bobUserId = "@bob:example.org";
|
||||
const bobDeviceId = "HIJKLMN";
|
||||
const [aliceOlmMachine, aliceDeviceKeys, aliceCrossSigningKeys] = await initOlmMachineAndKeys(
|
||||
aliceUserId,
|
||||
aliceDeviceId,
|
||||
);
|
||||
const [bobOlmMachine, bobDeviceKeys, bobCrossSigningKeys] = await initOlmMachineAndKeys(bobUserId, bobDeviceId);
|
||||
|
||||
const aliceRequestLoop = makeRequestLoop(
|
||||
aliceOlmMachine,
|
||||
aliceDeviceKeys,
|
||||
aliceCrossSigningKeys,
|
||||
bobOlmMachine,
|
||||
bobDeviceKeys,
|
||||
bobCrossSigningKeys,
|
||||
);
|
||||
const bobRequestLoop = makeRequestLoop(
|
||||
bobOlmMachine,
|
||||
bobDeviceKeys,
|
||||
bobCrossSigningKeys,
|
||||
aliceOlmMachine,
|
||||
aliceDeviceKeys,
|
||||
aliceCrossSigningKeys,
|
||||
);
|
||||
|
||||
try {
|
||||
await aliceOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(bobUserId)]);
|
||||
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
|
||||
|
||||
// Alice requests verification
|
||||
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId));
|
||||
|
||||
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
|
||||
const methods = [verificationMethodIdentifierToMethod("m.sas.v1")];
|
||||
const innerVerificationRequest = await bobUserIdentity.requestVerification(
|
||||
roomId,
|
||||
new RustSdkCryptoJs.EventId("$m.key.verification.request"),
|
||||
methods,
|
||||
);
|
||||
const aliceVerificationRequest = new RustVerificationRequest(
|
||||
aliceOlmMachine,
|
||||
innerVerificationRequest,
|
||||
aliceRequestLoop as unknown as OutgoingRequestProcessor,
|
||||
["m.sas.v1"],
|
||||
);
|
||||
|
||||
const verificationRequestContent = JSON.parse(await bobUserIdentity.verificationRequestContent(methods));
|
||||
await bobOlmMachine.receiveVerificationEvent(
|
||||
JSON.stringify({
|
||||
type: "m.room.message",
|
||||
sender: aliceUserId,
|
||||
event_id: "$m.key.verification.request",
|
||||
content: verificationRequestContent,
|
||||
origin_server_ts: Date.now(),
|
||||
unsigned: {
|
||||
age: 0,
|
||||
},
|
||||
}),
|
||||
roomId,
|
||||
);
|
||||
|
||||
// Bob accepts
|
||||
const bobInnerVerificationRequest = bobOlmMachine.getVerificationRequest(
|
||||
new RustSdkCryptoJs.UserId(aliceUserId),
|
||||
"$m.key.verification.request",
|
||||
)!;
|
||||
const bobVerificationRequest = new RustVerificationRequest(
|
||||
bobOlmMachine,
|
||||
bobInnerVerificationRequest,
|
||||
bobRequestLoop as unknown as OutgoingRequestProcessor,
|
||||
["m.sas.v1"],
|
||||
);
|
||||
|
||||
await bobVerificationRequest.accept();
|
||||
|
||||
// Alice starts the verification
|
||||
const bobVerifierPromise: Promise<Verifier> = new Promise((resolve, reject) => {
|
||||
bobVerificationRequest.on(VerificationRequestEvent.Change, () => {
|
||||
const verifier = bobVerificationRequest.verifier;
|
||||
if (verifier) {
|
||||
resolve(verifier);
|
||||
}
|
||||
});
|
||||
});
|
||||
const aliceVerifier = await aliceVerificationRequest.startVerification("m.sas.v1");
|
||||
const bobVerifier = await bobVerifierPromise;
|
||||
|
||||
// create a function to compare the SAS, and then let the verification run
|
||||
let otherCallbacks: ShowSasCallbacks | undefined;
|
||||
const compareSas = (callbacks: ShowSasCallbacks): void => {
|
||||
if (otherCallbacks) {
|
||||
const ourDecimal = callbacks.sas.decimal!;
|
||||
const theirDecimal = otherCallbacks.sas.decimal!;
|
||||
if (ourDecimal.every((el, idx) => el == theirDecimal[idx])) {
|
||||
otherCallbacks.confirm();
|
||||
callbacks.confirm();
|
||||
} else {
|
||||
otherCallbacks.mismatch();
|
||||
callbacks.mismatch();
|
||||
}
|
||||
} else {
|
||||
otherCallbacks = callbacks;
|
||||
}
|
||||
};
|
||||
aliceVerifier.on(VerifierEvent.ShowSas, compareSas);
|
||||
bobVerifier.on(VerifierEvent.ShowSas, compareSas);
|
||||
|
||||
await Promise.all([aliceVerifier.verify(), await bobVerifier.verify()]);
|
||||
} finally {
|
||||
await aliceRequestLoop.stop();
|
||||
await bobRequestLoop.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("can handle simultaneous starts in SAS", async () => {
|
||||
const aliceUserId = "@alice:example.org";
|
||||
const aliceDeviceId = "ABCDEFG";
|
||||
const bobUserId = "@bob:example.org";
|
||||
const bobDeviceId = "HIJKLMN";
|
||||
const [aliceOlmMachine, aliceDeviceKeys, aliceCrossSigningKeys] = await initOlmMachineAndKeys(
|
||||
aliceUserId,
|
||||
aliceDeviceId,
|
||||
);
|
||||
const [bobOlmMachine, bobDeviceKeys, bobCrossSigningKeys] = await initOlmMachineAndKeys(bobUserId, bobDeviceId);
|
||||
|
||||
let aliceStartRequest: RustSdkCryptoJs.RoomMessageRequest | undefined;
|
||||
const aliceRequestLoop = makeRequestLoop(
|
||||
aliceOlmMachine,
|
||||
aliceDeviceKeys,
|
||||
aliceCrossSigningKeys,
|
||||
bobOlmMachine,
|
||||
bobDeviceKeys,
|
||||
bobCrossSigningKeys,
|
||||
async (request): Promise<any> => {
|
||||
// If the request is sending the m.key.verification.start
|
||||
// event, we delay sending it until after Bob has also started
|
||||
// a verification
|
||||
if (
|
||||
!aliceStartRequest &&
|
||||
request instanceof RustSdkCryptoJs.RoomMessageRequest &&
|
||||
request.event_type == "m.key.verification.start"
|
||||
) {
|
||||
aliceStartRequest = request;
|
||||
return { event_id: "$m.key.verification.start" };
|
||||
}
|
||||
},
|
||||
);
|
||||
const bobRequestLoop = makeRequestLoop(
|
||||
bobOlmMachine,
|
||||
bobDeviceKeys,
|
||||
bobCrossSigningKeys,
|
||||
aliceOlmMachine,
|
||||
aliceDeviceKeys,
|
||||
aliceCrossSigningKeys,
|
||||
);
|
||||
|
||||
try {
|
||||
await aliceOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(bobUserId)]);
|
||||
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
|
||||
|
||||
// Alice requests verification
|
||||
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId));
|
||||
|
||||
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
|
||||
const methods = [verificationMethodIdentifierToMethod("m.sas.v1")];
|
||||
const innerVerificationRequest = await bobUserIdentity.requestVerification(
|
||||
roomId,
|
||||
new RustSdkCryptoJs.EventId("$m.key.verification.request"),
|
||||
methods,
|
||||
);
|
||||
const aliceVerificationRequest = new RustVerificationRequest(
|
||||
aliceOlmMachine,
|
||||
innerVerificationRequest,
|
||||
aliceRequestLoop as unknown as OutgoingRequestProcessor,
|
||||
["m.sas.v1"],
|
||||
);
|
||||
|
||||
const verificationRequestContent = JSON.parse(await bobUserIdentity.verificationRequestContent(methods));
|
||||
await bobOlmMachine.receiveVerificationEvent(
|
||||
JSON.stringify({
|
||||
type: "m.room.message",
|
||||
sender: aliceUserId,
|
||||
event_id: "$m.key.verification.request",
|
||||
content: verificationRequestContent,
|
||||
origin_server_ts: Date.now(),
|
||||
unsigned: {
|
||||
age: 0,
|
||||
},
|
||||
}),
|
||||
roomId,
|
||||
);
|
||||
|
||||
// Bob accepts
|
||||
const bobInnerVerificationRequest = bobOlmMachine.getVerificationRequest(
|
||||
new RustSdkCryptoJs.UserId(aliceUserId),
|
||||
"$m.key.verification.request",
|
||||
)!;
|
||||
const bobVerificationRequest = new RustVerificationRequest(
|
||||
bobOlmMachine,
|
||||
bobInnerVerificationRequest,
|
||||
bobRequestLoop as unknown as OutgoingRequestProcessor,
|
||||
["m.sas.v1"],
|
||||
);
|
||||
|
||||
await bobVerificationRequest.accept();
|
||||
|
||||
// Alice and Bob both start the verification
|
||||
const aliceVerifier = await aliceVerificationRequest.startVerification("m.sas.v1");
|
||||
const bobVerifier = await bobVerificationRequest.startVerification("m.sas.v1");
|
||||
// We can now send Alice's start message to Bob
|
||||
await aliceRequestLoop.makeOutgoingRequest(aliceStartRequest!);
|
||||
|
||||
// create a function to compare the SAS, and then let the verification run
|
||||
let otherCallbacks: ShowSasCallbacks | undefined;
|
||||
const compareSas = (callbacks: ShowSasCallbacks) => {
|
||||
if (otherCallbacks) {
|
||||
const ourDecimal = callbacks.sas.decimal!;
|
||||
const theirDecimal = otherCallbacks.sas.decimal!;
|
||||
if (ourDecimal.every((el, idx) => el == theirDecimal[idx])) {
|
||||
otherCallbacks.confirm();
|
||||
callbacks.confirm();
|
||||
} else {
|
||||
otherCallbacks.mismatch();
|
||||
callbacks.mismatch();
|
||||
}
|
||||
} else {
|
||||
otherCallbacks = callbacks;
|
||||
}
|
||||
};
|
||||
aliceVerifier.on(VerifierEvent.ShowSas, compareSas);
|
||||
bobVerifier.on(VerifierEvent.ShowSas, compareSas);
|
||||
|
||||
await Promise.all([aliceVerifier.verify(), await bobVerifier.verify()]);
|
||||
} finally {
|
||||
await aliceRequestLoop.stop();
|
||||
await bobRequestLoop.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("can verify by QR code", async () => {
|
||||
const aliceUserId = "@alice:example.org";
|
||||
const aliceDeviceId = "ABCDEFG";
|
||||
const bobUserId = "@bob:example.org";
|
||||
const bobDeviceId = "HIJKLMN";
|
||||
const [aliceOlmMachine, aliceDeviceKeys, aliceCrossSigningKeys] = await initOlmMachineAndKeys(
|
||||
aliceUserId,
|
||||
aliceDeviceId,
|
||||
);
|
||||
const [bobOlmMachine, bobDeviceKeys, bobCrossSigningKeys] = await initOlmMachineAndKeys(bobUserId, bobDeviceId);
|
||||
|
||||
const aliceRequestLoop = makeRequestLoop(
|
||||
aliceOlmMachine,
|
||||
aliceDeviceKeys,
|
||||
aliceCrossSigningKeys,
|
||||
bobOlmMachine,
|
||||
bobDeviceKeys,
|
||||
bobCrossSigningKeys,
|
||||
);
|
||||
const bobRequestLoop = makeRequestLoop(
|
||||
bobOlmMachine,
|
||||
bobDeviceKeys,
|
||||
bobCrossSigningKeys,
|
||||
aliceOlmMachine,
|
||||
aliceDeviceKeys,
|
||||
aliceCrossSigningKeys,
|
||||
);
|
||||
|
||||
try {
|
||||
await aliceOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(bobUserId)]);
|
||||
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
|
||||
|
||||
// Alice requests verification
|
||||
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId));
|
||||
|
||||
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
|
||||
const methods = [
|
||||
verificationMethodIdentifierToMethod("m.reciprocate.v1"),
|
||||
verificationMethodIdentifierToMethod("m.qr_code.show.v1"),
|
||||
];
|
||||
const innerVerificationRequest = await bobUserIdentity.requestVerification(
|
||||
roomId,
|
||||
new RustSdkCryptoJs.EventId("$m.key.verification.request"),
|
||||
methods,
|
||||
);
|
||||
const aliceVerificationRequest = new RustVerificationRequest(
|
||||
aliceOlmMachine,
|
||||
innerVerificationRequest,
|
||||
aliceRequestLoop as unknown as OutgoingRequestProcessor,
|
||||
["m.reciprocate.v1", "m.qr_code.show.v1"],
|
||||
);
|
||||
|
||||
const verificationRequestContent = JSON.parse(await bobUserIdentity.verificationRequestContent(methods));
|
||||
await bobOlmMachine.receiveVerificationEvent(
|
||||
JSON.stringify({
|
||||
type: "m.room.message",
|
||||
sender: aliceUserId,
|
||||
event_id: "$m.key.verification.request",
|
||||
content: verificationRequestContent,
|
||||
origin_server_ts: Date.now(),
|
||||
unsigned: {
|
||||
age: 0,
|
||||
},
|
||||
}),
|
||||
roomId,
|
||||
);
|
||||
|
||||
// Bob accepts
|
||||
const bobInnerVerificationRequest = bobOlmMachine.getVerificationRequest(
|
||||
new RustSdkCryptoJs.UserId(aliceUserId),
|
||||
"$m.key.verification.request",
|
||||
)!;
|
||||
const bobVerificationRequest = new RustVerificationRequest(
|
||||
bobOlmMachine,
|
||||
bobInnerVerificationRequest,
|
||||
bobRequestLoop as unknown as OutgoingRequestProcessor,
|
||||
["m.reciprocate.v1", "m.qr_code.show.v1", "m.qr_code.scan.v1"],
|
||||
);
|
||||
|
||||
await bobVerificationRequest.accept();
|
||||
|
||||
// Bob scans
|
||||
const qrCode = await aliceVerificationRequest.generateQRCode();
|
||||
|
||||
const aliceVerifierPromise: Promise<Verifier> = new Promise((resolve, reject) => {
|
||||
aliceVerificationRequest.on(VerificationRequestEvent.Change, () => {
|
||||
const verifier = aliceVerificationRequest.verifier;
|
||||
if (verifier) {
|
||||
resolve(verifier);
|
||||
}
|
||||
});
|
||||
});
|
||||
const bobVerifier = await bobVerificationRequest.scanQRCode(qrCode!);
|
||||
|
||||
const aliceVerifier = await aliceVerifierPromise;
|
||||
aliceVerifier.on(VerifierEvent.ShowReciprocateQr, (showQrCodeCallbacks) => {
|
||||
showQrCodeCallbacks.confirm();
|
||||
});
|
||||
|
||||
await Promise.all([aliceVerifier.verify(), await bobVerifier.verify()]);
|
||||
} finally {
|
||||
await aliceRequestLoop.stop();
|
||||
await bobRequestLoop.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("isVerificationEvent", () => {
|
||||
@@ -152,3 +511,148 @@ function makeMockedInner(): Mocked<RustSdkCryptoJs.VerificationRequest> {
|
||||
},
|
||||
} as unknown as Mocked<RustSdkCryptoJs.VerificationRequest>;
|
||||
}
|
||||
|
||||
interface CrossSigningKeys {
|
||||
master_key: any;
|
||||
self_signing_key: any;
|
||||
user_signing_key: any;
|
||||
}
|
||||
|
||||
/** create an Olm machine and device/cross-signing keys for a user */
|
||||
async function initOlmMachineAndKeys(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
): Promise<[RustSdkCryptoJs.OlmMachine, IDeviceKeys, CrossSigningKeys]> {
|
||||
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(
|
||||
new RustSdkCryptoJs.UserId(userId),
|
||||
new RustSdkCryptoJs.DeviceId(deviceId),
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
const { uploadKeysRequest, uploadSignaturesRequest, uploadSigningKeysRequest } =
|
||||
await olmMachine.bootstrapCrossSigning(true);
|
||||
const deviceKeys = JSON.parse(uploadKeysRequest.body).device_keys;
|
||||
await olmMachine.markRequestAsSent(
|
||||
uploadKeysRequest.id,
|
||||
uploadKeysRequest.type,
|
||||
'{"one_time_key_counts":{"signed_curve25519":100}}',
|
||||
);
|
||||
const crossSigningSignatures = JSON.parse(uploadSignaturesRequest.body);
|
||||
for (const [keyId, signature] of Object.entries(crossSigningSignatures[userId][deviceId]["signatures"][userId])) {
|
||||
deviceKeys["signatures"][userId][keyId] = signature;
|
||||
}
|
||||
const crossSigningKeys = JSON.parse(uploadSigningKeysRequest.body);
|
||||
// note: the upload signatures request and upload signing keys requests
|
||||
// don't need to be marked as sent in the Olm machine
|
||||
|
||||
return [olmMachine, deviceKeys, crossSigningKeys];
|
||||
}
|
||||
|
||||
type CustomRequestHandler = (request: OutgoingRequest | RustSdkCryptoJs.UploadSigningKeysRequest) => Promise<any>;
|
||||
|
||||
/** Loop for handling outgoing requests from an Olm machine.
|
||||
*
|
||||
* Simulates a server with two users: "us" and "them". Handles key query
|
||||
* requests, querying either our keys or the other user's keys. Room messages
|
||||
* are sent as incoming verification events to the other user. A custom
|
||||
* handler can be added to override default request processing (the handler
|
||||
* should return a response body to inhibit default processing).
|
||||
*
|
||||
* Can also be used as an OutgoingRequestProcessor. */
|
||||
function makeRequestLoop(
|
||||
ourOlmMachine: RustSdkCryptoJs.OlmMachine,
|
||||
ourDeviceKeys: IDeviceKeys,
|
||||
ourCrossSigningKeys: CrossSigningKeys,
|
||||
theirOlmMachine: RustSdkCryptoJs.OlmMachine,
|
||||
theirDeviceKeys: IDeviceKeys,
|
||||
theirCrossSigningKeys: CrossSigningKeys,
|
||||
customHandler?: CustomRequestHandler,
|
||||
) {
|
||||
let stopRequestLoop = false;
|
||||
const ourUserId = ourOlmMachine.userId.toString();
|
||||
const ourDeviceId = ourOlmMachine.deviceId.toString();
|
||||
const theirUserId = theirOlmMachine.userId.toString();
|
||||
const theirDeviceId = theirOlmMachine.deviceId.toString();
|
||||
|
||||
function defaultHandler(request: OutgoingRequest | RustSdkCryptoJs.UploadSigningKeysRequest): any {
|
||||
if (request instanceof RustSdkCryptoJs.KeysQueryRequest) {
|
||||
const resp: Record<string, any> = {
|
||||
device_keys: {},
|
||||
};
|
||||
const body = JSON.parse(request.body);
|
||||
const query = body.device_keys;
|
||||
const masterKeys: Record<string, any> = {};
|
||||
const selfSigningKeys: Record<string, any> = {};
|
||||
if (ourUserId in query) {
|
||||
resp.device_keys[ourUserId] = { [ourDeviceId]: ourDeviceKeys };
|
||||
masterKeys[ourUserId] = ourCrossSigningKeys.master_key;
|
||||
selfSigningKeys[ourUserId] = ourCrossSigningKeys.self_signing_key;
|
||||
resp.user_signing_keys = {
|
||||
[ourUserId]: ourCrossSigningKeys.user_signing_key,
|
||||
};
|
||||
}
|
||||
if (theirUserId in query) {
|
||||
resp.device_keys[theirUserId] = {
|
||||
[theirDeviceId]: theirDeviceKeys,
|
||||
};
|
||||
masterKeys[theirUserId] = theirCrossSigningKeys.master_key;
|
||||
selfSigningKeys[theirUserId] = theirCrossSigningKeys.self_signing_key;
|
||||
}
|
||||
if (Object.keys(masterKeys).length) {
|
||||
resp.master_keys = masterKeys;
|
||||
}
|
||||
if (Object.keys(selfSigningKeys).length) {
|
||||
resp.self_signing_keys = selfSigningKeys;
|
||||
}
|
||||
return resp;
|
||||
} else if (request instanceof RustSdkCryptoJs.RoomMessageRequest) {
|
||||
theirOlmMachine.receiveVerificationEvent(
|
||||
JSON.stringify({
|
||||
type: request.event_type,
|
||||
sender: ourUserId,
|
||||
event_id: "$" + request.event_type,
|
||||
content: JSON.parse(request.body),
|
||||
origin_server_ts: Date.now(),
|
||||
unsigned: {
|
||||
age: 0,
|
||||
},
|
||||
}),
|
||||
new RustSdkCryptoJs.RoomId(request.room_id),
|
||||
);
|
||||
return { event_id: "$" + request.event_type };
|
||||
} else if (request instanceof RustSdkCryptoJs.SignatureUploadRequest) {
|
||||
// this only gets called at the end after the verification
|
||||
// succeeds, so we don't actually have to do anything.
|
||||
return { failures: {} };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function makeOutgoingRequest(
|
||||
request: OutgoingRequest | RustSdkCryptoJs.UploadSigningKeysRequest,
|
||||
): Promise<any> {
|
||||
const resp = (await customHandler?.(request)) ?? defaultHandler(request);
|
||||
if (!(request instanceof RustSdkCryptoJs.UploadSigningKeysRequest) && request.id) {
|
||||
await ourOlmMachine.markRequestAsSent(request.id!, request.type, JSON.stringify(resp));
|
||||
}
|
||||
}
|
||||
|
||||
async function runLoop() {
|
||||
while (!stopRequestLoop) {
|
||||
const requests = await ourOlmMachine.outgoingRequests();
|
||||
for (const request of requests) {
|
||||
await makeOutgoingRequest(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loopCompletedPromise = runLoop();
|
||||
|
||||
return {
|
||||
makeOutgoingRequest,
|
||||
stop: async () => {
|
||||
stopRequestLoop = true;
|
||||
await loopCompletedPromise;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,9 +23,8 @@ import { isProvided } from "../extensible_events_v1/utilities";
|
||||
/**
|
||||
* Represents the stable and unstable values of a given namespace.
|
||||
*/
|
||||
export type TSNamespace<N> = N extends NamespacedValue<infer S, infer U>
|
||||
? TSNamespaceValue<S> | TSNamespaceValue<U>
|
||||
: never;
|
||||
export type TSNamespace<N> =
|
||||
N extends NamespacedValue<infer S, infer U> ? TSNamespaceValue<S> | TSNamespaceValue<U> : never;
|
||||
|
||||
/**
|
||||
* Represents a namespaced value, if the value is a string. Used to extract provided types
|
||||
|
||||
+125
-91
@@ -1305,7 +1305,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
protected txnCtr = 0;
|
||||
protected mediaHandler = new MediaHandler(this);
|
||||
protected sessionId: string;
|
||||
protected pendingEventEncryption = new Map<string, Promise<void>>();
|
||||
|
||||
/** IDs of events which are currently being encrypted.
|
||||
*
|
||||
* This is part of the cancellation mechanism: if the event is no longer listed here when encryption completes,
|
||||
* that tells us that it has been cancelled, and we should not send it.
|
||||
*/
|
||||
private eventsBeingEncrypted = new Set<string>();
|
||||
|
||||
private useE2eForGroupCall = true;
|
||||
private toDeviceMessageQueue: ToDeviceMessageQueue;
|
||||
@@ -1445,7 +1451,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// correctly handle notification counts on encrypted rooms.
|
||||
// This fixes https://github.com/vector-im/element-web/issues/9421
|
||||
this.on(RoomEvent.Receipt, (event, room) => {
|
||||
if (room && this.isRoomEncrypted(room.roomId)) {
|
||||
if (room?.hasEncryptionStateEvent()) {
|
||||
// Figure out if we've read something or if it's just informational
|
||||
const content = event.getContent();
|
||||
const isSelf =
|
||||
@@ -2354,6 +2360,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
CryptoEvent.KeyBackupSessionsRemaining,
|
||||
CryptoEvent.KeyBackupFailed,
|
||||
CryptoEvent.KeyBackupDecryptionKeyCached,
|
||||
CryptoEvent.KeysChanged,
|
||||
CryptoEvent.DevicesUpdated,
|
||||
CryptoEvent.WillUpdateDevices,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3245,6 +3254,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param roomId - The room ID to enable encryption in.
|
||||
* @param config - The encryption config for the room.
|
||||
* @returns A promise that will resolve when encryption is set up.
|
||||
*
|
||||
* @deprecated Not supported for Rust Cryptography. To enable encryption in a room, send an `m.room.encryption`
|
||||
* state event.
|
||||
*/
|
||||
public setRoomEncryption(roomId: string, config: IRoomEncryption): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
@@ -3257,6 +3269,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* Whether encryption is enabled for a room.
|
||||
* @param roomId - the room id to query.
|
||||
* @returns whether encryption is enabled.
|
||||
*
|
||||
* @deprecated Not correctly supported for Rust Cryptography. Use {@link CryptoApi.isEncryptionEnabledInRoom} and/or
|
||||
* {@link Room.hasEncryptionStateEvent}.
|
||||
*/
|
||||
public isRoomEncrypted(roomId: string): boolean {
|
||||
const room = this.getRoom(roomId);
|
||||
@@ -3268,8 +3283,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
// if there is an 'm.room.encryption' event in this room, it should be
|
||||
// encrypted (independently of whether we actually support encryption)
|
||||
const ev = room.currentState.getStateEvents(EventType.RoomEncryption, "");
|
||||
if (ev) {
|
||||
if (room.hasEncryptionStateEvent()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4449,9 +4463,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
throw new Error("cannot cancel an event with status " + event.status);
|
||||
}
|
||||
|
||||
// if the event is currently being encrypted then
|
||||
// If the event is currently being encrypted then remove it from the pending list, to indicate that it should
|
||||
// not be sent.
|
||||
if (event.status === EventStatus.ENCRYPTING) {
|
||||
this.pendingEventEncryption.delete(event.getId()!);
|
||||
this.eventsBeingEncrypted.delete(event.getId()!);
|
||||
} else if (this.scheduler && event.status === EventStatus.QUEUED) {
|
||||
// tell the scheduler to forget about it, if it's queued
|
||||
this.scheduler.removeEventFromQueue(event);
|
||||
@@ -4750,96 +4765,102 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* encrypts the event if necessary; adds the event to the queue, or sends it; marks the event as sent/unsent
|
||||
* @returns returns a promise which resolves with the result of the send request
|
||||
*/
|
||||
protected encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse> {
|
||||
let cancelled = false;
|
||||
// Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections,
|
||||
// so that we can handle synchronous and asynchronous exceptions with the
|
||||
// same code path.
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const encryptionPromise = this.encryptEventIfNeeded(event, room ?? undefined);
|
||||
if (!encryptionPromise) return null; // doesn't need encryption
|
||||
protected async encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse> {
|
||||
try {
|
||||
let cancelled: boolean;
|
||||
this.eventsBeingEncrypted.add(event.getId()!);
|
||||
try {
|
||||
await this.encryptEventIfNeeded(event, room ?? undefined);
|
||||
} finally {
|
||||
cancelled = !this.eventsBeingEncrypted.delete(event.getId()!);
|
||||
}
|
||||
|
||||
this.pendingEventEncryption.set(event.getId()!, encryptionPromise);
|
||||
this.updatePendingEventStatus(room, event, EventStatus.ENCRYPTING);
|
||||
return encryptionPromise.then(() => {
|
||||
if (!this.pendingEventEncryption.has(event.getId()!)) {
|
||||
// cancelled via MatrixClient::cancelPendingEvent
|
||||
cancelled = true;
|
||||
return;
|
||||
}
|
||||
this.updatePendingEventStatus(room, event, EventStatus.SENDING);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
if (cancelled) return {} as ISendEventResponse;
|
||||
let promise: Promise<ISendEventResponse> | null = null;
|
||||
if (this.scheduler) {
|
||||
// if this returns a promise then the scheduler has control now and will
|
||||
// resolve/reject when it is done. Internally, the scheduler will invoke
|
||||
// processFn which is set to this._sendEventHttpRequest so the same code
|
||||
// path is executed regardless.
|
||||
promise = this.scheduler.queueEvent(event);
|
||||
if (promise && this.scheduler.getQueueForEvent(event)!.length > 1) {
|
||||
// event is processed FIFO so if the length is 2 or more we know
|
||||
// this event is stuck behind an earlier event.
|
||||
this.updatePendingEventStatus(room, event, EventStatus.QUEUED);
|
||||
}
|
||||
}
|
||||
if (cancelled) {
|
||||
// cancelled via MatrixClient::cancelPendingEvent
|
||||
return {} as ISendEventResponse;
|
||||
}
|
||||
|
||||
if (!promise) {
|
||||
promise = this.sendEventHttpRequest(event);
|
||||
if (room) {
|
||||
promise = promise.then((res) => {
|
||||
room.updatePendingEvent(event, EventStatus.SENT, res["event_id"]);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
// encryptEventIfNeeded may have updated the status from SENDING to ENCRYPTING. If so, we need
|
||||
// to put it back.
|
||||
if (event.status === EventStatus.ENCRYPTING) {
|
||||
this.updatePendingEventStatus(room, event, EventStatus.SENDING);
|
||||
}
|
||||
|
||||
return promise;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error("Error sending event", err.stack || err);
|
||||
try {
|
||||
// set the error on the event before we update the status:
|
||||
// updating the status emits the event, so the state should be
|
||||
// consistent at that point.
|
||||
event.error = err;
|
||||
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
|
||||
} catch (e) {
|
||||
this.logger.error("Exception in error handler!", (<Error>e).stack || err);
|
||||
let promise: Promise<ISendEventResponse> | null = null;
|
||||
if (this.scheduler) {
|
||||
// if this returns a promise then the scheduler has control now and will
|
||||
// resolve/reject when it is done. Internally, the scheduler will invoke
|
||||
// processFn which is set to this._sendEventHttpRequest so the same code
|
||||
// path is executed regardless.
|
||||
promise = this.scheduler.queueEvent(event);
|
||||
if (promise && this.scheduler.getQueueForEvent(event)!.length > 1) {
|
||||
// event is processed FIFO so if the length is 2 or more we know
|
||||
// this event is stuck behind an earlier event.
|
||||
this.updatePendingEventStatus(room, event, EventStatus.QUEUED);
|
||||
}
|
||||
if (err instanceof MatrixError) {
|
||||
err.event = event;
|
||||
}
|
||||
|
||||
if (!promise) {
|
||||
promise = this.sendEventHttpRequest(event);
|
||||
if (room) {
|
||||
promise = promise.then((res) => {
|
||||
room.updatePendingEvent(event, EventStatus.SENT, res["event_id"]);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
return await promise;
|
||||
} catch (err) {
|
||||
this.logger.error("Error sending event", err);
|
||||
try {
|
||||
// set the error on the event before we update the status:
|
||||
// updating the status emits the event, so the state should be
|
||||
// consistent at that point.
|
||||
event.error = <MatrixError>err;
|
||||
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
|
||||
} catch (e) {
|
||||
this.logger.error("Exception in error handler!", e);
|
||||
}
|
||||
if (err instanceof MatrixError) {
|
||||
err.event = event;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private encryptEventIfNeeded(event: MatrixEvent, room?: Room): Promise<void> | null {
|
||||
private async encryptEventIfNeeded(event: MatrixEvent, room?: Room): Promise<void> {
|
||||
// If the room is unknown, we cannot encrypt for it
|
||||
if (!room) return;
|
||||
|
||||
if (!(await this.shouldEncryptEventForRoom(event, room))) return;
|
||||
|
||||
if (!this.cryptoBackend && this.usingExternalCrypto) {
|
||||
// The client has opted to allow sending messages to encrypted
|
||||
// rooms even if the room is encrypted, and we haven't set up
|
||||
// crypto. This is useful for users of matrix-org/pantalaimon
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("This room is configured to use encryption, but your client does not support encryption.");
|
||||
}
|
||||
|
||||
this.updatePendingEventStatus(room, event, EventStatus.ENCRYPTING);
|
||||
await this.cryptoBackend.encryptEvent(event, room);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a given event should be encrypted when we send it to the given room.
|
||||
*
|
||||
* This takes into account event type and room configuration.
|
||||
*/
|
||||
private async shouldEncryptEventForRoom(event: MatrixEvent, room: Room): Promise<boolean> {
|
||||
if (event.isEncrypted()) {
|
||||
// this event has already been encrypted; this happens if the
|
||||
// encryption step succeeded, but the send step failed on the first
|
||||
// attempt.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (event.isRedaction()) {
|
||||
// Redactions do not support encryption in the spec at this time,
|
||||
// whilst it mostly worked in some clients, it wasn't compliant.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!room || !this.isRoomEncrypted(event.getRoomId()!)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.cryptoBackend && this.usingExternalCrypto) {
|
||||
// The client has opted to allow sending messages to encrypted
|
||||
// rooms even if the room is encrypted, and we haven't setup
|
||||
// crypto. This is useful for users of matrix-org/pantalaimon
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.getType() === EventType.Reaction) {
|
||||
@@ -4853,14 +4874,23 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// The reaction key / content / emoji value does warrant encrypting, but
|
||||
// this will be handled separately by encrypting just this value.
|
||||
// See https://github.com/matrix-org/matrix-doc/pull/1849#pullrequestreview-248763642
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("This room is configured to use encryption, but your client does not support encryption.");
|
||||
if (event.isRedaction()) {
|
||||
// Redactions do not support encryption in the spec at this time.
|
||||
// Whilst it mostly worked in some clients, it wasn't compliant.
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.cryptoBackend.encryptEvent(event, room);
|
||||
// If the room has an m.room.encryption event, we should encrypt.
|
||||
if (room.hasEncryptionStateEvent()) return true;
|
||||
|
||||
// If we have a crypto impl, and *it* thinks we should encrypt, then we should.
|
||||
if (await this.cryptoBackend?.isEncryptionEnabledInRoom(room.roomId)) return true;
|
||||
|
||||
// Otherwise, no need to encrypt.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4875,7 +4905,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventType?: EventType | string | null,
|
||||
): EventType | string | null | undefined {
|
||||
if (eventType === EventType.Reaction) return eventType;
|
||||
return this.isRoomEncrypted(roomId) ? EventType.RoomMessageEncrypted : eventType;
|
||||
return this.getRoom(roomId)?.hasEncryptionStateEvent() ? EventType.RoomMessageEncrypted : eventType;
|
||||
}
|
||||
|
||||
protected updatePendingEventStatus(room: Room | null, event: MatrixEvent, newStatus: EventStatus): void {
|
||||
@@ -5805,6 +5835,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param allowDirectLinks - If true, return any non-mxc URLs
|
||||
* directly. Fetching such URLs will leak information about the user to
|
||||
* anyone they share a room with. If false, will return null for such URLs.
|
||||
* @param allowRedirects - If true, the caller supports the URL being 307 or
|
||||
* 308 redirected to another resource upon request. If false, redirects
|
||||
* are not expected.
|
||||
* @returns the avatar URL or null.
|
||||
*/
|
||||
public mxcUrlToHttp(
|
||||
@@ -5813,8 +5846,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
height?: number,
|
||||
resizeMethod?: string,
|
||||
allowDirectLinks?: boolean,
|
||||
allowRedirects?: boolean,
|
||||
): string | null {
|
||||
return getHttpUriForMxc(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks);
|
||||
return getHttpUriForMxc(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks, allowRedirects);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,9 @@ import { encodeParams } from "./utils";
|
||||
* directly. Fetching such URLs will leak information about the user to
|
||||
* anyone they share a room with. If false, will return the emptry string
|
||||
* for such URLs.
|
||||
* @param allowRedirects - If true, the caller supports the URL being 307 or
|
||||
* 308 redirected to another resource upon request. If false, redirects
|
||||
* are not expected.
|
||||
* @returns The complete URL to the content.
|
||||
*/
|
||||
export function getHttpUriForMxc(
|
||||
@@ -37,6 +40,7 @@ export function getHttpUriForMxc(
|
||||
height?: number,
|
||||
resizeMethod?: string,
|
||||
allowDirectLinks = false,
|
||||
allowRedirects?: boolean,
|
||||
): string {
|
||||
if (typeof mxc !== "string" || !mxc) {
|
||||
return "";
|
||||
@@ -67,6 +71,11 @@ export function getHttpUriForMxc(
|
||||
prefix = "/_matrix/media/v3/thumbnail/";
|
||||
}
|
||||
|
||||
if (typeof allowRedirects === "boolean") {
|
||||
// We add this after, so we don't convert everything to a thumbnail request.
|
||||
params["allow_redirect"] = JSON.stringify(allowRedirects);
|
||||
}
|
||||
|
||||
const fragmentOffset = serverAndMediaId.indexOf("#");
|
||||
let fragment = "";
|
||||
if (fragmentOffset >= 0) {
|
||||
|
||||
+13
-1
@@ -53,6 +53,18 @@ export interface CryptoApi {
|
||||
*/
|
||||
getOwnDeviceKeys(): Promise<OwnDeviceKeys>;
|
||||
|
||||
/**
|
||||
* Check if we believe the given room to be encrypted.
|
||||
*
|
||||
* This method returns true if the room has been configured with encryption. The setting is persistent, so that
|
||||
* even if the encryption event is removed from the room state, it still returns true. This helps to guard against
|
||||
* a downgrade attack wherein a server admin attempts to remove encryption.
|
||||
*
|
||||
* @returns `true` if the room with the supplied ID is encrypted. `false` if the room is not encrypted, or is unknown to
|
||||
* us.
|
||||
*/
|
||||
isEncryptionEnabledInRoom(roomId: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Perform any background tasks that can be done before a message is ready to
|
||||
* send, in order to speed up sending of the message.
|
||||
@@ -189,7 +201,7 @@ export interface CryptoApi {
|
||||
* Cross-signing a device indicates, to our other devices and to other users, that we have verified that it really
|
||||
* belongs to us.
|
||||
*
|
||||
* Requires that cross-signing has been set up on this device (normally by calling {@link bootstrapCrossSigning}.
|
||||
* Requires that cross-signing has been set up on this device (normally by calling {@link bootstrapCrossSigning}).
|
||||
*
|
||||
* *Note*: Do not call this unless you have verified, somehow, that the device is genuine!
|
||||
*
|
||||
|
||||
@@ -64,7 +64,7 @@ export interface InboundGroupSessionData {
|
||||
room_id: string; // eslint-disable-line camelcase
|
||||
/** pickled Olm.InboundGroupSession */
|
||||
session: string;
|
||||
keysClaimed: Record<string, string>;
|
||||
keysClaimed?: Record<string, string>;
|
||||
/** Devices involved in forwarding this session to us (normally empty). */
|
||||
forwardingCurve25519KeyChain: string[];
|
||||
/** whether this session is untrusted. */
|
||||
|
||||
@@ -4273,6 +4273,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return this.roomList.isRoomEncrypted(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#isEncryptionEnabledInRoom}.
|
||||
*/
|
||||
public async isEncryptionEnabledInRoom(roomId: string): Promise<boolean> {
|
||||
return this.isRoomEncrypted(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns information about the encryption on the room with the supplied
|
||||
* ID, or null if the room is not encrypted or unknown to us.
|
||||
|
||||
@@ -325,6 +325,9 @@ export enum MigrationState {
|
||||
|
||||
/** OLM_SESSIONS_MIGRATED, and in addition, we have migrated all the Megolm sessions. */
|
||||
MEGOLM_SESSIONS_MIGRATED,
|
||||
|
||||
/** MEGOLM_SESSIONS_MIGRATED, and in addition, we have migrated all the room settings. */
|
||||
ROOM_SETTINGS_MIGRATED,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,8 @@ export interface CallMembershipData {
|
||||
scope: CallScope;
|
||||
device_id: string;
|
||||
created_ts?: number;
|
||||
expires: number;
|
||||
expires?: number;
|
||||
expires_ts?: number;
|
||||
foci_active?: Focus[];
|
||||
membershipID: string;
|
||||
}
|
||||
@@ -41,7 +42,20 @@ export class CallMembership {
|
||||
private parentEvent: MatrixEvent,
|
||||
private data: CallMembershipData,
|
||||
) {
|
||||
if (typeof data.expires !== "number") throw new Error("Malformed membership: expires must be numeric");
|
||||
if (!(data.expires || data.expires_ts)) {
|
||||
throw new Error("Malformed membership: expires_ts or expires must be present");
|
||||
}
|
||||
if (data.expires) {
|
||||
if (typeof data.expires !== "number") {
|
||||
throw new Error("Malformed membership: expires must be numeric");
|
||||
}
|
||||
}
|
||||
if (data.expires_ts) {
|
||||
if (typeof data.expires_ts !== "number") {
|
||||
throw new Error("Malformed membership: expires_ts must be numeric");
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data.device_id !== "string") throw new Error("Malformed membership event: device_id must be string");
|
||||
if (typeof data.call_id !== "string") throw new Error("Malformed membership event: call_id must be string");
|
||||
if (typeof data.scope !== "string") throw new Error("Malformed membership event: scope must be string");
|
||||
@@ -77,16 +91,27 @@ export class CallMembership {
|
||||
}
|
||||
|
||||
public getAbsoluteExpiry(): number {
|
||||
return this.createdTs() + this.data.expires;
|
||||
if (this.data.expires) {
|
||||
return this.createdTs() + this.data.expires;
|
||||
} else {
|
||||
// We know it exists because we checked for this in the constructor.
|
||||
return this.data.expires_ts!;
|
||||
}
|
||||
}
|
||||
|
||||
// gets the expiry time of the event, converted into the device's local time
|
||||
public getLocalExpiry(): number {
|
||||
const relativeCreationTime = this.parentEvent.getTs() - this.createdTs();
|
||||
if (this.data.expires) {
|
||||
const relativeCreationTime = this.parentEvent.getTs() - this.createdTs();
|
||||
|
||||
const localCreationTs = this.parentEvent.localTimestamp - relativeCreationTime;
|
||||
const localCreationTs = this.parentEvent.localTimestamp - relativeCreationTime;
|
||||
|
||||
return localCreationTs + this.data.expires;
|
||||
return localCreationTs + this.data.expires;
|
||||
} else {
|
||||
// With expires_ts we cannot convert to local time.
|
||||
// TODO: Check the server timestamp and compute a diff to local time.
|
||||
return this.data.expires_ts!;
|
||||
}
|
||||
}
|
||||
|
||||
public getMsUntilExpiry(): number {
|
||||
|
||||
@@ -624,6 +624,9 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
};
|
||||
|
||||
if (prevMembership) m.created_ts = prevMembership.createdTs();
|
||||
if (m.created_ts) m.expires_ts = m.created_ts + (m.expires ?? 0);
|
||||
// TODO: Date.now() should be the origin_server_ts (now).
|
||||
else m.expires_ts = Date.now() + (m.expires ?? 0);
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -1003,8 +1003,9 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
|
||||
if (!shouldLiveInRoom && !shouldLiveInThread) {
|
||||
logger.warn(
|
||||
`EventTimelineSet:canContain event encountered which cannot be added to any timeline roomId=${this.room
|
||||
?.roomId} eventId=${event.getId()} threadId=${event.threadRootId}`,
|
||||
`EventTimelineSet:canContain event encountered which cannot be added to any timeline roomId=${
|
||||
this.room?.roomId
|
||||
} eventId=${event.getId()} threadId=${event.threadRootId}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -350,7 +350,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
/**
|
||||
* most recent error associated with sending the event, if any
|
||||
* @privateRemarks
|
||||
* Should be read-only
|
||||
* Should be read-only. May not be a MatrixError.
|
||||
*/
|
||||
public error: MatrixError | null = null;
|
||||
/**
|
||||
|
||||
@@ -79,7 +79,7 @@ export abstract class ReadReceipt<
|
||||
private receiptCacheByEventId: ReceiptCache = new Map();
|
||||
|
||||
public abstract getUnfilteredTimelineSet(): EventTimelineSet;
|
||||
public abstract timeline: MatrixEvent[];
|
||||
public abstract get timeline(): MatrixEvent[];
|
||||
|
||||
/**
|
||||
* Gets the latest receipt for a given user in the room
|
||||
|
||||
+30
-14
@@ -381,13 +381,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* The room summary.
|
||||
*/
|
||||
public summary: RoomSummary | null = null;
|
||||
/**
|
||||
* The live event timeline for this room, with the oldest event at index 0.
|
||||
*
|
||||
* @deprecated Present for backwards compatibility.
|
||||
* Use getLiveTimeline().getEvents() instead
|
||||
*/
|
||||
public timeline!: MatrixEvent[];
|
||||
/**
|
||||
* oldState The state of the room at the time of the oldest event in the live timeline.
|
||||
*
|
||||
@@ -793,6 +786,16 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
return this.getUnfilteredTimelineSet().getLiveTimeline();
|
||||
}
|
||||
|
||||
/**
|
||||
* The live event timeline for this room, with the oldest event at index 0.
|
||||
*
|
||||
* @deprecated Present for backwards compatibility.
|
||||
* Use getLiveTimeline().getEvents() instead
|
||||
*/
|
||||
public get timeline(): MatrixEvent[] {
|
||||
return this.getLiveTimeline().getEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp of the last message in the room
|
||||
*
|
||||
@@ -980,7 +983,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
// that this function is only called once (unless loading the members
|
||||
// fails), since loadMembersIfNeeded always returns this.membersPromise
|
||||
// if set, which will be the result of the first (successful) call.
|
||||
if (rawMembersEvents === null || (this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId))) {
|
||||
if (rawMembersEvents === null || this.hasEncryptionStateEvent()) {
|
||||
fromServer = true;
|
||||
rawMembersEvents = await this.loadMembersFromServer();
|
||||
logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`);
|
||||
@@ -1221,11 +1224,9 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
const previousOldState = this.oldState;
|
||||
const previousCurrentState = this.currentState;
|
||||
|
||||
// maintain this.timeline as a reference to the live timeline,
|
||||
// and this.oldState and this.currentState as references to the
|
||||
// maintain this.oldState and this.currentState as references to the
|
||||
// state at the start and end of that timeline. These are more
|
||||
// for backwards-compatibility than anything else.
|
||||
this.timeline = this.getLiveTimeline().getEvents();
|
||||
this.oldState = this.getLiveTimeline().getState(EventTimeline.BACKWARDS)!;
|
||||
this.currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
|
||||
@@ -1275,9 +1276,12 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* error will be thrown.
|
||||
*
|
||||
* @returns the result
|
||||
*
|
||||
* @deprecated Not supported under rust crypto. Instead, call {@link Room.getEncryptionTargetMembers},
|
||||
* {@link CryptoApi.getUserDeviceInfo}, and {@link CryptoApi.getDeviceVerificationStatus}.
|
||||
*/
|
||||
public async hasUnverifiedDevices(): Promise<boolean> {
|
||||
if (!this.client.isRoomEncrypted(this.roomId)) {
|
||||
if (!this.hasEncryptionStateEvent()) {
|
||||
return false;
|
||||
}
|
||||
const e2eMembers = await this.getEncryptionTargetMembers();
|
||||
@@ -2565,7 +2569,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
.filter((event) => {
|
||||
// Filter out the unencrypted messages if the room is encrypted
|
||||
const isEventEncrypted = event.type === EventType.RoomMessageEncrypted;
|
||||
const isRoomEncrypted = this.client.isRoomEncrypted(this.roomId);
|
||||
const isRoomEncrypted = this.hasEncryptionStateEvent();
|
||||
return isEventEncrypted || !isRoomEncrypted;
|
||||
});
|
||||
|
||||
@@ -3170,7 +3174,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
public maySendMessage(): boolean {
|
||||
return (
|
||||
this.getMyMembership() === "join" &&
|
||||
(this.client.isRoomEncrypted(this.roomId)
|
||||
(this.hasEncryptionStateEvent()
|
||||
? this.currentState.maySendEvent(EventType.RoomMessageEncrypted, this.myUserId)
|
||||
: this.currentState.maySendEvent(EventType.RoomMessage, this.myUserId))
|
||||
);
|
||||
@@ -3672,6 +3676,18 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
public compareEventOrdering(leftEventId: string, rightEventId: string): number | null {
|
||||
return compareEventOrdering(this, leftEventId, rightEventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this room has an `m.room.encryption` state event.
|
||||
*
|
||||
* If this returns `true`, events sent to this room should be encrypted (and `MatrixClient.sendEvent` and friends
|
||||
* will encrypt outgoing events).
|
||||
*/
|
||||
public hasEncryptionStateEvent(): boolean {
|
||||
return Boolean(
|
||||
this.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(EventType.RoomEncryption, ""),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// a map from current event status to a list of allowed next statuses
|
||||
|
||||
+10
-6
@@ -84,7 +84,6 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
* A reference to all the events ID at the bottom of the threads
|
||||
*/
|
||||
public readonly timelineSet: EventTimelineSet;
|
||||
public timeline: MatrixEvent[] = [];
|
||||
|
||||
private _currentUserParticipated = false;
|
||||
|
||||
@@ -323,7 +322,6 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
fromCache: false,
|
||||
roomState: this.roomState,
|
||||
});
|
||||
this.timeline = this.events;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,9 +348,6 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
return;
|
||||
}
|
||||
this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState);
|
||||
|
||||
// As far as we know, timeline should always be the same as events
|
||||
this.timeline = this.events;
|
||||
}
|
||||
|
||||
public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
@@ -483,7 +478,6 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
this.setEventMetadata(event);
|
||||
await this.fetchEditsWhereNeeded(event);
|
||||
}
|
||||
this.timeline = this.events;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -727,6 +721,16 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply();
|
||||
}
|
||||
|
||||
/**
|
||||
* The live event timeline for this thread.
|
||||
* @deprecated Present for backwards compatibility.
|
||||
* Use this.events instead
|
||||
* @returns The live event timeline for this thread.
|
||||
*/
|
||||
public get timeline(): MatrixEvent[] {
|
||||
return this.events;
|
||||
}
|
||||
|
||||
public get events(): MatrixEvent[] {
|
||||
return this.liveTimeline.getEvents();
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@ limitations under the License.
|
||||
import { UnstableValue } from "matrix-events-sdk";
|
||||
|
||||
import { RendezvousChannel, RendezvousFailureListener, RendezvousFailureReason, RendezvousIntent } from ".";
|
||||
import { ICrossSigningKey, IGetLoginTokenCapability, MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "../client";
|
||||
import { CrossSigningInfo } from "../crypto/CrossSigning";
|
||||
import { DeviceInfo } from "../crypto/deviceinfo";
|
||||
import { IGetLoginTokenCapability, MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "../client";
|
||||
import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature";
|
||||
import { logger } from "../logger";
|
||||
import { sleep } from "../utils";
|
||||
import { CrossSigningKey } from "../crypto-api";
|
||||
import { Device } from "../matrix";
|
||||
|
||||
enum PayloadType {
|
||||
Start = "m.login.start",
|
||||
@@ -116,7 +116,7 @@ export class MSC3906Rendezvous {
|
||||
|
||||
await this.send({ type: PayloadType.Progress, protocols: [LOGIN_TOKEN_PROTOCOL.name] });
|
||||
|
||||
logger.info("Waiting for other device to chose protocol");
|
||||
logger.info("Waiting for other device to choose protocol");
|
||||
const { type, protocol, outcome } = await this.receive();
|
||||
|
||||
if (type === PayloadType.Finish) {
|
||||
@@ -178,12 +178,8 @@ export class MSC3906Rendezvous {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
private async verifyAndCrossSignDevice(
|
||||
deviceInfo: DeviceInfo,
|
||||
): Promise<CrossSigningInfo | DeviceInfo | ICrossSigningKey | undefined> {
|
||||
if (!this.client.crypto) {
|
||||
throw new Error("Crypto not available on client");
|
||||
}
|
||||
private async verifyAndCrossSignDevice(deviceInfo: Device): Promise<void> {
|
||||
const crypto = this.client.getCrypto()!;
|
||||
|
||||
if (!this.newDeviceId) {
|
||||
throw new Error("No new device ID set");
|
||||
@@ -196,36 +192,32 @@ export class MSC3906Rendezvous {
|
||||
);
|
||||
}
|
||||
|
||||
const userId = this.client.getUserId();
|
||||
const userId = this.client.getSafeUserId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("No user ID set");
|
||||
}
|
||||
// mark the device as verified locally + cross sign
|
||||
logger.info(`Marking device ${this.newDeviceId} as verified`);
|
||||
const info = await this.client.crypto.setDeviceVerification(userId, this.newDeviceId, true, false, true);
|
||||
await crypto.setDeviceVerified(userId, this.newDeviceId, true);
|
||||
await crypto.crossSignDevice(this.newDeviceId);
|
||||
|
||||
const masterPublicKey = this.client.crypto.crossSigningInfo.getId("master")!;
|
||||
const masterPublicKey = (await crypto.getCrossSigningKeyId(CrossSigningKey.Master)) ?? undefined;
|
||||
|
||||
const ourDeviceId = this.client.getDeviceId()!;
|
||||
const ourDeviceKey = (await crypto.getOwnDeviceKeys()).ed25519;
|
||||
|
||||
await this.send({
|
||||
type: PayloadType.Finish,
|
||||
outcome: Outcome.Verified,
|
||||
verifying_device_id: this.client.getDeviceId()!,
|
||||
verifying_device_key: this.client.getDeviceEd25519Key()!,
|
||||
verifying_device_id: ourDeviceId,
|
||||
verifying_device_key: ourDeviceKey,
|
||||
master_key: masterPublicKey,
|
||||
});
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the device and cross-sign it.
|
||||
* @param timeout - time in milliseconds to wait for device to come online
|
||||
* @returns the new device info if the device was verified
|
||||
*/
|
||||
public async verifyNewDeviceOnExistingDevice(
|
||||
timeout = 10 * 1000,
|
||||
): Promise<DeviceInfo | CrossSigningInfo | ICrossSigningKey | undefined> {
|
||||
public async verifyNewDeviceOnExistingDevice(timeout = 10 * 1000): Promise<void> {
|
||||
if (!this.newDeviceId) {
|
||||
throw new Error("No new device to sign");
|
||||
}
|
||||
@@ -235,31 +227,33 @@ export class MSC3906Rendezvous {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!this.client.crypto) {
|
||||
const crypto = this.client.getCrypto();
|
||||
if (!crypto) {
|
||||
throw new Error("Crypto not available on client");
|
||||
}
|
||||
|
||||
const userId = this.client.getUserId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("No user ID set");
|
||||
}
|
||||
|
||||
let deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId);
|
||||
let deviceInfo = await this.getOwnDevice(this.newDeviceId);
|
||||
|
||||
if (!deviceInfo) {
|
||||
logger.info("Going to wait for new device to be online");
|
||||
await sleep(timeout);
|
||||
deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId);
|
||||
deviceInfo = await this.getOwnDevice(this.newDeviceId);
|
||||
}
|
||||
|
||||
if (deviceInfo) {
|
||||
return await this.verifyAndCrossSignDevice(deviceInfo);
|
||||
await this.verifyAndCrossSignDevice(deviceInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Device not online within timeout");
|
||||
}
|
||||
|
||||
private async getOwnDevice(deviceId: string): Promise<Device | undefined> {
|
||||
const userId = this.client.getSafeUserId();
|
||||
const ownDeviceInfo = await this.client.getCrypto()!.getUserDeviceInfo([userId]);
|
||||
return ownDeviceInfo.get(userId)?.get(deviceId);
|
||||
}
|
||||
|
||||
public async cancel(reason: RendezvousFailureReason): Promise<void> {
|
||||
this.onFailure?.(reason);
|
||||
await this.channel.cancel(reason);
|
||||
|
||||
@@ -88,7 +88,8 @@ export class RoomEncryptor {
|
||||
*/
|
||||
public onCryptoEvent(config: IContent): void {
|
||||
if (JSON.stringify(this.encryptionSettings) != JSON.stringify(config)) {
|
||||
this.prefixedLogger.error(`Ignoring m.room.encryption event which requests a change of config`);
|
||||
// This should currently be unreachable, since the Rust SDK will reject any attempts to change config.
|
||||
throw new Error("Cannot reconfigure an active RoomEncryptor");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -212,15 +212,18 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
}
|
||||
keysByRoom.get(roomId)!.set(key.session_id, key);
|
||||
}
|
||||
await this.olmMachine.importBackedUpRoomKeys(keysByRoom, (progress: BigInt, total: BigInt): void => {
|
||||
const importOpt: ImportRoomKeyProgressData = {
|
||||
total: Number(total),
|
||||
successes: Number(progress),
|
||||
stage: "load_keys",
|
||||
failures: 0,
|
||||
};
|
||||
opts?.progressCallback?.(importOpt);
|
||||
});
|
||||
await this.olmMachine.importBackedUpRoomKeys(
|
||||
keysByRoom,
|
||||
(progress: BigInt, total: BigInt, failures: BigInt): void => {
|
||||
const importOpt: ImportRoomKeyProgressData = {
|
||||
total: Number(total),
|
||||
successes: Number(progress),
|
||||
stage: "load_keys",
|
||||
failures: Number(failures),
|
||||
};
|
||||
opts?.progressCallback?.(importOpt);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private keyBackupCheckInProgress: Promise<KeyBackupCheck | null> | null = null;
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ServerSideSecretStorage } from "../secret-storage";
|
||||
import { ICryptoCallbacks } from "../crypto";
|
||||
import { Logger } from "../logger";
|
||||
import { CryptoStore } from "../crypto/store/base";
|
||||
import { migrateFromLegacyCrypto } from "./libolm_migration";
|
||||
import { migrateFromLegacyCrypto, migrateRoomSettingsFromLegacyCrypto } from "./libolm_migration";
|
||||
|
||||
/**
|
||||
* Create a new `RustCrypto` implementation
|
||||
@@ -112,6 +112,7 @@ export async function initRustCrypto(args: {
|
||||
args.secretStorage,
|
||||
args.cryptoCallbacks,
|
||||
storeHandle,
|
||||
args.legacyCryptoStore,
|
||||
);
|
||||
|
||||
storeHandle.free();
|
||||
@@ -128,6 +129,7 @@ async function initOlmMachine(
|
||||
secretStorage: ServerSideSecretStorage,
|
||||
cryptoCallbacks: ICryptoCallbacks,
|
||||
storeHandle: StoreHandle,
|
||||
legacyCryptoStore?: CryptoStore,
|
||||
): Promise<RustCrypto> {
|
||||
logger.debug("Init OlmMachine");
|
||||
|
||||
@@ -137,6 +139,15 @@ async function initOlmMachine(
|
||||
storeHandle,
|
||||
);
|
||||
|
||||
// A final migration step, now that we have an OlmMachine.
|
||||
if (legacyCryptoStore) {
|
||||
await migrateRoomSettingsFromLegacyCrypto({
|
||||
logger,
|
||||
legacyStore: legacyCryptoStore,
|
||||
olmMachine,
|
||||
});
|
||||
}
|
||||
|
||||
// Disable room key requests, per https://github.com/vector-im/element-web/issues/26524.
|
||||
olmMachine.roomKeyRequestsEnabled = false;
|
||||
|
||||
@@ -147,6 +158,7 @@ async function initOlmMachine(
|
||||
await olmMachine.registerUserIdentityUpdatedCallback((userId: RustSdkCryptoJs.UserId) =>
|
||||
rustCrypto.onUserIdentityUpdated(userId),
|
||||
);
|
||||
await olmMachine.registerDevicesUpdatedCallback((userIds: string[]) => rustCrypto.onDevicesUpdated(userIds));
|
||||
|
||||
// Check if there are any key backup secrets pending processing. There may be multiple secrets to process if several devices have gossiped them.
|
||||
// The `registerReceiveSecretCallback` function will only be triggered for new secrets. If the client is restarted before processing them, the secrets will need to be manually handled.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2023-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.
|
||||
@@ -22,10 +22,14 @@ import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
|
||||
import { decryptAES, IEncryptedPayload } from "../crypto/aes";
|
||||
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
||||
import { requestKeyBackupVersion } from "./backup";
|
||||
import { IRoomEncryption } from "../crypto/RoomList";
|
||||
|
||||
/**
|
||||
* Determine if any data needs migrating from the legacy store, and do so.
|
||||
*
|
||||
* This migrates the base account data, and olm and megolm sessions. It does *not* migrate the room list, which should
|
||||
* happen after an `OlmMachine` is created, via {@link migrateRoomSettingsFromLegacyCrypto}.
|
||||
*
|
||||
* @param args - Arguments object.
|
||||
*/
|
||||
export async function migrateFromLegacyCrypto(args: {
|
||||
@@ -76,8 +80,8 @@ export async function migrateFromLegacyCrypto(args: {
|
||||
await legacyStore.startup();
|
||||
let migrationState = await legacyStore.getMigrationState();
|
||||
|
||||
if (migrationState === MigrationState.MEGOLM_SESSIONS_MIGRATED) {
|
||||
// All migration is done.
|
||||
if (migrationState >= MigrationState.MEGOLM_SESSIONS_MIGRATED) {
|
||||
// All migration is done for now. The room list comes later, once we have an OlmMachine.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -232,17 +236,55 @@ async function migrateMegolmSessions(
|
||||
logger.debug(`Migrating batch of ${batch.length} megolm sessions`);
|
||||
const migrationData: RustSdkCryptoJs.PickledInboundGroupSession[] = [];
|
||||
for (const session of batch) {
|
||||
const sessionData = session.sessionData!;
|
||||
|
||||
const pickledSession = new RustSdkCryptoJs.PickledInboundGroupSession();
|
||||
pickledSession.pickle = session.sessionData!.session;
|
||||
pickledSession.roomId = new RustSdkCryptoJs.RoomId(session.sessionData!.room_id);
|
||||
pickledSession.pickle = sessionData.session;
|
||||
pickledSession.roomId = new RustSdkCryptoJs.RoomId(sessionData.room_id);
|
||||
pickledSession.senderKey = session.senderKey;
|
||||
pickledSession.senderSigningKey = session.sessionData!.keysClaimed["ed25519"];
|
||||
pickledSession.senderSigningKey = sessionData.keysClaimed?.["ed25519"];
|
||||
pickledSession.backedUp = !session.needsBackup;
|
||||
|
||||
// Not sure if we can reliably distinguish imported vs not-imported sessions in the libolm database.
|
||||
// For now at least, let's be conservative and say that all the sessions are imported (which means that
|
||||
// the Rust SDK treats them as less secure).
|
||||
pickledSession.imported = true;
|
||||
// The Rust SDK `imported` flag is used to indicate the authenticity status of a Megolm
|
||||
// session, which tells us whether we can reliably tell which Olm device is the owner
|
||||
// (creator) of the session.
|
||||
//
|
||||
// If `imported` is true, then we have no cryptographic proof that the session is owned
|
||||
// by the device with the identity key `senderKey`.
|
||||
//
|
||||
// Only Megolm sessions received directly from the owning device via an encrypted
|
||||
// `m.room_key` to-device message should have `imported` flag set to false. Megolm
|
||||
// sessions received by any other currently available means (i.e. from a
|
||||
// `m.forwarded_room_key`, from v1 asymmetric server-side key backup, imported from a
|
||||
// file, etc) should have the `imported` flag set to true.
|
||||
//
|
||||
// Messages encrypted with such Megolm sessions will have a grey shield in the UI
|
||||
// ("Authenticity of this message cannot be guaranteed").
|
||||
//
|
||||
// However, we don't want to bluntly mark all sessions as `imported` during migration
|
||||
// because users will suddenly start seeing all their historic messages decorated with a
|
||||
// grey shield, which would be seen as a non-actionable regression.
|
||||
//
|
||||
// In the legacy crypto stack, the flag encoding similar information was called
|
||||
// `InboundGroupSessionData.untrusted`. The value of this flag was set as follows:
|
||||
//
|
||||
// - For outbound Megolm sessions created by our own device, `untrusted` is `undefined`.
|
||||
// - For Megolm sessions received via a `m.room_key` to-device message, `untrusted` is
|
||||
// `undefined`.
|
||||
// - For Megolm sessions received via a `m.forwarded_room_key` to-device message,
|
||||
// `untrusted` is `true`.
|
||||
// - For Megolm sessions imported from a (v1 asymmetric / "legacy") server-side key
|
||||
// backup, `untrusted` is `true`.
|
||||
// - For Megolm sessions imported from a file, untrusted is `undefined`.
|
||||
//
|
||||
// The main difference between the legacy crypto stack and the Rust crypto stack is that
|
||||
// the Rust stack considers sessions imported from a file as `imported` (not
|
||||
// authenticated). This is because the Megolm session export file format does not
|
||||
// encode this authenticity information.
|
||||
//
|
||||
// Given this migration is only a one-time thing, we make a concession to accept the
|
||||
// loss of information in this case, to avoid degrading UX in a non-actionable way.
|
||||
pickledSession.imported = sessionData.untrusted === true;
|
||||
|
||||
migrationData.push(pickledSession);
|
||||
}
|
||||
@@ -253,6 +295,72 @@ async function migrateMegolmSessions(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if any room settings need migrating from the legacy store, and do so.
|
||||
*
|
||||
* @param args - Arguments object.
|
||||
*/
|
||||
export async function migrateRoomSettingsFromLegacyCrypto({
|
||||
logger,
|
||||
legacyStore,
|
||||
olmMachine,
|
||||
}: {
|
||||
/** A `Logger` instance that will be used for debug output. */
|
||||
logger: Logger;
|
||||
|
||||
/** Store to migrate data from. */
|
||||
legacyStore: CryptoStore;
|
||||
|
||||
/** OlmMachine to store the new data on. */
|
||||
olmMachine: RustSdkCryptoJs.OlmMachine;
|
||||
}): Promise<void> {
|
||||
if (!(await legacyStore.containsData())) {
|
||||
// This store was never used. Nothing to migrate.
|
||||
return;
|
||||
}
|
||||
|
||||
const migrationState = await legacyStore.getMigrationState();
|
||||
|
||||
if (migrationState >= MigrationState.ROOM_SETTINGS_MIGRATED) {
|
||||
// We've already migrated the room settings.
|
||||
return;
|
||||
}
|
||||
|
||||
let rooms: Record<string, IRoomEncryption> = {};
|
||||
|
||||
await legacyStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
|
||||
legacyStore.getEndToEndRooms(txn, (result) => {
|
||||
rooms = result;
|
||||
});
|
||||
});
|
||||
|
||||
logger.debug(`Migrating ${Object.keys(rooms).length} sets of room settings`);
|
||||
for (const [roomId, legacySettings] of Object.entries(rooms)) {
|
||||
try {
|
||||
const rustSettings = new RustSdkCryptoJs.RoomSettings();
|
||||
|
||||
if (legacySettings.algorithm !== "m.megolm.v1.aes-sha2") {
|
||||
logger.warn(`Room ${roomId}: ignoring room with invalid algorithm ${legacySettings.algorithm}`);
|
||||
continue;
|
||||
}
|
||||
rustSettings.algorithm = RustSdkCryptoJs.EncryptionAlgorithm.MegolmV1AesSha2;
|
||||
rustSettings.sessionRotationPeriodMs = legacySettings.rotation_period_ms;
|
||||
rustSettings.sessionRotationPeriodMessages = legacySettings.rotation_period_msgs;
|
||||
await olmMachine.setRoomSettings(new RustSdkCryptoJs.RoomId(roomId), rustSettings);
|
||||
|
||||
// We don't attempt to clear out the settings from the old store, or record where we've gotten up to,
|
||||
// which means that if the app gets restarted while we're in the middle of this migration, we'll start
|
||||
// again from scratch. So be it. Given that legacy crypto loads the whole room list into memory on startup
|
||||
// anyway, we know it can't be that big.
|
||||
} catch (e) {
|
||||
logger.warn(`Room ${roomId}: ignoring settings ${JSON.stringify(legacySettings)} which caused error ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Completed room settings migration`);
|
||||
await legacyStore.setMigrationState(MigrationState.ROOM_SETTINGS_MIGRATED);
|
||||
}
|
||||
|
||||
async function getAndDecryptCachedSecretKey(
|
||||
legacyStore: CryptoStore,
|
||||
legacyPickleKey: Uint8Array,
|
||||
|
||||
@@ -311,6 +311,16 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
return `Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#isEncryptionEnabledInRoom}.
|
||||
*/
|
||||
public async isEncryptionEnabledInRoom(roomId: string): Promise<boolean> {
|
||||
const roomSettings: RustSdkCryptoJs.RoomSettings | undefined = await this.olmMachine.getRoomSettings(
|
||||
new RustSdkCryptoJs.RoomId(roomId),
|
||||
);
|
||||
return Boolean(roomSettings?.algorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getOwnDeviceKeys}.
|
||||
*/
|
||||
@@ -1285,7 +1295,27 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
*/
|
||||
public async onCryptoEvent(room: Room, event: MatrixEvent): Promise<void> {
|
||||
const config = event.getContent();
|
||||
const settings = new RustSdkCryptoJs.RoomSettings();
|
||||
|
||||
if (config.algorithm === "m.megolm.v1.aes-sha2") {
|
||||
settings.algorithm = RustSdkCryptoJs.EncryptionAlgorithm.MegolmV1AesSha2;
|
||||
} else {
|
||||
// Among other situations, this happens if the crypto state event is redacted.
|
||||
this.logger.warn(`Room ${room.roomId}: ignoring crypto event with invalid algorithm ${config.algorithm}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
settings.sessionRotationPeriodMs = config.rotation_period_ms;
|
||||
settings.sessionRotationPeriodMessages = config.rotation_period_msgs;
|
||||
await this.olmMachine.setRoomSettings(new RustSdkCryptoJs.RoomId(room.roomId), settings);
|
||||
} catch (e) {
|
||||
this.logger.warn(`Room ${room.roomId}: ignoring crypto event which caused error: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we got this far, the SDK found the event acceptable.
|
||||
// We need to either create or update the active RoomEncryptor.
|
||||
const existingEncryptor = this.roomEncryptors[room.roomId];
|
||||
if (existingEncryptor) {
|
||||
existingEncryptor.onCryptoEvent(config);
|
||||
@@ -1411,7 +1441,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
* Callback for `OlmMachine.registerUserIdentityUpdatedCallback`
|
||||
*
|
||||
* Called by the rust-sdk whenever there is an update to any user's cross-signing status. We re-check their trust
|
||||
* status and emit a `UserTrustStatusChanged` event.
|
||||
* status and emit a `UserTrustStatusChanged` event, as well as a `KeysChanged` if it is our own identity that changed.
|
||||
*
|
||||
* @param userId - the user with the updated identity
|
||||
*/
|
||||
@@ -1422,10 +1452,26 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
|
||||
// If our own user identity has changed, we may now trust the key backup where we did not before.
|
||||
// So, re-check the key backup status and enable it if available.
|
||||
if (userId.toString() === this.userId) {
|
||||
this.emit(CryptoEvent.KeysChanged, {});
|
||||
await this.checkKeyBackupAndEnable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `OlmMachine.registerDevicesUpdatedCallback`
|
||||
*
|
||||
* Called when users' devices have updated. Emits `WillUpdateDevices` and `DevicesUpdated`. In the JavaScript
|
||||
* crypto backend, these events are called at separate times, with `WillUpdateDevices` being emitted just before
|
||||
* the devices are saved, and `DevicesUpdated` being emitted just after. But the OlmMachine only gives us
|
||||
* one event, so we emit both events here.
|
||||
*
|
||||
* @param userIds - an array of user IDs of users whose devices have updated.
|
||||
*/
|
||||
public async onDevicesUpdated(userIds: string[]): Promise<void> {
|
||||
this.emit(CryptoEvent.WillUpdateDevices, userIds, false);
|
||||
this.emit(CryptoEvent.DevicesUpdated, userIds, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles secret received from the rust secret inbox.
|
||||
*
|
||||
@@ -1798,6 +1844,9 @@ function rustEncryptionInfoToJsEncryptionInfo(
|
||||
type RustCryptoEvents =
|
||||
| CryptoEvent.VerificationRequestReceived
|
||||
| CryptoEvent.UserTrustStatusChanged
|
||||
| CryptoEvent.KeysChanged
|
||||
| CryptoEvent.WillUpdateDevices
|
||||
| CryptoEvent.DevicesUpdated
|
||||
| RustBackupCryptoEvents;
|
||||
|
||||
type RustCryptoEventMap = {
|
||||
@@ -1812,4 +1861,31 @@ type RustCryptoEventMap = {
|
||||
[CryptoEvent.UserTrustStatusChanged]: (userId: string, userTrustLevel: UserVerificationStatus) => void;
|
||||
|
||||
[CryptoEvent.KeyBackupDecryptionKeyCached]: (version: string) => void;
|
||||
/**
|
||||
* Fires when the user's cross-signing keys have changed or cross-signing
|
||||
* has been enabled/disabled. The client can use getStoredCrossSigningForUser
|
||||
* with the user ID of the logged in user to check if cross-signing is
|
||||
* enabled on the account. If enabled, it can test whether the current key
|
||||
* is trusted using with checkUserTrust with the user ID of the logged
|
||||
* in user. The checkOwnCrossSigningTrust function may be used to reconcile
|
||||
* the trust in the account key.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
* @experimental
|
||||
*/
|
||||
[CryptoEvent.KeysChanged]: (data: {}) => void;
|
||||
/**
|
||||
* Fires whenever the stored devices for a user will be updated
|
||||
* @param users - A list of user IDs that will be updated
|
||||
* @param initialFetch - If true, the store is empty (apart
|
||||
* from our own device) and is being seeded.
|
||||
*/
|
||||
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
|
||||
/**
|
||||
* Fires whenever the stored devices for a user have changed
|
||||
* @param users - A list of user IDs that were updated
|
||||
* @param initialFetch - If true, the store was empty (apart
|
||||
* from our own device) and has been seeded.
|
||||
*/
|
||||
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
|
||||
} & RustBackupCryptoEventMap;
|
||||
|
||||
@@ -34,6 +34,7 @@ import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProc
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { EventType, MsgType } from "../@types/event";
|
||||
import { defer, IDeferred } from "../utils";
|
||||
|
||||
/**
|
||||
* An incoming, or outgoing, request to verify a user or a device via cross-signing.
|
||||
@@ -76,11 +77,16 @@ export class RustVerificationRequest
|
||||
const onChange = async (): Promise<void> => {
|
||||
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
|
||||
|
||||
// If we now have a `Verification` where we lacked one before, or we have transitioned from QR to SAS,
|
||||
// wrap the new rust Verification as a js-sdk Verifier.
|
||||
// Set the _verifier object (wrapping the rust `Verification` as a js-sdk Verifier) if:
|
||||
// - we now have a `Verification` where we lacked one before
|
||||
// - we have transitioned from QR to SAS
|
||||
// - we are verifying with SAS, but we need to replace our verifier with a new one because both parties
|
||||
// tried to start verification at the same time, and we lost the tie breaking
|
||||
if (verification instanceof RustSdkCryptoJs.Sas) {
|
||||
if (this._verifier === undefined || this._verifier instanceof RustQrCodeVerifier) {
|
||||
this.setVerifier(new RustSASVerifier(verification, this, outgoingRequestProcessor));
|
||||
} else if (this._verifier instanceof RustSASVerifier) {
|
||||
this._verifier.replaceInner(verification);
|
||||
}
|
||||
} else if (verification instanceof RustSdkCryptoJs.Qr && this._verifier === undefined) {
|
||||
this.setVerifier(new RustQrCodeVerifier(verification, outgoingRequestProcessor));
|
||||
@@ -426,7 +432,7 @@ export class RustVerificationRequest
|
||||
* this verification.
|
||||
*/
|
||||
public get cancellationCode(): string | null {
|
||||
throw new Error("not implemented");
|
||||
return this.inner.cancelInfo?.cancelCode() ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -435,7 +441,14 @@ export class RustVerificationRequest
|
||||
* Only defined when phase is Cancelled
|
||||
*/
|
||||
public get cancellingUserId(): string | undefined {
|
||||
throw new Error("not implemented");
|
||||
const cancelInfo = this.inner.cancelInfo;
|
||||
if (!cancelInfo) {
|
||||
return undefined;
|
||||
} else if (cancelInfo.cancelledbyUs()) {
|
||||
return this.olmMachine.userId.toString();
|
||||
} else {
|
||||
return this.inner.otherUserId.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,46 +462,45 @@ abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCry
|
||||
VerifierEvent | VerificationRequestEvent,
|
||||
VerifierEventHandlerMap & VerificationRequestEventHandlerMap
|
||||
> {
|
||||
/** A promise which completes when the verification completes (or rejects when it is cancelled/fails) */
|
||||
protected readonly completionPromise: Promise<void>;
|
||||
/** A deferred which completes when the verification completes (or rejects when it is cancelled/fails) */
|
||||
protected readonly completionDeferred: IDeferred<void>;
|
||||
|
||||
public constructor(
|
||||
protected readonly inner: InnerType,
|
||||
protected inner: InnerType,
|
||||
protected readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.completionPromise = new Promise<void>((resolve, reject) => {
|
||||
const onChange = async (): Promise<void> => {
|
||||
this.onChange();
|
||||
|
||||
if (this.inner.isDone()) {
|
||||
resolve(undefined);
|
||||
} else if (this.inner.isCancelled()) {
|
||||
const cancelInfo = this.inner.cancelInfo()!;
|
||||
reject(
|
||||
new Error(
|
||||
`Verification cancelled by ${
|
||||
cancelInfo.cancelledbyUs() ? "us" : "them"
|
||||
} with code ${cancelInfo.cancelCode()}: ${cancelInfo.reason()}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
};
|
||||
inner.registerChangesCallback(onChange);
|
||||
this.completionDeferred = defer();
|
||||
inner.registerChangesCallback(async () => {
|
||||
this.onChange();
|
||||
});
|
||||
// stop the runtime complaining if nobody catches a failure
|
||||
this.completionPromise.catch(() => null);
|
||||
this.completionDeferred.promise.catch(() => null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook which is called when the underlying rust class notifies us that there has been a change.
|
||||
*
|
||||
* Can be overridden by subclasses to see if we can notify the application about an update.
|
||||
* Can be overridden by subclasses to see if we can notify the application about an update. The overriding method
|
||||
* must call `super.onChange()`.
|
||||
*/
|
||||
protected onChange(): void {}
|
||||
protected onChange(): void {
|
||||
if (this.inner.isDone()) {
|
||||
this.completionDeferred.resolve(undefined);
|
||||
} else if (this.inner.isCancelled()) {
|
||||
const cancelInfo = this.inner.cancelInfo()!;
|
||||
this.completionDeferred.reject(
|
||||
new Error(
|
||||
`Verification cancelled by ${
|
||||
cancelInfo.cancelledbyUs() ? "us" : "them"
|
||||
} with code ${cancelInfo.cancelCode()}: ${cancelInfo.reason()}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.emit(VerificationRequestEvent.Change);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the verification has been cancelled, either by us or the other side.
|
||||
@@ -558,6 +570,8 @@ export class RustQrCodeVerifier extends BaseRustVerifer<RustSdkCryptoJs.Qr> impl
|
||||
cancel: () => this.cancel(),
|
||||
};
|
||||
}
|
||||
|
||||
super.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -573,7 +587,7 @@ export class RustQrCodeVerifier extends BaseRustVerifer<RustSdkCryptoJs.Qr> impl
|
||||
this.emit(VerifierEvent.ShowReciprocateQr, this.callbacks);
|
||||
}
|
||||
// Nothing to do here but wait.
|
||||
await this.completionPromise;
|
||||
await this.completionDeferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -650,15 +664,24 @@ export class RustSASVerifier extends BaseRustVerifer<RustSdkCryptoJs.Sas> implem
|
||||
* or times out.
|
||||
*/
|
||||
public async verify(): Promise<void> {
|
||||
await this.sendAccept();
|
||||
await this.completionDeferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the accept or start event, if it hasn't already been sent
|
||||
*/
|
||||
private async sendAccept(): Promise<void> {
|
||||
const req: undefined | OutgoingRequest = this.inner.accept();
|
||||
if (req) {
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||
}
|
||||
await this.completionPromise;
|
||||
}
|
||||
|
||||
/** if we can now show the callbacks, do so */
|
||||
protected onChange(): void {
|
||||
super.onChange();
|
||||
|
||||
if (this.callbacks === null) {
|
||||
const emoji = this.inner.emoji();
|
||||
const decimal = this.inner.decimals();
|
||||
@@ -710,6 +733,25 @@ export class RustSASVerifier extends BaseRustVerifer<RustSdkCryptoJs.Sas> implem
|
||||
public getShowSasCallbacks(): ShowSasCallbacks | null {
|
||||
return this.callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the inner Rust verifier with a different one.
|
||||
*
|
||||
* @param inner - the new Rust verifier
|
||||
* @internal
|
||||
*/
|
||||
public replaceInner(inner: RustSdkCryptoJs.Sas): void {
|
||||
if (this.inner != inner) {
|
||||
this.inner = inner;
|
||||
inner.registerChangesCallback(async () => {
|
||||
this.onChange();
|
||||
});
|
||||
// replaceInner will only get called if we started the verification at the same time as the other side, and we lost
|
||||
// the tie breaker. So we need to re-accept their verification.
|
||||
this.sendAccept();
|
||||
this.onChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** For each specced verification method, the rust-side `VerificationMethod` corresponding to it */
|
||||
|
||||
@@ -615,7 +615,7 @@ export class SlidingSyncSdk {
|
||||
}
|
||||
}
|
||||
|
||||
const encrypted = this.client.isRoomEncrypted(room.roomId);
|
||||
const encrypted = room.hasEncryptionStateEvent();
|
||||
// we do this first so it's correct when any of the events fire
|
||||
if (roomData.notification_count != null) {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, roomData.notification_count);
|
||||
|
||||
@@ -90,10 +90,10 @@ export class IndexedDBStore extends MemoryStore {
|
||||
* ```
|
||||
* let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage };
|
||||
* let store = new IndexedDBStore(opts);
|
||||
* await store.startup(); // load from indexed db
|
||||
* let client = sdk.createClient({
|
||||
* store: store,
|
||||
* });
|
||||
* await store.startup(); // load from indexed db, must be called after createClient
|
||||
* client.startClient();
|
||||
* client.on("sync", function(state, prevState, data) {
|
||||
* if (state === "PREPARED") {
|
||||
@@ -140,7 +140,9 @@ export class IndexedDBStore extends MemoryStore {
|
||||
logger.log(`IndexedDBStore.startup: processing presence events`);
|
||||
userPresenceEvents.forEach(([userId, rawEvent]) => {
|
||||
if (!this.createUser) {
|
||||
throw new Error("createUser is undefined, it should be set with setUserCreator()!");
|
||||
throw new Error(
|
||||
"`IndexedDBStore.startup` must be called after assigning it to the client, not before!",
|
||||
);
|
||||
}
|
||||
const u = this.createUser(userId);
|
||||
if (rawEvent) {
|
||||
|
||||
+2
-2
@@ -1760,11 +1760,11 @@ export class SyncApi {
|
||||
return events?.find((e) => e.getType() === EventType.RoomEncryption && e.getStateKey() === "");
|
||||
}
|
||||
|
||||
// When processing the sync response we cannot rely on MatrixClient::isRoomEncrypted before we actually
|
||||
// When processing the sync response we cannot rely on Room.hasEncryptionStateEvent we actually
|
||||
// inject the events into the room object, so we have to inspect the events themselves.
|
||||
private isRoomEncrypted(room: Room, stateEventList: MatrixEvent[], timelineEventList?: MatrixEvent[]): boolean {
|
||||
return (
|
||||
this.client.isRoomEncrypted(room.roomId) ||
|
||||
room.hasEncryptionStateEvent() ||
|
||||
!!this.findEncryptionEvent(stateEventList) ||
|
||||
!!this.findEncryptionEvent(timelineEventList)
|
||||
);
|
||||
|
||||
+6
-4
@@ -1309,8 +1309,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
const track = stream.getTracks().find((track) => track.kind === "video");
|
||||
|
||||
const sender = this.transceivers.get(getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, "video"))
|
||||
?.sender;
|
||||
const sender = this.transceivers.get(
|
||||
getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, "video"),
|
||||
)?.sender;
|
||||
|
||||
sender?.replaceTrack(track ?? null);
|
||||
|
||||
@@ -1326,8 +1327,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
} else {
|
||||
const track = this.localUsermediaStream?.getTracks().find((track) => track.kind === "video");
|
||||
const sender = this.transceivers.get(getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, "video"))
|
||||
?.sender;
|
||||
const sender = this.transceivers.get(
|
||||
getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, "video"),
|
||||
)?.sender;
|
||||
sender?.replaceTrack(track ?? null);
|
||||
|
||||
this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream!);
|
||||
|
||||
+3
-3
@@ -8,12 +8,12 @@
|
||||
"noUnusedLocals": true,
|
||||
"noEmit": true,
|
||||
"declaration": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./spec/**/*.ts"],
|
||||
"typedocOptions": {
|
||||
"entryPoints": ["src/matrix.ts"],
|
||||
"excludeExternals": true,
|
||||
"out": "_docs"
|
||||
}
|
||||
"out": "_docs",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1674,10 +1674,10 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@matrix-org/matrix-sdk-crypto-wasm@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.0.0.tgz#b33bae9c418c5516d0dbce29662c6db803003626"
|
||||
integrity sha512-a883HchJViPo6ukM0fEDmBgvMI6lWEujqAjMZgwaKEYNZTPgezN5PQvSNz2d+b96/R1y4QOC71zXM1yNylXA6Q==
|
||||
"@matrix-org/matrix-sdk-crypto-wasm@^4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.3.0.tgz#ef08e7eafae6e9e85658c14a41f0d74a48c03f4a"
|
||||
integrity sha512-05+NO78pXda/MTxi05NJwBbbAsOmU6WywBOcAk9GarPzgvrj4GvOuYTY6VR5PD7Gzb3AI+vNx/Ho4V0GFKPW/w==
|
||||
|
||||
"@matrix-org/olm@3.2.15":
|
||||
version "3.2.15"
|
||||
@@ -2023,9 +2023,9 @@
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/node@18":
|
||||
version "18.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.6.tgz#537beece2c8ad4d9abdaa3b0f428e601eb57dac8"
|
||||
integrity sha512-X36s5CXMrrJOs2lQCdDF68apW4Rfx9ixYMawlepwmE4Anezv/AV2LSpKD1Ub8DAc+urp5bk0BGZ6NtmBitfnsg==
|
||||
version "18.19.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.8.tgz#c1e42b165e5a526caf1f010747e0522cb2c9c36a"
|
||||
integrity sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
@@ -2082,15 +2082,15 @@
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^6.0.0":
|
||||
version "6.18.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.18.1.tgz#0df881a47da1c1a9774f39495f5f7052f86b72e0"
|
||||
integrity sha512-nISDRYnnIpk7VCFrGcu1rnZfM1Dh9LRHnfgdkjcbi/l7g16VYRri3TjXi9Ir4lOZSw5N/gnV/3H7jIPQ8Q4daA==
|
||||
version "6.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz#bb0676af940bc23bf299ca58dbdc6589c2548c2e"
|
||||
integrity sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.5.1"
|
||||
"@typescript-eslint/scope-manager" "6.18.1"
|
||||
"@typescript-eslint/type-utils" "6.18.1"
|
||||
"@typescript-eslint/utils" "6.18.1"
|
||||
"@typescript-eslint/visitor-keys" "6.18.1"
|
||||
"@typescript-eslint/scope-manager" "6.19.1"
|
||||
"@typescript-eslint/type-utils" "6.19.1"
|
||||
"@typescript-eslint/utils" "6.19.1"
|
||||
"@typescript-eslint/visitor-keys" "6.19.1"
|
||||
debug "^4.3.4"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.2.4"
|
||||
@@ -2099,14 +2099,14 @@
|
||||
ts-api-utils "^1.0.1"
|
||||
|
||||
"@typescript-eslint/parser@^6.0.0":
|
||||
version "6.18.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.18.1.tgz#3c3987e186b38c77b30b6bfa5edf7c98ae2ec9d3"
|
||||
integrity sha512-zct/MdJnVaRRNy9e84XnVtRv9Vf91/qqe+hZJtKanjojud4wAVy/7lXxJmMyX6X6J+xc6c//YEWvpeif8cAhWA==
|
||||
version "6.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.19.1.tgz#68a87bb21afaf0b1689e9cdce0e6e75bc91ada78"
|
||||
integrity sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "6.18.1"
|
||||
"@typescript-eslint/types" "6.18.1"
|
||||
"@typescript-eslint/typescript-estree" "6.18.1"
|
||||
"@typescript-eslint/visitor-keys" "6.18.1"
|
||||
"@typescript-eslint/scope-manager" "6.19.1"
|
||||
"@typescript-eslint/types" "6.19.1"
|
||||
"@typescript-eslint/typescript-estree" "6.19.1"
|
||||
"@typescript-eslint/visitor-keys" "6.19.1"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.62.0":
|
||||
@@ -2117,21 +2117,21 @@
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
"@typescript-eslint/visitor-keys" "5.62.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@6.18.1":
|
||||
version "6.18.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.18.1.tgz#28c31c60f6e5827996aa3560a538693cb4bd3848"
|
||||
integrity sha512-BgdBwXPFmZzaZUuw6wKiHKIovms97a7eTImjkXCZE04TGHysG+0hDQPmygyvgtkoB/aOQwSM/nWv3LzrOIQOBw==
|
||||
"@typescript-eslint/scope-manager@6.19.1":
|
||||
version "6.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz#2f527ee30703a6169a52b31d42a1103d80acd51b"
|
||||
integrity sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "6.18.1"
|
||||
"@typescript-eslint/visitor-keys" "6.18.1"
|
||||
"@typescript-eslint/types" "6.19.1"
|
||||
"@typescript-eslint/visitor-keys" "6.19.1"
|
||||
|
||||
"@typescript-eslint/type-utils@6.18.1":
|
||||
version "6.18.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.18.1.tgz#115cf535f8b39db8301677199ce51151e2daee96"
|
||||
integrity sha512-wyOSKhuzHeU/5pcRDP2G2Ndci+4g653V43gXTpt4nbyoIOAASkGDA9JIAgbQCdCkcr1MvpSYWzxTz0olCn8+/Q==
|
||||
"@typescript-eslint/type-utils@6.19.1":
|
||||
version "6.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.19.1.tgz#6a130e3afe605a4898e043fa9f72e96309b54935"
|
||||
integrity sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "6.18.1"
|
||||
"@typescript-eslint/utils" "6.18.1"
|
||||
"@typescript-eslint/typescript-estree" "6.19.1"
|
||||
"@typescript-eslint/utils" "6.19.1"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^1.0.1"
|
||||
|
||||
@@ -2140,10 +2140,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
|
||||
integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
|
||||
|
||||
"@typescript-eslint/types@6.18.1":
|
||||
version "6.18.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.18.1.tgz#91617d8080bcd99ac355d9157079970d1d49fefc"
|
||||
integrity sha512-4TuMAe+tc5oA7wwfqMtB0Y5OrREPF1GeJBAjqwgZh1lEMH5PJQgWgHGfYufVB51LtjD+peZylmeyxUXPfENLCw==
|
||||
"@typescript-eslint/types@6.19.1":
|
||||
version "6.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.19.1.tgz#2d4c9d492a63ede15e7ba7d129bdf7714b77f771"
|
||||
integrity sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.62.0":
|
||||
version "5.62.0"
|
||||
@@ -2158,13 +2158,13 @@
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@6.18.1":
|
||||
version "6.18.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.1.tgz#a12b6440175b4cbc9d09ab3c4966c6b245215ab4"
|
||||
integrity sha512-fv9B94UAhywPRhUeeV/v+3SBDvcPiLxRZJw/xZeeGgRLQZ6rLMG+8krrJUyIf6s1ecWTzlsbp0rlw7n9sjufHA==
|
||||
"@typescript-eslint/typescript-estree@6.19.1":
|
||||
version "6.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz#796d88d88882f12e85bb33d6d82d39e1aea54ed1"
|
||||
integrity sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "6.18.1"
|
||||
"@typescript-eslint/visitor-keys" "6.18.1"
|
||||
"@typescript-eslint/types" "6.19.1"
|
||||
"@typescript-eslint/visitor-keys" "6.19.1"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
@@ -2172,17 +2172,17 @@
|
||||
semver "^7.5.4"
|
||||
ts-api-utils "^1.0.1"
|
||||
|
||||
"@typescript-eslint/utils@6.18.1":
|
||||
version "6.18.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.18.1.tgz#3451cfe2e56babb6ac657e10b6703393d4b82955"
|
||||
integrity sha512-zZmTuVZvD1wpoceHvoQpOiewmWu3uP9FuTWo8vqpy2ffsmfCE8mklRPi+vmnIYAIk9t/4kOThri2QCDgor+OpQ==
|
||||
"@typescript-eslint/utils@6.19.1":
|
||||
version "6.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.19.1.tgz#df93497f9cfddde2bcc2a591da80536e68acd151"
|
||||
integrity sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@types/json-schema" "^7.0.12"
|
||||
"@types/semver" "^7.5.0"
|
||||
"@typescript-eslint/scope-manager" "6.18.1"
|
||||
"@typescript-eslint/types" "6.18.1"
|
||||
"@typescript-eslint/typescript-estree" "6.18.1"
|
||||
"@typescript-eslint/scope-manager" "6.19.1"
|
||||
"@typescript-eslint/types" "6.19.1"
|
||||
"@typescript-eslint/typescript-estree" "6.19.1"
|
||||
semver "^7.5.4"
|
||||
|
||||
"@typescript-eslint/utils@^5.10.0":
|
||||
@@ -2207,12 +2207,12 @@
|
||||
"@typescript-eslint/types" "5.62.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@6.18.1":
|
||||
version "6.18.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.1.tgz#704d789bda2565a15475e7d22f145b8fe77443f4"
|
||||
integrity sha512-/kvt0C5lRqGoCfsbmm7/CwMqoSkY3zzHLIjdhHZQW3VFrnz7ATecOHR7nb7V+xn4286MBxfnQfQhAmCI0u+bJA==
|
||||
"@typescript-eslint/visitor-keys@6.19.1":
|
||||
version "6.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz#2164073ed4fc34a5ff3b5e25bb5a442100454c4c"
|
||||
integrity sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "6.18.1"
|
||||
"@typescript-eslint/types" "6.19.1"
|
||||
eslint-visitor-keys "^3.4.1"
|
||||
|
||||
"@ungap/structured-clone@^1.2.0":
|
||||
@@ -2275,10 +2275,10 @@ ajv@^6.12.4, ajv@~6.12.6:
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
allchange@^1.0.6:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/allchange/-/allchange-1.1.0.tgz#f8fa129e4b40c0b0a2c072c530f2324c6590e208"
|
||||
integrity sha512-brDWf2feuL3FRyivSyC6AKOgpX+bYgs1Z7+ZmLti6PnBdZgIjRSnKvlc68N8+1UX2rCISx2I+XuUvE3/GJNG2A==
|
||||
allchange@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/allchange/-/allchange-1.3.0.tgz#0d38e76e069eacd0279fb9a171770426d8e57d37"
|
||||
integrity sha512-orTQYJQzY98ZNvh9VFpBpxLry9obXvDOYuQZXDnTL/YJL3sphgr93norJrR8Qz8mNlJ3yEm1YS+aEEbC3/3Wjg==
|
||||
dependencies:
|
||||
"@actions/core" "^1.4.0"
|
||||
"@actions/github" "^5.0.0"
|
||||
@@ -2287,7 +2287,7 @@ allchange@^1.0.6:
|
||||
js-yaml "^4.1.0"
|
||||
loglevel "^1.7.1"
|
||||
semver "^7.3.5"
|
||||
yargs "^17.0.1"
|
||||
yargs "^17.5.1"
|
||||
|
||||
another-json@^0.2.0:
|
||||
version "0.2.0"
|
||||
@@ -3306,9 +3306,9 @@ eslint-plugin-import@^2.26.0:
|
||||
tsconfig-paths "^3.15.0"
|
||||
|
||||
eslint-plugin-jest@^27.1.6:
|
||||
version "27.6.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.6.2.tgz#8e69404fcd5dfeac03cac478f0ebb9bf2d8db56b"
|
||||
integrity sha512-CI1AlKrsNhYFoP48VU8BVWOi7+qHTq4bRxyUlGjeU8SfFt8abjXhjOuDzUoMp68DoXIx17KpNpIkMrl4s4ZW0g==
|
||||
version "27.6.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.6.3.tgz#8acb8b1e45597fe1f4d4cf25163d90119efc12be"
|
||||
integrity sha512-+YsJFVH6R+tOiO3gCJon5oqn4KWc+mDq2leudk8mrp8RFubLOo9CVyi3cib4L7XMpxExmkmBZQTPDYVBzgpgOA==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^5.10.0"
|
||||
|
||||
@@ -5526,10 +5526,10 @@ prelude-ls@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848"
|
||||
integrity sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==
|
||||
prettier@3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.4.tgz#4723cadeac2ce7c9227de758e5ff9b14e075f283"
|
||||
integrity sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==
|
||||
|
||||
pretty-format@^28.1.3:
|
||||
version "28.1.3"
|
||||
@@ -6719,7 +6719,7 @@ yargs-parser@^21.1.1:
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs@^17.0.1, yargs@^17.3.1, yargs@^17.5.1:
|
||||
yargs@^17.3.1, yargs@^17.5.1:
|
||||
version "17.7.2"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
|
||||
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
|
||||
|
||||
Reference in New Issue
Block a user