Skip to content

Commit

Permalink
Merge branch 'dev' of https://github.com/wireapp/wire-webapp into imp…
Browse files Browse the repository at this point in the history
…rovements/certificate-renewal
  • Loading branch information
arjita-mitra committed Jan 16, 2024
2 parents d3a3040 + e58d79b commit 4abde5f
Show file tree
Hide file tree
Showing 9 changed files with 318 additions and 193 deletions.
32 changes: 16 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
{
"dependencies": {
"@datadog/browser-logs": "^5.6.0",
"@datadog/browser-rum": "^5.6.0",
"@datadog/browser-logs": "^5.7.0",
"@datadog/browser-rum": "^5.7.0",
"@emotion/react": "11.11.3",
"@lexical/history": "0.12.5",
"@lexical/react": "0.12.5",
"@peculiar/x509": "1.9.6",
"@wireapp/avs": "9.5.13",
"@wireapp/commons": "5.2.4",
"@wireapp/core": "43.5.4",
"@wireapp/react-ui-kit": "9.12.5",
"@wireapp/react-ui-kit": "9.12.6",
"@wireapp/store-engine-dexie": "2.1.7",
"@wireapp/webapp-events": "0.18.3",
"@wireapp/webapp-events": "0.20.1",
"amplify": "https://github.com/wireapp/amplify#head=master",
"beautiful-react-hooks": "^5.0.1",
"classnames": "2.5.1",
"copy-webpack-plugin": "11.0.0",
"core-js": "3.35.0",
"countly-sdk-web": "23.12.4",
"date-fns": "3.1.0",
"date-fns": "3.2.0",
"dexie-batch": "0.4.3",
"dexie-encrypted": "2.0.0",
"emoji-picker-react": "4.6.5",
"emoji-picker-react": "4.6.10",
"highlight.js": "11.9.0",
"http-status-codes": "2.3.0",
"jimp": "0.22.10",
Expand All @@ -43,8 +43,8 @@
"react-error-boundary": "4.0.12",
"react-intl": "6.5.5",
"react-redux": "8.1.3",
"react-router": "6.21.1",
"react-router-dom": "6.21.1",
"react-router": "6.21.2",
"react-router-dom": "6.21.2",
"react-transition-group": "4.4.5",
"redux": "4.2.1",
"redux-logdown": "1.0.4",
Expand All @@ -61,7 +61,7 @@
"@babel/core": "7.23.7",
"@babel/eslint-parser": "7.23.3",
"@babel/plugin-proposal-decorators": "7.23.7",
"@babel/preset-env": "7.23.7",
"@babel/preset-env": "7.23.8",
"@babel/preset-react": "7.23.3",
"@babel/preset-typescript": "7.23.3",
"@emotion/eslint-plugin": "^11.11.0",
Expand All @@ -82,7 +82,7 @@
"@types/linkify-it": "3.0.5",
"@types/loadable__component": "^5",
"@types/markdown-it": "13.0.7",
"@types/node": "^20.10.7",
"@types/node": "^20.11.0",
"@types/open-graph": "0.2.5",
"@types/platform": "1.3.6",
"@types/react": "18.2.46",
Expand All @@ -91,26 +91,26 @@
"@types/react-transition-group": "4.4.10",
"@types/redux-mock-store": "1.0.6",
"@types/seedrandom": "^3",
"@types/sinon": "17.0.2",
"@types/sinon": "17.0.3",
"@types/speakingurl": "13.0.6",
"@types/underscore": "1.11.15",
"@types/webpack-env": "1.18.4",
"@wireapp/copy-config": "2.1.13",
"@wireapp/eslint-config": "3.0.4",
"@wireapp/copy-config": "2.1.14",
"@wireapp/eslint-config": "3.0.5",
"@wireapp/prettier-config": "0.6.3",
"@wireapp/store-engine": "^5.1.4",
"archiver": "^6.0.1",
"autoprefixer": "^10.4.16",
"babel-loader": "9.1.3",
"babel-plugin-transform-import-meta": "^2.2.1",
"cross-env": "7.0.3",
"css-loader": "^6.8.1",
"css-loader": "^6.9.0",
"cssnano": "6.0.3",
"dexie": "3.2.4",
"dotenv": "16.3.1",
"dpdm": "3.14.0",
"eslint": "^8.56.0",
"eslint-plugin-prettier": "^5.1.2",
"eslint-plugin-prettier": "^5.1.3",
"fake-indexeddb": "5.0.2",
"generate-changelog": "1.8.0",
"html-webpack-plugin": "^5.6.0",
Expand Down Expand Up @@ -141,7 +141,7 @@
"seedrandom": "^3.0.5",
"simple-git": "3.22.0",
"sinon": "17.0.1",
"style-loader": "^3.3.3",
"style-loader": "^3.3.4",
"stylelint": "^16",
"stylelint-config-idiomatic-order": "10.0.0",
"svg-inline-loader": "0.8.2",
Expand Down
2 changes: 1 addition & 1 deletion src/script/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const config = {
ALLOWED_IMAGE_TYPES: ['image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/png'],

/** Which min and max version of the backend api do we support */
SUPPORTED_API_RANGE: [1, env.ENABLE_DEV_BACKEND_API ? Infinity : 4],
SUPPORTED_API_RANGE: [1, env.ENABLE_DEV_BACKEND_API ? Infinity : 5],

/** DataDog client api keys acces */
dataDog: {
Expand Down
1 change: 1 addition & 0 deletions src/script/E2EIdentity/E2EIdentityEnrollment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ describe('E2EIHandler', () => {
.spyOn(container.resolve(UserState), 'self')
.mockReturnValue({name: () => 'John Doe', username: () => 'johndoe'});
jest.spyOn(container.resolve(Core), 'enrollE2EI').mockResolvedValue(true);
container.resolve(Core).key = new Uint8Array();
});

it('should create instance with valid params', async () => {
Expand Down
27 changes: 16 additions & 11 deletions src/script/E2EIdentity/E2EIdentityEnrollment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
private readonly userState = container.resolve(UserState);
private config?: EnrollmentConfig;
private currentStep: E2EIHandlerStep | null = E2EIHandlerStep.UNINITIALIZED;
private oidcService: OIDCService | null = null;
private oidcService?: OIDCService;

private get coreE2EIService() {
const e2eiService = this.core.service?.e2eIdentity;
Expand All @@ -93,6 +93,14 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
return e2eiService;
}

private createOIDCService() {
const key = this.core.key;
if (!key) {
throw new Error('encryption key not set');
}
return new OIDCService(key);
}

/**
* Get the singleton instance of GracePeriodTimer or create a new one
* For the first time, params are required to create the instance
Expand Down Expand Up @@ -190,7 +198,7 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
* Renew the certificate without user action
*/
private async renewCertificate(): Promise<void> {
this.oidcService = new OIDCService();
this.oidcService = this.createOIDCService();
try {
// Use the oidc service to get the user data via silent authentication (refresh token)
const userData = await this.oidcService.handleSilentAuthentication();
Expand Down Expand Up @@ -237,7 +245,7 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
private async storeRedirectTargetAndRedirect(targetURL: string): Promise<void> {
// store the target url in the persistent oidc service store, since the oidc service will be destroyed after the redirect
OIDCServiceStore.store.targetURL(targetURL);
this.oidcService = new OIDCService();
this.oidcService = this.createOIDCService();
await this.oidcService.authenticate();
}

Expand All @@ -248,7 +256,7 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
// Remove the url parameters of the failed enrolment
removeUrlParameters();
// Clear the oidc service progress
this.oidcService = new OIDCService();
this.oidcService = this.createOIDCService();
await this.oidcService.clearProgress(includeOidcServiceUserData);
// Clear the e2e identity progress
this.coreE2EIService.clearAllProgress();
Expand All @@ -262,23 +270,20 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
// Notify user about E2EI enrolment in progress
this.currentStep = E2EIHandlerStep.ENROLL;
const isCertificateRenewal = await hasActiveCertificate();
this.showLoadingMessage(isCertificateRenewal);
let oAuthIdToken: string | undefined;
this.showLoadingMessage();

if (!userData) {
// 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()) {
// The redirect-url which is needed inside the OIDCService is stored in the OIDCServiceStore previously
this.oidcService = new OIDCService();
const userData = await this.oidcService.handleAuthentication();
this.oidcService = this.createOIDCService();
userData = await this.oidcService.handleAuthentication();
if (!userData) {
throw new Error('Received no user data from OIDC service');
}
oAuthIdToken = userData?.id_token;
}
} else {
oAuthIdToken = userData?.id_token;
}
const oAuthIdToken = userData?.id_token;

const displayName = this.userState.self()?.name();
const handle = this.userState.self()?.username();
Expand Down
17 changes: 9 additions & 8 deletions src/script/E2EIdentity/OIDCService/OIDCService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ import {UserManager, User, UserManagerSettings, WebStorageStateStore} from 'oidc
import {clearKeysStartingWith} from 'Util/localStorage';
import {Logger, getLogger} from 'Util/Logger';

import {EncryptedStorage} from './OauthEncryptedStore';
import {OIDCServiceStore} from './OIDCServiceStorage';

export class OIDCService {
private readonly userManager: UserManager;
private readonly logger: Logger;

constructor() {
constructor(secretKey: Uint8Array) {
// Get the targetURL from the OIDCServiceStore
// It has been set by the E2EIdentityEnrollment
const targetURL = OIDCServiceStore.get.targetURL();
Expand Down Expand Up @@ -66,8 +67,10 @@ export class OIDCService {
access_type: 'offline',
prompt: 'consent',
},
stateStore: new WebStorageStateStore({store: window.localStorage}),
userStore: new WebStorageStateStore({store: window.localStorage}),
stateStore: new WebStorageStateStore({store: window.sessionStorage}),
userStore: new WebStorageStateStore({
store: new EncryptedStorage(secretKey),
}),
};

this.userManager = new UserManager(dexioConfig);
Expand Down Expand Up @@ -101,14 +104,12 @@ export class OIDCService {
return this.userManager.clearStaleState();
}

public handleSilentAuthentication(): Promise<User | null> {
public async handleSilentAuthentication(): Promise<User | null> {
try {
return this.userManager.signinSilent().then(user => {
return user;
});
return this.userManager.signinSilent();
} catch (error) {
this.logger.log('Silent authentication with refresh token failed', error);
return Promise.resolve(null);
}
return null;
}
}
72 changes: 72 additions & 0 deletions src/script/E2EIdentity/OIDCService/OauthEncryptedStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Wire
* Copyright (C) 2024 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 {Encoder, Decoder} from 'bazinga64';

export class EncryptedStorage {
private encryptionKey: Promise<CryptoKey>;
length = Promise.resolve(0);

constructor(secretKey: Uint8Array) {
this.encryptionKey = crypto.subtle.importKey('raw', secretKey, 'AES-GCM', false, ['encrypt', 'decrypt']);
}

async setItem(key: string, value: string) {
try {
const encryptionKey = await this.encryptionKey;
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encryptedValue = await window.crypto.subtle.encrypt(
{name: 'AES-GCM', iv},
encryptionKey,
Encoder.toBase64(value).asBytes,
);
const base64Value = Encoder.toBase64(encryptedValue).asString;
window.localStorage.setItem(key, JSON.stringify({value: base64Value, iv: Array.from(iv)}));
} catch (error) {
console.error(error);
throw error;
}
}

async getItem(key: string) {
const entry = localStorage.getItem(key);
if (entry) {
const encryptionKey = await this.encryptionKey;
const {value, iv} = JSON.parse(entry);
const decrypted = await crypto.subtle.decrypt(
{name: 'AES-GCM', iv: new Uint8Array(iv)},
encryptionKey,
Decoder.fromBase64(value).asBytes,
);
return Decoder.fromBase64(Array.from(new Uint8Array(decrypted))).asString;
}
return null;
}

async removeItem(key: string) {
localStorage.removeItem(key);
}

async clear() {
localStorage.clear();
}
async key(): Promise<null> {
return null;
}
}
20 changes: 18 additions & 2 deletions src/script/conversation/ConversationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1735,7 +1735,7 @@ export class ConversationRepository {
const otherUserId = this.getUserIdOf1to1Conversation(conversation);

if (!otherUserId) {
this.logger.error(`Could not find other user id in 1:1 conversation ${conversation.id}`);
this.logger.warn(`Could not find other user id in 1:1 conversation ${conversation.id}`);
return conversation;
}

Expand Down Expand Up @@ -1940,7 +1940,23 @@ export class ConversationRepository {
conversation => conversation.is1to1() && !conversation.connection(),
);

for (const conversation of team1To1Conversations) {
// Sort conversations so mls 1:1 conversations are initialised first
const sortedConverstions = [...team1To1Conversations].sort((a, b) => {
const aIsMLSConversation = isMLSConversation(a);
const bIsMLSConversation = isMLSConversation(b);

if (aIsMLSConversation && !bIsMLSConversation) {
return -1;
}

if (!aIsMLSConversation && bIsMLSConversation) {
return 1;
}

return 0;
});

for (const conversation of sortedConverstions) {
try {
await this.init1to1Conversation(conversation);
} catch (error) {
Expand Down
3 changes: 3 additions & 0 deletions src/script/service/CoreSingleton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ declare global {

@singleton()
export class Core extends Account {
public key?: Uint8Array;

constructor(apiClient = container.resolve(APIClient)) {
const enableCoreCrypto = supportsMLS() || Config.getConfig().FEATURE.USE_CORE_CRYPTO;
super(apiClient, {
createStore: async (storeName, key) => {
this.key = key;
return createStorageEngine(storeName, DatabaseTypes.PERMANENT, {
key: Config.getConfig().FEATURE.ENABLE_ENCRYPTION_AT_REST ? key : undefined,
});
Expand Down
Loading

0 comments on commit 4abde5f

Please sign in to comment.