diff --git a/apps/browser/config/base.json b/apps/browser/config/base.json index 81b11cd38b7..348f00d1f36 100644 --- a/apps/browser/config/base.json +++ b/apps/browser/config/base.json @@ -1,6 +1,7 @@ { "dev_flags": {}, "flags": { - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/browser/config/development.json b/apps/browser/config/development.json index 972812a9c59..eafd0ffd878 100644 --- a/apps/browser/config/development.json +++ b/apps/browser/config/development.json @@ -6,6 +6,7 @@ } }, "flags": { - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/browser/config/production.json b/apps/browser/config/production.json index b04d1531a2f..f57c3d9bc38 100644 --- a/apps/browser/config/production.json +++ b/apps/browser/config/production.json @@ -1,3 +1,5 @@ { - "flags": {} + "flags": { + "enableCipherKeyEncryption": false + } } diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 1cb006fa3a2..73bdc2cd16f 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -464,7 +464,7 @@ export default class NotificationBackground { private async getDecryptedCipherById(cipherId: string) { const cipher = await this.cipherService.get(cipherId); if (cipher != null && cipher.type === CipherType.Login) { - return await cipher.decrypt(); + return await cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher)); } return null; } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3aba0c77679..7a9863f3a70 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -322,23 +322,6 @@ export default class MainBackground { ); this.searchService = new SearchService(this.logService, this.i18nService); - this.cipherService = new CipherService( - this.cryptoService, - this.settingsService, - this.apiService, - this.i18nService, - this.searchService, - this.stateService, - this.encryptService, - this.cipherFileUploadService - ); - this.folderService = new BrowserFolderService( - this.cryptoService, - this.i18nService, - this.cipherService, - this.stateService - ); - this.folderApiService = new FolderApiService(this.folderService, this.apiService); this.collectionService = new CollectionService( this.cryptoService, this.i18nService, @@ -362,14 +345,6 @@ export default class MainBackground { this.cryptoFunctionService, logoutCallback ); - this.vaultFilterService = new VaultFilterService( - this.stateService, - this.organizationService, - this.folderService, - this.cipherService, - this.collectionService, - this.policyService - ); this.passwordStrengthService = new PasswordStrengthService(); @@ -436,6 +411,36 @@ export default class MainBackground { this.userVerificationApiService ); + this.configApiService = new ConfigApiService(this.apiService, this.authService); + + this.configService = new BrowserConfigService( + this.stateService, + this.configApiService, + this.authService, + this.environmentService, + this.logService, + true + ); + + this.cipherService = new CipherService( + this.cryptoService, + this.settingsService, + this.apiService, + this.i18nService, + this.searchService, + this.stateService, + this.encryptService, + this.cipherFileUploadService, + this.configService + ); + this.folderService = new BrowserFolderService( + this.cryptoService, + this.i18nService, + this.cipherService, + this.stateService + ); + this.folderApiService = new FolderApiService(this.folderService, this.apiService); + this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( this.cryptoService, this.tokenService, @@ -444,6 +449,15 @@ export default class MainBackground { this.userVerificationService ); + this.vaultFilterService = new VaultFilterService( + this.stateService, + this.organizationService, + this.folderService, + this.cipherService, + this.collectionService, + this.policyService + ); + this.vaultTimeoutService = new VaultTimeoutService( this.cipherService, this.folderService, @@ -533,16 +547,6 @@ export default class MainBackground { this.messagingService ); - this.configApiService = new ConfigApiService(this.apiService, this.authService); - - this.configService = new BrowserConfigService( - this.stateService, - this.configApiService, - this.authService, - this.environmentService, - this.logService, - true - ); this.browserPopoutWindowService = new BrowserPopoutWindowService(); const systemUtilsServiceReloadCallback = () => { diff --git a/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts b/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts new file mode 100644 index 00000000000..e9e1b86488a --- /dev/null +++ b/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts @@ -0,0 +1,32 @@ +import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; +import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; + +import { + authServiceFactory, + AuthServiceInitOptions, +} from "../../../auth/background/service-factories/auth-service.factory"; + +import { apiServiceFactory, ApiServiceInitOptions } from "./api-service.factory"; +import { FactoryOptions, CachedServices, factory } from "./factory-options"; + +type ConfigApiServiceFactoyOptions = FactoryOptions; + +export type ConfigApiServiceInitOptions = ConfigApiServiceFactoyOptions & + ApiServiceInitOptions & + AuthServiceInitOptions; + +export function configApiServiceFactory( + cache: { configApiService?: ConfigApiServiceAbstraction } & CachedServices, + opts: ConfigApiServiceInitOptions +): Promise { + return factory( + cache, + "configApiService", + opts, + async () => + new ConfigApiService( + await apiServiceFactory(cache, opts), + await authServiceFactory(cache, opts) + ) + ); +} diff --git a/apps/browser/src/platform/background/service-factories/config-service.factory.ts b/apps/browser/src/platform/background/service-factories/config-service.factory.ts new file mode 100644 index 00000000000..a5dc6016c65 --- /dev/null +++ b/apps/browser/src/platform/background/service-factories/config-service.factory.ts @@ -0,0 +1,49 @@ +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; + +import { + authServiceFactory, + AuthServiceInitOptions, +} from "../../../auth/background/service-factories/auth-service.factory"; + +import { configApiServiceFactory, ConfigApiServiceInitOptions } from "./config-api.service.factory"; +import { + environmentServiceFactory, + EnvironmentServiceInitOptions, +} from "./environment-service.factory"; +import { FactoryOptions, CachedServices, factory } from "./factory-options"; +import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; +import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; + +type ConfigServiceFactoryOptions = FactoryOptions & { + configServiceOptions?: { + subscribe?: boolean; + }; +}; + +export type ConfigServiceInitOptions = ConfigServiceFactoryOptions & + StateServiceInitOptions & + ConfigApiServiceInitOptions & + AuthServiceInitOptions & + EnvironmentServiceInitOptions & + LogServiceInitOptions; + +export function configServiceFactory( + cache: { configService?: ConfigServiceAbstraction } & CachedServices, + opts: ConfigServiceInitOptions +): Promise { + return factory( + cache, + "configService", + opts, + async () => + new ConfigService( + await stateServiceFactory(cache, opts), + await configApiServiceFactory(cache, opts), + await authServiceFactory(cache, opts), + await environmentServiceFactory(cache, opts), + await logServiceFactory(cache, opts), + opts.configServiceOptions?.subscribe ?? true + ) + ); +} diff --git a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts index 006daadc1af..46062ebc9cc 100644 --- a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts +++ b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts @@ -18,8 +18,12 @@ import { ApiServiceInitOptions, } from "../../../platform/background/service-factories/api-service.factory"; import { - CryptoServiceInitOptions, + configServiceFactory, + ConfigServiceInitOptions, +} from "../../../platform/background/service-factories/config-service.factory"; +import { cryptoServiceFactory, + CryptoServiceInitOptions, } from "../../../platform/background/service-factories/crypto-service.factory"; import { EncryptServiceInitOptions, @@ -49,7 +53,8 @@ export type CipherServiceInitOptions = CipherServiceFactoryOptions & I18nServiceInitOptions & SearchServiceInitOptions & StateServiceInitOptions & - EncryptServiceInitOptions; + EncryptServiceInitOptions & + ConfigServiceInitOptions; export function cipherServiceFactory( cache: { cipherService?: AbstractCipherService } & CachedServices, @@ -68,7 +73,8 @@ export function cipherServiceFactory( await searchServiceFactory(cache, opts), await stateServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), - await cipherFileUploadServiceFactory(cache, opts) + await cipherFileUploadServiceFactory(cache, opts), + await configServiceFactory(cache, opts) ) ); } diff --git a/apps/cli/config/development.json b/apps/cli/config/development.json index b04d1531a2f..f57c3d9bc38 100644 --- a/apps/cli/config/development.json +++ b/apps/cli/config/development.json @@ -1,3 +1,5 @@ { - "flags": {} + "flags": { + "enableCipherKeyEncryption": false + } } diff --git a/apps/cli/config/production.json b/apps/cli/config/production.json index b04d1531a2f..f57c3d9bc38 100644 --- a/apps/cli/config/production.json +++ b/apps/cli/config/production.json @@ -1,3 +1,5 @@ { - "flags": {} + "flags": { + "enableCipherKeyEncryption": false + } } diff --git a/apps/cli/src/admin-console/commands/share.command.ts b/apps/cli/src/admin-console/commands/share.command.ts index 68bd8a18056..88fe256af18 100644 --- a/apps/cli/src/admin-console/commands/share.command.ts +++ b/apps/cli/src/admin-console/commands/share.command.ts @@ -45,11 +45,15 @@ export class ShareCommand { if (cipher.organizationId != null) { return Response.badRequest("This item already belongs to an organization."); } - const cipherView = await cipher.decrypt(); + const cipherView = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); try { await this.cipherService.shareWithServer(cipherView, organizationId, req); const updatedCipher = await this.cipherService.get(cipher.id); - const decCipher = await updatedCipher.decrypt(); + const decCipher = await updatedCipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher) + ); const res = new CipherResponse(decCipher); return Response.success(res); } catch (e) { diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 42ba158ee72..ca501690193 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -25,11 +25,13 @@ import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.ser import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { ClientType, KeySuffixOptions, LogLevelType } from "@bitwarden/common/enums"; +import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service"; +import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; @@ -75,6 +77,7 @@ import { } from "@bitwarden/importer"; import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service"; +import { CliConfigService } from "./platform/services/cli-config.service"; import { CliPlatformUtilsService } from "./platform/services/cli-platform-utils.service"; import { ConsoleLogService } from "./platform/services/console-log.service"; import { I18nService } from "./platform/services/i18n.service"; @@ -147,6 +150,8 @@ export class Main { devicesApiService: DevicesApiServiceAbstraction; deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; authRequestCryptoService: AuthRequestCryptoServiceAbstraction; + configApiService: ConfigApiServiceAbstraction; + configService: CliConfigService; constructor() { let p = null; @@ -252,28 +257,8 @@ export class Main { this.searchService = new SearchService(this.logService, this.i18nService); - this.cipherService = new CipherService( - this.cryptoService, - this.settingsService, - this.apiService, - this.i18nService, - this.searchService, - this.stateService, - this.encryptService, - this.cipherFileUploadService - ); - this.broadcasterService = new BroadcasterService(); - this.folderService = new FolderService( - this.cryptoService, - this.i18nService, - this.cipherService, - this.stateService - ); - - this.folderApiService = new FolderApiService(this.folderService, this.apiService); - this.collectionService = new CollectionService( this.cryptoService, this.i18nService, @@ -349,6 +334,38 @@ export class Main { this.authRequestCryptoService ); + this.configApiService = new ConfigApiService(this.apiService, this.authService); + + this.configService = new CliConfigService( + this.stateService, + this.configApiService, + this.authService, + this.environmentService, + this.logService, + true + ); + + this.cipherService = new CipherService( + this.cryptoService, + this.settingsService, + this.apiService, + this.i18nService, + this.searchService, + this.stateService, + this.encryptService, + this.cipherFileUploadService, + this.configService + ); + + this.folderService = new FolderService( + this.cryptoService, + this.i18nService, + this.cipherService, + this.stateService + ); + + this.folderApiService = new FolderApiService(this.folderService, this.apiService); + const lockedCallback = async (userId?: string) => await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto); @@ -472,6 +489,7 @@ export class Main { const locale = await this.stateService.getLocale(); await this.i18nService.init(locale); this.twoFactorService.init(); + this.configService.init(); const installedVersion = await this.stateService.getInstalledVersion(); const currentVersion = await this.platformUtilsService.getApplicationVersion(); diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 960b0999146..60e5ee7936e 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -77,7 +77,9 @@ export class EditCommand { return Response.notFound(); } - let cipherView = await cipher.decrypt(); + let cipherView = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); if (cipherView.isDeleted) { return Response.badRequest("You may not edit a deleted item. Use the restore command first."); } @@ -86,7 +88,9 @@ export class EditCommand { try { await this.cipherService.updateWithServer(encCipher); const updatedCipher = await this.cipherService.get(cipher.id); - const decCipher = await updatedCipher.decrypt(); + const decCipher = await updatedCipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher) + ); const res = new CipherResponse(decCipher); return Response.success(res); } catch (e) { @@ -109,7 +113,9 @@ export class EditCommand { try { await this.cipherService.saveCollectionsWithServer(cipher); const updatedCipher = await this.cipherService.get(cipher.id); - const decCipher = await updatedCipher.decrypt(); + const decCipher = await updatedCipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher) + ); const res = new CipherResponse(decCipher); return Response.success(res); } catch (e) { diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 265e24c9d4e..4a84f1efefd 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -103,7 +103,9 @@ export class GetCommand extends DownloadCommand { if (Utils.isGuid(id)) { const cipher = await this.cipherService.get(id); if (cipher != null) { - decCipher = await cipher.decrypt(); + decCipher = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); } } else if (id.trim() !== "") { let ciphers = await this.cipherService.getAllDecrypted(); diff --git a/apps/cli/src/platform/services/cli-config.service.ts b/apps/cli/src/platform/services/cli-config.service.ts new file mode 100644 index 00000000000..6faa1b12e8a --- /dev/null +++ b/apps/cli/src/platform/services/cli-config.service.ts @@ -0,0 +1,9 @@ +import { NEVER } from "rxjs"; + +import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; + +export class CliConfigService extends ConfigService { + // The rxjs timer uses setTimeout/setInterval under the hood, which prevents the node process from exiting + // when the command is finished. Cli should never be alive long enough to use the timer, so we disable it. + protected refreshTimer$ = NEVER; +} diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index 3f4d53e9737..01217dbc307 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -80,7 +80,9 @@ export class CreateCommand { try { await this.cipherService.createWithServer(cipher); const newCipher = await this.cipherService.get(cipher.id); - const decCipher = await newCipher.decrypt(); + const decCipher = await newCipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(newCipher) + ); const res = new CipherResponse(decCipher); return Response.success(res); } catch (e) { @@ -141,7 +143,9 @@ export class CreateCommand { new Uint8Array(fileBuf).buffer ); const updatedCipher = await this.cipherService.get(cipher.id); - const decCipher = await updatedCipher.decrypt(); + const decCipher = await updatedCipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher) + ); return Response.success(new CipherResponse(decCipher)); } catch (e) { return Response.error(e); diff --git a/apps/desktop/config/base.json b/apps/desktop/config/base.json index 3ae895a19cf..cb408a87d87 100644 --- a/apps/desktop/config/base.json +++ b/apps/desktop/config/base.json @@ -1,6 +1,7 @@ { "dev_flags": {}, "flags": { - "multithreadDecryption": false + "multithreadDecryption": false, + "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/config/development.json b/apps/desktop/config/development.json index b587e9ecfb9..d2b10738124 100644 --- a/apps/desktop/config/development.json +++ b/apps/desktop/config/development.json @@ -1,6 +1,7 @@ { "devFlags": {}, "flags": { - "showDDGSetting": true + "showDDGSetting": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/config/production.json b/apps/desktop/config/production.json index 56f19341304..39b78094d0f 100644 --- a/apps/desktop/config/production.json +++ b/apps/desktop/config/production.json @@ -1,5 +1,6 @@ { "flags": { - "showDDGSetting": true + "showDDGSetting": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 48e3f0e7bd0..d3a13313e9e 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1,4 +1,4 @@ -{ +{ "bitwarden": { "message": "Bitwarden" }, diff --git a/apps/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts index 1889bfc7451..1ce52cfecc0 100644 --- a/apps/desktop/src/services/encrypted-message-handler.service.ts +++ b/apps/desktop/src/services/encrypted-message-handler.service.ts @@ -190,7 +190,9 @@ export class EncryptedMessageHandlerService { if (cipher === null) { return { status: "failure" }; } - const cipherView = await cipher.decrypt(); + const cipherView = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); cipherView.name = credentialUpdatePayload.name; cipherView.login.password = credentialUpdatePayload.password; cipherView.login.username = credentialUpdatePayload.userName; diff --git a/apps/web/config/base.json b/apps/web/config/base.json index ed0bc0a850d..a377298c637 100644 --- a/apps/web/config/base.json +++ b/apps/web/config/base.json @@ -12,6 +12,7 @@ }, "flags": { "secretsManager": false, - "showPasswordless": false + "showPasswordless": false, + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/cloud.json b/apps/web/config/cloud.json index 45269d18c63..6e5c65af1d3 100644 --- a/apps/web/config/cloud.json +++ b/apps/web/config/cloud.json @@ -18,6 +18,7 @@ }, "flags": { "secretsManager": true, - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/development.json b/apps/web/config/development.json index 7aeffe55d0d..e3107f8788b 100644 --- a/apps/web/config/development.json +++ b/apps/web/config/development.json @@ -11,6 +11,7 @@ }, "flags": { "secretsManager": true, - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/euprd.json b/apps/web/config/euprd.json index 19864cdab66..4b6c9fa9098 100644 --- a/apps/web/config/euprd.json +++ b/apps/web/config/euprd.json @@ -12,6 +12,7 @@ }, "flags": { "secretsManager": true, - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/qa.json b/apps/web/config/qa.json index 6f514079010..be9911cc578 100644 --- a/apps/web/config/qa.json +++ b/apps/web/config/qa.json @@ -12,6 +12,7 @@ }, "flags": { "secretsManager": true, - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/selfhosted.json b/apps/web/config/selfhosted.json index 8ffbc0f9e5a..6b290baa6b5 100644 --- a/apps/web/config/selfhosted.json +++ b/apps/web/config/selfhosted.json @@ -8,6 +8,7 @@ }, "flags": { "secretsManager": false, - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index 46d8440d489..bea55856a4a 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -110,7 +110,7 @@ export class AddEditComponent extends BaseAddEditComponent { if (!this.organization.canEditAnyCollection) { return super.encryptCipher(); } - return this.cipherService.encrypt(this.cipher, null, this.originalCipher); + return this.cipherService.encrypt(this.cipher, null, null, this.originalCipher); } protected async deleteCipher() { diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 2fa5b5d3d3a..945a79d1d78 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -37,7 +37,9 @@ export class CollectionsComponent implements OnInit { async load() { this.cipherDomain = await this.loadCipher(); this.collectionIds = this.loadCipherCollections(); - this.cipher = await this.cipherDomain.decrypt(); + this.cipher = await this.cipherDomain.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain) + ); this.collections = await this.loadCollections(); this.collections.forEach((c) => ((c as any).checked = false)); diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 73792f91fef..6d0764be9b3 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -66,7 +66,9 @@ export class ShareComponent implements OnInit, OnDestroy { }); const cipherDomain = await this.cipherService.get(this.cipherId); - this.cipher = await cipherDomain.decrypt(); + this.cipher = await cipherDomain.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain) + ); this.filterCollections(); } @@ -94,7 +96,9 @@ export class ShareComponent implements OnInit, OnDestroy { } const cipherDomain = await this.cipherService.get(this.cipherId); - const cipherView = await cipherDomain.decrypt(); + const cipherView = await cipherDomain.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain) + ); const orgs = await firstValueFrom(this.organizations$); const orgName = orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization"); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 02f6278fec7..5c19d0fe771 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -271,7 +271,8 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; searchService: SearchServiceAbstraction, stateService: StateServiceAbstraction, encryptService: EncryptService, - fileUploadService: CipherFileUploadServiceAbstraction + fileUploadService: CipherFileUploadServiceAbstraction, + configService: ConfigServiceAbstraction ) => new CipherService( cryptoService, @@ -281,7 +282,8 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; searchService, stateService, encryptService, - fileUploadService + fileUploadService, + configService ), deps: [ CryptoServiceAbstraction, @@ -292,6 +294,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; StateServiceAbstraction, EncryptService, CipherFileUploadServiceAbstraction, + ConfigServiceAbstraction, ], }, { diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 377fe88b63f..3aca51e44a8 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -225,7 +225,9 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.cipher == null) { if (this.editMode) { const cipher = await this.loadCipher(); - this.cipher = await cipher.decrypt(); + this.cipher = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); // Adjust Cipher Name if Cloning if (this.cloneMode) { diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index c7a8dd2ee27..e1974e2b7d5 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -73,7 +73,9 @@ export class AttachmentsComponent implements OnInit { try { this.formPromise = this.saveCipherAttachment(files[0]); this.cipherDomain = await this.formPromise; - this.cipher = await this.cipherDomain.decrypt(); + this.cipher = await this.cipherDomain.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain) + ); this.platformUtilsService.showToast("success", null, this.i18nService.t("attachmentSaved")); this.onUploadedAttachment.emit(); } catch (e) { @@ -179,7 +181,9 @@ export class AttachmentsComponent implements OnInit { protected async init() { this.cipherDomain = await this.loadCipher(); - this.cipher = await this.cipherDomain.decrypt(); + this.cipher = await this.cipherDomain.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain) + ); const canAccessPremium = await this.stateService.getCanAccessPremium(); this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; @@ -229,7 +233,9 @@ export class AttachmentsComponent implements OnInit { decBuf, admin ); - this.cipher = await this.cipherDomain.decrypt(); + this.cipher = await this.cipherDomain.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain) + ); // 3. Delete old this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); diff --git a/libs/angular/src/vault/components/password-history.component.ts b/libs/angular/src/vault/components/password-history.component.ts index 8fbea9d8fb9..3a25b6930a8 100644 --- a/libs/angular/src/vault/components/password-history.component.ts +++ b/libs/angular/src/vault/components/password-history.component.ts @@ -33,7 +33,9 @@ export class PasswordHistoryComponent implements OnInit { protected async init() { const cipher = await this.cipherService.get(this.cipherId); - const decCipher = await cipher.decrypt(); + const decCipher = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory; } } diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 18ea5474fd6..40523afd850 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -113,7 +113,9 @@ export class ViewComponent implements OnDestroy, OnInit { this.cleanUp(); const cipher = await this.cipherService.get(this.cipherId); - this.cipher = await cipher.decrypt(); + this.cipher = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); this.canAccessPremium = await this.stateService.getCanAccessPremium(); this.showPremiumRequiredTotp = this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; diff --git a/libs/common/src/models/export/cipher.export.ts b/libs/common/src/models/export/cipher.export.ts index 342cf59fd86..3ae6c9757dd 100644 --- a/libs/common/src/models/export/cipher.export.ts +++ b/libs/common/src/models/export/cipher.export.ts @@ -88,6 +88,7 @@ export class CipherExport { domain.notes = req.notes != null ? new EncString(req.notes) : null; domain.favorite = req.favorite; domain.reprompt = req.reprompt ?? CipherRepromptType.None; + domain.key = req.key != null ? new EncString(req.key) : null; if (req.fields != null) { domain.fields = req.fields.map((f) => FieldExport.toDomain(f)); @@ -135,6 +136,7 @@ export class CipherExport { revisionDate: Date = null; creationDate: Date = null; deletedDate: Date = null; + key: string; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print build(o: CipherView | CipherDomain) { @@ -149,6 +151,7 @@ export class CipherExport { } else { this.name = o.name?.encryptedString; this.notes = o.notes?.encryptedString; + this.key = o.key?.encryptedString; } this.favorite = o.favorite; diff --git a/libs/common/src/platform/abstractions/config/config.service.abstraction.ts b/libs/common/src/platform/abstractions/config/config.service.abstraction.ts index 59f87b0fa29..67f7f2f4ce7 100644 --- a/libs/common/src/platform/abstractions/config/config.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/config/config.service.abstraction.ts @@ -1,4 +1,5 @@ import { Observable } from "rxjs"; +import { SemVer } from "semver"; import { FeatureFlag } from "../../../enums/feature-flag.enum"; import { Region } from "../environment.service"; @@ -16,6 +17,9 @@ export abstract class ConfigServiceAbstraction { key: FeatureFlag, defaultValue?: T ) => Promise; + checkServerMeetsVersionRequirement$: ( + minimumRequiredServerVersion: SemVer + ) => Observable; /** * Force ConfigService to fetch an updated config from the server and emit it from serverConfig$ diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index a868484bd04..8f1d9c48662 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -6,6 +6,7 @@ import { KeySuffixOptions, KdfType, HashPurpose } from "../../enums"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncString } from "../models/domain/enc-string"; import { + CipherKey, MasterKey, OrgKey, PinKey, @@ -372,6 +373,11 @@ export abstract class CryptoService { */ rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise; randomNumber: (min: number, max: number) => Promise; + /** + * Generates a new cipher key + * @returns A new cipher key + */ + makeCipherKey: () => Promise; /** * Initialize all necessary crypto keys needed for a new account. diff --git a/libs/common/src/platform/interfaces/decryptable.interface.ts b/libs/common/src/platform/interfaces/decryptable.interface.ts index ae5e8ebbf82..35895bfd6ff 100644 --- a/libs/common/src/platform/interfaces/decryptable.interface.ts +++ b/libs/common/src/platform/interfaces/decryptable.interface.ts @@ -8,5 +8,5 @@ import { InitializerMetadata } from "./initializer-metadata.interface"; * @example Cipher implements Decryptable */ export interface Decryptable extends InitializerMetadata { - decrypt: (key?: SymmetricCryptoKey) => Promise; + decrypt: (key: SymmetricCryptoKey) => Promise; } diff --git a/libs/common/src/platform/misc/flags.ts b/libs/common/src/platform/misc/flags.ts index c1bdafaa0f3..53609505675 100644 --- a/libs/common/src/platform/misc/flags.ts +++ b/libs/common/src/platform/misc/flags.ts @@ -3,6 +3,7 @@ export type SharedFlags = { multithreadDecryption: boolean; showPasswordless?: boolean; + enableCipherKeyEncryption?: boolean; }; // required to avoid linting errors when there are no flags diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index 818155ef98d..8f3b46f077c 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -83,3 +83,4 @@ export type MasterKey = Opaque; export type PinKey = Opaque; export type OrgKey = Opaque; export type ProviderKey = Opaque; +export type CipherKey = Opaque; diff --git a/libs/common/src/platform/services/config/config.service.ts b/libs/common/src/platform/services/config/config.service.ts index 008f5a764d3..45db66af0eb 100644 --- a/libs/common/src/platform/services/config/config.service.ts +++ b/libs/common/src/platform/services/config/config.service.ts @@ -10,6 +10,7 @@ import { merge, timer, } from "rxjs"; +import { SemVer } from "semver"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; @@ -25,10 +26,13 @@ import { ServerConfigData } from "../../models/data/server-config.data"; const ONE_HOUR_IN_MILLISECONDS = 1000 * 3600; export class ConfigService implements ConfigServiceAbstraction { + private inited = false; + protected _serverConfig = new ReplaySubject(1); serverConfig$ = this._serverConfig.asObservable(); + private _forceFetchConfig = new Subject(); - private inited = false; + protected refreshTimer$ = timer(ONE_HOUR_IN_MILLISECONDS, ONE_HOUR_IN_MILLISECONDS); // after 1 hour, then every hour cloudRegion$ = this.serverConfig$.pipe( map((config) => config?.environment?.cloudRegion ?? Region.US) @@ -62,7 +66,7 @@ export class ConfigService implements ConfigServiceAbstraction { // If you need to fetch a new config when an event occurs, add an observable that emits on that event here merge( - timer(ONE_HOUR_IN_MILLISECONDS, ONE_HOUR_IN_MILLISECONDS), // after 1 hour, then every hour + this.refreshTimer$, // an overridable interval this.environmentService.urls, // when environment URLs change (including when app is started) this._forceFetchConfig // manual ) @@ -103,4 +107,21 @@ export class ConfigService implements ConfigServiceAbstraction { await this.stateService.setServerConfig(data); this.environmentService.setCloudWebVaultUrl(data.environment?.cloudRegion); } + + /** + * Verifies whether the server version meets the minimum required version + * @param minimumRequiredServerVersion The minimum version required + * @returns True if the server version is greater than or equal to the minimum required version + */ + checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) { + return this.serverConfig$.pipe( + map((serverConfig) => { + if (serverConfig == null) { + return false; + } + const serverVersion = new SemVer(serverConfig.version); + return serverVersion.compare(minimumRequiredServerVersion) >= 0; + }) + ); + } } diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 3b4090ef344..0ea9acc53da 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -27,6 +27,7 @@ import { EFFLongWordList } from "../misc/wordlist"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncString } from "../models/domain/enc-string"; import { + CipherKey, MasterKey, OrgKey, PinKey, @@ -596,6 +597,11 @@ export class CryptoService implements CryptoServiceAbstraction { return new SymmetricCryptoKey(sendKey); } + async makeCipherKey(): Promise { + const randomBytes = await this.cryptoFunctionService.aesGenerateKey(512); + return new SymmetricCryptoKey(randomBytes) as CipherKey; + } + async clearKeys(userId?: string): Promise { await this.clearUserKey(true, userId); await this.clearMasterKeyHash(userId); diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index e328c5bd491..404a58abb1a 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -11,7 +11,8 @@ export abstract class CipherService { clearCache: (userId?: string) => Promise; encrypt: ( model: CipherView, - key?: SymmetricCryptoKey, + keyForEncryption?: SymmetricCryptoKey, + keyForCipherKeyDecryption?: SymmetricCryptoKey, originalCipher?: Cipher ) => Promise; encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise; @@ -81,4 +82,5 @@ export abstract class CipherService { organizationId?: string, asAdmin?: boolean ) => Promise; + getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise; } diff --git a/libs/common/src/vault/models/data/cipher.data.ts b/libs/common/src/vault/models/data/cipher.data.ts index 2f83ee194b4..1452ffe7ee0 100644 --- a/libs/common/src/vault/models/data/cipher.data.ts +++ b/libs/common/src/vault/models/data/cipher.data.ts @@ -33,6 +33,7 @@ export class CipherData { creationDate: string; deletedDate: string; reprompt: CipherRepromptType; + key: string; constructor(response?: CipherResponse, collectionIds?: string[]) { if (response == null) { @@ -54,6 +55,7 @@ export class CipherData { this.creationDate = response.creationDate; this.deletedDate = response.deletedDate; this.reprompt = response.reprompt; + this.key = response.key; switch (this.type) { case CipherType.Login: diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index d5c141487a2..6234fe7029b 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -2,10 +2,14 @@ import { Substitute, Arg } from "@fluffy-spoon/substitute"; import { Jsonify } from "type-fest"; -import { mockEnc, mockFromJson } from "../../../../spec"; +import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; import { FieldType, SecureNoteType, UriMatchType } from "../../../enums"; +import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; +import { ContainerService } from "../../../platform/services/container.service"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; +import { CipherService } from "../../abstractions/cipher.service"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherType } from "../../enums/cipher-type"; import { CipherData } from "../../models/data/cipher.data"; @@ -47,6 +51,7 @@ describe("Cipher DTO", () => { attachments: null, fields: null, passwordHistory: null, + key: null, }); }); @@ -69,6 +74,7 @@ describe("Cipher DTO", () => { creationDate: "2022-01-01T12:00:00.000Z", deletedDate: null, reprompt: CipherRepromptType.None, + key: "EncryptedString", login: { uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }], username: "EncryptedString", @@ -136,6 +142,7 @@ describe("Cipher DTO", () => { creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: null, reprompt: 0, + key: { encryptedString: "EncryptedString", encryptionType: 0 }, login: { passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"), autofillOnPageLoad: false, @@ -206,6 +213,7 @@ describe("Cipher DTO", () => { cipher.creationDate = new Date("2022-01-01T12:00:00.000Z"); cipher.deletedDate = null; cipher.reprompt = CipherRepromptType.None; + cipher.key = mockEnc("EncKey"); const loginView = new LoginView(); loginView.username = "username"; @@ -215,7 +223,20 @@ describe("Cipher DTO", () => { login.decrypt(Arg.any(), Arg.any()).resolves(loginView); cipher.login = login; - const cipherView = await cipher.decrypt(); + const cryptoService = Substitute.for(); + const encryptService = Substitute.for(); + const cipherService = Substitute.for(); + + encryptService.decryptToBytes(Arg.any(), Arg.any()).resolves(makeStaticByteArray(64)); + + (window as any).bitwardenContainerService = new ContainerService( + cryptoService, + encryptService + ); + + const cipherView = await cipher.decrypt( + await cipherService.getKeyForCipherKeyDecryption(cipher) + ); expect(cipherView).toMatchObject({ id: "id", @@ -261,6 +282,7 @@ describe("Cipher DTO", () => { creationDate: "2022-01-01T12:00:00.000Z", deletedDate: null, reprompt: CipherRepromptType.None, + key: "EncKey", secureNote: { type: SecureNoteType.Generic, }, @@ -292,6 +314,7 @@ describe("Cipher DTO", () => { attachments: null, fields: null, passwordHistory: null, + key: { encryptedString: "EncKey", encryptionType: 0 }, }); }); @@ -318,8 +341,22 @@ describe("Cipher DTO", () => { cipher.reprompt = CipherRepromptType.None; cipher.secureNote = new SecureNote(); cipher.secureNote.type = SecureNoteType.Generic; + cipher.key = mockEnc("EncKey"); - const cipherView = await cipher.decrypt(); + const cryptoService = Substitute.for(); + const encryptService = Substitute.for(); + const cipherService = Substitute.for(); + + encryptService.decryptToBytes(Arg.any(), Arg.any()).resolves(makeStaticByteArray(64)); + + (window as any).bitwardenContainerService = new ContainerService( + cryptoService, + encryptService + ); + + const cipherView = await cipher.decrypt( + await cipherService.getKeyForCipherKeyDecryption(cipher) + ); expect(cipherView).toMatchObject({ id: "id", @@ -373,6 +410,7 @@ describe("Cipher DTO", () => { expYear: "EncryptedString", code: "EncryptedString", }, + key: "EncKey", }; }); @@ -408,6 +446,7 @@ describe("Cipher DTO", () => { attachments: null, fields: null, passwordHistory: null, + key: { encryptedString: "EncKey", encryptionType: 0 }, }); }); @@ -432,6 +471,7 @@ describe("Cipher DTO", () => { cipher.creationDate = new Date("2022-01-01T12:00:00.000Z"); cipher.deletedDate = null; cipher.reprompt = CipherRepromptType.None; + cipher.key = mockEnc("EncKey"); const cardView = new CardView(); cardView.cardholderName = "cardholderName"; @@ -441,7 +481,20 @@ describe("Cipher DTO", () => { card.decrypt(Arg.any(), Arg.any()).resolves(cardView); cipher.card = card; - const cipherView = await cipher.decrypt(); + const cryptoService = Substitute.for(); + const encryptService = Substitute.for(); + const cipherService = Substitute.for(); + + encryptService.decryptToBytes(Arg.any(), Arg.any()).resolves(makeStaticByteArray(64)); + + (window as any).bitwardenContainerService = new ContainerService( + cryptoService, + encryptService + ); + + const cipherView = await cipher.decrypt( + await cipherService.getKeyForCipherKeyDecryption(cipher) + ); expect(cipherView).toMatchObject({ id: "id", @@ -487,6 +540,7 @@ describe("Cipher DTO", () => { creationDate: "2022-01-01T12:00:00.000Z", deletedDate: null, reprompt: CipherRepromptType.None, + key: "EncKey", identity: { title: "EncryptedString", firstName: "EncryptedString", @@ -554,6 +608,7 @@ describe("Cipher DTO", () => { attachments: null, fields: null, passwordHistory: null, + key: { encryptedString: "EncKey", encryptionType: 0 }, }); }); @@ -578,6 +633,7 @@ describe("Cipher DTO", () => { cipher.creationDate = new Date("2022-01-01T12:00:00.000Z"); cipher.deletedDate = null; cipher.reprompt = CipherRepromptType.None; + cipher.key = mockEnc("EncKey"); const identityView = new IdentityView(); identityView.firstName = "firstName"; @@ -587,7 +643,20 @@ describe("Cipher DTO", () => { identity.decrypt(Arg.any(), Arg.any()).resolves(identityView); cipher.identity = identity; - const cipherView = await cipher.decrypt(); + const cryptoService = Substitute.for(); + const encryptService = Substitute.for(); + const cipherService = Substitute.for(); + + encryptService.decryptToBytes(Arg.any(), Arg.any()).resolves(makeStaticByteArray(64)); + + (window as any).bitwardenContainerService = new ContainerService( + cryptoService, + encryptService + ); + + const cipherView = await cipher.decrypt( + await cipherService.getKeyForCipherKeyDecryption(cipher) + ); expect(cipherView).toMatchObject({ id: "id", diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index f85b6cb45c9..23349695a72 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -1,6 +1,7 @@ import { Jsonify } from "type-fest"; import { Decryptable } from "../../../platform/interfaces/decryptable.interface"; +import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -45,6 +46,7 @@ export class Cipher extends Domain implements Decryptable { creationDate: Date; deletedDate: Date; reprompt: CipherRepromptType; + key: EncString; constructor(obj?: CipherData, localData: LocalData = null) { super(); @@ -61,6 +63,7 @@ export class Cipher extends Domain implements Decryptable { folderId: null, name: null, notes: null, + key: null, }, ["id", "organizationId", "folderId"] ); @@ -117,9 +120,17 @@ export class Cipher extends Domain implements Decryptable { } } - async decrypt(encKey?: SymmetricCryptoKey): Promise { + // We are passing the organizationId into the EncString.decrypt() method here, but because the encKey will always be + // present and so the organizationId will not be used. + // We will refactor the EncString.decrypt() in https://bitwarden.atlassian.net/browse/PM-3762 to remove the dependency on the organizationId. + async decrypt(encKey: SymmetricCryptoKey): Promise { const model = new CipherView(this); + if (this.key != null) { + const encryptService = Utils.getContainerService().getEncryptService(); + encKey = new SymmetricCryptoKey(await encryptService.decryptToBytes(this.key, encKey)); + } + await this.decryptObj( model, { @@ -147,14 +158,12 @@ export class Cipher extends Domain implements Decryptable { break; } - const orgId = this.organizationId; - if (this.attachments != null && this.attachments.length > 0) { const attachments: any[] = []; await this.attachments.reduce((promise, attachment) => { return promise .then(() => { - return attachment.decrypt(orgId, encKey); + return attachment.decrypt(this.organizationId, encKey); }) .then((decAttachment) => { attachments.push(decAttachment); @@ -168,7 +177,7 @@ export class Cipher extends Domain implements Decryptable { await this.fields.reduce((promise, field) => { return promise .then(() => { - return field.decrypt(orgId, encKey); + return field.decrypt(this.organizationId, encKey); }) .then((decField) => { fields.push(decField); @@ -182,7 +191,7 @@ export class Cipher extends Domain implements Decryptable { await this.passwordHistory.reduce((promise, ph) => { return promise .then(() => { - return ph.decrypt(orgId, encKey); + return ph.decrypt(this.organizationId, encKey); }) .then((decPh) => { passwordHistory.push(decPh); @@ -209,6 +218,7 @@ export class Cipher extends Domain implements Decryptable { c.creationDate = this.creationDate != null ? this.creationDate.toISOString() : null; c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : null; c.reprompt = this.reprompt; + c.key = this.key?.encryptedString; this.buildDataModel(this, c, { name: null, @@ -257,6 +267,7 @@ export class Cipher extends Domain implements Decryptable { const attachments = obj.attachments?.map((a: any) => Attachment.fromJSON(a)); const fields = obj.fields?.map((f: any) => Field.fromJSON(f)); const passwordHistory = obj.passwordHistory?.map((ph: any) => Password.fromJSON(ph)); + const key = EncString.fromJSON(obj.key); Object.assign(domain, obj, { name, @@ -266,6 +277,7 @@ export class Cipher extends Domain implements Decryptable { attachments, fields, passwordHistory, + key, }); switch (obj.type) { diff --git a/libs/common/src/vault/models/request/cipher.request.ts b/libs/common/src/vault/models/request/cipher.request.ts index cae48fb7af9..0f34200e79e 100644 --- a/libs/common/src/vault/models/request/cipher.request.ts +++ b/libs/common/src/vault/models/request/cipher.request.ts @@ -29,6 +29,7 @@ export class CipherRequest { attachments2: { [id: string]: AttachmentRequest }; lastKnownRevisionDate: Date; reprompt: CipherRepromptType; + key: string; constructor(cipher: Cipher) { this.type = cipher.type; @@ -39,6 +40,7 @@ export class CipherRequest { this.favorite = cipher.favorite; this.lastKnownRevisionDate = cipher.revisionDate; this.reprompt = cipher.reprompt; + this.key = cipher.key?.encryptedString; switch (this.type) { case CipherType.Login: diff --git a/libs/common/src/vault/models/response/cipher.response.ts b/libs/common/src/vault/models/response/cipher.response.ts index 71e43373775..8bc8a37874e 100644 --- a/libs/common/src/vault/models/response/cipher.response.ts +++ b/libs/common/src/vault/models/response/cipher.response.ts @@ -32,6 +32,7 @@ export class CipherResponse extends BaseResponse { creationDate: string; deletedDate: string; reprompt: CipherRepromptType; + key: string; constructor(response: any) { super(response); @@ -90,5 +91,6 @@ export class CipherResponse extends BaseResponse { } this.reprompt = this.getResponseProperty("Reprompt") || CipherRepromptType.None; + this.key = this.getResponseProperty("Key") || null; } } diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 4df3d202ff2..2c9adce553b 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,15 +1,24 @@ -// eslint-disable-next-line no-restricted-imports import { mock, mockReset } from "jest-mock-extended"; +import { of } from "rxjs"; +import { makeStaticByteArray } from "../../../spec/utils"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { SettingsService } from "../../abstractions/settings.service"; import { UriMatchType, FieldType } from "../../enums"; +import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { StateService } from "../../platform/abstractions/state.service"; -import { OrgKey, SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; +import { EncString } from "../../platform/models/domain/enc-string"; +import { + CipherKey, + OrgKey, + SymmetricCryptoKey, +} from "../../platform/models/domain/symmetric-crypto-key"; +import { ContainerService } from "../../platform/services/container.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { CipherRepromptType } from "../enums/cipher-reprompt-type"; import { CipherType } from "../enums/cipher-type"; @@ -18,9 +27,13 @@ import { Cipher } from "../models/domain/cipher"; import { CipherCreateRequest } from "../models/request/cipher-create.request"; import { CipherPartialRequest } from "../models/request/cipher-partial.request"; import { CipherRequest } from "../models/request/cipher.request"; +import { CipherView } from "../models/view/cipher.view"; import { CipherService } from "./cipher.service"; +const ENCRYPTED_TEXT = "This data has been encrypted"; +const ENCRYPTED_BYTES = mock(); + const cipherData: CipherData = { id: "id", organizationId: "orgId", @@ -35,6 +48,7 @@ const cipherData: CipherData = { notes: "EncryptedString", creationDate: "2022-01-01T12:00:00.000Z", deletedDate: null, + key: "EncKey", reprompt: CipherRepromptType.None, login: { uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }], @@ -88,6 +102,7 @@ describe("Cipher Service", () => { const i18nService = mock(); const searchService = mock(); const encryptService = mock(); + const configService = mock(); let cipherService: CipherService; let cipherObj: Cipher; @@ -101,6 +116,12 @@ describe("Cipher Service", () => { mockReset(i18nService); mockReset(searchService); mockReset(encryptService); + mockReset(configService); + + encryptService.encryptToBytes.mockReturnValue(Promise.resolve(ENCRYPTED_BYTES)); + encryptService.encrypt.mockReturnValue(Promise.resolve(new EncString(ENCRYPTED_TEXT))); + + (window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService); cipherService = new CipherService( cryptoService, @@ -110,7 +131,8 @@ describe("Cipher Service", () => { searchService, stateService, encryptService, - cipherFileUploadService + cipherFileUploadService, + configService ); cipherObj = new Cipher(cipherData); @@ -125,6 +147,12 @@ describe("Cipher Service", () => { cryptoService.makeDataEncKey.mockReturnValue( Promise.resolve(new SymmetricCryptoKey(new Uint8Array(32))) ); + + configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(false)); + process.env.FLAGS = JSON.stringify({ + enableCipherKeyEncryption: false, + }); + const spy = jest.spyOn(cipherFileUploadService, "upload"); await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData); @@ -216,4 +244,68 @@ describe("Cipher Service", () => { expect(spy).toHaveBeenCalledWith(cipherObj.id, expectedObj); }); }); + + describe("encrypt", () => { + let cipherView: CipherView; + + beforeEach(() => { + cipherView = new CipherView(); + cipherView.type = CipherType.Login; + + encryptService.decryptToBytes.mockReturnValue(Promise.resolve(makeStaticByteArray(64))); + configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(true)); + cryptoService.makeCipherKey.mockReturnValue( + Promise.resolve(new SymmetricCryptoKey(makeStaticByteArray(64)) as CipherKey) + ); + cryptoService.encrypt.mockReturnValue(Promise.resolve(new EncString(ENCRYPTED_TEXT))); + }); + + describe("cipher.key", () => { + it("is null when enableCipherKeyEncryption flag is false", async () => { + process.env.FLAGS = JSON.stringify({ + enableCipherKeyEncryption: false, + }); + + const cipher = await cipherService.encrypt(cipherView); + + expect(cipher.key).toBeNull(); + }); + + it("is defined when enableCipherKeyEncryption flag is true", async () => { + process.env.FLAGS = JSON.stringify({ + enableCipherKeyEncryption: true, + }); + + const cipher = await cipherService.encrypt(cipherView); + + expect(cipher.key).toBeDefined(); + }); + }); + + describe("encryptWithCipherKey", () => { + beforeEach(() => { + jest.spyOn(cipherService, "encryptCipherWithCipherKey"); + }); + + it("is not called when enableCipherKeyEncryption is false", async () => { + process.env.FLAGS = JSON.stringify({ + enableCipherKeyEncryption: false, + }); + + await cipherService.encrypt(cipherView); + + expect(cipherService["encryptCipherWithCipherKey"]).not.toHaveBeenCalled(); + }); + + it("is called when enableCipherKeyEncryption is true", async () => { + process.env.FLAGS = JSON.stringify({ + enableCipherKeyEncryption: true, + }); + + await cipherService.encrypt(cipherView); + + expect(cipherService["encryptCipherWithCipherKey"]).toHaveBeenCalled(); + }); + }); + }); }); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 03c70cca84e..b9bbc3e291e 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,13 +1,18 @@ +import { firstValueFrom } from "rxjs"; +import { SemVer } from "semver"; + import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { SettingsService } from "../../abstractions/settings.service"; import { FieldType, UriMatchType } from "../../enums"; import { ErrorResponse } from "../../models/response/error.response"; import { View } from "../../models/view/view"; +import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { StateService } from "../../platform/abstractions/state.service"; +import { flagEnabled } from "../../platform/misc/flags"; import { sequentialize } from "../../platform/misc/sequentialize"; import { Utils } from "../../platform/misc/utils"; import Domain from "../../platform/models/domain/domain-base"; @@ -47,6 +52,8 @@ import { CipherView } from "../models/view/cipher.view"; import { FieldView } from "../models/view/field.view"; import { PasswordHistoryView } from "../models/view/password-history.view"; +const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2023.9.1"); + export class CipherService implements CipherServiceAbstraction { private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache( this.sortCiphersByLastUsed @@ -60,7 +67,8 @@ export class CipherService implements CipherServiceAbstraction { private searchService: SearchService, private stateService: StateService, private encryptService: EncryptService, - private cipherFileUploadService: CipherFileUploadService + private cipherFileUploadService: CipherFileUploadService, + private configService: ConfigServiceAbstraction ) {} async getDecryptedCipherCache(): Promise { @@ -85,63 +93,18 @@ export class CipherService implements CipherServiceAbstraction { async encrypt( model: CipherView, - key?: SymmetricCryptoKey, + keyForEncryption?: SymmetricCryptoKey, + keyForCipherKeyDecryption?: SymmetricCryptoKey, originalCipher: Cipher = null ): Promise { - // Adjust password history if (model.id != null) { if (originalCipher == null) { originalCipher = await this.get(model.id); } if (originalCipher != null) { - const existingCipher = await originalCipher.decrypt(); - model.passwordHistory = existingCipher.passwordHistory || []; - if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) { - if ( - existingCipher.login.password != null && - existingCipher.login.password !== "" && - existingCipher.login.password !== model.login.password - ) { - const ph = new PasswordHistoryView(); - ph.password = existingCipher.login.password; - ph.lastUsedDate = model.login.passwordRevisionDate = new Date(); - model.passwordHistory.splice(0, 0, ph); - } else { - model.login.passwordRevisionDate = existingCipher.login.passwordRevisionDate; - } - } - if (existingCipher.hasFields) { - const existingHiddenFields = existingCipher.fields.filter( - (f) => - f.type === FieldType.Hidden && - f.name != null && - f.name !== "" && - f.value != null && - f.value !== "" - ); - const hiddenFields = - model.fields == null - ? [] - : model.fields.filter( - (f) => f.type === FieldType.Hidden && f.name != null && f.name !== "" - ); - existingHiddenFields.forEach((ef) => { - const matchedField = hiddenFields.find((f) => f.name === ef.name); - if (matchedField == null || matchedField.value !== ef.value) { - const ph = new PasswordHistoryView(); - ph.password = ef.name + ": " + ef.value; - ph.lastUsedDate = new Date(); - model.passwordHistory.splice(0, 0, ph); - } - }); - } - } - if (model.passwordHistory != null && model.passwordHistory.length === 0) { - model.passwordHistory = null; - } else if (model.passwordHistory != null && model.passwordHistory.length > 5) { - // only save last 5 history - model.passwordHistory = model.passwordHistory.slice(0, 5); + await this.updateModelfromExistingCipher(model, originalCipher); } + this.adjustPasswordHistoryLength(model); } const cipher = new Cipher(); @@ -155,35 +118,32 @@ export class CipherService implements CipherServiceAbstraction { cipher.reprompt = model.reprompt; cipher.edit = model.edit; - if (key == null && cipher.organizationId != null) { - key = await this.cryptoService.getOrgKey(cipher.organizationId); - if (key == null) { - throw new Error("Cannot encrypt cipher for organization. No key."); - } - } - await Promise.all([ - this.encryptObjProperty( + if (await this.getCipherKeyEncryptionEnabled()) { + cipher.key = originalCipher?.key ?? null; + const userOrOrgKey = await this.getKeyForCipherKeyDecryption(cipher); + // The keyForEncryption is only used for encrypting the cipher key, not the cipher itself, since cipher key encryption is enabled. + // If the caller has provided a key for cipher key encryption, use it. Otherwise, use the user or org key. + keyForEncryption ||= userOrOrgKey; + // If the caller has provided a key for cipher key decryption, use it. Otherwise, use the user or org key. + keyForCipherKeyDecryption ||= userOrOrgKey; + return this.encryptCipherWithCipherKey( model, cipher, - { - name: null, - notes: null, - }, - key - ), - this.encryptCipherData(cipher, model, key), - this.encryptFields(model.fields, key).then((fields) => { - cipher.fields = fields; - }), - this.encryptPasswordHistories(model.passwordHistory, key).then((ph) => { - cipher.passwordHistory = ph; - }), - this.encryptAttachments(model.attachments, key).then((attachments) => { - cipher.attachments = attachments; - }), - ]); - - return cipher; + keyForEncryption, + keyForCipherKeyDecryption + ); + } else { + if (keyForEncryption == null && cipher.organizationId != null) { + keyForEncryption = await this.cryptoService.getOrgKey(cipher.organizationId); + if (keyForEncryption == null) { + throw new Error("Cannot encrypt cipher for organization. No key."); + } + } + // We want to ensure that the cipher key is null if cipher key encryption is disabled + // so that decryption uses the proper key. + cipher.key = null; + return this.encryptCipher(model, cipher, keyForEncryption); + } } async encryptAttachments( @@ -579,7 +539,7 @@ export class CipherService implements CipherServiceAbstraction { cipher.organizationId = organizationId; cipher.collectionIds = collectionIds; - const encCipher = await this.encrypt(cipher); + const encCipher = await this.encryptSharedCipher(cipher); const request = new CipherShareRequest(encCipher); const response = await this.apiService.putShareCipher(cipher.id, request); const data = new CipherData(response, collectionIds); @@ -597,7 +557,7 @@ export class CipherService implements CipherServiceAbstraction { cipher.organizationId = organizationId; cipher.collectionIds = collectionIds; promises.push( - this.encrypt(cipher).then((c) => { + this.encryptSharedCipher(cipher).then((c) => { encCiphers.push(c); }) ); @@ -645,14 +605,29 @@ export class CipherService implements CipherServiceAbstraction { data: Uint8Array, admin = false ): Promise { - let encKey: UserKey | OrgKey; - encKey = await this.cryptoService.getOrgKey(cipher.organizationId); - encKey ||= await this.cryptoService.getUserKeyWithLegacySupport(); + const encKey = await this.getKeyForCipherKeyDecryption(cipher); + const cipherKeyEncryptionEnabled = await this.getCipherKeyEncryptionEnabled(); - const dataEncKey = await this.cryptoService.makeDataEncKey(encKey); + const cipherEncKey = + cipherKeyEncryptionEnabled && cipher.key != null + ? (new SymmetricCryptoKey( + await this.encryptService.decryptToBytes(cipher.key, encKey) + ) as UserKey) + : encKey; - const encFileName = await this.encryptService.encrypt(filename, encKey); - const encData = await this.encryptService.encryptToBytes(data, dataEncKey[0]); + //if cipher key encryption is disabled but the item has an individual key, + //then we rollback to using the user key as the main key of encryption of the item + //in order to keep item and it's attachments with the same encryption level + if (cipher.key != null && !cipherKeyEncryptionEnabled) { + const model = await cipher.decrypt(await this.getKeyForCipherKeyDecryption(cipher)); + cipher = await this.encrypt(model); + await this.updateWithServer(cipher); + } + + const encFileName = await this.encryptService.encrypt(filename, cipherEncKey); + + const dataEncKey = await this.cryptoService.makeDataEncKey(cipherEncKey); + const encData = await this.encryptService.encryptToBytes(new Uint8Array(data), dataEncKey[0]); const response = await this.cipherFileUploadService.upload( cipher, @@ -963,8 +938,80 @@ export class CipherService implements CipherServiceAbstraction { await this.restore(restores); } + async getKeyForCipherKeyDecryption(cipher: Cipher): Promise { + return ( + (await this.cryptoService.getOrgKey(cipher.organizationId)) || + ((await this.cryptoService.getUserKeyWithLegacySupport()) as UserKey) + ); + } + // Helpers + // In the case of a cipher that is being shared with an organization, we want to decrypt the + // cipher key with the user's key and then re-encrypt it with the organization's key. + private async encryptSharedCipher(model: CipherView): Promise { + const keyForCipherKeyDecryption = await this.cryptoService.getUserKeyWithLegacySupport(); + return await this.encrypt(model, null, keyForCipherKeyDecryption); + } + + private async updateModelfromExistingCipher( + model: CipherView, + originalCipher: Cipher + ): Promise { + const existingCipher = await originalCipher.decrypt( + await this.getKeyForCipherKeyDecryption(originalCipher) + ); + model.passwordHistory = existingCipher.passwordHistory || []; + if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) { + if ( + existingCipher.login.password != null && + existingCipher.login.password !== "" && + existingCipher.login.password !== model.login.password + ) { + const ph = new PasswordHistoryView(); + ph.password = existingCipher.login.password; + ph.lastUsedDate = model.login.passwordRevisionDate = new Date(); + model.passwordHistory.splice(0, 0, ph); + } else { + model.login.passwordRevisionDate = existingCipher.login.passwordRevisionDate; + } + } + if (existingCipher.hasFields) { + const existingHiddenFields = existingCipher.fields.filter( + (f) => + f.type === FieldType.Hidden && + f.name != null && + f.name !== "" && + f.value != null && + f.value !== "" + ); + const hiddenFields = + model.fields == null + ? [] + : model.fields.filter( + (f) => f.type === FieldType.Hidden && f.name != null && f.name !== "" + ); + existingHiddenFields.forEach((ef) => { + const matchedField = hiddenFields.find((f) => f.name === ef.name); + if (matchedField == null || matchedField.value !== ef.value) { + const ph = new PasswordHistoryView(); + ph.password = ef.name + ": " + ef.value; + ph.lastUsedDate = new Date(); + model.passwordHistory.splice(0, 0, ph); + } + }); + } + } + + private adjustPasswordHistoryLength(model: CipherView) { + if (model.passwordHistory != null && model.passwordHistory.length === 0) { + model.passwordHistory = null; + } else if (model.passwordHistory != null && model.passwordHistory.length > 5) { + // only save last 5 history + model.passwordHistory = model.passwordHistory.slice(0, 5); + } + } + private async shareAttachmentWithServer( attachmentView: AttachmentView, cipherId: string, @@ -1193,4 +1240,69 @@ export class CipherService implements CipherServiceAbstraction { private clearSortedCiphers() { this.sortedCiphersCache.clear(); } + + private async encryptCipher( + model: CipherView, + cipher: Cipher, + key: SymmetricCryptoKey + ): Promise { + await Promise.all([ + this.encryptObjProperty( + model, + cipher, + { + name: null, + notes: null, + }, + key + ), + this.encryptCipherData(cipher, model, key), + this.encryptFields(model.fields, key).then((fields) => { + cipher.fields = fields; + }), + this.encryptPasswordHistories(model.passwordHistory, key).then((ph) => { + cipher.passwordHistory = ph; + }), + this.encryptAttachments(model.attachments, key).then((attachments) => { + cipher.attachments = attachments; + }), + ]); + + return cipher; + } + + private async encryptCipherWithCipherKey( + model: CipherView, + cipher: Cipher, + keyForCipherKeyEncryption: SymmetricCryptoKey, + keyForCipherKeyDecryption: SymmetricCryptoKey + ): Promise { + // First, we get the key for cipher key encryption, in its decrypted form + let decryptedCipherKey: SymmetricCryptoKey; + if (cipher.key == null) { + decryptedCipherKey = await this.cryptoService.makeCipherKey(); + } else { + decryptedCipherKey = new SymmetricCryptoKey( + await this.encryptService.decryptToBytes(cipher.key, keyForCipherKeyDecryption) + ); + } + + // Then, we have to encrypt the cipher key with the proper key. + cipher.key = await this.encryptService.encrypt( + decryptedCipherKey.key, + keyForCipherKeyEncryption + ); + + // Finally, we can encrypt the cipher with the decrypted cipher key. + return this.encryptCipher(model, cipher, decryptedCipherKey); + } + + private async getCipherKeyEncryptionEnabled(): Promise { + return ( + flagEnabled("enableCipherKeyEncryption") && + (await firstValueFrom( + this.configService.checkServerMeetsVersionRequirement$(CIPHER_KEY_ENC_MIN_SERVER_VER) + )) + ); + } } diff --git a/libs/exporter/src/vault-export/services/vault-export.service.ts b/libs/exporter/src/vault-export/services/vault-export.service.ts index 8609ce0d1c8..c214646d9aa 100644 --- a/libs/exporter/src/vault-export/services/vault-export.service.ts +++ b/libs/exporter/src/vault-export/services/vault-export.service.ts @@ -258,12 +258,15 @@ export class VaultExportService implements VaultExportServiceAbstraction { if (exportData.ciphers != null && exportData.ciphers.length > 0) { exportData.ciphers .filter((c) => c.deletedDate === null) - .forEach((c) => { + .forEach(async (c) => { const cipher = new Cipher(new CipherData(c)); exportPromises.push( - cipher.decrypt().then((decCipher) => { - decCiphers.push(decCipher); - }) + this.cipherService + .getKeyForCipherKeyDecryption(cipher) + .then((key) => cipher.decrypt(key)) + .then((decCipher) => { + decCiphers.push(decCipher); + }) ); }); } diff --git a/libs/importer/spec/bitwarden-password-protected-importer.spec.ts b/libs/importer/spec/bitwarden-password-protected-importer.spec.ts index 0f8f6418628..5d24cef4140 100644 --- a/libs/importer/spec/bitwarden-password-protected-importer.spec.ts +++ b/libs/importer/spec/bitwarden-password-protected-importer.spec.ts @@ -4,6 +4,7 @@ import { KdfType } from "@bitwarden/common/enums"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { BitwardenPasswordProtectedImporter, @@ -17,6 +18,7 @@ describe("BitwardenPasswordProtectedImporter", () => { let importer: BitwardenPasswordProtectedImporter; let cryptoService: MockProxy; let i18nService: MockProxy; + let cipherService: MockProxy; const password = Utils.newGuid(); const promptForPassword_callback = async () => { return password; @@ -25,10 +27,12 @@ describe("BitwardenPasswordProtectedImporter", () => { beforeEach(() => { cryptoService = mock(); i18nService = mock(); + cipherService = mock(); importer = new BitwardenPasswordProtectedImporter( cryptoService, i18nService, + cipherService, promptForPassword_callback ); }); diff --git a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts index 5c281dc6b73..895b64e91ce 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts @@ -6,6 +6,7 @@ import { import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { @@ -25,7 +26,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { protected constructor( protected cryptoService: CryptoService, - protected i18nService: I18nService + protected i18nService: I18nService, + protected cipherService: CipherService ) { super(); } @@ -96,7 +98,9 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { }); } - const view = await cipher.decrypt(); + const view = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); this.cleanupCipher(view); this.result.ciphers.push(view); } diff --git a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts index a8c2a711a0b..49288e9dd8c 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts @@ -4,21 +4,24 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { BitwardenPasswordProtectedFileFormat } from "@bitwarden/exporter/vault-export/bitwarden-json-export-types"; import { ImportResult } from "../../models/import-result"; import { Importer } from "../importer"; import { BitwardenJsonImporter } from "./bitwarden-json-importer"; + export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter implements Importer { private key: SymmetricCryptoKey; constructor( cryptoService: CryptoService, i18nService: I18nService, + cipherService: CipherService, private promptForPassword_callback: () => Promise ) { - super(cryptoService, i18nService); + super(cryptoService, i18nService, cipherService); } async parse(data: string): Promise { diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index 4ff15174c56..437e51436c5 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -203,6 +203,7 @@ export class ImportService implements ImportServiceAbstraction { return new BitwardenPasswordProtectedImporter( this.cryptoService, this.i18nService, + this.cipherService, promptForPassword_callback ); case "lastpasscsv":