diff --git a/package.json b/package.json index c9a45e15aa1..99f41648f99 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@peculiar/x509": "1.9.6", "@wireapp/avs": "9.6.9", "@wireapp/commons": "5.2.4", - "@wireapp/core": "43.9.1", + "@wireapp/core": "43.11.2", "@wireapp/react-ui-kit": "9.12.8", "@wireapp/store-engine-dexie": "2.1.7", "@wireapp/webapp-events": "0.20.1", diff --git a/src/__mocks__/@wireapp/core.ts b/src/__mocks__/@wireapp/core.ts index 3e4735c2d18..8a35812e90c 100644 --- a/src/__mocks__/@wireapp/core.ts +++ b/src/__mocks__/@wireapp/core.ts @@ -41,6 +41,10 @@ export class Account extends EventEmitter { getDeviceIdentities: jest.fn(), getConversationState: jest.fn(), registerServerCertificates: jest.fn(), + on: jest.fn(), + emit: jest.fn(), + off: jest.fn(), + initialize: jest.fn(), }, mls: { schedulePeriodicKeyMaterialRenewals: jest.fn(), diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json index aa0a2e263da..cc052fd8818 100644 --- a/src/i18n/en-US.json +++ b/src/i18n/en-US.json @@ -182,6 +182,10 @@ "acme.renewCertificate.gracePeriodOver.paragraph": "The end-to-end identity certificate for this device has expired. To keep your Wire communication at the highest security level, please update the certificate.

Enter your identity provider’s credentials in the next step to update the certificate automatically.

Learn more about end-to-end identity ", "acme.renewCertificate.headline.alt": "Update end-to-end identity certificate", "acme.renewCertificate.paragraph": "The end-to-end identity certificate for this device expires soon. To keep your communication at the highest security level, update your certificate now.

Enter your identity provider’s credentials in the next step to update the certificate automatically.

Learn more about end-to-end identity ", + "acme.selfCertificateRevoked.button.primary": "Log out", + "acme.selfCertificateRevoked.button.cancel": "Continue using this device", + "acme.selfCertificateRevoked.text": "Your team admin revoked the certificate for this device.
Log out to reduce security risks. Then log in again, get a new certificate, and reset your password.

If you keep using this device, your conversations are no longer verified.", + "acme.selfCertificateRevoked.title": "End-to-end identity certificate revoked", "acme.renewal.done.headline": "Certificate updated", "acme.renewal.done.paragraph": "The certificate is updated and your device is verified. You can find more details about this certificate in your [bold]Wire Preferences[/bold] under [bold]Devices.[/bold]

Learn more about end-to-end identity ", "acme.renewal.inProgress.headline": "Updating Certificate...", @@ -642,6 +646,10 @@ "featureConfigChangeModalAudioVideoDescriptionItemCameraDisabled": "Camera in calls is disabled", "featureConfigChangeModalAudioVideoDescriptionItemCameraEnabled": "Camera in calls is enabled", "featureConfigChangeModalAudioVideoHeadline": "There has been a change in {{brandName}}", + "featureConfigChangeModalDownloadPathDisabled": "Enforced Download Path is disabled. You will need to restart the app if you want to save downloads in a new location.", + "featureConfigChangeModalDownloadPathEnabled": "Enforced Download Path is enabled. The App will restart for the new settings to take effect.", + "featureConfigChangeModalDownloadPathChanged": "Enforced Download Path location is changed. The App will restart for the new settings to take effect.", + "featureConfigChangeModalDownloadPathHeadline": "There has been a change in {{brandName}}", "featureConfigChangeModalConferenceCallingEnabled": "Your team was upgraded to {{brandName}} Enterprise, which gives you access to features such as conference calls and more. [link]Learn more about {{brandName}} Enterprise[/link]", "featureConfigChangeModalConferenceCallingTitle": "{{brandName}} Enterprise", "featureConfigChangeModalConversationGuestLinksDescriptionItemConversationGuestLinksDisabled": "Generating guest links is now disabled for all group admins.", @@ -1521,4 +1529,4 @@ "wireMacos": "{{brandName}} for macOS", "wireWindows": "{{brandName}} for Windows", "wire_for_web": "{{brandName}} for Web" -} +} \ No newline at end of file diff --git a/src/script/E2EIdentity/E2EIdentityEnrollment.ts b/src/script/E2EIdentity/E2EIdentityEnrollment.ts index 5eb785614da..5b4f2d6367a 100644 --- a/src/script/E2EIdentity/E2EIdentityEnrollment.ts +++ b/src/script/E2EIdentity/E2EIdentityEnrollment.ts @@ -131,7 +131,16 @@ export class E2EIHandler extends TypedEventEmitter { }), }; - await this.coreE2EIService.registerServerCertificates(discoveryUrl); + await this.coreE2EIService.initialize(discoveryUrl); + await this.coreE2EIService.registerServerCertificates(); + + try { + //FIXME: this doesn't work on curernt core-crypto version + await this.coreE2EIService.validateSelfCrl(); + } catch (error) { + console.error('Error validating self CRL', error); + } + this.currentStep = E2EIHandlerStep.INITIALIZED; return this; } diff --git a/src/script/E2EIdentity/Modals/Modals.ts b/src/script/E2EIdentity/Modals/Modals.ts index 10d80d80757..38ade1f224a 100644 --- a/src/script/E2EIdentity/Modals/Modals.ts +++ b/src/script/E2EIdentity/Modals/Modals.ts @@ -30,6 +30,7 @@ export enum ModalType { SUCCESS = 'success', LOADING = 'loading', CERTIFICATE_RENEWAL = 'certificate_renewal', + SELF_CERTIFICATE_REVOKED = 'self_certificate_revoked', SNOOZE_REMINDER = 'snooze_reminder', } @@ -120,6 +121,21 @@ export const getModalOptions = ({ hideSecondary || secondaryActionFn === undefined ? PrimaryModal.type.ACKNOWLEDGE : PrimaryModal.type.CONFIRM; break; + case ModalType.SELF_CERTIFICATE_REVOKED: + options = { + text: { + closeBtnLabel: t('acme.selfCertificateRevoked.button.cancel'), + htmlMessage: t('acme.selfCertificateRevoked.text'), + title: t('acme.selfCertificateRevoked.title'), + }, + primaryAction: { + action: primaryActionFn, + text: t('acme.selfCertificateRevoked.button.primary'), + }, + }; + modalType = PrimaryModal.type.CONFIRM; + break; + case ModalType.SNOOZE_REMINDER: options = { text: { diff --git a/src/script/E2EIdentity/certificateDetails.ts b/src/script/E2EIdentity/certificateDetails.ts index 91496ea9be4..1ffc8b907d6 100644 --- a/src/script/E2EIdentity/certificateDetails.ts +++ b/src/script/E2EIdentity/certificateDetails.ts @@ -27,7 +27,7 @@ export const mapMLSStatus = (status?: CoreStatus) => { const statusMap: Record = { Valid: MLSStatuses.VALID, Expired: MLSStatuses.EXPIRED, - Revoked: MLSStatuses.EXPIRED, + Revoked: MLSStatuses.REVOKED, }; if (!status) { diff --git a/src/script/components/VerificationBadge/VerificationBadges.tsx b/src/script/components/VerificationBadge/VerificationBadges.tsx index c635d0fd0aa..0f073c4dcc2 100644 --- a/src/script/components/VerificationBadge/VerificationBadges.tsx +++ b/src/script/components/VerificationBadge/VerificationBadges.tsx @@ -146,6 +146,12 @@ const MLSVerificationBadge = ({context, MLSStatus}: {MLSStatus?: MLSStatuses; co ); + case MLSStatuses.REVOKED: + return ( + + + + ); case MLSStatuses.EXPIRES_SOON: return ( diff --git a/src/script/conversation/ConversationVerificationStateHandler/MLS/MLSStateHandler.test.ts b/src/script/conversation/ConversationVerificationStateHandler/MLS/MLSStateHandler.test.ts index cc13b3bee75..ce0e8395e2d 100644 --- a/src/script/conversation/ConversationVerificationStateHandler/MLS/MLSStateHandler.test.ts +++ b/src/script/conversation/ConversationVerificationStateHandler/MLS/MLSStateHandler.test.ts @@ -47,7 +47,7 @@ describe('MLSConversationVerificationStateHandler', () => { it('should do nothing if MLS service is not available', () => { core.service!.mls = undefined; - const t = () => registerMLSConversationVerificationStateHandler(undefined, conversationState, core); + const t = () => registerMLSConversationVerificationStateHandler(undefined, undefined, conversationState, core); expect(t).not.toThrow(); }); @@ -55,7 +55,7 @@ describe('MLSConversationVerificationStateHandler', () => { it('should do nothing if e2eIdentity service is not available', () => { core.service!.e2eIdentity = undefined; - registerMLSConversationVerificationStateHandler(undefined, conversationState, core); + registerMLSConversationVerificationStateHandler(undefined, undefined, conversationState, core); expect(core.service?.mls?.on).not.toHaveBeenCalled(); }); @@ -69,7 +69,7 @@ describe('MLSConversationVerificationStateHandler', () => { .spyOn(core.service!.mls!, 'on') .mockImplementation((_event, listener) => (triggerEpochChange = listener) as any); - registerMLSConversationVerificationStateHandler(undefined, conversationState, core); + registerMLSConversationVerificationStateHandler(undefined, undefined, conversationState, core); triggerEpochChange({groupId}); await new Promise(resolve => setTimeout(resolve, 0)); @@ -84,7 +84,7 @@ describe('MLSConversationVerificationStateHandler', () => { .spyOn(core.service!.mls!, 'on') .mockImplementation((_event, listener) => (triggerEpochChange = listener) as any); - registerMLSConversationVerificationStateHandler(undefined, conversationState, core); + registerMLSConversationVerificationStateHandler(undefined, undefined, conversationState, core); triggerEpochChange({groupId}); await new Promise(resolve => setTimeout(resolve, 0)); @@ -99,7 +99,7 @@ describe('MLSConversationVerificationStateHandler', () => { .spyOn(core.service!.mls!, 'on') .mockImplementation((_event, listener) => (triggerEpochChange = listener) as any); - registerMLSConversationVerificationStateHandler(undefined, conversationState, core); + registerMLSConversationVerificationStateHandler(undefined, undefined, conversationState, core); triggerEpochChange({groupId}); await new Promise(resolve => setTimeout(resolve, 0)); @@ -116,7 +116,7 @@ describe('MLSConversationVerificationStateHandler', () => { .spyOn(core.service!.mls!, 'on') .mockImplementation((_event, listener) => (triggerEpochChange = listener) as any); - registerMLSConversationVerificationStateHandler(undefined, conversationState, core); + registerMLSConversationVerificationStateHandler(undefined, undefined, conversationState, core); triggerEpochChange({groupId: newConversation.groupId}); setTimeout(() => { diff --git a/src/script/conversation/ConversationVerificationStateHandler/MLS/MLSStateHandler.ts b/src/script/conversation/ConversationVerificationStateHandler/MLS/MLSStateHandler.ts index ddde7d78d6d..abc780b830c 100644 --- a/src/script/conversation/ConversationVerificationStateHandler/MLS/MLSStateHandler.ts +++ b/src/script/conversation/ConversationVerificationStateHandler/MLS/MLSStateHandler.ts @@ -22,7 +22,13 @@ import {QualifiedId} from '@wireapp/api-client/lib/user'; import {E2eiConversationState} from '@wireapp/core/lib/messagingProtocols/mls'; import {container} from 'tsyringe'; -import {getConversationVerificationState, getUsersIdentities, MLSStatuses} from 'src/script/E2EIdentity'; +import { + getActiveWireIdentity, + getConversationVerificationState, + getUsersIdentities, + MLSStatuses, +} from 'src/script/E2EIdentity'; +import {Conversation} from 'src/script/entity/Conversation'; import {E2EIVerificationMessageType} from 'src/script/message/E2EIVerificationMessageType'; import {Core} from 'src/script/service/CoreSingleton'; import {Logger, getLogger} from 'Util/Logger'; @@ -38,6 +44,7 @@ class MLSConversationVerificationStateHandler { public constructor( private readonly onConversationVerificationStateChange: OnConversationE2EIVerificationStateChange, + private readonly onSelfClientCertificateRevoked: () => Promise, private readonly conversationState: ConversationState, private readonly core: Core, ) { @@ -48,7 +55,9 @@ class MLSConversationVerificationStateHandler { } // We hook into the newEpoch event of the MLS service to check if the conversation needs to be verified or degraded - this.core.service.mls.on('newEpoch', this.checkConversationVerificationState); + this.core.service.mls.on('newEpoch', this.onEpochChanged); + this.core.service.e2eIdentity.on('remoteCrlChanged', this.checkAllConversationsVerificationState); + this.core.service.e2eIdentity.on('selfCrlChanged', this.checkSelfCertificateRevocation); } /** @@ -89,7 +98,34 @@ class MLSConversationVerificationStateHandler { }); } - private checkConversationVerificationState = async ({groupId}: {groupId: string}): Promise => { + /** + * This function checks if self client certificate is revoked + */ + private checkSelfCertificateRevocation = async (): Promise => { + const activeIdentity = await getActiveWireIdentity(); + + if (!activeIdentity) { + return; + } + + const isRevoked = activeIdentity.status === MLSStatuses.REVOKED; + + if (isRevoked) { + await this.onSelfClientCertificateRevoked(); + } + + await this.checkAllConversationsVerificationState(); + }; + + /** + * This function checks all conversations if they are verified or degraded and updates them accordingly + */ + private checkAllConversationsVerificationState = async (): Promise => { + const conversations = this.conversationState.conversations(); + await Promise.all(conversations.map(conversation => this.checkConversationVerificationState(conversation))); + }; + + private onEpochChanged = async ({groupId}: {groupId: string}): Promise => { // There could be a race condition where we would receive an epoch update for a conversation that is not yet known by the webapp. // We just wait for it to be available and then check the verification state const conversation = await waitFor(() => @@ -100,12 +136,16 @@ class MLSConversationVerificationStateHandler { return this.logger.warn(`Epoch changed but conversation could not be found after waiting for 5 seconds`); } + return this.checkConversationVerificationState(conversation); + }; + + private checkConversationVerificationState = async (conversation: Conversation): Promise => { const isSelfConversation = conversation.type() === CONVERSATION_TYPE.SELF; if (!isMLSConversation(conversation) || isSelfConversation) { return; } - const verificationState = await getConversationVerificationState(groupId); + const verificationState = await getConversationVerificationState(conversation.groupId); if ( verificationState === E2eiConversationState.NotVerified && @@ -123,8 +163,14 @@ class MLSConversationVerificationStateHandler { export const registerMLSConversationVerificationStateHandler = ( onConversationVerificationStateChange: OnConversationE2EIVerificationStateChange = () => {}, + onSelfClientCertificateRevoked: () => Promise = async () => {}, conversationState: ConversationState = container.resolve(ConversationState), core: Core = container.resolve(Core), ): void => { - new MLSConversationVerificationStateHandler(onConversationVerificationStateChange, conversationState, core); + new MLSConversationVerificationStateHandler( + onConversationVerificationStateChange, + onSelfClientCertificateRevoked, + conversationState, + core, + ); }; diff --git a/src/script/main/app.ts b/src/script/main/app.ts index 34ea64aa867..bad85b644f0 100644 --- a/src/script/main/app.ts +++ b/src/script/main/app.ts @@ -31,6 +31,7 @@ import {container} from 'tsyringe'; import {Runtime} from '@wireapp/commons'; import {WebAppEvents} from '@wireapp/webapp-events'; +import {PrimaryModal} from 'Components/Modals/PrimaryModal'; import {E2EIHandler} from 'src/script/E2EIdentity'; import {initializeDataDog} from 'Util/DataDog'; import {DebugUtil} from 'Util/DebugUtil'; @@ -64,6 +65,7 @@ import {OnConversationE2EIVerificationStateChange} from '../conversation/Convers import {EventBuilder} from '../conversation/EventBuilder'; import {MessageRepository} from '../conversation/MessageRepository'; import {CryptographyRepository} from '../cryptography/CryptographyRepository'; +import {getModalOptions, ModalType} from '../E2EIdentity/Modals'; import {User} from '../entity/User'; import {AccessTokenError} from '../error/AccessTokenError'; import {AuthError} from '../error/AuthError'; @@ -450,7 +452,10 @@ export class App { if (supportsMLS()) { //if mls is supported, we need to initialize the callbacks (they are used when decrypting messages) conversationRepository.initMLSConversationRecoveredListener(); - registerMLSConversationVerificationStateHandler(this.updateConversationE2EIVerificationState); + registerMLSConversationVerificationStateHandler( + this.updateConversationE2EIVerificationState, + this.showClientCertificateRevokedWarning, + ); } onProgress(25, t('initReceivedUserData')); @@ -859,4 +864,13 @@ export class App { break; } }; + + private showClientCertificateRevokedWarning = async () => { + const {modalOptions, modalType} = getModalOptions({ + type: ModalType.SELF_CERTIFICATE_REVOKED, + primaryActionFn: () => this.logout(SIGN_OUT_REASON.APP_INIT, false), + }); + + PrimaryModal.show(modalType, modalOptions); + }; } diff --git a/src/script/page/components/FeatureConfigChange/FeatureConfigChangeNotifier/FeatureConfigChangeNotifier.test.tsx b/src/script/page/components/FeatureConfigChange/FeatureConfigChangeNotifier/FeatureConfigChangeNotifier.test.tsx index bcbfef682ec..1e9369925e1 100644 --- a/src/script/page/components/FeatureConfigChange/FeatureConfigChangeNotifier/FeatureConfigChangeNotifier.test.tsx +++ b/src/script/page/components/FeatureConfigChange/FeatureConfigChangeNotifier/FeatureConfigChangeNotifier.test.tsx @@ -19,6 +19,7 @@ import {act, render, waitFor} from '@testing-library/react'; import {FeatureStatus, FEATURE_KEY, FeatureList} from '@wireapp/api-client/lib/team/feature'; +import {Runtime} from '@wireapp/commons/lib/util/Runtime'; import {PrimaryModal} from 'Components/Modals/PrimaryModal'; import en from 'I18n/en-US.json'; @@ -36,6 +37,8 @@ describe('FeatureConfigChangeNotifier', () => { beforeEach(() => { showModalSpy.mockClear(); localStorage.clear(); + jest.spyOn(Runtime, 'isDesktopApp').mockReturnValue(true); + jest.spyOn(Runtime, 'isWindows').mockReturnValue(true); }); const baseConfig: FeatureList = { @@ -95,6 +98,7 @@ describe('FeatureConfigChangeNotifier', () => { ...baseConfig, [feature]: { status: FeatureStatus.ENABLED, + ...(feature === FEATURE_KEY.ENFORCE_DOWNLOAD_PATH && {config: {enforcedDownloadLocation: 'dlpath'}}), }, }); }); @@ -116,6 +120,7 @@ describe('FeatureConfigChangeNotifier', () => { ...baseConfig, [feature]: { status: FeatureStatus.DISABLED, + ...(feature === FEATURE_KEY.ENFORCE_DOWNLOAD_PATH && {config: {enforcedDownloadLocation: ''}}), }, }); }); diff --git a/src/script/page/components/FeatureConfigChange/FeatureConfigChangeNotifier/FeatureConfigChangeNotifier.ts b/src/script/page/components/FeatureConfigChange/FeatureConfigChangeNotifier/FeatureConfigChangeNotifier.ts index b0c81f3e14c..dbc62d2a8b0 100644 --- a/src/script/page/components/FeatureConfigChange/FeatureConfigChangeNotifier/FeatureConfigChangeNotifier.ts +++ b/src/script/page/components/FeatureConfigChange/FeatureConfigChangeNotifier/FeatureConfigChangeNotifier.ts @@ -80,31 +80,42 @@ const featureNotifications: Partial< }; }, [FEATURE_KEY.ENFORCE_DOWNLOAD_PATH]: (oldConfig, newConfig) => { - const status = wasTurnedOnOrOff(oldConfig, newConfig); - if (!status) { - return undefined; - } - if (newConfig && 'config' in newConfig) { + if ( + newConfig && + 'config' in newConfig && + oldConfig && + 'config' in oldConfig && + newConfig?.config?.enforcedDownloadLocation !== oldConfig?.config?.enforcedDownloadLocation && + Runtime.isDesktopApp() && + Runtime.isWindows() + ) { + const status = wasTurnedOnOrOff(oldConfig, newConfig); + const configStatus = newConfig?.config?.enforcedDownloadLocation !== oldConfig?.config?.enforcedDownloadLocation; + if (!status && !configStatus) { + return undefined; + } amplify.publish( WebAppEvents.TEAM.DOWNLOAD_PATH_UPDATE, newConfig.status === FeatureStatus.ENABLED ? newConfig.config.enforcedDownloadLocation : undefined, ); - } - return { - htmlMessage: - status === FeatureStatus.ENABLED - ? t('featureConfigChangeModalDownloadPathEnabled') - : t('featureConfigChangeModalDownloadPathDisabled'), - title: 'featureConfigChangeModalDownloadPathHeadline', - primaryAction: { - action: () => { - if (Runtime.isDesktopApp() && status === FeatureStatus.ENABLED) { - // if we are in a desktop env, we just warn the wrapper that we need to reload. It then decide what should be done - amplify.publish(WebAppEvents.LIFECYCLE.RESTART); - } + return { + htmlMessage: + status === FeatureStatus.ENABLED + ? t('featureConfigChangeModalDownloadPathEnabled') + : status === FeatureStatus.DISABLED + ? t('featureConfigChangeModalDownloadPathDisabled') + : t('featureConfigChangeModalDownloadPathChanged'), + title: 'featureConfigChangeModalDownloadPathHeadline', + primaryAction: { + action: () => { + if (Runtime.isDesktopApp() && status !== FeatureStatus.DISABLED) { + amplify.publish(WebAppEvents.LIFECYCLE.RESTART); + } + }, }, - }, - }; + }; + } + return undefined; }, [FEATURE_KEY.SELF_DELETING_MESSAGES]: (oldConfig, newConfig) => { if (!oldConfig || !('config' in oldConfig) || !newConfig || !('config' in newConfig)) { diff --git a/yarn.lock b/yarn.lock index 3e04d321260..ce2a582cac0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5118,20 +5118,20 @@ __metadata: languageName: node linkType: hard -"@wireapp/core-crypto@npm:1.0.0-rc.32": - version: 1.0.0-rc.32 - resolution: "@wireapp/core-crypto@npm:1.0.0-rc.32" - checksum: b8d5c8b28308e276419fd8528cee1ca2eb55f74e47d1ea7ca24054e6a13dd76a4f8fb761bf0697848942a06f412a6ee22491bb14eae30954bf09e2433f9181b6 +"@wireapp/core-crypto@npm:1.0.0-rc.33": + version: 1.0.0-rc.33 + resolution: "@wireapp/core-crypto@npm:1.0.0-rc.33" + checksum: 44b88f4b7a1ec2ce9c13ce48329c53193c91833f13345abf28e3bfcf51f41ab510187d1192dcf76293c23b9fa4167e67ee1bd084a9739f2dc598ca7987258d27 languageName: node linkType: hard -"@wireapp/core@npm:43.9.1": - version: 43.9.1 - resolution: "@wireapp/core@npm:43.9.1" +"@wireapp/core@npm:43.11.2": + version: 43.11.2 + resolution: "@wireapp/core@npm:43.11.2" dependencies: "@wireapp/api-client": ^26.10.1 "@wireapp/commons": ^5.2.4 - "@wireapp/core-crypto": 1.0.0-rc.32 + "@wireapp/core-crypto": 1.0.0-rc.33 "@wireapp/cryptobox": 12.8.0 "@wireapp/promise-queue": ^2.2.9 "@wireapp/protocol-messaging": 1.44.0 @@ -5147,7 +5147,7 @@ __metadata: long: ^5.2.0 uuidjs: 4.2.13 zod: 3.22.4 - checksum: ad69c04e17c82c01f96fda511c47a335c7972c475ac755042097f2c4e95f97846f8416795666be3eff4f4b4e5caa33022890472a1e99cdfbf89b5f7df928acde + checksum: 685d047c9dd296e528ae8500e6b82c1ad6aa07762fd6ae549672028e8f9741cc88c3dbab15959b68d3b0fc47870daab7ba30d3b2127c15d256409551165df002 languageName: node linkType: hard @@ -17846,7 +17846,7 @@ __metadata: "@wireapp/avs": 9.6.9 "@wireapp/commons": 5.2.4 "@wireapp/copy-config": 2.1.14 - "@wireapp/core": 43.9.1 + "@wireapp/core": 43.11.2 "@wireapp/eslint-config": 3.0.5 "@wireapp/prettier-config": 0.6.3 "@wireapp/react-ui-kit": 9.12.8