Merge branch 'develop' into florianduros/rip-out-legacy-crypto/migrate-roomview-isencrypted
This commit is contained in:
commit
223dd9698c
26 changed files with 188 additions and 63 deletions
33
.github/actions/download-verify-element-tarball/action.yml
vendored
Normal file
33
.github/actions/download-verify-element-tarball/action.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
name: Upload release assets
|
||||||
|
description: Uploads assets to an existing release and optionally signs them
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: GitHub release tag to fetch assets from.
|
||||||
|
required: true
|
||||||
|
out-file-path:
|
||||||
|
description: Path to where the webapp should be extracted to.
|
||||||
|
required: true
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Download current version for its old bundles
|
||||||
|
id: current_download
|
||||||
|
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
|
||||||
|
with:
|
||||||
|
tag: steps.current_version.outputs.version
|
||||||
|
fileName: element-*.tar.gz*
|
||||||
|
out-file-path: ${{ runner.temp }}/download-verify-element-tarball
|
||||||
|
|
||||||
|
- name: Verify tarball
|
||||||
|
run: gpg --verify element-*.tar.gz.asc element-*.tar.gz
|
||||||
|
working-directory: ${{ runner.temp }}/download-verify-element-tarball
|
||||||
|
|
||||||
|
- name: Extract tarball
|
||||||
|
run: tar xvzf element-*.tar.gz -C webapp --strip-components=1
|
||||||
|
working-directory: ${{ runner.temp }}/download-verify-element-tarball
|
||||||
|
|
||||||
|
- name: Move webapp to out-file-path
|
||||||
|
run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }}
|
||||||
|
|
||||||
|
- name: Clean up temp directory
|
||||||
|
run: rm -R ${{ runner.temp }}/download-verify-element-tarball
|
1
.github/workflows/build_develop.yml
vendored
1
.github/workflows/build_develop.yml
vendored
|
@ -20,6 +20,7 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
checks: read
|
checks: read
|
||||||
pages: write
|
pages: write
|
||||||
|
deployments: write
|
||||||
env:
|
env:
|
||||||
R2_BUCKET: "element-web-develop"
|
R2_BUCKET: "element-web-develop"
|
||||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||||
|
|
88
.github/workflows/deploy.yml
vendored
Normal file
88
.github/workflows/deploy.yml
vendored
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
# Manual deploy workflow for deploying to app.element.io & staging.element.io
|
||||||
|
# Runs automatically for staging.element.io when an RC or Release is published
|
||||||
|
# Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand
|
||||||
|
name: Build and Deploy ${{ inputs.site || 'staging.element.io' }}
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
site:
|
||||||
|
description: Which site to deploy to
|
||||||
|
required: true
|
||||||
|
default: staging.element.io
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- staging.element.io
|
||||||
|
- app.element.io
|
||||||
|
concurrency: ${{ inputs.site || 'staging.element.io' }}
|
||||||
|
permissions: {}
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: "Deploy to Cloudflare Pages"
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
environment: ${{ inputs.site || 'staging.element.io' }}
|
||||||
|
permissions:
|
||||||
|
checks: read
|
||||||
|
deployments: write
|
||||||
|
env:
|
||||||
|
SITE: ${{ inputs.site || 'staging.element.io' }}
|
||||||
|
steps:
|
||||||
|
- name: Load GPG key
|
||||||
|
run: |
|
||||||
|
curl https://packages.element.io/element-release-key.gpg | gpg --import
|
||||||
|
gpg -k "$GPG_FINGERPRINT"
|
||||||
|
env:
|
||||||
|
GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }}
|
||||||
|
|
||||||
|
- name: Check current version on deployment
|
||||||
|
id: current_version
|
||||||
|
run: |
|
||||||
|
echo "version=$(curl -s https://$SITE/version)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# The current version bundle melding dance is skipped if the version we're deploying is the same
|
||||||
|
# as then we're just doing a re-deploy of the same version with potentially different configs.
|
||||||
|
- name: Download current version for its old bundles
|
||||||
|
id: current_download
|
||||||
|
if: steps.current_version.outputs.version != github.ref_name
|
||||||
|
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }}
|
||||||
|
with:
|
||||||
|
tag: steps.current_version.outputs.version
|
||||||
|
out-file-path: current_version
|
||||||
|
|
||||||
|
- name: Download target version
|
||||||
|
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }}
|
||||||
|
with:
|
||||||
|
tag: ${{ github.ref_name }}
|
||||||
|
out-file-path: _deploy
|
||||||
|
|
||||||
|
- name: Merge current bundles into target
|
||||||
|
if: steps.current_download.outcome == 'success'
|
||||||
|
run: cp -vnpr current_version/bundles/* _deploy/bundles/
|
||||||
|
|
||||||
|
- name: Copy config
|
||||||
|
run: cp element.io/app/config.json _deploy/config.json
|
||||||
|
|
||||||
|
- name: Populate 404.html
|
||||||
|
run: echo "404 Not Found" > _deploy/404.html
|
||||||
|
|
||||||
|
- name: Populate _headers
|
||||||
|
run: cp .github/cfp_headers _deploy/_headers
|
||||||
|
|
||||||
|
- name: Wait for other steps to succeed
|
||||||
|
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||||
|
with:
|
||||||
|
ref: ${{ github.sha }}
|
||||||
|
running-workflow-name: "Build and Deploy ${{ env.SITE }}"
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
wait-interval: 10
|
||||||
|
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$
|
||||||
|
|
||||||
|
- name: Deploy to Cloudflare Pages
|
||||||
|
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CF_PAGES_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CF_PAGES_ACCOUNT_ID }}
|
||||||
|
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
|
||||||
|
directory: _deploy
|
||||||
|
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
|
@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
|
||||||
// Docker tag to use for synapse docker image.
|
// Docker tag to use for synapse docker image.
|
||||||
// We target a specific digest as every now and then a Synapse update will break our CI.
|
// We target a specific digest as every now and then a Synapse update will break our CI.
|
||||||
// This digest is updated by the playwright-image-updates.yaml workflow periodically.
|
// This digest is updated by the playwright-image-updates.yaml workflow periodically.
|
||||||
const DOCKER_TAG = "develop@sha256:127c68d4468019ce363c8b2fd7a42a3ef50710eb3aaf288a2295dd4623ce9f54";
|
const DOCKER_TAG = "develop@sha256:34da08a44994e0ad2def7ed5f28c3cc7a2f7ead9722f4ae87b23030f59384ea5";
|
||||||
|
|
||||||
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
|
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
|
||||||
const templateDir = path.join(__dirname, "templates", opts.template);
|
const templateDir = path.join(__dirname, "templates", opts.template);
|
||||||
|
|
7
src/@types/matrix-js-sdk.d.ts
vendored
7
src/@types/matrix-js-sdk.d.ts
vendored
|
@ -22,6 +22,13 @@ declare module "matrix-js-sdk/src/types" {
|
||||||
[BLURHASH_FIELD]?: string;
|
[BLURHASH_FIELD]?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImageInfo {
|
||||||
|
/**
|
||||||
|
* @see https://github.com/matrix-org/matrix-spec-proposals/pull/4230
|
||||||
|
*/
|
||||||
|
"org.matrix.msc4230.is_animated"?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StateEvents {
|
export interface StateEvents {
|
||||||
// Jitsi-backed video room state events
|
// Jitsi-backed video room state events
|
||||||
[JitsiCallMemberEventType]: JitsiCallMemberContent;
|
[JitsiCallMemberEventType]: JitsiCallMemberContent;
|
||||||
|
|
|
@ -56,6 +56,7 @@ import { createThumbnail } from "./utils/image-media";
|
||||||
import { attachMentions, attachRelation } from "./components/views/rooms/SendMessageComposer";
|
import { attachMentions, attachRelation } from "./components/views/rooms/SendMessageComposer";
|
||||||
import { doMaybeLocalRoomAction } from "./utils/local-room";
|
import { doMaybeLocalRoomAction } from "./utils/local-room";
|
||||||
import { SdkContextClass } from "./contexts/SDKContext";
|
import { SdkContextClass } from "./contexts/SDKContext";
|
||||||
|
import { blobIsAnimated } from "./utils/Image.ts";
|
||||||
|
|
||||||
// scraped out of a macOS hidpi (5660ppm) screenshot png
|
// scraped out of a macOS hidpi (5660ppm) screenshot png
|
||||||
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
||||||
|
@ -150,15 +151,20 @@ async function infoForImageFile(matrixClient: MatrixClient, roomId: string, imag
|
||||||
thumbnailType = "image/jpeg";
|
thumbnailType = "image/jpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We don't await this immediately so it can happen in the background
|
||||||
|
const isAnimatedPromise = blobIsAnimated(imageFile.type, imageFile);
|
||||||
|
|
||||||
const imageElement = await loadImageElement(imageFile);
|
const imageElement = await loadImageElement(imageFile);
|
||||||
|
|
||||||
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
|
const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
|
||||||
const imageInfo = result.info;
|
const imageInfo = result.info;
|
||||||
|
|
||||||
|
imageInfo["org.matrix.msc4230.is_animated"] = await isAnimatedPromise;
|
||||||
|
|
||||||
// For lesser supported image types, always include the thumbnail even if it is larger
|
// For lesser supported image types, always include the thumbnail even if it is larger
|
||||||
if (!ALWAYS_INCLUDE_THUMBNAIL.includes(imageFile.type)) {
|
if (!ALWAYS_INCLUDE_THUMBNAIL.includes(imageFile.type)) {
|
||||||
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
|
// we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
|
||||||
const sizeDifference = imageFile.size - imageInfo.thumbnail_info!.size;
|
const sizeDifference = imageFile.size - imageInfo.thumbnail_info!.size!;
|
||||||
if (
|
if (
|
||||||
// image is small enough already
|
// image is small enough already
|
||||||
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL ||
|
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL ||
|
||||||
|
|
|
@ -230,12 +230,15 @@ export default class DeviceListener {
|
||||||
private async getKeyBackupInfo(): Promise<KeyBackupInfo | null> {
|
private async getKeyBackupInfo(): Promise<KeyBackupInfo | null> {
|
||||||
if (!this.client) return null;
|
if (!this.client) return null;
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
|
const crypto = this.client.getCrypto();
|
||||||
|
if (!crypto) return null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.keyBackupInfo ||
|
!this.keyBackupInfo ||
|
||||||
!this.keyBackupFetchedAt ||
|
!this.keyBackupFetchedAt ||
|
||||||
this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL
|
this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL
|
||||||
) {
|
) {
|
||||||
this.keyBackupInfo = await this.client.getKeyBackupVersion();
|
this.keyBackupInfo = await crypto.getKeyBackupInfo();
|
||||||
this.keyBackupFetchedAt = now;
|
this.keyBackupFetchedAt = now;
|
||||||
}
|
}
|
||||||
return this.keyBackupInfo;
|
return this.keyBackupInfo;
|
||||||
|
|
|
@ -279,7 +279,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||||
if (!forceReset) {
|
if (!forceReset) {
|
||||||
try {
|
try {
|
||||||
this.setState({ phase: Phase.Loading });
|
this.setState({ phase: Phase.Loading });
|
||||||
backupInfo = await cli.getKeyBackupVersion();
|
backupInfo = await crypto.getKeyBackupInfo();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error fetching backup data from server", e);
|
logger.error("Error fetching backup data from server", e);
|
||||||
this.setState({ phase: Phase.LoadError });
|
this.setState({ phase: Phase.LoadError });
|
||||||
|
|
|
@ -1638,7 +1638,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
} else {
|
} else {
|
||||||
// otherwise check the server to see if there's a new one
|
// otherwise check the server to see if there's a new one
|
||||||
try {
|
try {
|
||||||
newVersionInfo = await cli.getKeyBackupVersion();
|
newVersionInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
|
||||||
if (newVersionInfo !== null) haveNewVersion = true;
|
if (newVersionInfo !== null) haveNewVersion = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Saw key backup error but failed to check backup version!", e);
|
logger.error("Saw key backup error but failed to check backup version!", e);
|
||||||
|
|
|
@ -109,7 +109,7 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// backup is not active. see if there is a backup version on the server we ought to back up to.
|
// backup is not active. see if there is a backup version on the server we ought to back up to.
|
||||||
const backupInfo = await client.getKeyBackupVersion();
|
const backupInfo = await crypto.getKeyBackupInfo();
|
||||||
this.setState({ backupStatus: backupInfo ? BackupStatus.SERVER_BACKUP_BUT_DISABLED : BackupStatus.NO_BACKUP });
|
this.setState({ backupStatus: backupInfo ? BackupStatus.SERVER_BACKUP_BUT_DISABLED : BackupStatus.NO_BACKUP });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -258,7 +258,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const cli = MatrixClientPeg.safeGet();
|
const cli = MatrixClientPeg.safeGet();
|
||||||
const backupInfo = await cli.getKeyBackupVersion();
|
const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
|
||||||
const has4S = await cli.secretStorage.hasKey();
|
const has4S = await cli.secretStorage.hasKey();
|
||||||
const backupKeyStored = has4S ? await cli.isKeyBackupKeyStored() : null;
|
const backupKeyStored = has4S ? await cli.isKeyBackupKeyStored() : null;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -275,7 +275,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = this.props.mxEvent.getContent<ImageContent>();
|
const content = this.props.mxEvent.getContent<ImageContent>();
|
||||||
let isAnimated = mayBeAnimated(content.info?.mimetype);
|
let isAnimated = content.info?.["org.matrix.msc4230.is_animated"] ?? mayBeAnimated(content.info?.mimetype);
|
||||||
|
|
||||||
// If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server
|
// If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server
|
||||||
// because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail.
|
// because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail.
|
||||||
|
@ -298,8 +298,15 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await this.props.mediaEventHelper!.sourceBlob.value;
|
// If we didn't receive the MSC4230 is_animated flag
|
||||||
if (!(await blobIsAnimated(content.info?.mimetype, blob))) {
|
// then we need to check if the image is animated by downloading it.
|
||||||
|
if (
|
||||||
|
content.info?.["org.matrix.msc4230.is_animated"] === false ||
|
||||||
|
!(await blobIsAnimated(
|
||||||
|
content.info?.mimetype,
|
||||||
|
await this.props.mediaEventHelper!.sourceBlob.value,
|
||||||
|
))
|
||||||
|
) {
|
||||||
isAnimated = false;
|
isAnimated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
this.getUpdatedDiagnostics();
|
this.getUpdatedDiagnostics();
|
||||||
try {
|
try {
|
||||||
const cli = MatrixClientPeg.safeGet();
|
const cli = MatrixClientPeg.safeGet();
|
||||||
const backupInfo = await cli.getKeyBackupVersion();
|
const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
|
||||||
const backupTrustInfo = backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined;
|
const backupTrustInfo = backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined;
|
||||||
|
|
||||||
const activeBackupVersion = (await cli.getCrypto()?.getActiveSessionBackupVersion()) ?? null;
|
const activeBackupVersion = (await cli.getCrypto()?.getActiveSessionBackupVersion()) ?? null;
|
||||||
|
@ -192,12 +192,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
if (!proceed) return;
|
if (!proceed) return;
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
const versionToDelete = this.state.backupInfo!.version!;
|
const versionToDelete = this.state.backupInfo!.version!;
|
||||||
MatrixClientPeg.safeGet()
|
// deleteKeyBackupVersion fires a key backup status event
|
||||||
.getCrypto()
|
// which will update the UI
|
||||||
?.deleteKeyBackupVersion(versionToDelete)
|
MatrixClientPeg.safeGet().getCrypto()?.deleteKeyBackupVersion(versionToDelete);
|
||||||
.then(() => {
|
|
||||||
this.loadBackupStatus();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -125,7 +125,7 @@ export class SetupEncryptionStore extends EventEmitter {
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
try {
|
try {
|
||||||
const cli = MatrixClientPeg.safeGet();
|
const cli = MatrixClientPeg.safeGet();
|
||||||
const backupInfo = await cli.getKeyBackupVersion();
|
const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null;
|
||||||
this.backupInfo = backupInfo;
|
this.backupInfo = backupInfo;
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EncryptedFile } from "matrix-js-sdk/src/types";
|
import { ImageInfo } from "matrix-js-sdk/src/types";
|
||||||
|
|
||||||
import { BlurhashEncoder } from "../BlurhashEncoder";
|
import { BlurhashEncoder } from "../BlurhashEncoder";
|
||||||
|
|
||||||
|
@ -15,19 +15,7 @@ type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
||||||
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
|
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
|
||||||
|
|
||||||
interface IThumbnail {
|
interface IThumbnail {
|
||||||
info: {
|
info: ImageInfo;
|
||||||
thumbnail_info?: {
|
|
||||||
w: number;
|
|
||||||
h: number;
|
|
||||||
mimetype: string;
|
|
||||||
size: number;
|
|
||||||
};
|
|
||||||
w: number;
|
|
||||||
h: number;
|
|
||||||
[BLURHASH_FIELD]?: string;
|
|
||||||
thumbnail_url?: string;
|
|
||||||
thumbnail_file?: EncryptedFile;
|
|
||||||
};
|
|
||||||
thumbnail: Blob;
|
thumbnail: Blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -474,10 +474,8 @@ export default class ElectronPlatform extends BasePlatform {
|
||||||
const url = super.getOidcCallbackUrl();
|
const url = super.getOidcCallbackUrl();
|
||||||
url.protocol = "io.element.desktop";
|
url.protocol = "io.element.desktop";
|
||||||
// Trim the double slash into a single slash to comply with https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
|
// Trim the double slash into a single slash to comply with https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
|
||||||
// Chrome seems to have a strange issue where non-standard protocols prevent URL object mutations on pathname
|
if (url.href.startsWith(`${url.protocol}://`)) {
|
||||||
// field, so we cannot mutate `pathname` reliably and instead have to rewrite the href manually.
|
url.href = url.href.replace("://", ":/");
|
||||||
if (url.pathname.startsWith("//")) {
|
|
||||||
url.href = url.href.replace(url.pathname, url.pathname.slice(1));
|
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,7 +143,6 @@ export const mockClientMethodsCrypto = (): Partial<
|
||||||
> => ({
|
> => ({
|
||||||
isKeyBackupKeyStored: jest.fn(),
|
isKeyBackupKeyStored: jest.fn(),
|
||||||
getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }),
|
getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }),
|
||||||
getKeyBackupVersion: jest.fn().mockResolvedValue(null),
|
|
||||||
secretStorage: { hasKey: jest.fn() },
|
secretStorage: { hasKey: jest.fn() },
|
||||||
getCrypto: jest.fn().mockReturnValue({
|
getCrypto: jest.fn().mockReturnValue({
|
||||||
getUserDeviceInfo: jest.fn(),
|
getUserDeviceInfo: jest.fn(),
|
||||||
|
@ -163,6 +162,7 @@ export const mockClientMethodsCrypto = (): Partial<
|
||||||
getOwnDeviceKeys: jest.fn().mockReturnValue(new Promise(() => {})),
|
getOwnDeviceKeys: jest.fn().mockReturnValue(new Promise(() => {})),
|
||||||
getCrossSigningKeyId: jest.fn(),
|
getCrossSigningKeyId: jest.fn(),
|
||||||
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
|
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
|
||||||
|
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,6 @@ export function createTestClient(): MatrixClient {
|
||||||
getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }),
|
getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }),
|
||||||
getSessionId: jest.fn().mockReturnValue("iaszphgvfku"),
|
getSessionId: jest.fn().mockReturnValue("iaszphgvfku"),
|
||||||
credentials: { userId: "@userId:matrix.org" },
|
credentials: { userId: "@userId:matrix.org" },
|
||||||
getKeyBackupVersion: jest.fn(),
|
|
||||||
|
|
||||||
secretStorage: {
|
secretStorage: {
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
|
@ -135,6 +134,7 @@ export function createTestClient(): MatrixClient {
|
||||||
restoreKeyBackupWithPassphrase: jest.fn(),
|
restoreKeyBackupWithPassphrase: jest.fn(),
|
||||||
loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(),
|
loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(),
|
||||||
storeSessionBackupPrivateKey: jest.fn(),
|
storeSessionBackupPrivateKey: jest.fn(),
|
||||||
|
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getPushActionsForEvent: jest.fn(),
|
getPushActionsForEvent: jest.fn(),
|
||||||
|
|
|
@ -96,12 +96,12 @@ describe("DeviceListener", () => {
|
||||||
}),
|
}),
|
||||||
getSessionBackupPrivateKey: jest.fn(),
|
getSessionBackupPrivateKey: jest.fn(),
|
||||||
isEncryptionEnabledInRoom: jest.fn(),
|
isEncryptionEnabledInRoom: jest.fn(),
|
||||||
|
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||||
} as unknown as Mocked<CryptoApi>;
|
} as unknown as Mocked<CryptoApi>;
|
||||||
mockClient = getMockClientWithEventEmitter({
|
mockClient = getMockClientWithEventEmitter({
|
||||||
isGuest: jest.fn(),
|
isGuest: jest.fn(),
|
||||||
getUserId: jest.fn().mockReturnValue(userId),
|
getUserId: jest.fn().mockReturnValue(userId),
|
||||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||||
getKeyBackupVersion: jest.fn().mockResolvedValue(undefined),
|
|
||||||
getRooms: jest.fn().mockReturnValue([]),
|
getRooms: jest.fn().mockReturnValue([]),
|
||||||
isVersionSupported: jest.fn().mockResolvedValue(true),
|
isVersionSupported: jest.fn().mockResolvedValue(true),
|
||||||
isInitialSyncComplete: jest.fn().mockReturnValue(true),
|
isInitialSyncComplete: jest.fn().mockReturnValue(true),
|
||||||
|
@ -354,7 +354,7 @@ describe("DeviceListener", () => {
|
||||||
|
|
||||||
it("shows set up encryption toast when user has a key backup available", async () => {
|
it("shows set up encryption toast when user has a key backup available", async () => {
|
||||||
// non falsy response
|
// non falsy response
|
||||||
mockClient!.getKeyBackupVersion.mockResolvedValue({} as unknown as KeyBackupInfo);
|
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo);
|
||||||
await createAndStart();
|
await createAndStart();
|
||||||
|
|
||||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||||
|
@ -673,7 +673,7 @@ describe("DeviceListener", () => {
|
||||||
describe("When Room Key Backup is not enabled", () => {
|
describe("When Room Key Backup is not enabled", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// no backup
|
// no backup
|
||||||
mockClient.getKeyBackupVersion.mockResolvedValue(null);
|
mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should report recovery state as Enabled", async () => {
|
it("Should report recovery state as Enabled", async () => {
|
||||||
|
@ -722,7 +722,7 @@ describe("DeviceListener", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// no backup
|
// no backup
|
||||||
mockClient.getKeyBackupVersion.mockResolvedValue(null);
|
mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
|
||||||
|
|
||||||
await createAndStart();
|
await createAndStart();
|
||||||
|
|
||||||
|
@ -872,7 +872,7 @@ describe("DeviceListener", () => {
|
||||||
describe("When Room Key Backup is enabled", () => {
|
describe("When Room Key Backup is enabled", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// backup enabled - just need a mock object
|
// backup enabled - just need a mock object
|
||||||
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
|
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
const testCases = [
|
const testCases = [
|
||||||
|
|
|
@ -139,6 +139,7 @@ describe("<MatrixChat />", () => {
|
||||||
globalBlacklistUnverifiedDevices: false,
|
globalBlacklistUnverifiedDevices: false,
|
||||||
// This needs to not finish immediately because we need to test the screen appears
|
// This needs to not finish immediately because we need to test the screen appears
|
||||||
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
|
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
|
||||||
|
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||||
}),
|
}),
|
||||||
secretStorage: {
|
secretStorage: {
|
||||||
isStored: jest.fn().mockReturnValue(null),
|
isStored: jest.fn().mockReturnValue(null),
|
||||||
|
@ -148,7 +149,6 @@ describe("<MatrixChat />", () => {
|
||||||
whoami: jest.fn(),
|
whoami: jest.fn(),
|
||||||
logout: jest.fn(),
|
logout: jest.fn(),
|
||||||
getDeviceId: jest.fn(),
|
getDeviceId: jest.fn(),
|
||||||
getKeyBackupVersion: jest.fn().mockResolvedValue(null),
|
|
||||||
});
|
});
|
||||||
let mockClient: Mocked<MatrixClient>;
|
let mockClient: Mocked<MatrixClient>;
|
||||||
const serverConfig = {
|
const serverConfig = {
|
||||||
|
|
|
@ -22,7 +22,6 @@ describe("LogoutDialog", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockClient = getMockClientWithEventEmitter({
|
mockClient = getMockClientWithEventEmitter({
|
||||||
...mockClientMethodsCrypto(),
|
...mockClientMethodsCrypto(),
|
||||||
getKeyBackupVersion: jest.fn(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mockCrypto = mocked(mockClient.getCrypto()!);
|
mockCrypto = mocked(mockClient.getCrypto()!);
|
||||||
|
@ -50,14 +49,14 @@ describe("LogoutDialog", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Prompts user to connect backup if there is a backup on the server", async () => {
|
it("Prompts user to connect backup if there is a backup on the server", async () => {
|
||||||
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
|
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo);
|
||||||
const rendered = renderComponent();
|
const rendered = renderComponent();
|
||||||
await rendered.findByText("Connect this session to Key Backup");
|
await rendered.findByText("Connect this session to Key Backup");
|
||||||
expect(rendered.container).toMatchSnapshot();
|
expect(rendered.container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Prompts user to set up backup if there is no backup on the server", async () => {
|
it("Prompts user to set up backup if there is no backup on the server", async () => {
|
||||||
mockClient.getKeyBackupVersion.mockResolvedValue(null);
|
mockCrypto.getKeyBackupInfo.mockResolvedValue(null);
|
||||||
const rendered = renderComponent();
|
const rendered = renderComponent();
|
||||||
await rendered.findByText("Start using Key Backup");
|
await rendered.findByText("Start using Key Backup");
|
||||||
expect(rendered.container).toMatchSnapshot();
|
expect(rendered.container).toMatchSnapshot();
|
||||||
|
@ -69,7 +68,7 @@ describe("LogoutDialog", () => {
|
||||||
describe("when there is an error fetching backups", () => {
|
describe("when there is an error fetching backups", () => {
|
||||||
filterConsole("Unable to fetch key backup status");
|
filterConsole("Unable to fetch key backup status");
|
||||||
it("prompts user to set up backup", async () => {
|
it("prompts user to set up backup", async () => {
|
||||||
mockClient.getKeyBackupVersion.mockImplementation(async () => {
|
mockCrypto.getKeyBackupInfo.mockImplementation(async () => {
|
||||||
throw new Error("beep");
|
throw new Error("beep");
|
||||||
});
|
});
|
||||||
const rendered = renderComponent();
|
const rendered = renderComponent();
|
||||||
|
|
|
@ -77,7 +77,7 @@ describe("CreateSecretStorageDialog", () => {
|
||||||
filterConsole("Error fetching backup data from server");
|
filterConsole("Error fetching backup data from server");
|
||||||
|
|
||||||
it("shows an error", async () => {
|
it("shows an error", async () => {
|
||||||
mockClient.getKeyBackupVersion.mockImplementation(async () => {
|
jest.spyOn(mockClient.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => {
|
||||||
throw new Error("bleh bleh");
|
throw new Error("bleh bleh");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ describe("CreateSecretStorageDialog", () => {
|
||||||
expect(result.container).toMatchSnapshot();
|
expect(result.container).toMatchSnapshot();
|
||||||
|
|
||||||
// Now we can get the backup and we retry
|
// Now we can get the backup and we retry
|
||||||
mockClient.getKeyBackupVersion.mockRestore();
|
jest.spyOn(mockClient.getCrypto()!, "getKeyBackupInfo").mockRestore();
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Retry" }));
|
await userEvent.click(screen.getByRole("button", { name: "Retry" }));
|
||||||
await screen.findByText("Your keys are now being backed up from this device.");
|
await screen.findByText("Your keys are now being backed up from this device.");
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,7 +28,7 @@ describe("<RestoreKeyBackupDialog />", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
matrixClient = stubClient();
|
matrixClient = stubClient();
|
||||||
jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockReturnValue(new Uint8Array(32));
|
jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockReturnValue(new Uint8Array(32));
|
||||||
jest.spyOn(matrixClient, "getKeyBackupVersion").mockResolvedValue({ version: "1" } as KeyBackupInfo);
|
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({ version: "1" } as KeyBackupInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render", async () => {
|
it("should render", async () => {
|
||||||
|
@ -99,7 +99,7 @@ describe("<RestoreKeyBackupDialog />", () => {
|
||||||
|
|
||||||
test("should restore key backup when passphrase is filled", async () => {
|
test("should restore key backup when passphrase is filled", async () => {
|
||||||
// Determine that the passphrase is required
|
// Determine that the passphrase is required
|
||||||
jest.spyOn(matrixClient, "getKeyBackupVersion").mockResolvedValue({
|
jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
|
||||||
version: "1",
|
version: "1",
|
||||||
auth_data: {
|
auth_data: {
|
||||||
private_key_salt: "salt",
|
private_key_salt: "salt",
|
||||||
|
|
|
@ -28,14 +28,13 @@ describe("<SecureBackupPanel />", () => {
|
||||||
const client = getMockClientWithEventEmitter({
|
const client = getMockClientWithEventEmitter({
|
||||||
...mockClientMethodsUser(userId),
|
...mockClientMethodsUser(userId),
|
||||||
...mockClientMethodsCrypto(),
|
...mockClientMethodsCrypto(),
|
||||||
getKeyBackupVersion: jest.fn().mockReturnValue("1"),
|
|
||||||
getClientWellKnown: jest.fn(),
|
getClientWellKnown: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getComponent = () => render(<SecureBackupPanel />);
|
const getComponent = () => render(<SecureBackupPanel />);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
client.getKeyBackupVersion.mockResolvedValue({
|
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({
|
||||||
version: "1",
|
version: "1",
|
||||||
algorithm: "test",
|
algorithm: "test",
|
||||||
auth_data: {
|
auth_data: {
|
||||||
|
@ -52,7 +51,6 @@ describe("<SecureBackupPanel />", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false);
|
mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false);
|
||||||
client.getKeyBackupVersion.mockClear();
|
|
||||||
|
|
||||||
mocked(accessSecretStorage).mockClear().mockResolvedValue();
|
mocked(accessSecretStorage).mockClear().mockResolvedValue();
|
||||||
});
|
});
|
||||||
|
@ -65,8 +63,8 @@ describe("<SecureBackupPanel />", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles error fetching backup", async () => {
|
it("handles error fetching backup", async () => {
|
||||||
// getKeyBackupVersion can fail for various reasons
|
// getKeyBackupInfo can fail for various reasons
|
||||||
client.getKeyBackupVersion.mockImplementation(async () => {
|
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => {
|
||||||
throw new Error("beep beep");
|
throw new Error("beep beep");
|
||||||
});
|
});
|
||||||
const renderResult = getComponent();
|
const renderResult = getComponent();
|
||||||
|
@ -75,9 +73,9 @@ describe("<SecureBackupPanel />", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles absence of backup", async () => {
|
it("handles absence of backup", async () => {
|
||||||
client.getKeyBackupVersion.mockResolvedValue(null);
|
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue(null);
|
||||||
getComponent();
|
getComponent();
|
||||||
// flush getKeyBackupVersion promise
|
// flush getKeyBackupInfo promise
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
expect(screen.getByText("Back up your keys before signing out to avoid losing them.")).toBeInTheDocument();
|
expect(screen.getByText("Back up your keys before signing out to avoid losing them.")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
@ -120,7 +118,7 @@ describe("<SecureBackupPanel />", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("deletes backup after confirmation", async () => {
|
it("deletes backup after confirmation", async () => {
|
||||||
client.getKeyBackupVersion
|
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo")
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
version: "1",
|
version: "1",
|
||||||
algorithm: "test",
|
algorithm: "test",
|
||||||
|
@ -157,7 +155,7 @@ describe("<SecureBackupPanel />", () => {
|
||||||
// flush checkKeyBackup promise
|
// flush checkKeyBackup promise
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
client.getKeyBackupVersion.mockClear();
|
jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockClear();
|
||||||
mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear();
|
mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear();
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Reset"));
|
fireEvent.click(screen.getByText("Reset"));
|
||||||
|
@ -167,7 +165,7 @@ describe("<SecureBackupPanel />", () => {
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
// backup status refreshed
|
// backup status refreshed
|
||||||
expect(client.getKeyBackupVersion).toHaveBeenCalled();
|
expect(client.getCrypto()!.getKeyBackupInfo).toHaveBeenCalled();
|
||||||
expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled();
|
expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,7 +34,6 @@ describe("<SecurityUserSettingsTab />", () => {
|
||||||
...mockClientMethodsCrypto(),
|
...mockClientMethodsCrypto(),
|
||||||
getRooms: jest.fn().mockReturnValue([]),
|
getRooms: jest.fn().mockReturnValue([]),
|
||||||
getIgnoredUsers: jest.fn(),
|
getIgnoredUsers: jest.fn(),
|
||||||
getKeyBackupVersion: jest.fn(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sdkContext = new SdkContextClass();
|
const sdkContext = new SdkContextClass();
|
||||||
|
|
|
@ -37,6 +37,7 @@ describe("SetupEncryptionStore", () => {
|
||||||
getDeviceVerificationStatus: jest.fn(),
|
getDeviceVerificationStatus: jest.fn(),
|
||||||
isDehydrationSupported: jest.fn().mockResolvedValue(false),
|
isDehydrationSupported: jest.fn().mockResolvedValue(false),
|
||||||
startDehydration: jest.fn(),
|
startDehydration: jest.fn(),
|
||||||
|
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||||
} as unknown as Mocked<CryptoApi>;
|
} as unknown as Mocked<CryptoApi>;
|
||||||
client.getCrypto.mockReturnValue(mockCrypto);
|
client.getCrypto.mockReturnValue(mockCrypto);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue