1
0
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:
Shane
2025-03-25 11:27:48 -07:00
parent 7b9162405c
commit 4051a4e70d
11 changed files with 830 additions and 557 deletions

View File

@@ -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,

View File

@@ -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(

View File

@@ -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({

View File

@@ -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>;
}

View File

@@ -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,

View File

@@ -0,0 +1 @@
export abstract class vNextCipherService {}

View File

@@ -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();

View File

@@ -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(

View File

@@ -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,
);
},
);
});
});

View File

@@ -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;
}
}

View File

@@ -0,0 +1,3 @@
import { vNextCipherService } from "@bitwarden/common/vault/abstractions/vnext-cipher.service";
export class DefaultvNextCipherService implements vNextCipherService {}