diff --git a/package.json b/package.json index 412c6371cdb..756e2f3fe4f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@peculiar/x509": "1.9.6", "@wireapp/avs": "9.5.13", "@wireapp/commons": "5.2.4", - "@wireapp/core": "43.5.0", + "@wireapp/core": "43.5.1", "@wireapp/react-ui-kit": "9.12.5", "@wireapp/store-engine-dexie": "2.1.7", "@wireapp/webapp-events": "0.18.3", diff --git a/src/script/service/CoreSingleton.ts b/src/script/service/CoreSingleton.ts index e0b26eed1ed..66c8b8a4a65 100644 --- a/src/script/service/CoreSingleton.ts +++ b/src/script/service/CoreSingleton.ts @@ -17,7 +17,6 @@ * */ -import {generateSecretKey as generateKey} from '@wireapp/core/lib/secretStore/secretKeyGenerator'; import {container, singleton} from 'tsyringe'; import {Account} from '@wireapp/core'; @@ -26,54 +25,34 @@ import {supportsMLS} from 'Util/util'; import {APIClient} from './APIClientSingleton'; import {createStorageEngine, DatabaseTypes} from './StoreEngineProvider'; +import {SystemCrypto, wrapSystemCrypto} from './utils/systemCryptoWrapper'; import {Config} from '../Config'; import {isE2EIEnabled} from '../E2EIdentity'; declare global { interface Window { - systemCrypto?: - | { - encrypt: (value: Uint8Array) => Promise; - decrypt: (payload: Uint8Array) => Promise; - version: undefined; - } - | { - encrypt: (value: string) => Promise; - decrypt: (payload: Uint8Array) => Promise; - version: 1; - }; + systemCrypto?: SystemCrypto; } } -const generateSecretKey = async (storeName: string, keyId: string, keySize: number) => { - return generateKey({ - keyId, - keySize, - dbName: `secrets-${storeName}`, - /* - * When in an electron context, the window.systemCrypto will be populated by the renderer process. - * We then give those crypto primitives to the key generator that will use them to encrypt secrets. - * When in a browser context, then this systemCrypto will be undefined and the key generator will then use it's internal encryption system - */ - systemCrypto: window.systemCrypto, - }); -}; - @singleton() export class Core extends Account { constructor(apiClient = container.resolve(APIClient)) { const enableCoreCrypto = supportsMLS() || Config.getConfig().FEATURE.USE_CORE_CRYPTO; super(apiClient, { - createStore: async storeName => { - const key = Config.getConfig().FEATURE.ENABLE_ENCRYPTION_AT_REST - ? await generateSecretKey(storeName, 'db-key', 32) - : undefined; + createStore: async (storeName, key) => { return createStorageEngine(storeName, DatabaseTypes.PERMANENT, { - key: key?.key, + key: Config.getConfig().FEATURE.ENABLE_ENCRYPTION_AT_REST ? key : undefined, }); }, + /* + * When in an electron context, the window.systemCrypto will be populated by the renderer process. + * We then give those crypto primitives to the key generator that will use them to encrypt secrets. + * When in a browser context, then this systemCrypto will be undefined and the key generator will then use it's internal encryption system + */ + systemCrypto: window.systemCrypto ? wrapSystemCrypto(window.systemCrypto) : undefined, coreCryptoConfig: enableCoreCrypto ? { wasmFilePath: '/min/core-crypto.wasm', @@ -84,8 +63,6 @@ export class Core extends Account { useE2EI: isE2EIEnabled(), } : undefined, - - generateSecretKey, } : undefined, diff --git a/src/script/service/utils/systemCryptoWrapper.test.ts b/src/script/service/utils/systemCryptoWrapper.test.ts new file mode 100644 index 00000000000..d71f76bf6f3 --- /dev/null +++ b/src/script/service/utils/systemCryptoWrapper.test.ts @@ -0,0 +1,85 @@ +/* + * 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'; + +import {SystemCrypto, wrapSystemCrypto} from './systemCryptoWrapper'; + +const systemCryptos = { + v0: { + encrypt: async (value: Uint8Array) => { + return value; + }, + decrypt: async (value: Uint8Array) => { + return value; + }, + version: undefined, + } as SystemCrypto, + + v01: { + encrypt: async (value: Uint8Array) => { + return Encoder.toBase64(value).asBytes; + }, + decrypt: async (value: Uint8Array) => { + return Decoder.fromBase64(Array.from(value.values())).asBytes; + }, + version: undefined, + } as SystemCrypto, + + v1: { + encrypt: async (value: string) => { + const encoder = new TextEncoder(); + return encoder.encode(value); + }, + decrypt: async (value: Uint8Array) => { + const decoder = new TextDecoder(); + return decoder.decode(value); + }, + version: 1, + } as SystemCrypto, +} as const; + +describe('systemCryptoWrapper', () => { + it.each(Object.entries(systemCryptos))( + 'generates and store a secret key encrypted using system crypto (%s)', + async (_name, systemCrypto) => { + const {encrypt, decrypt} = wrapSystemCrypto(systemCrypto); + + const value = new Uint8Array([1, 2, 3, 4]); + const encrypted = await encrypt(value); + const decrypted = await decrypt(encrypted); + + expect(value).toEqual(decrypted); + }, + ); + + it.each([['v01 > v1', systemCryptos.v01, systemCryptos.v1]])( + 'handles migration from old system crypto (%s)', + async (_name, crypto1, crypto2) => { + const wrap1 = wrapSystemCrypto(crypto1); + const wrap2 = wrapSystemCrypto(crypto2); + + const value = new Uint8Array([1, 2, 3, 4]); + const encrypted = await wrap1.encrypt(value); + const decrypted = await wrap2.decrypt(encrypted); + + expect(value).toEqual(decrypted); + }, + ); +}); diff --git a/src/script/service/utils/systemCryptoWrapper.ts b/src/script/service/utils/systemCryptoWrapper.ts new file mode 100644 index 00000000000..467c3c4bcc2 --- /dev/null +++ b/src/script/service/utils/systemCryptoWrapper.ts @@ -0,0 +1,62 @@ +/* + * 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'; + +type SystemCryptoV0 = { + encrypt: (value: Uint8Array) => Promise; + decrypt: (payload: Uint8Array) => Promise; + version: undefined; +}; +type SystemCryptoV1 = { + encrypt: (value: string) => Promise; + decrypt: (payload: Uint8Array) => Promise; + version: 1; +}; + +export type SystemCrypto = SystemCryptoV0 | SystemCryptoV1; + +export function wrapSystemCrypto(baseCrypto: SystemCrypto) { + const isBase64 = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/; + return { + encrypt: (value: Uint8Array) => { + if (baseCrypto.version === 1) { + const strValue = Encoder.toBase64(value).asString; + return (baseCrypto as SystemCryptoV1).encrypt(strValue); + } + // In previous versions of the systemCrypto (prior to February 2023), encrypt took a uint8Array + return (baseCrypto as SystemCryptoV0).encrypt(value); + }, + + decrypt: async (value: Uint8Array) => { + if (typeof baseCrypto.version === 'undefined') { + // In previous versions of the systemCrypto (prior to February 2023), the decrypt function returned a Uint8Array + return (baseCrypto as SystemCryptoV0).decrypt(value); + } + const decrypted = await (baseCrypto as SystemCryptoV1).decrypt(value); + if (isBase64.test(decrypted)) { + return Decoder.fromBase64(decrypted).asBytes; + } + // Between June 2022 and October 2022, the systemCrypto returned a string encoded in UTF-8 + const encoder = new TextEncoder(); + + return encoder.encode(decrypted); + }, + }; +} diff --git a/yarn.lock b/yarn.lock index 769b547ff46..0576a3f4122 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4966,14 +4966,14 @@ __metadata: languageName: node linkType: hard -"@wireapp/api-client@npm:^26.8.1": - version: 26.8.1 - resolution: "@wireapp/api-client@npm:26.8.1" +"@wireapp/api-client@npm:^26.8.2": + version: 26.8.2 + resolution: "@wireapp/api-client@npm:26.8.2" dependencies: "@wireapp/commons": ^5.2.4 "@wireapp/priority-queue": ^2.1.4 "@wireapp/protocol-messaging": 1.44.0 - axios: 1.6.3 + axios: 1.6.5 axios-retry: 4.0.0 exponential-backoff: 3.1.1 http-status-codes: 2.3.0 @@ -4984,7 +4984,7 @@ __metadata: tough-cookie: 4.1.3 ws: 8.16.0 zod: 3.22.4 - checksum: dc2b560c9cdb229b0a7ed8fad1126cb5949c5e100aa217d0e25f75c3bea745f81fc774e89fa5138bc46a5464b8cf80d14509829ca10e938b6421b33549500487 + checksum: c65b2de7758f4dbe720d170b0d979ac98a9d795e11273a3be553ff147b393e2593c9d420fc1d705f3f0fbce5809cac90d747a94406f5aea1b6912f01bff08567 languageName: node linkType: hard @@ -5037,11 +5037,11 @@ __metadata: languageName: node linkType: hard -"@wireapp/core@npm:43.5.0": - version: 43.5.0 - resolution: "@wireapp/core@npm:43.5.0" +"@wireapp/core@npm:43.5.1": + version: 43.5.1 + resolution: "@wireapp/core@npm:43.5.1" dependencies: - "@wireapp/api-client": ^26.8.1 + "@wireapp/api-client": ^26.8.2 "@wireapp/commons": ^5.2.4 "@wireapp/core-crypto": 1.0.0-rc.21 "@wireapp/cryptobox": 12.8.0 @@ -5049,7 +5049,7 @@ __metadata: "@wireapp/protocol-messaging": 1.44.0 "@wireapp/store-engine": 5.1.5 "@wireapp/store-engine-dexie": ^2.1.7 - axios: 1.6.3 + axios: 1.6.5 bazinga64: ^6.3.4 deepmerge-ts: 5.1.0 hash.js: 1.1.7 @@ -5059,7 +5059,7 @@ __metadata: long: ^5.2.0 uuidjs: 4.2.13 zod: 3.22.4 - checksum: af9ec4d14ca2df4bb5ced1cee5ec7efd6cf855c6ec0eb05615c7f7ca55725bcc61b65a6ccc8ae916a73818d60fd48de51418f9e25444b38041f19b5ea1aea4c4 + checksum: c0c80ac0d9014453067c90338101c5492abfda058a848ff7203448fc3d0c0cc489cc66588642acf9be2e478ad2672b0c593286e24d414ca04c7d452dac83bcff languageName: node linkType: hard @@ -5892,6 +5892,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:1.6.5": + version: 1.6.5 + resolution: "axios@npm:1.6.5" + dependencies: + follow-redirects: ^1.15.4 + form-data: ^4.0.0 + proxy-from-env: ^1.1.0 + checksum: e28d67b2d9134cb4608c44d8068b0678cfdccc652742e619006f27264a30c7aba13b2cd19c6f1f52ae195b5232734925928fb192d5c85feea7edd2f273df206d + languageName: node + linkType: hard + "axobject-query@npm:^3.1.1": version: 3.2.1 resolution: "axobject-query@npm:3.2.1" @@ -8981,6 +8992,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.15.4": + version: 1.15.4 + resolution: "follow-redirects@npm:1.15.4" + peerDependenciesMeta: + debug: + optional: true + checksum: e178d1deff8b23d5d24ec3f7a94cde6e47d74d0dc649c35fc9857041267c12ec5d44650a0c5597ef83056ada9ea6ca0c30e7c4f97dbf07d035086be9e6a5b7b6 + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -17748,7 +17769,7 @@ __metadata: "@wireapp/avs": 9.5.13 "@wireapp/commons": 5.2.4 "@wireapp/copy-config": 2.1.13 - "@wireapp/core": 43.5.0 + "@wireapp/core": 43.5.1 "@wireapp/eslint-config": 3.0.4 "@wireapp/prettier-config": 0.6.3 "@wireapp/react-ui-kit": 9.12.5