Skip to content

Commit

Permalink
feat: display users and devices E2E identity verification badges (#15575
Browse files Browse the repository at this point in the history
)
  • Loading branch information
phoenixhdd authored Nov 29, 2023
1 parent bc91ed1 commit 67b1b5c
Show file tree
Hide file tree
Showing 115 changed files with 2,523 additions and 1,303 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions server/config/client.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions server/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
14 changes: 14 additions & 0 deletions src/__mocks__/@wireapp/core-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
17 changes: 13 additions & 4 deletions src/__mocks__/@wireapp/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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: {
Expand Down
49 changes: 43 additions & 6 deletions src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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: {},
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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);
});

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 67b1b5c

Please sign in to comment.