diff --git a/package.json b/package.json
index 82693fb0548..759b2bf3aed 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"@peculiar/x509": "1.9.5",
"@wireapp/avs": "9.5.2",
"@wireapp/commons": "5.2.3",
- "@wireapp/core": "42.21.0",
+ "@wireapp/core": "42.25.2",
"@wireapp/react-ui-kit": "9.12.0",
"@wireapp/store-engine-dexie": "2.1.6",
"@wireapp/webapp-events": "0.18.3",
diff --git a/server/config/client.config.ts b/server/config/client.config.ts
index 90c2a882da5..2319b0b14db 100644
--- a/server/config/client.config.ts
+++ b/server/config/client.config.ts
@@ -99,6 +99,7 @@ export function generateConfig(params: ConfigGeneratorParams, env: Env) {
OAUTH_LEARN_MORE: env.URL_SUPPORT_OAUTH_LEARN_MORE,
OFFLINE_BACKEND: env.URL_SUPPORT_OFFLINE_BACKEND,
FEDERATION_STOP: env.URL_SUPPORT_FEDERATION_STOP,
+ E2EI_VERIFICATION: env.URL_SUPPORT_E2EI_VERIFICATION,
},
TEAMS_BASE: env.URL_TEAMS_BASE,
TEAMS_CREATE: env.URL_TEAMS_CREATE,
diff --git a/server/config/env.ts b/server/config/env.ts
index 484d0e7d06a..7204548716e 100644
--- a/server/config/env.ts
+++ b/server/config/env.ts
@@ -252,6 +252,7 @@ export type Env = {
URL_SUPPORT_OFFLINE_BACKEND: string;
URL_SUPPORT_FEDERATION_STOP: string;
+ URL_SUPPORT_E2EI_VERIFICATION: string;
URL_WHATS_NEW: string;
diff --git a/src/__mocks__/@wireapp/core-crypto.ts b/src/__mocks__/@wireapp/core-crypto.ts
index 0f1756edff7..745f3f5c370 100644
--- a/src/__mocks__/@wireapp/core-crypto.ts
+++ b/src/__mocks__/@wireapp/core-crypto.ts
@@ -41,3 +41,17 @@ export enum CredentialType {
Basic = 1,
X509 = 2,
}
+export enum E2eiConversationState {
+ /**
+ * All clients have a valid E2EI certificate
+ */
+ Verified = 1,
+ /**
+ * Some clients are either still Basic or their certificate is expired
+ */
+ Degraded = 2,
+ /**
+ * All clients are still Basic. If all client have expired certificates, Degraded is returned.
+ */
+ NotEnabled = 3,
+}
diff --git a/src/__mocks__/@wireapp/core.ts b/src/__mocks__/@wireapp/core.ts
index 016a95cda7f..9559b515562 100644
--- a/src/__mocks__/@wireapp/core.ts
+++ b/src/__mocks__/@wireapp/core.ts
@@ -32,8 +32,17 @@ export class Account extends EventEmitter {
};
configureMLSCallbacks = jest.fn();
-
+ enrollE2EI = jest.fn();
service = {
+ e2eIdentity: {
+ isEnrollmentInProgress: jest.fn(),
+ clearAllProgress: jest.fn(),
+ hasActiveCertificate: jest.fn(),
+ getCertificateData: jest.fn(),
+ getUsersIdentities: jest.fn(() => new Map()),
+ getDeviceIdentities: jest.fn(),
+ getConversationState: jest.fn(),
+ },
mls: {
schedulePeriodicKeyMaterialRenewals: jest.fn(),
addUsersToExistingConversation: jest.fn(),
@@ -45,9 +54,9 @@ export class Account extends EventEmitter {
getClientIds: jest.fn(),
getEpoch: jest.fn(),
exportSecretKey: jest.fn(),
- on: this.on,
- emit: this.emit,
- off: this.off,
+ on: jest.fn(),
+ emit: jest.fn(),
+ off: jest.fn(),
scheduleKeyMaterialRenewal: jest.fn(),
},
asset: {
diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json
index 75e56cdfa2c..8275340a1d7 100644
--- a/src/i18n/en-US.json
+++ b/src/i18n/en-US.json
@@ -544,7 +544,29 @@
"customEnvRedirect.credentialsInfo": "Provide credentials only if you're sure this is your organization's login.",
"customEnvRedirect.redirectHeadline": "Redirecting...",
"customEnvRedirect.redirectTo": "You are being redirected to your dedicated enterprise service.",
- "downloadLatestMLS": "Download the latest MLS Wire version",
+ "E2EI.verified": "Verified (End-to-end Identity)",
+ "E2EI.deviceVerified": "Device verified (End-to-end identity)",
+ "E2EI.certificateExpired": "End-to-end identity certificate expired",
+ "E2EI.certificateExpiresSoon": "End-to-end identity certificate expires soon",
+ "E2EI.certificateRevoked": "End-to-end identity certificate revoked",
+ "E2EI.status": "Status: ",
+ "E2EI.certificateTitle": "End-to-end identity certificate",
+ "E2EI.valid": "Valid",
+ "E2EI.not_downloaded": "Not downloaded",
+ "E2EI.expired": "Expired",
+ "E2EI.expires_soon": "Valid (expires soon)",
+ "E2EI.not_activated": "Not activated",
+ "E2EI.serialNumber": "Serial number: ",
+ "E2EI.notAvailable": "Not available",
+ "E2EI.showCertificateDetails": "Show Certificate Details",
+ "E2EI.getCertificate": "Get Certificate",
+ "E2EI.updateCertificate": "Update Certificate",
+ "E2EI.certificateDetails": "Certificate details (PEM format)",
+ "E2EI.downloadCertificate": "Download",
+ "E2EI.copyCertificate": "Copy to Clipboard",
+ "E2EI.certificateCopied": "Text copied!",
+ "proteusVerifiedDetails": "Verified (Proteus)",
+ "proteusDeviceVerified": "Device verified (Proteus)",
"enumerationAnd": ", and ",
"ephemeralRemaining": "remaining",
"ephemeralUnitsDay": "day",
@@ -756,6 +778,12 @@
"mlsConversationRecovered": "You haven't used this device for a while, or an issue has occurred. Some older messages may not appear here.",
"mlsToggleInfo": "When this is on, conversation will use the new messaging layer security (MLS) protocol.",
"mlsToggleName": "MLS",
+ "mlsSignature": "MLS with {{signature}} Signature",
+ "mlsThumbprint": "MLS Thumbprint",
+ "selfNotSupportMLSMsgPart1": "You can't communicate with [bold]{{selfUserName}}[/bold] anymore, as your device doesn't support the suitable protocol.",
+ "downloadLatestMLS": "Download the latest MLS Wire version",
+ "selfNotSupportMLSMsgPart2": "to call, and send messages and files again.",
+ "otherUserNotSupportMLSMsg": "You can't communicate with [bold]{{participantName}}[/bold] anymore, as you two now use different protocols. When [bold]{{participantName}}[/bold] gets an update, you can call and send messages and files again.",
"modalAccountCreateAction": "OK",
"modalAccountCreateHeadline": "Create an account?",
"modalAccountCreateMessage": "By creating an account you will lose the conversation history in this guest room.",
@@ -1050,12 +1078,15 @@
"ongoingGroupAudioCall": "Ongoing conference call with {{conversationName}}.",
"ongoingGroupVideoCall": "Ongoing video conference call with {{conversationName}}, your camera is {{cameraStatus}}.",
"ongoingVideoCall": "Ongoing video call with {{conversationName}}, your camera is {{cameraStatus}}.",
- "otherUserNotSupportMLSMsg": "You can't communicate with [bold]{{participantName}}[/bold] anymore, as you two now use different protocols. When [bold]{{participantName}}[/bold] gets an update, you can call and send messages and files again.",
+ "participantDevicesProteusDeviceVerification": "Proteus Device Verification",
+ "participantDevicesProteusKeyFingerprint": "Proteus Key Fingerprint",
"participantDevicesDetailHeadline": "Verify that this matches the fingerprint shown on [bold]{{user}}’s device[/bold].",
"participantDevicesDetailHowTo": "How do I do that?",
"participantDevicesDetailResetSession": "Reset session",
"participantDevicesDetailShowMyDevice": "Show my device fingerprint",
+ "preferencesDeviceDetailsVerificationStatus": "Verification Status",
"participantDevicesDetailVerify": "Verified",
+ "preferencesDeviceDetailsFingerprintNotMatch": "If fingerprints don’t match, reset the session to generate new encryption keys on both sides.",
"participantDevicesHeader": "Devices",
"participantDevicesHeadline": "{{brandName}} gives every device a unique fingerprint. Compare them with {{user}} and verify your conversation.",
"participantDevicesLearnMore": "Learn more",
@@ -1134,13 +1165,14 @@
"preferencesDeviceDetails": "Device Details",
"preferencesDeviceNotVerified": "not verified",
"preferencesDevices": "Devices",
- "preferencesDevicesActivatedOn": "Activated [bold]{{date}}[/bold]",
+ "preferencesDevicesActivatedOn": "Activated",
"preferencesDevicesActive": "Active",
"preferencesDevicesActiveDetail": "If you don’t recognize a device above, remove it and reset your password.",
"preferencesDevicesCurrent": "Current",
"preferencesDevicesFingerprint": "Key fingerprint",
"preferencesDevicesFingerprintDetail": "{{brandName}} gives every device a unique fingerprint. Compare them and verify your devices and conversations.",
"preferencesDevicesId": "ID: ",
+ "preferencesMLSThumbprint": "MLS Thumbprint: ",
"preferencesDevicesRemove": "Remove Device",
"preferencesDevicesRemoveCancel": "Cancel",
"preferencesDevicesRemoveDetail": "Remove this device if you have stopped using it. You will be logged out of this device immediately.",
@@ -1194,6 +1226,10 @@
"preferencesOptionsPreviewsSendCheckbox": "Create previews for links you send",
"preferencesOptionsPreviewsSendDetail": "Previews may still be shown for links from other people.",
"preferencesOptionsUseDarkMode": "Dark theme",
+ "proteusDeviceDetails": "Proteus Device Details",
+ "proteusID": "Proteus ID",
+ "proteusVerified": "Verified",
+ "proteusNotVerified": "Not Verified",
"readReceiptsToggleInfo": "When this is on, people can see when their messages in this conversation are read.",
"readReceiptsToggleName": "Read receipts",
"receiptToggleInfo": "When this is on, people can see when their messages in this conversation are read.",
@@ -1250,8 +1286,6 @@
"searchTrySearch": "Find people by\nname or username",
"searchTrySearchFederation": "Find people in Wire by name or\n@username\n\nFind people from another domain\nby @username@domainname",
"searchTrySearchLearnMore": "Learn more",
- "selfNotSupportMLSMsgPart1": "You can't communicate with [bold]{{selfUserName}}[/bold] anymore, as your device doesn't support the suitable protocol.",
- "selfNotSupportMLSMsgPart2": "to call, and send messages and files again.",
"selfProfileImageAlt": "Your profile picture",
"servicesOptionsTitle": "Services",
"servicesRoomToggleInfo": "Open this conversation to services.",
@@ -1302,7 +1336,10 @@
"timedMessageDisclaimer": "Self-deleting messages will be turned on for all the participants in this conversation.",
"timedMessagesTitle": "Self-deleting messages",
"tooltipConversationAddImage": "Add picture",
- "tooltipConversationAllVerified": "All fingerprints are verified",
+ "tooltipConversationAllVerified": "All fingerprints are verified (Proteus)",
+ "tooltipConversationAllDevicesVerified": "All device fingerprints verified (Proteus)",
+ "tooltipConversationAllE2EIVerified": "All devices are verified (end-to-end identity). [link]Learn more[/link]",
+ "tooltipConversationAllE2EIVerifiedShort": "All devices verified (end-to-end identity)",
"tooltipConversationCall": "Call",
"tooltipConversationDetailsAddPeople": "Add participants to conversation ({{shortcut}})",
"tooltipConversationDetailsRename": "Change conversation name",
diff --git a/src/script/E2EIdentity/E2EIdentity.test.ts b/src/script/E2EIdentity/E2EIdentityEnrollment.test.ts
similarity index 61%
rename from src/script/E2EIdentity/E2EIdentity.test.ts
rename to src/script/E2EIdentity/E2EIdentityEnrollment.test.ts
index d8f0853d1e2..ad79ba6407b 100644
--- a/src/script/E2EIdentity/E2EIdentity.test.ts
+++ b/src/script/E2EIdentity/E2EIdentityEnrollment.test.ts
@@ -26,7 +26,7 @@ import {Core} from 'src/script/service/CoreSingleton';
import {UserState} from 'src/script/user/UserState';
import * as util from 'Util/util';
-import {E2EIHandler, E2EIHandlerStep} from './E2EIdentity';
+import {E2EIHandler, E2EIHandlerStep} from './E2EIdentityEnrollment';
import {getModalOptions, ModalType} from './Modals';
import {OIDCService} from './OIDCService/OIDCService';
@@ -35,24 +35,7 @@ jest.mock('./OIDCService', () => ({
clearProgress: jest.fn(),
} as unknown as OIDCService),
}));
-jest.mock('Util/util');
-jest.mock('src/script/Config');
-jest.mock('tsyringe');
-jest.mock('src/script/service/CoreSingleton', () => {
- return {
- Core: jest.fn().mockImplementation(() => {
- return {enrollE2EI: jest.fn()};
- }),
- };
-});
-jest.mock('src/script/user/UserState', () => {
- return {
- UserState: jest.fn().mockImplementation(() => {
- return {self: jest.fn()};
- }),
- };
-});
-jest.mock('Components/Modals/PrimaryModal');
+
jest.mock('./Modals', () => ({
getModalOptions: jest.fn().mockReturnValue({
modalOptions: {},
@@ -65,53 +48,30 @@ jest.mock('./Modals', () => ({
ENROLL: 'enroll',
},
}));
-jest.mock('src/script/service/CoreSingleton', () => ({
- Core: jest.fn().mockImplementation(() => ({
- enrollE2EI: jest.fn(),
- })),
-}));
-jest.mock('src/script/user/UserState', () => ({
- UserState: jest.fn().mockImplementation(() => ({
- self: jest.fn(),
- })),
-}));
describe('E2EIHandler', () => {
const params = {discoveryUrl: 'http://example.com', gracePeriodInSeconds: 30};
const newParams = {discoveryUrl: 'http://new-example.com', gracePeriodInSeconds: 60};
const user = {name: () => 'John Doe', username: () => 'johndoe'};
- let coreMock: Core;
- let userStateMock: UserState;
beforeEach(() => {
- (util.supportsMLS as jest.Mock).mockReturnValue(true);
+ jest.spyOn(util, 'supportsMLS').mockReturnValue(true);
// Reset the singleton instance before each test
E2EIHandler.resetInstance();
// Clear all mocks before each test
jest.clearAllMocks();
- // Setup the Core and UserState services mocks
- coreMock = new Core();
- userStateMock = new UserState();
- (userStateMock.self as unknown as jest.Mock).mockReturnValue({name: () => 'John Doe', username: () => 'johndoe'});
- (coreMock.enrollE2EI as jest.Mock).mockResolvedValue(true);
-
- (container.resolve as jest.Mock).mockImplementation(service => {
- if (service === Core) {
- return coreMock;
- }
- if (service === UserState) {
- return userStateMock;
- }
- return null;
- });
// Mock the Config service to return true for ENABLE_E2EI
(util.supportsMLS as jest.Mock).mockReturnValue(true);
Config.getConfig = jest.fn().mockReturnValue({FEATURE: {ENABLE_E2EI: true}});
- // Mock the PrimaryModal service to return a mock modal
- (PrimaryModal.show as jest.Mock).mockClear();
+ jest.spyOn(PrimaryModal, 'show');
(getModalOptions as jest.Mock).mockClear();
+
+ jest
+ .spyOn(container.resolve(UserState), 'self')
+ .mockReturnValue({name: () => 'John Doe', username: () => 'johndoe'});
+ jest.spyOn(container.resolve(Core), 'enrollE2EI').mockResolvedValue(true);
});
it('should create instance with valid params', () => {
@@ -143,48 +103,32 @@ describe('E2EIHandler', () => {
expect(instance['gracePeriodInMS']).toEqual(newParams.gracePeriodInSeconds * TimeInMillis.SECOND);
});
- it('should return true when supportsMLS returns true and ENABLE_E2EI is true', () => {
- const instance = E2EIHandler.getInstance(params);
- expect(instance.isE2EIEnabled).toBe(true);
- });
-
- it('should return false when supportsMLS returns false', () => {
- (util.supportsMLS as jest.Mock).mockReturnValue(false);
-
- const instance = E2EIHandler.getInstance(params);
- expect(instance.isE2EIEnabled).toBe(false);
- });
-
- it('should return false when ENABLE_E2EI is false', () => {
- Config.getConfig = jest.fn().mockReturnValue({FEATURE: {ENABLE_E2EI: false}});
-
- const instance = E2EIHandler.getInstance(params);
- expect(instance.isE2EIEnabled).toBe(false);
- });
-
it('should set currentStep to INITIALIZE after initialize is called', () => {
const instance = E2EIHandler.getInstance(params);
instance.initialize();
expect(instance['currentStep']).toBe(E2EIHandlerStep.INITIALIZED);
});
- it('should set currentStep to ENROLL when enrollE2EI is called and enrollment succeeds', async () => {
+ it('should set currentStep to SUCCESS when enrollE2EI is called and enrollment succeeds', async () => {
+ jest
+ .spyOn(container.resolve(UserState), 'self')
+ .mockReturnValue({name: () => 'John Doe', username: () => 'johndoe'});
+
+ jest.spyOn(container.resolve(Core), 'enrollE2EI').mockResolvedValueOnce(true);
+
const instance = E2EIHandler.getInstance(params);
- await instance['enrollE2EI']();
+ await instance['enroll']();
+
expect(instance['currentStep']).toBe(E2EIHandlerStep.SUCCESS);
});
- it('should set currentStep to ERROR when enrollE2EI is called and enrollment fails', async () => {
+ it('should set currentStep to ERROR when enrolE2EI is called and enrolment fails', async () => {
// Mock the Core service to return an error
- (container.resolve as any) = jest.fn(service => {
- if (service === Core) {
- return {enrollE2EI: jest.fn(() => Promise.reject())};
- }
- return {self: () => user};
- });
+ jest.spyOn(container.resolve(Core), 'enrollE2EI').mockImplementationOnce(jest.fn(() => Promise.reject()));
+ jest.spyOn(container.resolve(UserState), 'self').mockImplementationOnce(() => user);
const instance = E2EIHandler.getInstance(params);
- await instance['enrollE2EI']();
+ await instance['enroll']();
expect(instance['currentStep']).toBe(E2EIHandlerStep.ERROR);
});
@@ -198,9 +142,9 @@ describe('E2EIHandler', () => {
);
});
- it('should display loading message when enrolled', async () => {
+ it('should display loading message when enroled', async () => {
const handler = E2EIHandler.getInstance(params);
- await handler['enrollE2EI']();
+ await handler['enroll']();
expect(getModalOptions).toHaveBeenCalledWith(
expect.objectContaining({
type: ModalType.LOADING,
@@ -209,9 +153,11 @@ describe('E2EIHandler', () => {
});
it('should display success message when enrollment is done', async () => {
+ jest.spyOn(container.resolve(Core), 'enrollE2EI').mockResolvedValueOnce(true);
+
const handler = E2EIHandler.getInstance(params);
handler['showLoadingMessage'] = jest.fn();
- await handler['enrollE2EI']();
+ await handler['enroll']();
expect(getModalOptions).toHaveBeenCalledWith(
expect.objectContaining({
type: ModalType.SUCCESS,
@@ -220,18 +166,11 @@ describe('E2EIHandler', () => {
});
it('should display error message when enrollment fails', async () => {
- (container.resolve as jest.Mock).mockImplementation(service => {
- if (service === Core) {
- return {startE2EIEnrollment: jest.fn(() => Promise.reject(false))};
- }
- if (service === UserState) {
- return userStateMock;
- }
- return null;
- });
+ jest.spyOn(container.resolve(Core), 'enrollE2EI').mockRejectedValueOnce(false);
+
const handler = E2EIHandler.getInstance(params);
handler['showLoadingMessage'] = jest.fn();
- await handler['enrollE2EI']();
+ await handler['enroll']();
expect(getModalOptions).toHaveBeenCalledWith(
expect.objectContaining({
type: ModalType.ERROR,
diff --git a/src/script/E2EIdentity/E2EIdentity.ts b/src/script/E2EIdentity/E2EIdentityEnrollment.ts
similarity index 82%
rename from src/script/E2EIdentity/E2EIdentity.ts
rename to src/script/E2EIdentity/E2EIdentityEnrollment.ts
index 34e1fa08a67..24921a2862d 100644
--- a/src/script/E2EIdentity/E2EIdentity.ts
+++ b/src/script/E2EIdentity/E2EIdentityEnrollment.ts
@@ -20,14 +20,13 @@
import {container} from 'tsyringe';
import {PrimaryModal, removeCurrentModal} from 'Components/Modals/PrimaryModal';
-import {Config} from 'src/script/Config';
import {Core} from 'src/script/service/CoreSingleton';
import {UserState} from 'src/script/user/UserState';
import {TIME_IN_MILLIS} from 'Util/TimeUtil';
import {removeUrlParameters} from 'Util/UrlUtil';
-import {supportsMLS} from 'Util/util';
import {DelayTimerService} from './DelayTimer/DelayTimer';
+import {hasActiveCertificate, isE2EIEnabled} from './E2EIdentityVerification';
import {getModalOptions, ModalType} from './Modals';
import {getOIDCServiceInstance} from './OIDCService';
import {OIDCServiceStore} from './OIDCService/OIDCServiceStorage';
@@ -46,7 +45,7 @@ interface E2EIHandlerParams {
gracePeriodInSeconds: number;
}
-class E2EIHandler {
+export class E2EIHandler {
private static instance: E2EIHandler | null = null;
private readonly core = container.resolve(Core);
private readonly userState = container.resolve(UserState);
@@ -66,6 +65,16 @@ class E2EIHandler {
});
}
+ private get coreE2EIService() {
+ const e2eiService = this.core.service?.e2eIdentity;
+
+ if (!e2eiService) {
+ throw new Error('E2EI Service not available');
+ }
+
+ return e2eiService;
+ }
+
/**
* Get the singleton instance of GracePeriodTimer or create a new one
* For the first time, params are required to create the instance
@@ -105,17 +114,13 @@ class E2EIHandler {
}
public initialize(): void {
- if (this.isE2EIEnabled) {
- if (!this.core.service?.e2eIdentity?.hasActiveCertificate()) {
+ if (isE2EIEnabled()) {
+ if (!hasActiveCertificate()) {
this.showE2EINotificationMessage();
}
}
}
- get isE2EIEnabled(): boolean {
- return supportsMLS() && Config.getConfig().FEATURE.ENABLE_E2EI;
- }
-
private async storeRedirectTargetAndRedirect(targetURL: string): Promise {
// store the target url in the persistent oidc service store, since the oidc service will be destroyed after the redirect
OIDCServiceStore.store.targetURL(targetURL);
@@ -123,15 +128,15 @@ class E2EIHandler {
await oidcService.authenticate();
}
- private async enrollE2EI() {
+ public async enroll(refreshActiveCertificate: boolean = false) {
try {
- // Notify user about E2EI enrollment in progress
+ // Notify user about E2EI enrolment in progress
this.currentStep = E2EIHandlerStep.ENROLL;
this.showLoadingMessage();
let oAuthIdToken: string | undefined;
- // If the enrollment is in progress, we need to get the id token from the oidc service, since oauth should have already been completed
- if (this.core.service?.e2eIdentity?.isEnrollmentInProgress()) {
+ // If the enrolment is in progress, we need to get the id token from the oidc service, since oauth should have already been completed
+ if (this.coreE2EIService.isEnrollmentInProgress()) {
const oidcService = getOIDCServiceInstance();
const userData = await oidcService.handleAuthentication();
if (!userData) {
@@ -140,16 +145,23 @@ class E2EIHandler {
oAuthIdToken = userData?.id_token;
}
- const data = await this.core.enrollE2EI(
- this.userState.self().name(),
- this.userState.self().username(),
- this.discoveryUrl,
+ const displayName = this.userState.self()?.name();
+ const handle = this.userState.self()?.username();
+ // If the user has no username or handle, we cannot enroll
+ if (!displayName || !handle) {
+ throw new Error('Username or handle not found');
+ }
+ const data = await this.core.enrollE2EI({
+ discoveryUrl: this.discoveryUrl,
+ displayName,
+ handle,
oAuthIdToken,
- );
+ refreshActiveCertificate,
+ });
- // If the data is false or we dont get the ACMEChallenge, enrollment failed
+ // If the data is false or we dont get the ACMEChallenge, enrolment failed
if (!data) {
- throw new Error('E2EI enrollment failed');
+ throw new Error('E2EI enrolment failed');
}
// Check if the data is a boolean, if not, we need to handle the oauth redirect
@@ -157,7 +169,7 @@ class E2EIHandler {
await this.storeRedirectTargetAndRedirect(data.target);
}
- // Notify user about E2EI enrollment success
+ // Notify user about E2EI enrolment success
// This setTimeout is needed because there was a timing with the success modal and the loading modal
setTimeout(() => {
removeCurrentModal();
@@ -165,7 +177,7 @@ class E2EIHandler {
this.currentStep = E2EIHandlerStep.SUCCESS;
this.showSuccessMessage();
- // Remove the url parameters after enrollment
+ // Remove the url parameters after enrolment
removeUrlParameters();
} catch (error) {
this.currentStep = E2EIHandlerStep.ERROR;
@@ -206,20 +218,20 @@ class E2EIHandler {
return;
}
- // Remove the url parameters of the failed enrollment
+ // Remove the url parameters of the failed enrolment
removeUrlParameters();
// Clear the oidc service progress
const oidcService = getOIDCServiceInstance();
await oidcService.clearProgress();
// Clear the e2e identity progress
- this.core.service?.e2eIdentity?.clearAllProgress();
+ this.coreE2EIService.clearAllProgress();
const {modalOptions, modalType} = getModalOptions({
type: ModalType.ERROR,
hideClose: true,
primaryActionFn: () => {
this.currentStep = E2EIHandlerStep.INITIALIZED;
- void this.enrollE2EI();
+ void this.enroll();
},
secondaryActionFn: () => {
this.showE2EINotificationMessage();
@@ -230,10 +242,10 @@ class E2EIHandler {
}
private showE2EINotificationMessage(): void {
- // If the user has already started enrollment, don't show the notification. Instead, show the loading modal
+ // If the user has already started enrolment, don't show the notification. Instead, show the loading modal
// This will occur after the redirect from the oauth provider
- if (this.core.service?.e2eIdentity?.isEnrollmentInProgress()) {
- void this.enrollE2EI();
+ if (this.coreE2EIService.isEnrollmentInProgress()) {
+ void this.enroll();
return;
}
@@ -260,7 +272,7 @@ class E2EIHandler {
if (!this.timer.isDelayTimerActive()) {
const {modalOptions, modalType} = getModalOptions({
hideSecondary: !this.timer.isSnoozeTimeAvailable(),
- primaryActionFn: () => this.enrollE2EI(),
+ primaryActionFn: () => this.enroll(),
secondaryActionFn: () => {
this.currentStep = E2EIHandlerStep.SNOOZE;
this.timer.delayPrompt();
@@ -272,5 +284,3 @@ class E2EIHandler {
}
}
}
-
-export {E2EIHandler};
diff --git a/src/script/E2EIdentity/E2EIdentityVerification.ts b/src/script/E2EIdentity/E2EIdentityVerification.ts
new file mode 100644
index 00000000000..ef1c4350903
--- /dev/null
+++ b/src/script/E2EIdentity/E2EIdentityVerification.ts
@@ -0,0 +1,78 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {QualifiedId} from '@wireapp/api-client/lib/user';
+import {DeviceIdentity} from '@wireapp/core/lib/messagingProtocols/mls';
+import {container} from 'tsyringe';
+
+import {Core} from 'src/script/service/CoreSingleton';
+import {base64ToArray, supportsMLS} from 'Util/util';
+
+import {mapMLSStatus} from './certificateDetails';
+
+import {Config} from '../Config';
+
+export enum MLSStatuses {
+ VALID = 'valid',
+ NOT_DOWNLOADED = 'not_downloaded',
+ EXPIRED = 'expired',
+ EXPIRES_SOON = 'expires_soon',
+}
+
+export type WireIdentity = Omit & {
+ status: MLSStatuses;
+};
+
+export function getE2EIdentityService() {
+ const e2eIdentityService = container.resolve(Core).service?.e2eIdentity;
+ if (!e2eIdentityService) {
+ throw new Error('trying to query E2EIdentity data in an non-e2eidentity environment');
+ }
+ return e2eIdentityService;
+}
+
+export function isE2EIEnabled(): boolean {
+ return supportsMLS() && Config.getConfig().FEATURE.ENABLE_E2EI;
+}
+
+export async function getUsersIdentities(groupId: string, userIds: QualifiedId[]) {
+ const userVerifications = await getE2EIdentityService().getUsersIdentities(groupId, userIds);
+
+ const mappedUsers = new Map();
+
+ for (const [userId, identities] of userVerifications.entries()) {
+ mappedUsers.set(
+ userId,
+ identities.map(identity => ({...identity, status: mapMLSStatus(identity.status)})),
+ );
+ }
+
+ return mappedUsers;
+}
+
+export async function getConversationVerificationState(groupId: string) {
+ return getE2EIdentityService().getConversationState(base64ToArray(groupId));
+}
+
+/**
+ * Checks if E2EI has active certificate.
+ */
+export function hasActiveCertificate() {
+ return getE2EIdentityService().hasActiveCertificate();
+}
diff --git a/src/script/page/components/FeatureConfigChange/guards.ts b/src/script/E2EIdentity/certificateDetails.ts
similarity index 52%
rename from src/script/page/components/FeatureConfigChange/guards.ts
rename to src/script/E2EIdentity/certificateDetails.ts
index d07f2104988..91496ea9be4 100644
--- a/src/script/page/components/FeatureConfigChange/guards.ts
+++ b/src/script/E2EIdentity/certificateDetails.ts
@@ -17,14 +17,21 @@
*
*/
-import {FeatureMLS, FeatureMLSE2EId} from '@wireapp/api-client/lib/team';
+import {WireIdentity} from '@wireapp/core/lib/messagingProtocols/mls';
-const isObject = (value: unknown): value is {} => typeof value === 'object' && value !== null;
-const isFeatureWithConfig = (feature: unknown): feature is {config: {}} =>
- isObject(feature) && 'config' in feature && isObject(feature.config);
+import {MLSStatuses} from './E2EIdentityVerification';
-export const hasE2EIVerificationExpiration = (feature: unknown): feature is FeatureMLSE2EId =>
- isFeatureWithConfig(feature) && 'verificationExpiration' in feature.config;
+type CoreStatus = WireIdentity['status'];
-export const hasMLSDefaultProtocol = (feature: unknown): feature is FeatureMLS =>
- isFeatureWithConfig(feature) && 'defaultProtocol' in feature.config;
+export const mapMLSStatus = (status?: CoreStatus) => {
+ const statusMap: Record = {
+ Valid: MLSStatuses.VALID,
+ Expired: MLSStatuses.EXPIRED,
+ Revoked: MLSStatuses.EXPIRED,
+ };
+
+ if (!status) {
+ return MLSStatuses.NOT_DOWNLOADED;
+ }
+ return statusMap[status];
+};
diff --git a/src/script/E2EIdentity/index.ts b/src/script/E2EIdentity/index.ts
index 6092e227756..ed5294a4610 100644
--- a/src/script/E2EIdentity/index.ts
+++ b/src/script/E2EIdentity/index.ts
@@ -17,4 +17,5 @@
*
*/
-export * from './E2EIdentity';
+export * from './E2EIdentityEnrollment';
+export * from './E2EIdentityVerification';
diff --git a/src/script/client/ClientEntity.ts b/src/script/client/ClientEntity.ts
index 666284d93dc..d3f61569510 100644
--- a/src/script/client/ClientEntity.ts
+++ b/src/script/client/ClientEntity.ts
@@ -26,6 +26,10 @@ import {ClientMapper} from './ClientMapper';
import {ClientRecord} from '../storage';
+export enum MLSPublicKeys {
+ ED25519 = 'ed25519',
+}
+
export class ClientEntity {
static CONFIG = {
DEFAULT_VALUE: '?',
@@ -40,14 +44,14 @@ export class ClientEntity {
label?: string;
meta: {
- isVerified?: ko.Observable;
- isMLSVerified?: ko.Observable;
+ isVerified: ko.Observable;
primaryKey?: string;
userId?: string;
};
model?: string;
time?: string;
type?: ClientType.PERMANENT | ClientType.TEMPORARY;
+ mlsPublicKeys?: Partial>;
constructor(isSelfClient: boolean, domain: string | null, id = '') {
this.isSelfClient = isSelfClient;
@@ -68,7 +72,6 @@ export class ClientEntity {
// Metadata maintained by us
this.meta = {
isVerified: ko.observable(false),
- isMLSVerified: ko.observable(false),
primaryKey: undefined,
};
}
diff --git a/src/script/client/ClientMapper.test.ts b/src/script/client/ClientMapper.test.ts
index f98ebee2fc4..a95655d2417 100644
--- a/src/script/client/ClientMapper.test.ts
+++ b/src/script/client/ClientMapper.test.ts
@@ -56,7 +56,6 @@ describe('ClientMapper', () => {
expect(clientEntity.id).toBe(clientPayload.id);
expect(clientEntity.label).toBe(clientPayload.label);
expect(clientEntity.meta.isVerified?.()).toBe(false);
- expect(clientEntity.meta.isMLSVerified?.()).toBe(false);
expect(clientEntity.model).toBe(clientPayload.model);
expect(clientEntity.time).toBe(clientPayload.time);
expect(clientEntity.type).toBe(ClientType.TEMPORARY);
@@ -72,7 +71,6 @@ describe('ClientMapper', () => {
expect(clientEntity.id).toBe(clientPayload.id);
expect(clientEntity.class).toBe(clientPayload.class);
expect(clientEntity.meta.isVerified?.()).toBe(false);
- expect(clientEntity.meta.isMLSVerified?.()).toBe(false);
expect(clientEntity.isPermanent()).toBe(false);
expect(clientEntity.isTemporary()).toBe(false);
});
@@ -83,7 +81,6 @@ describe('ClientMapper', () => {
id: '66d0515a23a0ef25',
meta: {
is_verified: true,
- is_mls_verified: true,
},
};
@@ -103,7 +100,6 @@ describe('ClientMapper', () => {
domain: '',
meta: {
is_verified: true,
- is_mls_verified: true,
},
};
@@ -141,7 +137,6 @@ describe('ClientMapper', () => {
expect(clientEntity.id).toBe(clientPayload.id);
expect(clientEntity.label).toBe(clientPayload.label);
expect(clientEntity.meta.isVerified?.()).toBe(false);
- expect(clientEntity.meta.isMLSVerified?.()).toBe(false);
expect(clientEntity.model).toBe(clientPayload.model);
expect(clientEntity.time).toBe(clientPayload.time);
expect(clientEntity.type).toBe(ClientType.PERMANENT);
@@ -162,7 +157,6 @@ describe('ClientMapper', () => {
expect(clientEntity.id).toBe(clientPayload.id);
expect(clientEntity.label).toBe(clientPayload.label);
expect(clientEntity.meta.isVerified?.()).toBe(false);
- expect(clientEntity.meta.isMLSVerified?.()).toBe(false);
expect(clientEntity.model).toBe(clientPayload.model);
expect(clientEntity.time).toBe(clientPayload.time);
expect(clientEntity.type).toBe(ClientType.PERMANENT);
diff --git a/src/script/client/ClientMapper.ts b/src/script/client/ClientMapper.ts
index 8e22617a296..54bcb81edff 100644
--- a/src/script/client/ClientMapper.ts
+++ b/src/script/client/ClientMapper.ts
@@ -23,7 +23,7 @@ import {ClientEntity} from './ClientEntity';
import {parseClientId} from './ClientIdUtil';
import {ClientRecord} from '../storage';
-import {isClientRecord} from '../util/TypePredicateUtil';
+import {isClientRecord, isClientWithMLSPublicKeys} from '../util/TypePredicateUtil';
export class ClientMapper {
static get CONFIG() {
@@ -58,11 +58,14 @@ export class ClientMapper {
const {userId} = parseClientId(clientPayload.meta.primary_key);
clientEntity.meta.isVerified?.(!!clientPayload.meta.is_verified);
- clientEntity.meta.isMLSVerified?.(!!clientPayload.meta.is_mls_verified);
clientEntity.meta.primaryKey = clientPayload.meta.primary_key;
clientEntity.meta.userId = userId;
}
+ if (isClientWithMLSPublicKeys(clientPayload)) {
+ clientEntity.mlsPublicKeys = clientPayload.mls_public_keys;
+ }
+
return clientEntity;
}
diff --git a/src/script/components/AvailabilityState.test.tsx b/src/script/components/AvailabilityState.test.tsx
index 73b420b5ce2..d9dff16020f 100644
--- a/src/script/components/AvailabilityState.test.tsx
+++ b/src/script/components/AvailabilityState.test.tsx
@@ -23,9 +23,13 @@ import {Availability} from '@wireapp/protocol-messaging';
import {AvailabilityState} from './AvailabilityState';
+import {User} from '../entity/User';
+
+const user = new User();
+
const defaultProps = {
- availability: Availability.Type.AVAILABLE,
dataUieName: 'example-data-uie',
+ user,
label: 'example',
showArrow: false,
theme: false,
@@ -33,6 +37,7 @@ const defaultProps = {
describe('AvailabilityState', () => {
it('renders available icon', async () => {
+ user.availability(Availability.Type.AVAILABLE);
const {getByTestId} = render();
const statusAvailabilityIcon = getByTestId('status-availability-icon');
@@ -40,33 +45,27 @@ describe('AvailabilityState', () => {
});
it('renders away icon', async () => {
- const props = {
- ...defaultProps,
- availability: Availability.Type.AWAY,
- };
+ user.availability(Availability.Type.AWAY);
- const {getByTestId} = render();
+ const {getByTestId} = render();
const statusAvailabilityIcon = getByTestId('status-availability-icon');
expect(statusAvailabilityIcon.getAttribute('data-uie-value')).toEqual('away');
});
it('renders busy icon', async () => {
- const props = {
- ...defaultProps,
- availability: Availability.Type.BUSY,
- };
+ user.availability(Availability.Type.BUSY);
- const {getByTestId} = render();
+ const {getByTestId} = render();
const statusAvailabilityIcon = getByTestId('status-availability-icon');
expect(statusAvailabilityIcon.getAttribute('data-uie-value')).toEqual('busy');
});
it('renders availability icon with arrow', async () => {
+ user.availability(Availability.Type.BUSY);
const props = {
...defaultProps,
- availability: Availability.Type.BUSY,
showArrow: true,
};
@@ -77,18 +76,4 @@ describe('AvailabilityState', () => {
expect(getByTestId('availability-arrow')).not.toBeNull();
});
-
- it('shows label text', async () => {
- const label = 'cool label';
- const props = {
- ...defaultProps,
- availability: Availability.Type.BUSY,
- label,
- showArrow: true,
- };
-
- const {getByText} = render();
-
- expect(getByText(label)).not.toBeNull();
- });
});
diff --git a/src/script/components/AvailabilityState.tsx b/src/script/components/AvailabilityState.tsx
index 03c0e6c8213..c0a8cf7d1fb 100644
--- a/src/script/components/AvailabilityState.tsx
+++ b/src/script/components/AvailabilityState.tsx
@@ -24,20 +24,25 @@ import cx from 'classnames';
import {Availability} from '@wireapp/protocol-messaging';
+import {selfIndicator} from 'Components/ParticipantItemContent/ParticipantItem.styles';
+import {useKoSubscribableChildren} from 'Util/ComponentUtil';
import {CSS_SQUARE} from 'Util/CSSMixin';
import {KEY} from 'Util/KeyboardUtil';
import {Icon} from './Icon';
+import {User} from '../entity/User';
+
interface AvailabilityStateProps {
- availability: Availability.Type;
+ user: User;
className?: string;
dataUieName: string;
- label: string;
+ selfString?: string;
title?: string;
onClick?: (event: React.MouseEvent | React.KeyboardEvent) => void;
showArrow?: boolean;
theme?: boolean;
+ children?: React.ReactNode;
}
const iconStyles: CSSObject = {
@@ -55,15 +60,18 @@ const buttonCommonStyles: CSSObject = {
};
export const AvailabilityState: React.FC = ({
- availability,
+ user,
className,
dataUieName,
- label,
+ selfString,
title,
showArrow = false,
theme = false,
onClick,
+ children,
}) => {
+ const {availability, name: label} = useKoSubscribableChildren(user, ['availability', 'name']);
+
const isAvailable = availability === Availability.Type.AVAILABLE;
const isAway = availability === Availability.Type.AWAY;
const isBusy = availability === Availability.Type.BUSY;
@@ -125,6 +133,10 @@ export const AvailabilityState: React.FC = ({
)}
+ {selfString && {selfString}}
+
+ {children}
+
{showArrow && (
{
- it('can print device id', () => {
- const {getAllByTestId} = render();
-
- const deviceIdParts = getAllByTestId('element-device-id-part').map(node => node.textContent);
-
- expect(deviceIdParts).toEqual(['66', 'e6', '6c', '79', 'e8', 'd1', 'de', 'a4']);
- });
-
- it('can print device id and apply padding', () => {
- const {getAllByTestId} = render();
-
- const deviceIdParts = getAllByTestId('element-device-id-part').map(node => node.textContent);
-
- expect(deviceIdParts).toEqual(['06', 'e6', '6c', '79', 'e8', 'd1', 'de', 'a4']);
- });
-});
diff --git a/src/script/components/MessagesList/Message/DecryptErrorMessage.tsx b/src/script/components/MessagesList/Message/DecryptErrorMessage.tsx
index 0ea2ae94938..16a1aa3aab9 100644
--- a/src/script/components/MessagesList/Message/DecryptErrorMessage.tsx
+++ b/src/script/components/MessagesList/Message/DecryptErrorMessage.tsx
@@ -19,13 +19,14 @@
import React, {useState} from 'react';
-import {DeviceId} from 'Components/DeviceId';
import {Icon} from 'Components/Icon';
import {getDecryptErrorUrl} from 'src/script/externalRoute';
import {MotionDuration} from 'src/script/motion/MotionDuration';
import {t} from 'Util/LocalizerUtil';
+import {splitFingerprint} from 'Util/StringUtil';
import {DecryptErrorMessage as DecryptErrorMessageEntity} from '../../../entity/message/DecryptErrorMessage';
+import {FormattedId} from '../../../page/MainContent/panels/preferences/DevicesPreferences/components/FormattedId';
export interface DecryptErrorMessageProps {
message: DecryptErrorMessageEntity;
@@ -81,7 +82,7 @@ const DecryptErrorMessage: React.FC = ({message, onCli
{message.clientId && (
<>
{'ID: '}
-
+
>
)}
diff --git a/src/script/components/MessagesList/Message/SystemMessage/SystemMessage.tsx b/src/script/components/MessagesList/Message/SystemMessage/SystemMessage.tsx
index 6b842a1dc7e..565f25ba473 100644
--- a/src/script/components/MessagesList/Message/SystemMessage/SystemMessage.tsx
+++ b/src/script/components/MessagesList/Message/SystemMessage/SystemMessage.tsx
@@ -19,7 +19,10 @@
import React from 'react';
+import {MLSVerified} from '@wireapp/react-ui-kit';
+
import {Icon} from 'Components/Icon';
+import {E2EIVerificationMessage} from 'src/script/entity/message/E2EIVerificationMessage';
import {JoinedAfterMLSMigrationFinalisationMessage} from 'src/script/entity/message/JoinedAfterMLSMigrationFinalisationMessage';
import {MessageTimerUpdateMessage} from 'src/script/entity/message/MessageTimerUpdateMessage';
import {MLSConversationRecoveredMessage} from 'src/script/entity/message/MLSConversationRecoveredMessage';
@@ -59,6 +62,10 @@ export const SystemMessage: React.FC = ({message}) => {
return } />;
}
+ if (message instanceof E2EIVerificationMessage) {
+ return } />;
+ }
+
if (message instanceof ProtocolUpdateMessage) {
return ;
}
diff --git a/src/script/components/DeviceId.tsx b/src/script/components/Modals/CertificateDetailsModal/CertificateDetailsModal.styles.ts
similarity index 58%
rename from src/script/components/DeviceId.tsx
rename to src/script/components/Modals/CertificateDetailsModal/CertificateDetailsModal.styles.ts
index 2fbd2793343..8712a7cf1ad 100644
--- a/src/script/components/DeviceId.tsx
+++ b/src/script/components/Modals/CertificateDetailsModal/CertificateDetailsModal.styles.ts
@@ -1,6 +1,6 @@
/*
* Wire
- * Copyright (C) 2021 Wire Swiss GmbH
+ * Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,24 +17,24 @@
*
*/
-import React from 'react';
+import {CSSObject} from '@emotion/serialize';
-import {splitFingerprint} from 'Util/StringUtil';
-
-export interface DeviceIdProps {
- deviceId: string;
+interface Styles {
+ content: CSSObject;
+ modalWrapper: CSSObject;
}
-const DeviceId: React.FC = ({deviceId}) => {
- return (
- <>
- {splitFingerprint(deviceId).map((id, index) => (
-
- {id}
-
- ))}
- >
- );
+export const styles: Styles = {
+ modalWrapper: {
+ maxWidth: '460px',
+ width: '100%',
+ },
+ content: {
+ overflow: 'auto',
+ maxHeight: '251px',
+ fontSize: 'var(--font-size-small)',
+ letterSpacing: '0.05px',
+ lineHeight: 'var(--line-height-md)',
+ wordBreak: 'break-word',
+ },
};
-
-export {DeviceId};
diff --git a/src/script/components/Modals/CertificateDetailsModal/CertificateDetailsModal.test.tsx b/src/script/components/Modals/CertificateDetailsModal/CertificateDetailsModal.test.tsx
new file mode 100644
index 00000000000..213b73ca639
--- /dev/null
+++ b/src/script/components/Modals/CertificateDetailsModal/CertificateDetailsModal.test.tsx
@@ -0,0 +1,70 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {fireEvent, render, waitFor} from '@testing-library/react';
+
+import {CertificateDetailsModal} from './CertificateDetailsModal';
+
+const certificate: string =
+ '-----BEGIN CERTIFICATE-----MIICDzCCAbQCCQDzlU3qembswDAKBggqhkjOPQQDAjCBjjELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEOMAwGA1UEBwwFTWl0dGUxDTALBgNVBAoMBFdpcmUxFDASBgNVBAsMC0VuZ2luZWVyaW5nMRQwEgYDVQQDDAtjYS53aXJlLmNvbTEjMCEGCSqGSIb3DQEJARYUZW5naW5lZXJpbmdAd2lyZS5jb20wHhcNMjMwNTE2MTcxNjE3WhcNMjMwNjE1MTcxNjE3WjCBjjELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEOMAwGA1UEBwwFTWl0dGUxDTALBgNVBAoMBFdpcmUxFDASBgNVBAsMC0VuZ2luZWVyaW5nMRQwEgYDVQQDDAtjYS53aXJlLmNvbTEjMCEGCSqGSIb3DQEJARYUZW5naW5lZXJpbmdAd2lyZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASpxQ3hrzh5fPDan+vbcRT8fCQzaz3fIywUNxTRWzvGpPkRPPDegJ7h4G6aUqDfZFgvSsCCaaGaYYVF1di/tuYpMAoGCCqGSM49BAMCA0kAMEYCIQDHAeMUcjjP5J3Mbs3uIlPLd0tZQb0S6bEekXvHsxhYGAIhAKOoeyMqaHxj3qaHnpCBjY/0slt2QUbtDbpF3Lgz2l2S-----END CERTIFICATE-----';
+
+Object.assign(navigator, {
+ clipboard: {
+ writeText: jest.fn().mockImplementation(() => Promise.resolve()),
+ },
+});
+
+Object.assign(window, {
+ URL: {
+ revokeObjectURL: jest.fn(),
+ },
+});
+
+const defaultProps = {
+ certificate,
+ onClose: jest.fn(),
+};
+
+describe('CertificateDetailsModal', () => {
+ it('is certificate downloaded', async () => {
+ const {getByTestId} = render();
+
+ const downloadButton = getByTestId('download-certificate') as HTMLButtonElement;
+ expect(downloadButton).toBeDefined();
+ expect(downloadButton.disabled).toBe(false);
+
+ fireEvent.click(downloadButton);
+
+ expect(downloadButton.disabled).toBe(true);
+ });
+
+ it('is certificate copied', async () => {
+ const {getByTestId} = render();
+
+ const copyButton = getByTestId('copy-certificate') as HTMLButtonElement;
+ expect(copyButton).toBeDefined();
+ fireEvent.click(copyButton);
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(certificate);
+
+ await waitFor(() => {
+ expect(copyButton.disabled).toBe(true);
+ });
+ });
+});
diff --git a/src/script/components/Modals/CertificateDetailsModal/CertificateDetailsModal.tsx b/src/script/components/Modals/CertificateDetailsModal/CertificateDetailsModal.tsx
new file mode 100644
index 00000000000..d38811cb6b9
--- /dev/null
+++ b/src/script/components/Modals/CertificateDetailsModal/CertificateDetailsModal.tsx
@@ -0,0 +1,113 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {useState} from 'react';
+
+import {Icon} from 'Components/Icon';
+import {ModalComponent} from 'Components/ModalComponent';
+import {t} from 'Util/LocalizerUtil';
+import {downloadFile} from 'Util/util';
+
+import {styles} from './CertificateDetailsModal.styles';
+
+const COPY_MESSAGE_TIMEOUT = 3000;
+const DOWNLOAD_CERTIFICATE_TIMEOUT = 500;
+const CERTIFICATE_NAME = 'certificate.pem';
+const CERTIFICATE_TYPE = 'application/x-pem-file';
+
+export interface CertificateDetailsModalProps {
+ certificate: string;
+ onClose: () => void;
+}
+
+export const CertificateDetailsModal = ({certificate, onClose}: CertificateDetailsModalProps) => {
+ const [isTextCopied, setIsTextCopied] = useState(false);
+ const [isDownloading, setIsDownloading] = useState(false);
+
+ const onDownload = () => {
+ setIsDownloading(true);
+
+ const certificateUrl = `data:${CERTIFICATE_TYPE},${encodeURIComponent(certificate)}`;
+ downloadFile(certificateUrl, CERTIFICATE_NAME, CERTIFICATE_TYPE);
+
+ setTimeout(() => {
+ setIsDownloading(false);
+ }, DOWNLOAD_CERTIFICATE_TIMEOUT);
+ };
+
+ const onCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(certificate).then(() => {
+ setIsTextCopied(true);
+
+ setTimeout(() => {
+ setIsTextCopied(false);
+ }, COPY_MESSAGE_TIMEOUT);
+ });
+ } catch (err) {
+ console.error('Failed to copy: ', err);
+ }
+ };
+
+ return (
+
+
+
+ {t('E2EI.certificateDetails')}
+
+
+
+
+
+
+ {certificate}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/script/components/Modals/CertificateDetailsModal/index.ts b/src/script/components/Modals/CertificateDetailsModal/index.ts
new file mode 100644
index 00000000000..d8baa78a360
--- /dev/null
+++ b/src/script/components/Modals/CertificateDetailsModal/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+export * from './CertificateDetailsModal';
diff --git a/src/script/components/Modals/UserModal/UserModal.tsx b/src/script/components/Modals/UserModal/UserModal.tsx
index cee5bd677fb..c52e5e6db48 100644
--- a/src/script/components/Modals/UserModal/UserModal.tsx
+++ b/src/script/components/Modals/UserModal/UserModal.tsx
@@ -146,11 +146,10 @@ const UserModal: React.FC = ({
resetState();
};
const {classifiedDomains} = useKoSubscribableChildren(teamState, ['classifiedDomains']);
- const {
- is_trusted: isTrusted,
- is_verified: isSelfVerified,
- isActivatedAccount,
- } = useKoSubscribableChildren(selfUser, ['is_trusted', 'is_verified', 'isActivatedAccount']);
+ const {is_trusted: isTrusted, isActivatedAccount} = useKoSubscribableChildren(selfUser, [
+ 'is_trusted',
+ 'isActivatedAccount',
+ ]);
const isFederated = core.backendFeatures?.isFederated;
useEffect(() => {
@@ -203,14 +202,7 @@ const UserModal: React.FC = ({
+ );
+};
diff --git a/src/script/components/VerificationBadge/index.ts b/src/script/components/VerificationBadge/index.ts
new file mode 100644
index 00000000000..084f8ca3023
--- /dev/null
+++ b/src/script/components/VerificationBadge/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Wire
+ * Copyright (C) 2022 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+export * from './VerificationBadges';
diff --git a/src/script/components/calling/CallParticipantsListItem/CallParticipantItemContent/CallParticipantItemContent.tsx b/src/script/components/calling/CallParticipantsListItem/CallParticipantItemContent/CallParticipantItemContent.tsx
index 4ebdc04ef49..77c6bdc31aa 100644
--- a/src/script/components/calling/CallParticipantsListItem/CallParticipantItemContent/CallParticipantItemContent.tsx
+++ b/src/script/components/calling/CallParticipantsListItem/CallParticipantItemContent/CallParticipantItemContent.tsx
@@ -21,10 +21,10 @@ import React from 'react';
import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums';
-import {Availability} from '@wireapp/protocol-messaging';
-
import {AvailabilityState} from 'Components/AvailabilityState';
import {Icon} from 'Components/Icon';
+import {User} from 'src/script/entity/User';
+import {useKoSubscribableChildren} from 'Util/ComponentUtil';
import {t} from 'Util/LocalizerUtil';
import {capitalizeFirstChar} from 'Util/StringUtil';
@@ -40,24 +40,23 @@ import {
} from './CallParticipantItemContent.styles';
export interface CallParticipantItemContentProps {
- name: string;
+ user: User;
selfInTeam?: boolean;
isAudioEstablished: boolean;
- availability?: Availability.Type;
isSelf?: boolean;
showContextMenu: boolean;
onDropdownClick: (event: React.MouseEvent) => void;
}
export const CallParticipantItemContent = ({
- name,
+ user,
selfInTeam = false,
isAudioEstablished,
- availability = Availability.Type.NONE,
isSelf = false,
showContextMenu,
onDropdownClick,
}: CallParticipantItemContentProps) => {
+ const {name} = useKoSubscribableChildren(user, ['name']);
const selfString = `(${capitalizeFirstChar(t('conversationYouNominative'))})`;
return (
@@ -65,12 +64,7 @@ export const CallParticipantItemContent = ({
@@ -199,5 +127,3 @@ const DevicesPreferences = ({
);
};
-
-export {DevicesPreferences};
diff --git a/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/DetailedDevice.tsx b/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/DetailedDevice.tsx
new file mode 100644
index 00000000000..a6509685f1f
--- /dev/null
+++ b/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/DetailedDevice.tsx
@@ -0,0 +1,57 @@
+/*
+ * Wire
+ * Copyright (C) 2022 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import React from 'react';
+
+import {DeviceVerificationBadges} from 'Components/VerificationBadge';
+import {ClientEntity} from 'src/script/client/ClientEntity';
+import {WireIdentity} from 'src/script/E2EIdentity';
+
+import {MLSDeviceDetails} from './MLSDeviceDetails';
+import {ProteusDeviceDetails} from './ProteusDeviceDetails';
+
+export interface DeviceProps {
+ device: ClientEntity;
+ fingerprint: string;
+ isCurrentDevice?: boolean;
+ getDeviceIdentity?: (deviceId: string) => WireIdentity | undefined;
+ isProteusVerified?: boolean;
+}
+
+export const DetailedDevice: React.FC = ({
+ device,
+ fingerprint,
+ isCurrentDevice,
+ getDeviceIdentity,
+ isProteusVerified,
+}) => {
+ const getIdentity = getDeviceIdentity ? () => getDeviceIdentity(device.id) : undefined;
+ return (
+ <>
+
+ {device.model}
+
+
+
+ {getDeviceIdentity && }
+
+
+ >
+ );
+};
diff --git a/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/Device/Device.tsx b/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/Device/Device.tsx
new file mode 100644
index 00000000000..31f5c740b9c
--- /dev/null
+++ b/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/Device/Device.tsx
@@ -0,0 +1,122 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {MouseEvent, KeyboardEvent} from 'react';
+
+import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums';
+
+import {Icon} from 'Components/Icon';
+import {DeviceVerificationBadges} from 'Components/VerificationBadge';
+import {WireIdentity} from 'src/script/E2EIdentity';
+import {useKoSubscribableChildren} from 'Util/ComponentUtil';
+import {handleKeyDown} from 'Util/KeyboardUtil';
+import {t} from 'Util/LocalizerUtil';
+import {splitFingerprint} from 'Util/StringUtil';
+
+import {ClientEntity} from '../../../../../../../client';
+import {FormattedId} from '../FormattedId';
+
+interface DeviceProps {
+ device: ClientEntity;
+ isSSO: boolean;
+ getDeviceIdentity?: (deviceId: string) => WireIdentity | undefined;
+ onRemove: (device: ClientEntity) => void;
+ onSelect: (device: ClientEntity, currentDeviceIdentity?: WireIdentity) => void;
+ deviceNumber: number;
+}
+
+export const Device = ({device, isSSO, onSelect, onRemove, getDeviceIdentity, deviceNumber}: DeviceProps) => {
+ const {isVerified} = useKoSubscribableChildren(device.meta, ['isVerified']);
+ const verifiedLabel = isVerified ? t('preferencesDevicesVerification') : t('preferencesDeviceNotVerified');
+ const deviceAriaLabel = `${t('preferencesDevice')} ${deviceNumber}, ${device.getName()}, ${verifiedLabel}`;
+
+ const deviceIdentity = getDeviceIdentity?.(device.id);
+
+ const handleClick = (event: MouseEvent) => {
+ event.stopPropagation();
+ onRemove(device);
+ };
+
+ const handleKeyPress = (event: KeyboardEvent) => {
+ event.stopPropagation();
+ };
+
+ const onDeviceSelect = () => onSelect(device);
+
+ return (
+
+ );
+};
diff --git a/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/Device/index.ts b/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/Device/index.ts
new file mode 100644
index 00000000000..f134bf2cc9a
--- /dev/null
+++ b/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/Device/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+export * from './Device';
diff --git a/src/script/page/MainContent/panels/preferences/devices/DeviceDetailsPreferences.test.tsx b/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/DeviceDetailsPreferences/DeviceDetailsPreferences.test.tsx
similarity index 95%
rename from src/script/page/MainContent/panels/preferences/devices/DeviceDetailsPreferences.test.tsx
rename to src/script/page/MainContent/panels/preferences/DevicesPreferences/components/DeviceDetailsPreferences/DeviceDetailsPreferences.test.tsx
index 153e662b0a5..d2824309f36 100644
--- a/src/script/page/MainContent/panels/preferences/devices/DeviceDetailsPreferences.test.tsx
+++ b/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/DeviceDetailsPreferences/DeviceDetailsPreferences.test.tsx
@@ -32,10 +32,11 @@ describe('DeviceDetailsPreferences', () => {
const defaultParams = {
device,
getFingerprint: jest.fn().mockResolvedValue('00000000'),
+ getCertificate: jest.fn().mockResolvedValue('00000000'),
onClose: jest.fn(),
onRemove: jest.fn(),
onResetSession: jest.fn().mockResolvedValue(undefined),
- onVerify: jest.fn((_, isVerified) => device.meta.isVerified(isVerified)),
+ onVerify: jest.fn((_, isVerified) => device.meta?.isVerified(isVerified)),
};
it('shows device details', async () => {
const {getByText, getAllByText} = render(withTheme());
diff --git a/src/script/page/MainContent/panels/preferences/devices/DeviceDetailsPreferences.tsx b/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/DeviceDetailsPreferences/DeviceDetailsPreferences.tsx
similarity index 77%
rename from src/script/page/MainContent/panels/preferences/devices/DeviceDetailsPreferences.tsx
rename to src/script/page/MainContent/panels/preferences/DevicesPreferences/components/DeviceDetailsPreferences/DeviceDetailsPreferences.tsx
index 7f6ff5ca4d5..fa056133a12 100644
--- a/src/script/page/MainContent/panels/preferences/devices/DeviceDetailsPreferences.tsx
+++ b/src/script/page/MainContent/panels/preferences/DevicesPreferences/components/DeviceDetailsPreferences/DeviceDetailsPreferences.tsx
@@ -22,17 +22,18 @@ import React, {useEffect, useState} from 'react';
import {Button, ButtonVariant} from '@wireapp/react-ui-kit';
import {ClientEntity} from 'src/script/client/ClientEntity';
+import {WireIdentity} from 'src/script/E2EIdentity';
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
import {t} from 'Util/LocalizerUtil';
-import {DetailedDevice} from './components/DetailedDevice';
-
-import {Config} from '../../../../../Config';
-import {MotionDuration} from '../../../../../motion/MotionDuration';
+import {Config} from '../../../../../../../Config';
+import {MotionDuration} from '../../../../../../../motion/MotionDuration';
+import {DetailedDevice} from '../DetailedDevice';
interface DevicesPreferencesProps {
device: ClientEntity;
getFingerprint: (device: ClientEntity) => Promise;
+ getDeviceIdentity?: (deviceId: string) => WireIdentity | undefined;
onClose: () => void;
onRemove: (device: ClientEntity) => void;
onResetSession: (device: ClientEntity) => Promise;
@@ -45,9 +46,10 @@ enum SessionResetState {
RESET = 'reset',
}
-const DeviceDetailsPreferences: React.FC = ({
+export const DeviceDetailsPreferences: React.FC = ({
device,
getFingerprint,
+ getDeviceIdentity,
onVerify,
onRemove,
onClose,
@@ -66,7 +68,7 @@ const DeviceDetailsPreferences: React.FC = ({
};
useEffect(() => {
- getFingerprint(device).then(setFingerprint);
+ void getFingerprint(device).then(setFingerprint);
}, [device, getFingerprint]);
return (
@@ -76,6 +78,7 @@ const DeviceDetailsPreferences: React.FC = ({
data-uie-name="preferences-devices-details"
>