diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 74fa6acdf79..7a0dbddea14 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -185,6 +185,7 @@ import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-st import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; @@ -197,6 +198,7 @@ import { DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; @@ -308,6 +310,7 @@ export default class MainBackground { appIdService: AppIdServiceAbstraction; apiService: ApiServiceAbstraction; environmentService: BrowserEnvironmentService; + cipherEncryptionService: CipherEncryptionService; cipherService: CipherServiceAbstraction; folderService: InternalFolderServiceAbstraction; userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction; @@ -840,6 +843,12 @@ export default class MainBackground { this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService); + this.cipherEncryptionService = new DefaultCipherEncryptionService( + this.encryptService, + this.bulkEncryptService, + this.keyService, + this.configService, + ); this.cipherService = new CipherService( this.keyService, this.domainSettingsService, @@ -854,7 +863,9 @@ export default class MainBackground { this.configService, this.stateProvider, this.accountService, + this.cipherEncryptionService, ); + this.folderService = new FolderService( this.keyService, this.encryptService, diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index b7f423e8ff7..10b11f93cc5 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -7,19 +7,19 @@ import * as jsdom from "jsdom"; import { firstValueFrom } from "rxjs"; import { - OrganizationUserApiService, - DefaultOrganizationUserApiService, DefaultCollectionService, + DefaultOrganizationUserApiService, + OrganizationUserApiService, } from "@bitwarden/admin-console/common"; import { - InternalUserDecryptionOptionsServiceAbstraction, AuthRequestService, + InternalUserDecryptionOptionsServiceAbstraction, LoginStrategyService, LoginStrategyServiceAbstraction, PinService, PinServiceAbstraction, - UserDecryptionOptionsService, SsoUrlService, + UserDecryptionOptionsService, } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -88,8 +88,8 @@ import { MessageSender } from "@bitwarden/common/platform/messaging"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { - TaskSchedulerService, DefaultTaskSchedulerService, + TaskSchedulerService, } from "@bitwarden/common/platform/scheduling"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; @@ -139,12 +139,14 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherAuthorizationService, DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; @@ -160,11 +162,11 @@ import { ImportServiceAbstraction, } from "@bitwarden/importer-core"; import { - DefaultKdfConfigService, - KdfConfigService, - DefaultKeyService as KeyService, BiometricStateService, DefaultBiometricStateService, + DefaultKdfConfigService, + DefaultKeyService as KeyService, + KdfConfigService, } from "@bitwarden/key-management"; import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service"; import { @@ -211,6 +213,7 @@ export class ServiceContainer { appIdService: AppIdService; apiService: NodeApiService; environmentService: EnvironmentService; + cipherEncryptionService: CipherEncryptionService; cipherService: CipherService; folderService: InternalFolderService; organizationUserApiService: OrganizationUserApiService; @@ -674,6 +677,13 @@ export class ServiceContainer { this.policyService, ); + this.cipherEncryptionService = new DefaultCipherEncryptionService( + this.encryptService, + new FallbackBulkEncryptService(this.encryptService), + this.keyService, + this.configService, + ); + this.cipherService = new CipherService( this.keyService, this.domainSettingsService, @@ -688,6 +698,7 @@ export class ServiceContainer { this.configService, this.stateProvider, this.accountService, + this.cipherEncryptionService, ); this.folderService = new FolderService( diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9cb39a35856..e68eee442dc 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -4,48 +4,48 @@ import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core"; import { Subject } from "rxjs"; import { - OrganizationUserApiService, - DefaultOrganizationUserApiService, CollectionService, DefaultCollectionService, + DefaultOrganizationUserApiService, + OrganizationUserApiService, } from "@bitwarden/admin-console/common"; import { - SetPasswordJitService, - DefaultSetPasswordJitService, - RegistrationFinishService as RegistrationFinishServiceAbstraction, - DefaultRegistrationFinishService, AnonLayoutWrapperDataService, DefaultAnonLayoutWrapperDataService, - LoginComponentService, + DefaultLoginApprovalComponentService, DefaultLoginComponentService, - LoginDecryptionOptionsService, DefaultLoginDecryptionOptionsService, - TwoFactorAuthComponentService, + DefaultRegistrationFinishService, + DefaultSetPasswordJitService, DefaultTwoFactorAuthComponentService, DefaultTwoFactorAuthEmailComponentService, - TwoFactorAuthEmailComponentService, DefaultTwoFactorAuthWebAuthnComponentService, + LoginComponentService, + LoginDecryptionOptionsService, + RegistrationFinishService as RegistrationFinishServiceAbstraction, + SetPasswordJitService, + TwoFactorAuthComponentService, + TwoFactorAuthEmailComponentService, TwoFactorAuthWebAuthnComponentService, - DefaultLoginApprovalComponentService, } from "@bitwarden/auth/angular"; import { - AuthRequestServiceAbstraction, - AuthRequestService, - PinServiceAbstraction, - PinService, - LoginStrategyServiceAbstraction, - LoginStrategyService, - LoginEmailServiceAbstraction, - LoginEmailService, - InternalUserDecryptionOptionsServiceAbstraction, - UserDecryptionOptionsService, - UserDecryptionOptionsServiceAbstraction, - LogoutReason, AuthRequestApiService, + AuthRequestService, + AuthRequestServiceAbstraction, DefaultAuthRequestApiService, DefaultLoginSuccessHandlerService, - LoginSuccessHandlerService, + InternalUserDecryptionOptionsServiceAbstraction, LoginApprovalComponentServiceAbstraction, + LoginEmailService, + LoginEmailServiceAbstraction, + LoginStrategyService, + LoginStrategyServiceAbstraction, + LoginSuccessHandlerService, + LogoutReason, + PinService, + PinServiceAbstraction, + UserDecryptionOptionsService, + UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -117,16 +117,16 @@ import { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauth import { WebAuthnLoginPrfKeyService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-key.service"; import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service"; import { - AutofillSettingsServiceAbstraction, AutofillSettingsService, + AutofillSettingsServiceAbstraction, } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { - BadgeSettingsServiceAbstraction, BadgeSettingsService, + BadgeSettingsServiceAbstraction, } from "@bitwarden/common/autofill/services/badge-settings.service"; import { - DomainSettingsService, DefaultDomainSettingsService, + DomainSettingsService, } from "@bitwarden/common/autofill/services/domain-settings.service"; import { BillingApiServiceAbstraction, @@ -199,8 +199,8 @@ import { WebPushNotificationsApiService, } from "@bitwarden/common/platform/notifications/internal"; import { - TaskSchedulerService, DefaultTaskSchedulerService, + TaskSchedulerService, } from "@bitwarden/common/platform/scheduling"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; @@ -221,10 +221,10 @@ import { ValidationService } from "@bitwarden/common/platform/services/validatio import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { ActiveUserStateProvider, + DerivedStateProvider, GlobalStateProvider, SingleUserStateProvider, StateProvider, - DerivedStateProvider, } from "@bitwarden/common/platform/state"; /* eslint-disable import/no-restricted-paths -- We need the implementations to inject, but generally these should not be accessed */ import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider"; @@ -260,6 +260,7 @@ import { InternalSendService, SendService as SendServiceAbstraction, } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; @@ -274,6 +275,7 @@ import { DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; @@ -291,34 +293,34 @@ import { UsernameGenerationServiceAbstraction, } from "@bitwarden/generator-legacy"; import { - KeyService, - DefaultKeyService, + BiometricsService, BiometricStateService, DefaultBiometricStateService, - BiometricsService, DefaultKdfConfigService, - KdfConfigService, - UserAsymmetricKeysRegenerationService, - DefaultUserAsymmetricKeysRegenerationService, - UserAsymmetricKeysRegenerationApiService, + DefaultKeyService, DefaultUserAsymmetricKeysRegenerationApiService, + DefaultUserAsymmetricKeysRegenerationService, + KdfConfigService, + KeyService, + UserAsymmetricKeysRegenerationApiService, + UserAsymmetricKeysRegenerationService, } from "@bitwarden/key-management"; import { SafeInjectionToken } from "@bitwarden/ui-common"; import { - DefaultTaskService, DefaultEndUserNotificationService, + DefaultTaskService, EndUserNotificationService, NewDeviceVerificationNoticeService, PasswordRepromptService, TaskService, } from "@bitwarden/vault"; import { - VaultExportService, - VaultExportServiceAbstraction, - OrganizationVaultExportService, - OrganizationVaultExportServiceAbstraction, IndividualVaultExportService, IndividualVaultExportServiceAbstraction, + OrganizationVaultExportService, + OrganizationVaultExportServiceAbstraction, + VaultExportService, + VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction"; @@ -333,24 +335,24 @@ import { AbstractThemingService } from "../platform/services/theming/theming.ser import { safeProvider, SafeProvider } from "../platform/utils/safe-provider"; import { + CLIENT_TYPE, + DEFAULT_VAULT_TIMEOUT, + ENV_ADDITIONAL_REGIONS, + INTRAPROCESS_MESSAGING_SUBJECT, LOCALES_DIRECTORY, LOCKED_CALLBACK, - LOGOUT_CALLBACK, LOG_MAC_FAILURES, + LOGOUT_CALLBACK, MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, + REFRESH_ACCESS_TOKEN_ERROR_CALLBACK, SECURE_STORAGE, STATE_FACTORY, SUPPORTS_SECURE_STORAGE, SYSTEM_LANGUAGE, SYSTEM_THEME_OBSERVABLE, WINDOW, - DEFAULT_VAULT_TIMEOUT, - INTRAPROCESS_MESSAGING_SUBJECT, - CLIENT_TYPE, - REFRESH_ACCESS_TOKEN_ERROR_CALLBACK, - ENV_ADDITIONAL_REGIONS, } from "./injection-tokens"; import { ModalService } from "./modal.service"; @@ -489,6 +491,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultDomainSettingsService, deps: [StateProvider, ConfigService], }), + safeProvider({ + provide: CipherEncryptionService, + useClass: DefaultCipherEncryptionService, + deps: [EncryptService, BulkEncryptService, KeyService, ConfigService], + }), safeProvider({ provide: CipherServiceAbstraction, useFactory: ( @@ -505,6 +512,7 @@ const safeProviders: SafeProvider[] = [ configService: ConfigService, stateProvider: StateProvider, accountService: AccountServiceAbstraction, + cipherEncryptionService: CipherEncryptionService, ) => new CipherService( keyService, @@ -520,6 +528,7 @@ const safeProviders: SafeProvider[] = [ configService, stateProvider, accountService, + cipherEncryptionService, ), deps: [ KeyService, @@ -535,6 +544,7 @@ const safeProviders: SafeProvider[] = [ ConfigService, StateProvider, AccountServiceAbstraction, + CipherEncryptionService, ], }), safeProvider({ diff --git a/libs/common/src/vault/abstractions/cipher-encryption.service.ts b/libs/common/src/vault/abstractions/cipher-encryption.service.ts index 3134664fc3c..12ca310067f 100644 --- a/libs/common/src/vault/abstractions/cipher-encryption.service.ts +++ b/libs/common/src/vault/abstractions/cipher-encryption.service.ts @@ -1,8 +1,16 @@ +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserId } from "@bitwarden/common/types/guid"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +export type EncCipherAttachment = { + encFileName: EncString; + dataEncKey: [SymmetricCryptoKey, EncString]; + encData: EncArrayBuffer; +}; + /** * Service responsible for encrypting and decrypting ciphers. */ @@ -15,6 +23,13 @@ export abstract class CipherEncryptionService { originalCipher?: Cipher, ): Promise; - abstract decrypt(cipher: Cipher, userId: UserId): Promise; - abstract decryptMany(ciphers: Cipher[], userId: UserId): Promise; + abstract decrypt(cipher: Cipher, userId: UserId): Promise; + abstract decryptMany(ciphers: Cipher[], userId: UserId): Promise; + + abstract encryptCipherAttachmentData( + cipher: Cipher, + fileName: string, + data: Uint8Array, + userId: UserId, + ): Promise; } diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 1e4275ff89b..e47b6272637 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -12,10 +12,8 @@ import { CipherType } from "../enums/cipher-type"; import { CipherData } from "../models/data/cipher.data"; import { LocalData } from "../models/data/local.data"; import { Cipher } from "../models/domain/cipher"; -import { Field } from "../models/domain/field"; import { CipherWithIdRequest } from "../models/request/cipher-with-id.request"; import { CipherView } from "../models/view/cipher.view"; -import { FieldView } from "../models/view/field.view"; import { AddEditCipherInfo } from "../types/add-edit-cipher-info"; export abstract class CipherService implements UserKeyRotationDataProvider { @@ -40,16 +38,9 @@ export abstract class CipherService implements UserKeyRotationDataProvider; - abstract encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise; - abstract encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise; abstract get(id: string, userId: UserId): Promise; abstract getAll(userId: UserId): Promise; abstract getAllDecrypted(userId: UserId): Promise; - abstract getAllDecryptedForGrouping( - groupingId: string, - userId: UserId, - folder?: boolean, - ): Promise; abstract getAllDecryptedForUrl( url: string, userId: UserId, diff --git a/libs/common/src/vault/abstractions/vnext-cipher.service.ts b/libs/common/src/vault/abstractions/vnext-cipher.service.ts new file mode 100644 index 00000000000..d4b3fec4312 --- /dev/null +++ b/libs/common/src/vault/abstractions/vnext-cipher.service.ts @@ -0,0 +1 @@ +export abstract class vNextCipherService {} diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index f23e3c0c579..7966fa91f6f 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -54,8 +54,9 @@ export class Cipher extends Domain implements Decryptable { collectionIds: string[]; creationDate: Date; deletedDate: Date; + archivedDate: Date; reprompt: CipherRepromptType; - key: EncString; + key: EncString | null; constructor(obj?: CipherData, localData: LocalData = null) { super(); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index d774277c4a0..44f51340483 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -14,6 +14,7 @@ import { } from "rxjs"; import { SemVer } from "semver"; +import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { KeyService } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; @@ -27,15 +28,12 @@ import { EncryptService } from "../../key-management/crypto/abstractions/encrypt import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; -import { View } from "../../models/view/view"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { StateService } from "../../platform/abstractions/state.service"; import { sequentialize } from "../../platform/misc/sequentialize"; import { Utils } from "../../platform/misc/utils"; -import Domain from "../../platform/models/domain/domain-base"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; -import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { StateProvider } from "../../platform/state"; import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid"; @@ -46,18 +44,8 @@ import { FieldType } from "../enums"; import { CipherType } from "../enums/cipher-type"; import { CipherData } from "../models/data/cipher.data"; import { LocalData } from "../models/data/local.data"; -import { Attachment } from "../models/domain/attachment"; -import { Card } from "../models/domain/card"; import { Cipher } from "../models/domain/cipher"; -import { Fido2Credential } from "../models/domain/fido2-credential"; -import { Field } from "../models/domain/field"; -import { Identity } from "../models/domain/identity"; -import { Login } from "../models/domain/login"; -import { LoginUri } from "../models/domain/login-uri"; -import { Password } from "../models/domain/password"; -import { SecureNote } from "../models/domain/secure-note"; import { SortedCiphersCache } from "../models/domain/sorted-ciphers-cache"; -import { SshKey } from "../models/domain/ssh-key"; import { CipherBulkDeleteRequest } from "../models/request/cipher-bulk-delete.request"; import { CipherBulkMoveRequest } from "../models/request/cipher-bulk-move.request"; import { CipherBulkRestoreRequest } from "../models/request/cipher-bulk-restore.request"; @@ -72,7 +60,6 @@ import { CipherRequest } from "../models/request/cipher.request"; import { CipherResponse } from "../models/response/cipher.response"; import { AttachmentView } from "../models/view/attachment.view"; import { CipherView } from "../models/view/cipher.view"; -import { FieldView } from "../models/view/field.view"; import { PasswordHistoryView } from "../models/view/password-history.view"; import { AddEditCipherInfo } from "../types/add-edit-cipher-info"; @@ -111,6 +98,7 @@ export class CipherService implements CipherServiceAbstraction { private configService: ConfigService, private stateProvider: StateProvider, private accountService: AccountService, + private cipherEncryptionService: CipherEncryptionService, ) {} localData$(userId: UserId): Observable> { @@ -212,150 +200,13 @@ export class CipherService implements CipherServiceAbstraction { this.adjustPasswordHistoryLength(model); } - const cipher = new Cipher(); - cipher.id = model.id; - cipher.folderId = model.folderId; - cipher.favorite = model.favorite; - cipher.organizationId = model.organizationId; - cipher.type = model.type; - cipher.collectionIds = model.collectionIds; - cipher.revisionDate = model.revisionDate; - cipher.reprompt = model.reprompt; - cipher.edit = model.edit; - - if (await this.getCipherKeyEncryptionEnabled()) { - cipher.key = originalCipher?.key ?? null; - const userOrOrgKey = await this.getKeyForCipherKeyDecryption(cipher, userId); - // 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. - keyForCipherEncryption ||= 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, - keyForCipherEncryption, - keyForCipherKeyDecryption, - ); - } else { - keyForCipherEncryption ||= await this.getKeyForCipherKeyDecryption(cipher, userId); - // 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, keyForCipherEncryption); - } - } - - async encryptAttachments( - attachmentsModel: AttachmentView[], - key: SymmetricCryptoKey, - ): Promise { - if (attachmentsModel == null || attachmentsModel.length === 0) { - return null; - } - - const promises: Promise[] = []; - const encAttachments: Attachment[] = []; - attachmentsModel.forEach(async (model) => { - const attachment = new Attachment(); - attachment.id = model.id; - attachment.size = model.size; - attachment.sizeName = model.sizeName; - attachment.url = model.url; - const promise = this.encryptObjProperty( - model, - attachment, - { - fileName: null, - }, - key, - ).then(async () => { - if (model.key != null) { - attachment.key = await this.encryptService.encrypt(model.key.key, key); - } - encAttachments.push(attachment); - }); - promises.push(promise); - }); - - await Promise.all(promises); - return encAttachments; - } - - async encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise { - if (!fieldsModel || !fieldsModel.length) { - return null; - } - - const self = this; - const encFields: Field[] = []; - await fieldsModel.reduce(async (promise, field) => { - await promise; - const encField = await self.encryptField(field, key); - encFields.push(encField); - }, Promise.resolve()); - - return encFields; - } - - async encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise { - const field = new Field(); - field.type = fieldModel.type; - field.linkedId = fieldModel.linkedId; - // normalize boolean type field values - if (fieldModel.type === FieldType.Boolean && fieldModel.value !== "true") { - fieldModel.value = "false"; - } - - await this.encryptObjProperty( - fieldModel, - field, - { - name: null, - value: null, - }, - key, + return await this.cipherEncryptionService.encrypt( + model, + userId, + keyForCipherEncryption, + keyForCipherKeyDecryption, + originalCipher, ); - - return field; - } - - async encryptPasswordHistories( - phModels: PasswordHistoryView[], - key: SymmetricCryptoKey, - ): Promise { - if (!phModels || !phModels.length) { - return null; - } - - const self = this; - const encPhs: Password[] = []; - await phModels.reduce(async (promise, ph) => { - await promise; - const encPh = await self.encryptPasswordHistory(ph, key); - encPhs.push(encPh); - }, Promise.resolve()); - - return encPhs; - } - - async encryptPasswordHistory( - phModel: PasswordHistoryView, - key: SymmetricCryptoKey, - ): Promise { - const ph = new Password(); - ph.lastUsedDate = phModel.lastUsedDate; - - await this.encryptObjProperty( - phModel, - ph, - { - password: null, - }, - key, - ); - - return ph; } async get(id: string, userId: UserId): Promise { @@ -426,45 +277,14 @@ export class CipherService implements CipherServiceAbstraction { ciphers: Cipher[], userId: UserId, ): Promise<[CipherView[], CipherView[]]> { - const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true)); + const decryptedCiphers = await this.cipherEncryptionService.decryptMany(ciphers, userId); - if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) { - // return early if there are no keys to decrypt with + if (decryptedCiphers == null) { return [[], []]; } - // Group ciphers by orgId or under 'null' for the user's ciphers - const grouped = ciphers.reduce( - (agg, c) => { - agg[c.organizationId] ??= []; - agg[c.organizationId].push(c); - return agg; - }, - {} as Record, - ); - - const allCipherViews = ( - await Promise.all( - Object.entries(grouped).map(async ([orgId, groupedCiphers]) => { - if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { - return await this.bulkEncryptService.decryptItems( - groupedCiphers, - keys.orgKeys[orgId as OrganizationId] ?? keys.userKey, - ); - } else { - return await this.encryptService.decryptItems( - groupedCiphers, - keys.orgKeys[orgId as OrganizationId] ?? keys.userKey, - ); - } - }), - ) - ) - .flat() - .sort(this.getLocaleSortingFunction()); - // Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt - return allCipherViews.reduce( + return decryptedCiphers.sort(this.getLocaleSortingFunction()).reduce( (acc, c) => { if (c.decryptionFailure) { acc[1].push(c); @@ -485,32 +305,6 @@ export class CipherService implements CipherServiceAbstraction { await this.searchService.indexCiphers(userId, await this.getDecryptedCiphers(userId), userId); } } - - async getAllDecryptedForGrouping( - groupingId: string, - userId: UserId, - folder = true, - ): Promise { - const ciphers = await this.getAllDecrypted(userId); - - return ciphers.filter((cipher) => { - if (cipher.isDeleted) { - return false; - } - if (folder && cipher.folderId === groupingId) { - return true; - } else if ( - !folder && - cipher.collectionIds != null && - cipher.collectionIds.indexOf(groupingId) > -1 - ) { - return true; - } - - return false; - }); - } - async getAllDecryptedForUrl( url: string, userId: UserId, @@ -571,7 +365,10 @@ export class CipherService implements CipherServiceAbstraction { async getAllFromApiForOrganization(organizationId: string): Promise { const response = await this.apiService.getCiphersOrganization(organizationId); - return await this.decryptOrganizationCiphersResponse(response, organizationId); + return await this.decryptOrganizationCiphersResponse( + response, + organizationId as OrganizationId, + ); } async getManyFromApiForOrganization(organizationId: string): Promise { @@ -582,26 +379,31 @@ export class CipherService implements CipherServiceAbstraction { true, true, ); - return this.decryptOrganizationCiphersResponse(response, organizationId); + return this.decryptOrganizationCiphersResponse(response, organizationId as OrganizationId); } private async decryptOrganizationCiphersResponse( response: ListResponse, - organizationId: string, + organizationId: OrganizationId, + userId: UserId | null = null, ): Promise { if (response?.data == null || response.data.length < 1) { return []; } - const ciphers = response.data.map((cr) => new Cipher(new CipherData(cr))); - const key = await this.keyService.getOrgKey(organizationId); - let decCiphers: CipherView[] = []; - if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { - decCiphers = await this.bulkEncryptService.decryptItems(ciphers, key); - } else { - decCiphers = await this.encryptService.decryptItems(ciphers, key); + if (userId == null) { + userId = await firstValueFrom(this.stateProvider.activeUserId$); + if (userId == null) { + throw new Error("A userId is required to decrypt organization ciphers"); + } } + const ciphers = response.data.map((cr) => new Cipher(new CipherData(cr))); + const decCiphers: CipherView[] = await this.cipherEncryptionService.decryptMany( + ciphers, + userId, + ); + decCiphers.sort(this.getLocaleSortingFunction()); return decCiphers; } @@ -874,36 +676,30 @@ export class CipherService implements CipherServiceAbstraction { userId: UserId, admin = false, ): Promise { - const encKey = await this.getKeyForCipherKeyDecryption(cipher, userId); const cipherKeyEncryptionEnabled = await this.getCipherKeyEncryptionEnabled(); - const cipherEncKey = - cipherKeyEncryptionEnabled && cipher.key != null - ? (new SymmetricCryptoKey( - await this.encryptService.decryptToBytes(cipher.key, encKey), - ) as UserKey) - : encKey; - //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, userId)); + const model = await this.cipherEncryptionService.decrypt(cipher, userId); cipher = await this.encrypt(model, userId); await this.updateWithServer(cipher); } - const encFileName = await this.encryptService.encrypt(filename, cipherEncKey); - - const dataEncKey = await this.keyService.makeDataEncKey(cipherEncKey); - const encData = await this.encryptService.encryptToBytes(new Uint8Array(data), dataEncKey[0]); + const encAttachment = await this.cipherEncryptionService.encryptCipherAttachmentData( + cipher, + filename, + data, + userId, + ); const response = await this.cipherFileUploadService.upload( cipher, - encFileName, - encData, + encAttachment.encFileName, + encAttachment.encData, admin, - dataEncKey, + encAttachment.dataEncKey, ); const cData = new CipherData(response, cipher.collectionIds); @@ -1532,176 +1328,6 @@ export class CipherService implements CipherServiceAbstraction { } } - private async encryptObjProperty( - model: V, - obj: D, - map: any, - key: SymmetricCryptoKey, - ): Promise { - const promises = []; - const self = this; - - for (const prop in map) { - // eslint-disable-next-line - if (!map.hasOwnProperty(prop)) { - continue; - } - - (function (theProp, theObj) { - const p = Promise.resolve() - .then(() => { - const modelProp = (model as any)[map[theProp] || theProp]; - if (modelProp && modelProp !== "") { - return self.encryptService.encrypt(modelProp, key); - } - return null; - }) - .then((val: EncString) => { - (theObj as any)[theProp] = val; - }); - promises.push(p); - })(prop, obj); - } - - await Promise.all(promises); - } - - private async encryptCipherData(cipher: Cipher, model: CipherView, key: SymmetricCryptoKey) { - switch (cipher.type) { - case CipherType.Login: - cipher.login = new Login(); - cipher.login.passwordRevisionDate = model.login.passwordRevisionDate; - cipher.login.autofillOnPageLoad = model.login.autofillOnPageLoad; - await this.encryptObjProperty( - model.login, - cipher.login, - { - username: null, - password: null, - totp: null, - }, - key, - ); - - if (model.login.uris != null) { - cipher.login.uris = []; - model.login.uris = model.login.uris.filter((u) => u.uri != null && u.uri !== ""); - for (let i = 0; i < model.login.uris.length; i++) { - const loginUri = new LoginUri(); - loginUri.match = model.login.uris[i].match; - await this.encryptObjProperty( - model.login.uris[i], - loginUri, - { - uri: null, - }, - key, - ); - const uriHash = await this.encryptService.hash(model.login.uris[i].uri, "sha256"); - loginUri.uriChecksum = await this.encryptService.encrypt(uriHash, key); - cipher.login.uris.push(loginUri); - } - } - - if (model.login.fido2Credentials != null) { - cipher.login.fido2Credentials = await Promise.all( - model.login.fido2Credentials.map(async (viewKey) => { - const domainKey = new Fido2Credential(); - await this.encryptObjProperty( - viewKey, - domainKey, - { - credentialId: null, - keyType: null, - keyAlgorithm: null, - keyCurve: null, - keyValue: null, - rpId: null, - rpName: null, - userHandle: null, - userName: null, - userDisplayName: null, - origin: null, - }, - key, - ); - domainKey.counter = await this.encryptService.encrypt(String(viewKey.counter), key); - domainKey.discoverable = await this.encryptService.encrypt( - String(viewKey.discoverable), - key, - ); - domainKey.creationDate = viewKey.creationDate; - return domainKey; - }), - ); - } - return; - case CipherType.SecureNote: - cipher.secureNote = new SecureNote(); - cipher.secureNote.type = model.secureNote.type; - return; - case CipherType.Card: - cipher.card = new Card(); - await this.encryptObjProperty( - model.card, - cipher.card, - { - cardholderName: null, - brand: null, - number: null, - expMonth: null, - expYear: null, - code: null, - }, - key, - ); - return; - case CipherType.Identity: - cipher.identity = new Identity(); - await this.encryptObjProperty( - model.identity, - cipher.identity, - { - title: null, - firstName: null, - middleName: null, - lastName: null, - address1: null, - address2: null, - address3: null, - city: null, - state: null, - postalCode: null, - country: null, - company: null, - email: null, - phone: null, - ssn: null, - username: null, - passportNumber: null, - licenseNumber: null, - }, - key, - ); - return; - case CipherType.SshKey: - cipher.sshKey = new SshKey(); - await this.encryptObjProperty( - model.sshKey, - cipher.sshKey, - { - privateKey: null, - publicKey: null, - keyFingerprint: null, - }, - key, - ); - return; - default: - throw new Error("Unknown cipher type."); - } - } - private async getAutofillOnPageLoadDefault() { return await firstValueFrom(this.autofillSettingsService.autofillOnPageLoadDefault$); } @@ -1760,73 +1386,6 @@ export class CipherService implements CipherServiceAbstraction { this.sortedCiphersCache.clear(); } - /** - * Encrypts a cipher object. - * @param model The cipher view model. - * @param cipher The cipher object. - * @param key The encryption key to encrypt with. This can be the org key, user key or cipher key, but must never be null - */ - private async encryptCipher( - model: CipherView, - cipher: Cipher, - key: SymmetricCryptoKey, - ): Promise { - if (key == null) { - throw new Error( - "Key to encrypt cipher must not be null. Use the org key, user key or cipher key.", - ); - } - - 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.keyService.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 { const featureEnabled = await this.configService.getFeatureFlag(FeatureFlag.CipherKeyEncryption); const meetsServerVersion = await firstValueFrom( diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts new file mode 100644 index 00000000000..1f1ec9e4f5f --- /dev/null +++ b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts @@ -0,0 +1,218 @@ +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; +import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; +import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management"; + +import { makeSymmetricCryptoKey } from "../../../spec"; + +import clearAllMocks = jest.clearAllMocks; + +describe("DefaultCipherEncryptionService", () => { + let service: DefaultCipherEncryptionService; + + const userId = "user-id" as UserId; + const userKey = makeSymmetricCryptoKey(); + + const encryptServiceMock = mock(); + const bulkEncryptServiceMock = mock(); + const keyServiceMock = mock(); + const configServiceMock = mock(); + + beforeEach(() => { + clearAllMocks(); + + service = new DefaultCipherEncryptionService( + encryptServiceMock, + bulkEncryptServiceMock, + keyServiceMock, + configServiceMock, + ); + }); + + describe("encrypt", () => { + it("should map non-encrypted fields", async () => { + // TODO: Find means to catch new properties automatically + const cipherView = new CipherView({ + id: "cipher-id", + folderId: "folder-id", + organizationId: "organization-id", + favorite: true, + type: CipherType.Card, + collectionIds: ["collection-id"], + revisionDate: new Date(), + reprompt: CipherRepromptType.Password, + edit: true, + } as Cipher); + + service.encryptCipherWithCipherKey = jest.fn(); + service.encryptCipher = jest.fn(); + service.getCipherKeyEncryptionEnabled = jest.fn().mockResolvedValue(false); + service.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(userKey); + + await service.encrypt(cipherView, userId); + + expect(service.encryptCipher).toHaveBeenCalledWith( + cipherView, + expect.objectContaining({ + id: "cipher-id", + folderId: "folder-id", + organizationId: "organization-id", + favorite: true, + type: CipherType.Card, + collectionIds: ["collection-id"], + revisionDate: expect.any(Date), + reprompt: CipherRepromptType.Password, + edit: true, + } as Cipher), + userKey, + ); + }); + + it("should call encryptCipherWithKey when cipherKeyEncryption is enabled", async () => { + service.encryptCipherWithCipherKey = jest.fn(); + service.encryptCipher = jest.fn(); + service.getCipherKeyEncryptionEnabled = jest.fn().mockResolvedValue(true); + service.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(userKey); + + const cipherView = new CipherView(); + + await service.encrypt(cipherView, userId); + + expect(service.encryptCipherWithCipherKey).toHaveBeenCalledWith( + expect.any(CipherView), + expect.any(Cipher), + userKey, + userKey, + ); + expect(service.encryptCipher).not.toHaveBeenCalled(); + }); + + it("should call encryptCipher when cipherKeyEncryption is disabled", async () => { + service.encryptCipherWithCipherKey = jest.fn(); + service.encryptCipher = jest.fn(); + service.getCipherKeyEncryptionEnabled = jest.fn().mockResolvedValue(false); + service.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(userKey); + + const cipherView = new CipherView(); + + await service.encrypt(cipherView, userId); + + expect(service.encryptCipherWithCipherKey).not.toHaveBeenCalled(); + expect(service.encryptCipher).toHaveBeenCalledWith( + expect.any(CipherView), + expect.any(Cipher), + userKey, + ); + }); + }); + + describe("encryptCipher", () => { + it("should throw an error if the key is null", async () => { + const cipherView = new CipherView(); + + await expect(service.encryptCipher(cipherView, {} as Cipher, null as any)).rejects.toThrow( + "Key to encrypt cipher must not be null. Use the org key, user key or cipher key.", + ); + }); + + it("should call all internal encrypt methods with the correct parameters", async () => { + const cipherView = new CipherView({ + fields: [], + passwordHistory: [], + attachments: [], + } as unknown as Cipher); + const cipher = {} as Cipher; + + service.encryptObjProperty = jest.fn().mockResolvedValue(null); + service.encryptCipherData = jest.fn().mockResolvedValue(null); + service.encryptFields = jest.fn().mockResolvedValue(null); + service.encryptPasswordHistories = jest.fn().mockResolvedValue(null); + service.encryptAttachments = jest.fn().mockResolvedValue(null); + + await service.encryptCipher(cipherView, cipher, userKey); + + expect(service.encryptObjProperty).toHaveBeenCalledWith( + cipherView, + cipher, + { name: null, notes: null }, + userKey, + ); + expect(service.encryptCipherData).toHaveBeenCalledWith(cipher, cipherView, userKey); + expect(service.encryptFields).toHaveBeenCalledWith(cipherView.fields, userKey); + expect(service.encryptPasswordHistories).toHaveBeenCalledWith( + cipherView.passwordHistory, + userKey, + ); + expect(service.encryptAttachments).toHaveBeenCalledWith(cipherView.attachments, userKey); + }); + }); + + describe("decrypt", () => { + it("should return null if no keys are found for the user", async () => { + keyServiceMock.cipherDecryptionKeys$.mockReturnValue(of(null)); + + const result = await service.decrypt(new Cipher(), userId); + + expect(result).toEqual(null); + }); + + it.each([ + false, // Bulk encryption disabled + true, // Bulk encryption enabled + ])( + "should group ciphers by organizationId and call decryptMany for each group with autoEnrollEnabled=%s", + async (bulkEncryptionEnabled) => { + const userCipher = new Cipher({ id: "user-cipher" } as CipherData); + const cipher1 = new Cipher({ organizationId: "org1" } as CipherData); + const cipher2 = new Cipher({ organizationId: "org2" } as CipherData); + const ciphers = [userCipher, cipher1, cipher2]; + + const org1Key = makeSymmetricCryptoKey(); + const org2Key = makeSymmetricCryptoKey(); + + keyServiceMock.cipherDecryptionKeys$.mockReturnValue( + of({ + userKey: userKey as UserKey, + orgKeys: { + org1: org1Key, + org2: org2Key, + }, + } as CipherDecryptionKeys), + ); + + encryptServiceMock.decryptItems.mockResolvedValue([]); + bulkEncryptServiceMock.decryptItems.mockResolvedValue([]); + + const mockEncryptionMethod = bulkEncryptionEnabled + ? bulkEncryptServiceMock.decryptItems + : encryptServiceMock.decryptItems; + configServiceMock.getFeatureFlag.mockResolvedValue(bulkEncryptionEnabled); + + await service.decryptMany(ciphers, userId); + + expect(mockEncryptionMethod).toHaveBeenCalledWith( + expect.arrayContaining([userCipher]), + userKey, + ); + expect(mockEncryptionMethod).toHaveBeenCalledWith( + expect.arrayContaining([cipher1]), + org1Key, + ); + expect(mockEncryptionMethod).toHaveBeenCalledWith( + expect.arrayContaining([cipher2]), + org2Key, + ); + }, + ); + }); +}); diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.ts b/libs/common/src/vault/services/default-cipher-encryption.service.ts index 0318cd00723..18e9cefa647 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.ts @@ -1,17 +1,41 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; +import { SemVer } from "semver"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; +import { View } from "@bitwarden/common/models/view/view"; +import Domain from "@bitwarden/common/platform/models/domain/domain-base"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { OrgKey, UserKey } from "@bitwarden/common/types/key"; +import { CipherType, FieldType } from "@bitwarden/common/vault/enums"; +import { Attachment } from "@bitwarden/common/vault/models/domain/attachment"; +import { Card } from "@bitwarden/common/vault/models/domain/card"; +import { Fido2Credential } from "@bitwarden/common/vault/models/domain/fido2-credential"; +import { Field } from "@bitwarden/common/vault/models/domain/field"; +import { Identity } from "@bitwarden/common/vault/models/domain/identity"; +import { Login } from "@bitwarden/common/vault/models/domain/login"; +import { LoginUri } from "@bitwarden/common/vault/models/domain/login-uri"; +import { Password } from "@bitwarden/common/vault/models/domain/password"; +import { SecureNote } from "@bitwarden/common/vault/models/domain/secure-note"; +import { SshKey } from "@bitwarden/common/vault/models/domain/ssh-key"; +import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; +import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; +import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view"; import { KeyService } from "@bitwarden/key-management"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { OrganizationId, UserId } from "../../types/guid"; +import { + CipherEncryptionService, + EncCipherAttachment, +} from "../abstractions/cipher-encryption.service"; import { Cipher } from "../models/domain/cipher"; import { CipherView } from "../models/view/cipher.view"; +const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0"); + export class DefaultCipherEncryptionService implements CipherEncryptionService { constructor( private encryptService: EncryptService, @@ -20,21 +44,80 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { private configService: ConfigService, ) {} - encrypt( - cipher: CipherView, + async encrypt( + model: CipherView, userId: UserId, - keyForEncryption?: SymmetricCryptoKey, + keyForCipherEncryption?: SymmetricCryptoKey, keyForCipherKeyDecryption?: SymmetricCryptoKey, originalCipher?: Cipher, ): Promise { - throw new Error("Method not implemented."); + const cipher = new Cipher(); + cipher.id = model.id; + cipher.folderId = model.folderId; + cipher.favorite = model.favorite; + cipher.organizationId = model.organizationId; + cipher.type = model.type; + cipher.collectionIds = model.collectionIds; + cipher.revisionDate = model.revisionDate; + cipher.reprompt = model.reprompt; + cipher.edit = model.edit; + + if (await this.getCipherKeyEncryptionEnabled()) { + cipher.key = originalCipher?.key ?? null; + const userOrOrgKey = await this.getKeyForCipherKeyDecryption(cipher, userId); + // 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. + keyForCipherEncryption ||= 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, + keyForCipherEncryption, + keyForCipherKeyDecryption, + ); + } else { + keyForCipherEncryption ||= await this.getKeyForCipherKeyDecryption(cipher, userId); + // 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, keyForCipherEncryption); + } } - async decryptMany(ciphers: Cipher[], userId: UserId): Promise { + async encryptCipherAttachmentData( + cipher: Cipher, + fileName: string, + data: Uint8Array, + userId: UserId, + ): Promise { + const encKey = (await this.getKeyForCipherKeyDecryption(cipher, userId)) as SymmetricCryptoKey; + const cipherKeyEncryptionEnabled = await this.getCipherKeyEncryptionEnabled(); + let cipherEncKey: UserKey | OrgKey; + if (cipherKeyEncryptionEnabled && cipher.key != null) { + const keyBytes = await this.encryptService.decryptToBytes(cipher.key, encKey); + if (keyBytes == null) { + throw new Error("Cipher key decryption failed. Failed to decrypt the cipher key."); + } + cipherEncKey = new SymmetricCryptoKey(keyBytes) as UserKey | OrgKey; + } else { + cipherEncKey = encKey as UserKey | OrgKey; + } + const encFileName = await this.encryptService.encrypt(fileName, cipherEncKey); + const dataEncKey = await this.keyService.makeDataEncKey(cipherEncKey); + const encData = await this.encryptService.encryptToBytes(new Uint8Array(data), dataEncKey[0]); + return { + encFileName, + dataEncKey, + encData, + }; + } + + async decryptMany(ciphers: Cipher[], userId: UserId): Promise { const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true)); if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys ?? {}).length === 0)) { - return []; + return null; } // Group ciphers by orgId or under 'null' for the user's ciphers @@ -63,8 +146,378 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { return allCipherViews as CipherView[]; } - async decrypt(cipher: Cipher, userId: UserId): Promise { + async decrypt(cipher: Cipher, userId: UserId): Promise { const decrypted = await this.decryptMany([cipher], userId); - return decrypted[0]; + return decrypted?.[0] ?? null; + } + + async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise { + return ( + (await firstValueFrom( + this.keyService + .orgKeys$(userId) + .pipe(map((keys) => keys?.[cipher.organizationId as OrganizationId] ?? null)), + )) || ((await this.keyService.getUserKeyWithLegacySupport(userId)) as UserKey) + ); + } + + 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.keyService.makeCipherKey(); + } else { + const keyBytes = await this.encryptService.decryptToBytes( + cipher.key, + keyForCipherKeyDecryption, + ); + + if (keyBytes == null) { + throw new Error("Cipher key decryption failed. Failed to decrypt the cipher key."); + } + + decryptedCipherKey = new SymmetricCryptoKey(keyBytes); + } + + // 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); + } + + /** + * Encrypts a cipher object. + * @param model The cipher view model. + * @param cipher The cipher object. + * @param key The encryption key to encrypt with. This can be the org key, user key or cipher key, but must never be null + */ + async encryptCipher(model: CipherView, cipher: Cipher, key: SymmetricCryptoKey): Promise { + if (key == null) { + throw new Error( + "Key to encrypt cipher must not be null. Use the org key, user key or cipher key.", + ); + } + + 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; + } + + async encryptObjProperty( + model: V, + obj: D, + map: any, + key: SymmetricCryptoKey, + ): Promise { + const promises = []; + const self = this; + + for (const prop in map) { + // eslint-disable-next-line + if (!map.hasOwnProperty(prop)) { + continue; + } + + (function (theProp, theObj) { + const p = Promise.resolve() + .then(() => { + const modelProp = (model as any)[map[theProp] || theProp]; + if (modelProp && modelProp !== "") { + return self.encryptService.encrypt(modelProp, key); + } + return null; + }) + .then((val: EncString | null) => { + (theObj as any)[theProp] = val; + }); + promises.push(p); + })(prop, obj); + } + + await Promise.all(promises); + } + + async encryptCipherData(cipher: Cipher, model: CipherView, key: SymmetricCryptoKey) { + switch (cipher.type) { + case CipherType.Login: + cipher.login = new Login(); + cipher.login.passwordRevisionDate = model.login.passwordRevisionDate; + cipher.login.autofillOnPageLoad = model.login.autofillOnPageLoad; + await this.encryptObjProperty( + model.login, + cipher.login, + { + username: null, + password: null, + totp: null, + }, + key, + ); + + if (model.login.uris != null) { + cipher.login.uris = []; + model.login.uris = model.login.uris.filter((u) => u.uri != null && u.uri !== ""); + for (let i = 0; i < model.login.uris.length; i++) { + const loginUri = new LoginUri(); + loginUri.match = model.login.uris[i].match; + await this.encryptObjProperty( + model.login.uris[i], + loginUri, + { + uri: null, + }, + key, + ); + const uriHash = await this.encryptService.hash(model.login.uris[i].uri, "sha256"); + loginUri.uriChecksum = await this.encryptService.encrypt(uriHash, key); + cipher.login.uris.push(loginUri); + } + } + + if (model.login.fido2Credentials != null) { + cipher.login.fido2Credentials = await Promise.all( + model.login.fido2Credentials.map(async (viewKey) => { + const domainKey = new Fido2Credential(); + await this.encryptObjProperty( + viewKey, + domainKey, + { + credentialId: null, + keyType: null, + keyAlgorithm: null, + keyCurve: null, + keyValue: null, + rpId: null, + rpName: null, + userHandle: null, + userName: null, + userDisplayName: null, + origin: null, + }, + key, + ); + domainKey.counter = await this.encryptService.encrypt(String(viewKey.counter), key); + domainKey.discoverable = await this.encryptService.encrypt( + String(viewKey.discoverable), + key, + ); + domainKey.creationDate = viewKey.creationDate; + return domainKey; + }), + ); + } + return; + case CipherType.SecureNote: + cipher.secureNote = new SecureNote(); + cipher.secureNote.type = model.secureNote.type; + return; + case CipherType.Card: + cipher.card = new Card(); + await this.encryptObjProperty( + model.card, + cipher.card, + { + cardholderName: null, + brand: null, + number: null, + expMonth: null, + expYear: null, + code: null, + }, + key, + ); + return; + case CipherType.Identity: + cipher.identity = new Identity(); + await this.encryptObjProperty( + model.identity, + cipher.identity, + { + title: null, + firstName: null, + middleName: null, + lastName: null, + address1: null, + address2: null, + address3: null, + city: null, + state: null, + postalCode: null, + country: null, + company: null, + email: null, + phone: null, + ssn: null, + username: null, + passportNumber: null, + licenseNumber: null, + }, + key, + ); + return; + case CipherType.SshKey: + cipher.sshKey = new SshKey(); + await this.encryptObjProperty( + model.sshKey, + cipher.sshKey, + { + privateKey: null, + publicKey: null, + keyFingerprint: null, + }, + key, + ); + return; + default: + throw new Error("Unknown cipher type."); + } + } + + async encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise { + if (!fieldsModel || !fieldsModel.length) { + return null; + } + + const self = this; + const encFields: Field[] = []; + await fieldsModel.reduce(async (promise, field) => { + await promise; + const encField = await self.encryptField(field, key); + encFields.push(encField); + }, Promise.resolve()); + + return encFields; + } + + async encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise { + const field = new Field(); + field.type = fieldModel.type; + field.linkedId = fieldModel.linkedId; + // normalize boolean type field values + if (fieldModel.type === FieldType.Boolean && fieldModel.value !== "true") { + fieldModel.value = "false"; + } + + await this.encryptObjProperty( + fieldModel, + field, + { + name: null, + value: null, + }, + key, + ); + + return field; + } + + async getCipherKeyEncryptionEnabled(): Promise { + const featureEnabled = await this.configService.getFeatureFlag(FeatureFlag.CipherKeyEncryption); + const meetsServerVersion = await firstValueFrom( + this.configService.checkServerMeetsVersionRequirement$(CIPHER_KEY_ENC_MIN_SERVER_VER), + ); + return featureEnabled && meetsServerVersion; + } + + async encryptPasswordHistories( + phModels: PasswordHistoryView[], + key: SymmetricCryptoKey, + ): Promise { + if (!phModels || !phModels.length) { + return null; + } + + const self = this; + const encPhs: Password[] = []; + await phModels.reduce(async (promise, ph) => { + await promise; + const encPh = await self.encryptPasswordHistory(ph, key); + encPhs.push(encPh); + }, Promise.resolve()); + + return encPhs; + } + + async encryptPasswordHistory( + phModel: PasswordHistoryView, + key: SymmetricCryptoKey, + ): Promise { + const ph = new Password(); + ph.lastUsedDate = phModel.lastUsedDate; + + await this.encryptObjProperty( + phModel, + ph, + { + password: null, + }, + key, + ); + + return ph; + } + + async encryptAttachments( + attachmentsModel: AttachmentView[], + key: SymmetricCryptoKey, + ): Promise { + if (attachmentsModel == null || attachmentsModel.length === 0) { + return null; + } + + const promises: Promise[] = []; + const encAttachments: Attachment[] = []; + attachmentsModel.forEach(async (model) => { + const attachment = new Attachment(); + attachment.id = model.id; + attachment.size = model.size; + attachment.sizeName = model.sizeName; + attachment.url = model.url; + const promise = this.encryptObjProperty( + model, + attachment, + { + fileName: null, + }, + key, + ).then(async () => { + if (model.key != null) { + attachment.key = await this.encryptService.encrypt(model.key.key, key); + } + encAttachments.push(attachment); + }); + promises.push(promise); + }); + + await Promise.all(promises); + return encAttachments; } } diff --git a/libs/common/src/vault/services/defaultv-next-cipher.service.ts b/libs/common/src/vault/services/defaultv-next-cipher.service.ts new file mode 100644 index 00000000000..dc80b7dcad0 --- /dev/null +++ b/libs/common/src/vault/services/defaultv-next-cipher.service.ts @@ -0,0 +1,3 @@ +import { vNextCipherService } from "@bitwarden/common/vault/abstractions/vnext-cipher.service"; + +export class DefaultvNextCipherService implements vNextCipherService {}