Merge branch 'develop' into widget_state_no_update_invitation_room
commit
4ef11a03cd
|
@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||||
|
import type { CypressBot } from "../../support/bot";
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
type EmojiMapping = [emoji: string, name: string];
|
type EmojiMapping = [emoji: string, name: string];
|
||||||
interface CryptoTestContext extends Mocha.Context {
|
interface CryptoTestContext extends Mocha.Context {
|
||||||
synapse: SynapseInstance;
|
synapse: SynapseInstance;
|
||||||
bob: MatrixClient;
|
bob: CypressBot;
|
||||||
}
|
}
|
||||||
|
|
||||||
const waitForVerificationRequest = (cli: MatrixClient): Promise<VerificationRequest> => {
|
const waitForVerificationRequest = (cli: MatrixClient): Promise<VerificationRequest> => {
|
||||||
|
@ -197,7 +197,7 @@ describe("Cryptography", function () {
|
||||||
cy.bootstrapCrossSigning();
|
cy.bootstrapCrossSigning();
|
||||||
autoJoin(this.bob);
|
autoJoin(this.bob);
|
||||||
|
|
||||||
/* we need to have a room with the other user present, so we can open the verification panel */
|
// we need to have a room with the other user present, so we can open the verification panel
|
||||||
let roomId: string;
|
let roomId: string;
|
||||||
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }).then((_room1Id) => {
|
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }).then((_room1Id) => {
|
||||||
roomId = _room1Id;
|
roomId = _room1Id;
|
||||||
|
@ -210,4 +210,85 @@ describe("Cryptography", function () {
|
||||||
|
|
||||||
verify.call(this);
|
verify.call(this);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) {
|
||||||
|
cy.bootstrapCrossSigning();
|
||||||
|
|
||||||
|
// bob has a second, not cross-signed, device
|
||||||
|
cy.loginBot(this.synapse, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice");
|
||||||
|
|
||||||
|
autoJoin(this.bob);
|
||||||
|
|
||||||
|
// first create the room, so that we can open the verification panel
|
||||||
|
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] })
|
||||||
|
.as("testRoomId")
|
||||||
|
.then((roomId) => {
|
||||||
|
cy.log(`Created test room ${roomId}`);
|
||||||
|
cy.visit(`/#/room/${roomId}`);
|
||||||
|
|
||||||
|
// enable encryption
|
||||||
|
cy.getClient().then((cli) => {
|
||||||
|
cli.sendStateEvent(roomId, "m.room.encryption", { algorithm: "m.megolm.v1.aes-sha2" });
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait for Bob to join the room, otherwise our attempt to open his user details may race
|
||||||
|
// with his join.
|
||||||
|
cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
verify.call(this);
|
||||||
|
|
||||||
|
cy.get<string>("@testRoomId").then((roomId) => {
|
||||||
|
// bob sends a valid event
|
||||||
|
cy.wrap(this.bob.sendTextMessage(roomId, "Hoo!")).as("testEvent");
|
||||||
|
|
||||||
|
// the message should appear, decrypted, with no warning
|
||||||
|
cy.contains(".mx_EventTile_body", "Hoo!")
|
||||||
|
.closest(".mx_EventTile")
|
||||||
|
.should("have.class", "mx_EventTile_verified")
|
||||||
|
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning");
|
||||||
|
|
||||||
|
// bob sends an edit to the first message with his unverified device
|
||||||
|
cy.get<MatrixClient>("@bobSecondDevice").then((bobSecondDevice) => {
|
||||||
|
cy.get<ISendEventResponse>("@testEvent").then((testEvent) => {
|
||||||
|
bobSecondDevice.sendMessage(roomId, {
|
||||||
|
"m.new_content": {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "Haa!",
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: "m.replace",
|
||||||
|
event_id: testEvent.event_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// the edit should have a warning
|
||||||
|
cy.contains(".mx_EventTile_body", "Haa!")
|
||||||
|
.closest(".mx_EventTile")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".mx_EventTile_e2eIcon_warning").should("exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
// a second edit from the verified device should be ok
|
||||||
|
cy.get<ISendEventResponse>("@testEvent").then((testEvent) => {
|
||||||
|
this.bob.sendMessage(roomId, {
|
||||||
|
"m.new_content": {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "Hee!",
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: "m.replace",
|
||||||
|
event_id: testEvent.event_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.contains(".mx_EventTile_body", "Hee!")
|
||||||
|
.closest(".mx_EventTile")
|
||||||
|
.should("have.class", "mx_EventTile_verified")
|
||||||
|
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,6 +51,10 @@ const defaultCreateBotOptions = {
|
||||||
bootstrapCrossSigning: true,
|
bootstrapCrossSigning: true,
|
||||||
} as CreateBotOpts;
|
} as CreateBotOpts;
|
||||||
|
|
||||||
|
export interface CypressBot extends MatrixClient {
|
||||||
|
__cypress_password: string;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
|
@ -60,7 +64,7 @@ declare global {
|
||||||
* @param synapse the instance on which to register the bot user
|
* @param synapse the instance on which to register the bot user
|
||||||
* @param opts create bot options
|
* @param opts create bot options
|
||||||
*/
|
*/
|
||||||
getBot(synapse: SynapseInstance, opts: CreateBotOpts): Chainable<MatrixClient>;
|
getBot(synapse: SynapseInstance, opts: CreateBotOpts): Chainable<CypressBot>;
|
||||||
/**
|
/**
|
||||||
* Returns a new Bot instance logged in as an existing user
|
* Returns a new Bot instance logged in as an existing user
|
||||||
* @param synapse the instance on which to register the bot user
|
* @param synapse the instance on which to register the bot user
|
||||||
|
@ -156,14 +160,20 @@ function setupBotClient(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): Chainable<MatrixClient> => {
|
Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): Chainable<CypressBot> => {
|
||||||
opts = Object.assign({}, defaultCreateBotOptions, opts);
|
opts = Object.assign({}, defaultCreateBotOptions, opts);
|
||||||
const username = Cypress._.uniqueId(opts.userIdPrefix);
|
const username = Cypress._.uniqueId(opts.userIdPrefix);
|
||||||
const password = Cypress._.uniqueId("password_");
|
const password = Cypress._.uniqueId("password_");
|
||||||
return cy.registerUser(synapse, username, password, opts.displayName).then((credentials) => {
|
return cy
|
||||||
cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`);
|
.registerUser(synapse, username, password, opts.displayName)
|
||||||
return setupBotClient(synapse, credentials, opts);
|
.then((credentials) => {
|
||||||
});
|
cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`);
|
||||||
|
return setupBotClient(synapse, credentials, opts);
|
||||||
|
})
|
||||||
|
.then((client): Chainable<CypressBot> => {
|
||||||
|
Object.assign(client, { __cypress_password: password });
|
||||||
|
return cy.wrap(client as CypressBot);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
|
|
|
@ -232,7 +232,7 @@ interface IState {
|
||||||
// Whether the action bar is focused.
|
// Whether the action bar is focused.
|
||||||
actionBarFocused: boolean;
|
actionBarFocused: boolean;
|
||||||
// Whether the event's sender has been verified.
|
// Whether the event's sender has been verified.
|
||||||
verified: string;
|
verified: string | null;
|
||||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||||
reactions?: Relations | null | undefined;
|
reactions?: Relations | null | undefined;
|
||||||
|
|
||||||
|
@ -278,7 +278,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
this.state = {
|
this.state = {
|
||||||
// Whether the action bar is focused.
|
// Whether the action bar is focused.
|
||||||
actionBarFocused: false,
|
actionBarFocused: false,
|
||||||
// Whether the event's sender has been verified.
|
// Whether the event's sender has been verified. `null` if no attempt has yet been made to verify
|
||||||
|
// (including if the event is not encrypted).
|
||||||
verified: null,
|
verified: null,
|
||||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||||
reactions: this.getReactions(),
|
reactions: this.getReactions(),
|
||||||
|
@ -371,6 +372,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
|
client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
|
||||||
client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
|
client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
|
||||||
this.props.mxEvent.on(MatrixEventEvent.Decrypted, this.onDecrypted);
|
this.props.mxEvent.on(MatrixEventEvent.Decrypted, this.onDecrypted);
|
||||||
|
this.props.mxEvent.on(MatrixEventEvent.Replaced, this.onReplaced);
|
||||||
DecryptionFailureTracker.instance.addVisibleEvent(this.props.mxEvent);
|
DecryptionFailureTracker.instance.addVisibleEvent(this.props.mxEvent);
|
||||||
if (this.props.showReactions) {
|
if (this.props.showReactions) {
|
||||||
this.props.mxEvent.on(MatrixEventEvent.RelationsCreated, this.onReactionsCreated);
|
this.props.mxEvent.on(MatrixEventEvent.RelationsCreated, this.onReactionsCreated);
|
||||||
|
@ -395,7 +397,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
const room = client.getRoom(this.props.mxEvent.getRoomId());
|
const room = client.getRoom(this.props.mxEvent.getRoomId());
|
||||||
room?.on(ThreadEvent.New, this.onNewThread);
|
room?.on(ThreadEvent.New, this.onNewThread);
|
||||||
|
|
||||||
this.verifyEvent(this.props.mxEvent);
|
this.verifyEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private get supportsThreadNotifications(): boolean {
|
private get supportsThreadNotifications(): boolean {
|
||||||
|
@ -461,6 +463,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
}
|
}
|
||||||
this.isListeningForReceipts = false;
|
this.isListeningForReceipts = false;
|
||||||
this.props.mxEvent.removeListener(MatrixEventEvent.Decrypted, this.onDecrypted);
|
this.props.mxEvent.removeListener(MatrixEventEvent.Decrypted, this.onDecrypted);
|
||||||
|
this.props.mxEvent.removeListener(MatrixEventEvent.Replaced, this.onReplaced);
|
||||||
if (this.props.showReactions) {
|
if (this.props.showReactions) {
|
||||||
this.props.mxEvent.removeListener(MatrixEventEvent.RelationsCreated, this.onReactionsCreated);
|
this.props.mxEvent.removeListener(MatrixEventEvent.RelationsCreated, this.onReactionsCreated);
|
||||||
}
|
}
|
||||||
|
@ -470,7 +473,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
this.threadState?.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
|
this.threadState?.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Readonly<EventTileProps>) {
|
public componentDidUpdate(prevProps: Readonly<EventTileProps>, prevState: Readonly<IState>) {
|
||||||
|
// If the verification state changed, the height might have changed
|
||||||
|
if (prevState.verified !== this.state.verified && this.props.onHeightChanged) {
|
||||||
|
this.props.onHeightChanged();
|
||||||
|
}
|
||||||
// If we're not listening for receipts and expect to be, register a listener.
|
// If we're not listening for receipts and expect to be, register a listener.
|
||||||
if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) {
|
if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) {
|
||||||
MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt);
|
MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt);
|
||||||
|
@ -478,7 +485,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
}
|
}
|
||||||
// re-check the sender verification as outgoing events progress through the send process.
|
// re-check the sender verification as outgoing events progress through the send process.
|
||||||
if (prevProps.eventSendStatus !== this.props.eventSendStatus) {
|
if (prevProps.eventSendStatus !== this.props.eventSendStatus) {
|
||||||
this.verifyEvent(this.props.mxEvent);
|
this.verifyEvent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -586,26 +593,36 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
*/
|
*/
|
||||||
private onDecrypted = () => {
|
private onDecrypted = () => {
|
||||||
// we need to re-verify the sending device.
|
// we need to re-verify the sending device.
|
||||||
// (we call onHeightChanged in verifyEvent to handle the case where decryption
|
this.verifyEvent();
|
||||||
// has caused a change in size of the event tile)
|
// decryption might, of course, trigger a height change, so call onHeightChanged after the re-render
|
||||||
this.verifyEvent(this.props.mxEvent);
|
this.forceUpdate(this.props.onHeightChanged);
|
||||||
this.forceUpdate();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDeviceVerificationChanged = (userId: string, device: string): void => {
|
private onDeviceVerificationChanged = (userId: string, device: string): void => {
|
||||||
if (userId === this.props.mxEvent.getSender()) {
|
if (userId === this.props.mxEvent.getSender()) {
|
||||||
this.verifyEvent(this.props.mxEvent);
|
this.verifyEvent();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onUserVerificationChanged = (userId: string, _trustStatus: UserTrustLevel): void => {
|
private onUserVerificationChanged = (userId: string, _trustStatus: UserTrustLevel): void => {
|
||||||
if (userId === this.props.mxEvent.getSender()) {
|
if (userId === this.props.mxEvent.getSender()) {
|
||||||
this.verifyEvent(this.props.mxEvent);
|
this.verifyEvent();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private async verifyEvent(mxEvent: MatrixEvent): Promise<void> {
|
/** called when the event is edited after we show it. */
|
||||||
|
private onReplaced = () => {
|
||||||
|
// re-verify the event if it is replaced (the edit may not be verified)
|
||||||
|
this.verifyEvent();
|
||||||
|
};
|
||||||
|
|
||||||
|
private verifyEvent(): void {
|
||||||
|
// if the event was edited, show the verification info for the edit, not
|
||||||
|
// the original
|
||||||
|
const mxEvent = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent;
|
||||||
|
|
||||||
if (!mxEvent.isEncrypted() || mxEvent.isRedacted()) {
|
if (!mxEvent.isEncrypted() || mxEvent.isRedacted()) {
|
||||||
|
this.setState({ verified: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -615,12 +632,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
|
|
||||||
if (encryptionInfo.mismatchedSender) {
|
if (encryptionInfo.mismatchedSender) {
|
||||||
// something definitely wrong is going on here
|
// something definitely wrong is going on here
|
||||||
this.setState(
|
this.setState({ verified: E2EState.Warning });
|
||||||
{
|
|
||||||
verified: E2EState.Warning,
|
|
||||||
},
|
|
||||||
this.props.onHeightChanged,
|
|
||||||
); // Decryption may have caused a change in size
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -628,53 +640,28 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
// If the message is unauthenticated, then display a grey
|
// If the message is unauthenticated, then display a grey
|
||||||
// shield, otherwise if the user isn't cross-signed then
|
// shield, otherwise if the user isn't cross-signed then
|
||||||
// nothing's needed
|
// nothing's needed
|
||||||
this.setState(
|
this.setState({ verified: encryptionInfo.authenticated ? E2EState.Normal : E2EState.Unauthenticated });
|
||||||
{
|
|
||||||
verified: encryptionInfo.authenticated ? E2EState.Normal : E2EState.Unauthenticated,
|
|
||||||
},
|
|
||||||
this.props.onHeightChanged,
|
|
||||||
); // Decryption may have caused a change in size
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventSenderTrust =
|
const eventSenderTrust =
|
||||||
encryptionInfo.sender && MatrixClientPeg.get().checkDeviceTrust(senderId, encryptionInfo.sender.deviceId);
|
encryptionInfo.sender && MatrixClientPeg.get().checkDeviceTrust(senderId, encryptionInfo.sender.deviceId);
|
||||||
if (!eventSenderTrust) {
|
if (!eventSenderTrust) {
|
||||||
this.setState(
|
this.setState({ verified: E2EState.Unknown });
|
||||||
{
|
|
||||||
verified: E2EState.Unknown,
|
|
||||||
},
|
|
||||||
this.props.onHeightChanged,
|
|
||||||
); // Decryption may have caused a change in size
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!eventSenderTrust.isVerified()) {
|
if (!eventSenderTrust.isVerified()) {
|
||||||
this.setState(
|
this.setState({ verified: E2EState.Warning });
|
||||||
{
|
|
||||||
verified: E2EState.Warning,
|
|
||||||
},
|
|
||||||
this.props.onHeightChanged,
|
|
||||||
); // Decryption may have caused a change in size
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!encryptionInfo.authenticated) {
|
if (!encryptionInfo.authenticated) {
|
||||||
this.setState(
|
this.setState({ verified: E2EState.Unauthenticated });
|
||||||
{
|
|
||||||
verified: E2EState.Unauthenticated,
|
|
||||||
},
|
|
||||||
this.props.onHeightChanged,
|
|
||||||
); // Decryption may have caused a change in size
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(
|
this.setState({ verified: E2EState.Verified });
|
||||||
{
|
|
||||||
verified: E2EState.Verified,
|
|
||||||
},
|
|
||||||
this.props.onHeightChanged,
|
|
||||||
); // Decryption may have caused a change in size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private propsEqual(objA: EventTileProps, objB: EventTileProps): boolean {
|
private propsEqual(objA: EventTileProps, objB: EventTileProps): boolean {
|
||||||
|
@ -768,7 +755,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderE2EPadlock() {
|
private renderE2EPadlock() {
|
||||||
const ev = this.props.mxEvent;
|
// if the event was edited, show the verification info for the edit, not
|
||||||
|
// the original
|
||||||
|
const ev = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent;
|
||||||
|
|
||||||
// no icon for local rooms
|
// no icon for local rooms
|
||||||
if (isLocalRoom(ev.getRoomId()!)) return;
|
if (isLocalRoom(ev.getRoomId()!)) return;
|
||||||
|
|
|
@ -14,19 +14,22 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import * as React from "react";
|
||||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
|
||||||
|
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
|
||||||
|
import { IEncryptedEventInfo } from "matrix-js-sdk/src/crypto/api";
|
||||||
|
|
||||||
import EventTile, { EventTileProps } from "../../../../src/components/views/rooms/EventTile";
|
import EventTile, { EventTileProps } from "../../../../src/components/views/rooms/EventTile";
|
||||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
import { getRoomContext, mkEvent, mkMessage, stubClient } from "../../../test-utils";
|
import { getRoomContext, mkEncryptedEvent, mkEvent, mkMessage, stubClient } from "../../../test-utils";
|
||||||
import { mkThread } from "../../../test-utils/threads";
|
import { mkThread } from "../../../test-utils/threads";
|
||||||
|
|
||||||
describe("EventTile", () => {
|
describe("EventTile", () => {
|
||||||
|
@ -34,6 +37,7 @@ describe("EventTile", () => {
|
||||||
let mxEvent: MatrixEvent;
|
let mxEvent: MatrixEvent;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
|
|
||||||
// let changeEvent: (event: MatrixEvent) => void;
|
// let changeEvent: (event: MatrixEvent) => void;
|
||||||
|
|
||||||
function TestEventTile(props: Partial<EventTileProps>) {
|
function TestEventTile(props: Partial<EventTileProps>) {
|
||||||
|
@ -67,7 +71,7 @@ describe("EventTile", () => {
|
||||||
stubClient();
|
stubClient();
|
||||||
client = MatrixClientPeg.get();
|
client = MatrixClientPeg.get();
|
||||||
|
|
||||||
room = new Room(ROOM_ID, client, client.getUserId(), {
|
room = new Room(ROOM_ID, client, client.getUserId()!, {
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -140,18 +144,194 @@ describe("EventTile", () => {
|
||||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0);
|
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
room.setThreadUnreadNotificationCount(mxEvent.getId(), NotificationCountType.Total, 3);
|
room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Total, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
|
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
|
||||||
expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(0);
|
expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(0);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
room.setThreadUnreadNotificationCount(mxEvent.getId(), NotificationCountType.Highlight, 1);
|
room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Highlight, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
|
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
|
||||||
expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(1);
|
expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Event verification", () => {
|
||||||
|
// data for our stubbed getEventEncryptionInfo: a map from event id to result
|
||||||
|
const eventToEncryptionInfoMap = new Map<string, IEncryptedEventInfo>();
|
||||||
|
|
||||||
|
const TRUSTED_DEVICE = DeviceInfo.fromStorage({}, "TRUSTED_DEVICE");
|
||||||
|
const UNTRUSTED_DEVICE = DeviceInfo.fromStorage({}, "UNTRUSTED_DEVICE");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
eventToEncryptionInfoMap.clear();
|
||||||
|
|
||||||
|
// a mocked version of getEventEncryptionInfo which will pick its result from `eventToEncryptionInfoMap`
|
||||||
|
client.getEventEncryptionInfo = (event) => eventToEncryptionInfoMap.get(event.getId()!)!;
|
||||||
|
|
||||||
|
// a mocked version of checkUserTrust which always says the user is trusted (we do our testing via
|
||||||
|
// unverified devices).
|
||||||
|
const trustedUserTrustLevel = new UserTrustLevel(true, true, true);
|
||||||
|
client.checkUserTrust = (_userId) => trustedUserTrustLevel;
|
||||||
|
|
||||||
|
// a version of checkDeviceTrust which says that TRUSTED_DEVICE is trusted, and others are not.
|
||||||
|
const trustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, true, false);
|
||||||
|
const untrustedDeviceTrustLevel = DeviceTrustLevel.fromUserTrustLevel(trustedUserTrustLevel, false, false);
|
||||||
|
client.checkDeviceTrust = (userId, deviceId) => {
|
||||||
|
if (deviceId === TRUSTED_DEVICE.deviceId) {
|
||||||
|
return trustedDeviceTrustLevel;
|
||||||
|
} else {
|
||||||
|
return untrustedDeviceTrustLevel;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a warning for an event from an unverified device", async () => {
|
||||||
|
mxEvent = await mkEncryptedEvent({
|
||||||
|
plainContent: { msgtype: "m.text", body: "msg1" },
|
||||||
|
plainType: "m.room.message",
|
||||||
|
user: "@alice:example.org",
|
||||||
|
room: room.roomId,
|
||||||
|
});
|
||||||
|
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
|
||||||
|
authenticated: true,
|
||||||
|
sender: UNTRUSTED_DEVICE,
|
||||||
|
} as IEncryptedEventInfo);
|
||||||
|
|
||||||
|
const { container } = getComponent();
|
||||||
|
|
||||||
|
const eventTiles = container.getElementsByClassName("mx_EventTile");
|
||||||
|
expect(eventTiles).toHaveLength(1);
|
||||||
|
const eventTile = eventTiles[0];
|
||||||
|
|
||||||
|
expect(eventTile.classList).toContain("mx_EventTile_unverified");
|
||||||
|
|
||||||
|
// there should be a warning shield
|
||||||
|
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
|
||||||
|
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
|
||||||
|
"mx_EventTile_e2eIcon_warning",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows no shield for a verified event", async () => {
|
||||||
|
mxEvent = await mkEncryptedEvent({
|
||||||
|
plainContent: { msgtype: "m.text", body: "msg1" },
|
||||||
|
plainType: "m.room.message",
|
||||||
|
user: "@alice:example.org",
|
||||||
|
room: room.roomId,
|
||||||
|
});
|
||||||
|
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
|
||||||
|
authenticated: true,
|
||||||
|
sender: TRUSTED_DEVICE,
|
||||||
|
} as IEncryptedEventInfo);
|
||||||
|
|
||||||
|
const { container } = getComponent();
|
||||||
|
|
||||||
|
const eventTiles = container.getElementsByClassName("mx_EventTile");
|
||||||
|
expect(eventTiles).toHaveLength(1);
|
||||||
|
const eventTile = eventTiles[0];
|
||||||
|
|
||||||
|
expect(eventTile.classList).toContain("mx_EventTile_verified");
|
||||||
|
|
||||||
|
// there should be no warning
|
||||||
|
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update the warning when the event is edited", async () => {
|
||||||
|
// we start out with an event from the trusted device
|
||||||
|
mxEvent = await mkEncryptedEvent({
|
||||||
|
plainContent: { msgtype: "m.text", body: "msg1" },
|
||||||
|
plainType: "m.room.message",
|
||||||
|
user: "@alice:example.org",
|
||||||
|
room: room.roomId,
|
||||||
|
});
|
||||||
|
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
|
||||||
|
authenticated: true,
|
||||||
|
sender: TRUSTED_DEVICE,
|
||||||
|
} as IEncryptedEventInfo);
|
||||||
|
|
||||||
|
const { container } = getComponent();
|
||||||
|
|
||||||
|
const eventTiles = container.getElementsByClassName("mx_EventTile");
|
||||||
|
expect(eventTiles).toHaveLength(1);
|
||||||
|
const eventTile = eventTiles[0];
|
||||||
|
|
||||||
|
expect(eventTile.classList).toContain("mx_EventTile_verified");
|
||||||
|
|
||||||
|
// there should be no warning
|
||||||
|
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
|
||||||
|
|
||||||
|
// then we replace the event with one from the unverified device
|
||||||
|
const replacementEvent = await mkEncryptedEvent({
|
||||||
|
plainContent: { msgtype: "m.text", body: "msg1" },
|
||||||
|
plainType: "m.room.message",
|
||||||
|
user: "@alice:example.org",
|
||||||
|
room: room.roomId,
|
||||||
|
});
|
||||||
|
eventToEncryptionInfoMap.set(replacementEvent.getId()!, {
|
||||||
|
authenticated: true,
|
||||||
|
sender: UNTRUSTED_DEVICE,
|
||||||
|
} as IEncryptedEventInfo);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
mxEvent.makeReplaced(replacementEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// check it was updated
|
||||||
|
expect(eventTile.classList).toContain("mx_EventTile_unverified");
|
||||||
|
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
|
||||||
|
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
|
||||||
|
"mx_EventTile_e2eIcon_warning",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update the warning when the event is replaced with an unencrypted one", async () => {
|
||||||
|
jest.spyOn(client, "isRoomEncrypted").mockReturnValue(true);
|
||||||
|
|
||||||
|
// we start out with an event from the trusted device
|
||||||
|
mxEvent = await mkEncryptedEvent({
|
||||||
|
plainContent: { msgtype: "m.text", body: "msg1" },
|
||||||
|
plainType: "m.room.message",
|
||||||
|
user: "@alice:example.org",
|
||||||
|
room: room.roomId,
|
||||||
|
});
|
||||||
|
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
|
||||||
|
authenticated: true,
|
||||||
|
sender: TRUSTED_DEVICE,
|
||||||
|
} as IEncryptedEventInfo);
|
||||||
|
|
||||||
|
const { container } = getComponent();
|
||||||
|
|
||||||
|
const eventTiles = container.getElementsByClassName("mx_EventTile");
|
||||||
|
expect(eventTiles).toHaveLength(1);
|
||||||
|
const eventTile = eventTiles[0];
|
||||||
|
|
||||||
|
expect(eventTile.classList).toContain("mx_EventTile_verified");
|
||||||
|
|
||||||
|
// there should be no warning
|
||||||
|
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
|
||||||
|
|
||||||
|
// then we replace the event with an unencrypted one
|
||||||
|
const replacementEvent = await mkMessage({
|
||||||
|
msg: "msg2",
|
||||||
|
user: "@alice:example.org",
|
||||||
|
room: room.roomId,
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
mxEvent.makeReplaced(replacementEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// check it was updated
|
||||||
|
expect(eventTile.classList).not.toContain("mx_EventTile_verified");
|
||||||
|
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
|
||||||
|
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
|
||||||
|
"mx_EventTile_e2eIcon_warning",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,6 +38,8 @@ import { normalize } from "matrix-js-sdk/src/utils";
|
||||||
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
|
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
|
||||||
import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
|
import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||||
|
import { CryptoBackend } from "matrix-js-sdk/src/common-crypto/CryptoBackend";
|
||||||
|
import { IEventDecryptionResult } from "matrix-js-sdk/src/@types/crypto";
|
||||||
|
|
||||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
|
import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
|
||||||
|
@ -316,26 +318,48 @@ export function mkEvent(opts: MakeEventProps): MatrixEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an m.presence event.
|
* Create an m.room.encrypted event
|
||||||
* @param {Object} opts Values for the presence.
|
*
|
||||||
* @return {Object|MatrixEvent} The event
|
* @param opts - Values for the event
|
||||||
|
* @param opts.room - The ID of the room for the event
|
||||||
|
* @param opts.user - The sender of the event
|
||||||
|
* @param opts.plainType - The type the event will have, once it has been decrypted
|
||||||
|
* @param opts.plainContent - The content the event will have, once it has been decrypted
|
||||||
*/
|
*/
|
||||||
export function mkPresence(opts) {
|
export async function mkEncryptedEvent(opts: {
|
||||||
if (!opts.user) {
|
room: Room["roomId"];
|
||||||
throw new Error("Missing user");
|
user: User["userId"];
|
||||||
}
|
plainType: string;
|
||||||
const event = {
|
plainContent: IContent;
|
||||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
}): Promise<MatrixEvent> {
|
||||||
type: "m.presence",
|
// we construct an event which has been decrypted by stubbing out CryptoBackend.decryptEvent and then
|
||||||
sender: opts.user,
|
// calling MatrixEvent.attemptDecryption.
|
||||||
content: {
|
|
||||||
avatar_url: opts.url,
|
const mxEvent = mkEvent({
|
||||||
displayname: opts.name,
|
type: "m.room.encrypted",
|
||||||
last_active_ago: opts.ago,
|
room: opts.room,
|
||||||
presence: opts.presence || "offline",
|
user: opts.user,
|
||||||
|
event: true,
|
||||||
|
content: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptionResult: IEventDecryptionResult = {
|
||||||
|
claimedEd25519Key: "",
|
||||||
|
clearEvent: {
|
||||||
|
type: opts.plainType,
|
||||||
|
content: opts.plainContent,
|
||||||
},
|
},
|
||||||
|
forwardingCurve25519KeyChain: [],
|
||||||
|
senderCurve25519Key: "",
|
||||||
|
untrusted: false,
|
||||||
};
|
};
|
||||||
return opts.event ? new MatrixEvent(event) : event;
|
|
||||||
|
const mockCrypto = {
|
||||||
|
decryptEvent: async (_ev): Promise<IEventDecryptionResult> => decryptionResult,
|
||||||
|
} as CryptoBackend;
|
||||||
|
|
||||||
|
await mxEvent.attemptDecryption(mockCrypto);
|
||||||
|
return mxEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue