mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 05:30:01 +00:00
[BEEEP] WIP Implementation of cipher-encryption.service.ts
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<Cipher>;
|
||||
|
||||
abstract decrypt(cipher: Cipher, userId: UserId): Promise<CipherView>;
|
||||
abstract decryptMany(ciphers: Cipher[], userId: UserId): Promise<CipherView[]>;
|
||||
abstract decrypt(cipher: Cipher, userId: UserId): Promise<CipherView | null>;
|
||||
abstract decryptMany(ciphers: Cipher[], userId: UserId): Promise<CipherView[] | null>;
|
||||
|
||||
abstract encryptCipherAttachmentData(
|
||||
cipher: Cipher,
|
||||
fileName: string,
|
||||
data: Uint8Array,
|
||||
userId: UserId,
|
||||
): Promise<EncCipherAttachment>;
|
||||
}
|
||||
|
||||
@@ -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<CipherWithIdRequest> {
|
||||
@@ -40,16 +38,9 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
keyForCipherKeyDecryption?: SymmetricCryptoKey,
|
||||
originalCipher?: Cipher,
|
||||
): Promise<Cipher>;
|
||||
abstract encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise<Field[]>;
|
||||
abstract encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise<Field>;
|
||||
abstract get(id: string, userId: UserId): Promise<Cipher>;
|
||||
abstract getAll(userId: UserId): Promise<Cipher[]>;
|
||||
abstract getAllDecrypted(userId: UserId): Promise<CipherView[]>;
|
||||
abstract getAllDecryptedForGrouping(
|
||||
groupingId: string,
|
||||
userId: UserId,
|
||||
folder?: boolean,
|
||||
): Promise<CipherView[]>;
|
||||
abstract getAllDecryptedForUrl(
|
||||
url: string,
|
||||
userId: UserId,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export abstract class vNextCipherService {}
|
||||
@@ -54,8 +54,9 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
collectionIds: string[];
|
||||
creationDate: Date;
|
||||
deletedDate: Date;
|
||||
archivedDate: Date;
|
||||
reprompt: CipherRepromptType;
|
||||
key: EncString;
|
||||
key: EncString | null;
|
||||
|
||||
constructor(obj?: CipherData, localData: LocalData = null) {
|
||||
super();
|
||||
|
||||
@@ -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<Record<CipherId, LocalData>> {
|
||||
@@ -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<Attachment[]> {
|
||||
if (attachmentsModel == null || attachmentsModel.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const promises: Promise<any>[] = [];
|
||||
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<Field[]> {
|
||||
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<Field> {
|
||||
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<Password[]> {
|
||||
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<Password> {
|
||||
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<Cipher> {
|
||||
@@ -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<string, Cipher[]>,
|
||||
);
|
||||
|
||||
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<CipherView[]> {
|
||||
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<CipherView[]> {
|
||||
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<CipherView[]> {
|
||||
@@ -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<CipherResponse>,
|
||||
organizationId: string,
|
||||
organizationId: OrganizationId,
|
||||
userId: UserId | null = null,
|
||||
): Promise<CipherView[]> {
|
||||
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<Cipher> {
|
||||
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<V extends View, D extends Domain>(
|
||||
model: V,
|
||||
obj: D,
|
||||
map: any,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<void> {
|
||||
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<Cipher> {
|
||||
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<Cipher> {
|
||||
// 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<boolean> {
|
||||
const featureEnabled = await this.configService.getFeatureFlag(FeatureFlag.CipherKeyEncryption);
|
||||
const meetsServerVersion = await firstValueFrom(
|
||||
|
||||
@@ -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<EncryptService>();
|
||||
const bulkEncryptServiceMock = mock<BulkEncryptService>();
|
||||
const keyServiceMock = mock<KeyService>();
|
||||
const configServiceMock = mock<ConfigService>();
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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<Cipher> {
|
||||
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<CipherView[]> {
|
||||
async encryptCipherAttachmentData(
|
||||
cipher: Cipher,
|
||||
fileName: string,
|
||||
data: Uint8Array,
|
||||
userId: UserId,
|
||||
): Promise<EncCipherAttachment> {
|
||||
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<CipherView[] | null> {
|
||||
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<CipherView> {
|
||||
async decrypt(cipher: Cipher, userId: UserId): Promise<CipherView | null> {
|
||||
const decrypted = await this.decryptMany([cipher], userId);
|
||||
return decrypted[0];
|
||||
return decrypted?.[0] ?? null;
|
||||
}
|
||||
|
||||
async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise<UserKey | OrgKey> {
|
||||
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<Cipher> {
|
||||
// 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<Cipher> {
|
||||
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<V extends View, D extends Domain>(
|
||||
model: V,
|
||||
obj: D,
|
||||
map: any,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<void> {
|
||||
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<Field[] | null> {
|
||||
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<Field> {
|
||||
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<boolean> {
|
||||
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<Password[] | null> {
|
||||
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<Password> {
|
||||
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<Attachment[] | null> {
|
||||
if (attachmentsModel == null || attachmentsModel.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const promises: Promise<any>[] = [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { vNextCipherService } from "@bitwarden/common/vault/abstractions/vnext-cipher.service";
|
||||
|
||||
export class DefaultvNextCipherService implements vNextCipherService {}
|
||||
Reference in New Issue
Block a user