diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 55c282be55c..8a198663e06 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -400,12 +400,6 @@ export class LoginComponent implements OnInit, OnDestroy { await this.router.navigate(["/login-with-device"]); } - protected async emailIsValid(): Promise { - this.formGroup.controls.email.markAsTouched(); - this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true }); - return this.formGroup.controls.email.valid; - } - protected async toggleLoginUiState(value: LoginUiState): Promise { this.loginUiState = value; @@ -474,7 +468,7 @@ export class LoginComponent implements OnInit, OnDestroy { * Continue to the master password entry state (only if email is validated) */ protected async continue(): Promise { - const isEmailValid = await this.emailIsValid(); + const isEmailValid = this.validateEmail(); if (isEmailValid) { await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY); @@ -496,7 +490,7 @@ export class LoginComponent implements OnInit, OnDestroy { */ async handleSsoClick() { // Make sure the email is valid - const isEmailValid = await this.emailIsValid(); + const isEmailValid = this.validateEmail(); if (!isEmailValid) { return; } @@ -594,11 +588,21 @@ export class LoginComponent implements OnInit, OnDestroy { } }; + /** + * Validates the email and displays any validation errors. + * @returns true if the email is valid, false otherwise. + */ + protected validateEmail(): boolean { + this.formGroup.controls.email.markAsTouched(); + this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true }); + return this.formGroup.controls.email.valid; + } + /** * Persist the entered email address and the user's choice to remember it to state. */ private async persistEmailIfValid(): Promise { - if (await this.emailIsValid()) { + if (this.formGroup.controls.email.valid) { const email = this.formGroup.value.email; const rememberEmail = this.formGroup.value.rememberEmail ?? false; if (!email) { @@ -613,7 +617,7 @@ export class LoginComponent implements OnInit, OnDestroy { } /** - * Set the email value from the input field. + * Set the email value from the input field and persists to state if valid. * We only update the form controls onSubmit instead of onBlur because we don't want to show validation errors until * the user submits. This is because currently our validation errors are shown below the input fields, and * displaying them causes the screen to "jump". @@ -626,7 +630,7 @@ export class LoginComponent implements OnInit, OnDestroy { } /** - * Set the Remember Email value from the input field. + * Set the Remember Email value from the input field and persists to state if valid. * We only update the form controls onSubmit instead of onBlur because we don't want to show validation errors until * the user submits. This is because currently our validation errors are shown below the input fields, and * displaying them causes the screen to "jump". diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 8d9b362d784..149f9c6a9d9 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -39,6 +39,7 @@ export enum FeatureFlag { PrivateKeyRegeneration = "pm-12241-private-key-regeneration", UserKeyRotationV2 = "userkey-rotation-v2", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", + PM17987_BlockType0 = "pm-17987-block-type-0", /* Tools */ ItemShare = "item-share", @@ -123,6 +124,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.UserKeyRotationV2]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, + [FeatureFlag.PM17987_BlockType0]: FALSE, /* Platform */ [FeatureFlag.IpcChannelFramework]: FALSE, diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index d10061a2be8..10d29198ada 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -19,10 +19,17 @@ import { SymmetricCryptoKey, } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { + DefaultFeatureFlagValue, + FeatureFlag, + getFeatureFlagValue, +} from "../../../enums/feature-flag.enum"; import { ServerConfig } from "../../../platform/abstractions/config/server-config"; import { EncryptService } from "../abstractions/encrypt.service"; export class EncryptServiceImplementation implements EncryptService { + private blockType0: boolean = DefaultFeatureFlagValue[FeatureFlag.PM17987_BlockType0]; + constructor( protected cryptoFunctionService: CryptoFunctionService, protected logService: LogService, @@ -31,7 +38,7 @@ export class EncryptServiceImplementation implements EncryptService { // Handle updating private properties to turn on/off feature flags. onServerConfigChange(newConfig: ServerConfig): void { - return; + this.blockType0 = getFeatureFlagValue(newConfig, FeatureFlag.PM17987_BlockType0); } async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise { @@ -39,6 +46,12 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("No encryption key provided."); } + if (this.blockType0) { + if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + throw new Error("Type 0 encryption is not supported."); + } + } + if (plainValue == null) { return Promise.resolve(null); } @@ -70,6 +83,12 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("No encryption key provided."); } + if (this.blockType0) { + if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + throw new Error("Type 0 encryption is not supported."); + } + } + const innerKey = key.inner(); if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { const encValue = await this.aesEncrypt(plainValue, innerKey); diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 275fd266f84..c65c78d88d7 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -10,6 +10,8 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; import { makeStaticByteArray } from "../../../../spec"; +import { DefaultFeatureFlagValue, FeatureFlag } from "../../../enums/feature-flag.enum"; +import { ServerConfig } from "../../../platform/abstractions/config/server-config"; import { EncryptServiceImplementation } from "./encrypt.service.implementation"; @@ -26,17 +28,65 @@ describe("EncryptService", () => { encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true); }); + describe("onServerConfigChange", () => { + const newConfig = mock(); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("updates internal flag with default value when not present in config", () => { + encryptService.onServerConfigChange(newConfig); + + expect((encryptService as any).blockType0).toBe( + DefaultFeatureFlagValue[FeatureFlag.PM17987_BlockType0], + ); + }); + + test.each([true, false])("updates internal flag with value in config", (expectedValue) => { + newConfig.featureStates = { [FeatureFlag.PM17987_BlockType0]: expectedValue }; + + encryptService.onServerConfigChange(newConfig); + + expect((encryptService as any).blockType0).toBe(expectedValue); + }); + }); + describe("encrypt", () => { it("throws if no key is provided", () => { return expect(encryptService.encrypt(null, null)).rejects.toThrow( "No encryption key provided.", ); }); - it("returns null if no data is provided", async () => { - const key = mock(); + + it("throws if type 0 key is provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const key = new SymmetricCryptoKey(makeStaticByteArray(32)); + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + + await expect(encryptService.encrypt(null!, key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + await expect(encryptService.encrypt(null!, mock32Key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + + const plainValue = "data"; + await expect(encryptService.encrypt(plainValue, key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + await expect(encryptService.encrypt(plainValue, mock32Key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + }); + + it("returns null if no data is provided with valid key", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); const actual = await encryptService.encrypt(null, key); expect(actual).toBeNull(); }); + it("creates an EncString for Aes256Cbc", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const plainValue = "data"; @@ -53,6 +103,7 @@ describe("EncryptService", () => { expect(Utils.fromB64ToArray(result.data).length).toEqual(4); expect(Utils.fromB64ToArray(result.iv).length).toEqual(16); }); + it("creates an EncString for Aes256Cbc_HmacSha256_B64", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); const plainValue = "data"; @@ -90,6 +141,21 @@ describe("EncryptService", () => { ); }); + it("throws if type 0 key provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const key = new SymmetricCryptoKey(makeStaticByteArray(32)); + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + + await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + + await expect(encryptService.encryptToBytes(plainValue, mock32Key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + }); + it("encrypts data with provided Aes256Cbc key and returns correct encbuffer", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0)); const iv = makeStaticByteArray(16, 80);