1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

[PM-17987] Add feature flag (#13991)

* Add feature flag

* Add unit tests.
This commit is contained in:
Thomas Avery
2025-04-14 12:47:09 -05:00
committed by GitHub
parent ac1210a7ed
commit 95ea1b22ae
3 changed files with 90 additions and 3 deletions

View File

@@ -39,6 +39,7 @@ export enum FeatureFlag {
PrivateKeyRegeneration = "pm-12241-private-key-regeneration", PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
UserKeyRotationV2 = "userkey-rotation-v2", UserKeyRotationV2 = "userkey-rotation-v2",
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
PM17987_BlockType0 = "pm-17987-block-type-0",
/* Tools */ /* Tools */
ItemShare = "item-share", ItemShare = "item-share",
@@ -121,6 +122,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.PrivateKeyRegeneration]: FALSE,
[FeatureFlag.UserKeyRotationV2]: FALSE, [FeatureFlag.UserKeyRotationV2]: FALSE,
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
[FeatureFlag.PM17987_BlockType0]: FALSE,
/* Platform */ /* Platform */
[FeatureFlag.IpcChannelFramework]: FALSE, [FeatureFlag.IpcChannelFramework]: FALSE,

View File

@@ -19,10 +19,17 @@ import {
SymmetricCryptoKey, SymmetricCryptoKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; } 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 { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { EncryptService } from "../abstractions/encrypt.service"; import { EncryptService } from "../abstractions/encrypt.service";
export class EncryptServiceImplementation implements EncryptService { export class EncryptServiceImplementation implements EncryptService {
private blockType0: boolean = DefaultFeatureFlagValue[FeatureFlag.PM17987_BlockType0];
constructor( constructor(
protected cryptoFunctionService: CryptoFunctionService, protected cryptoFunctionService: CryptoFunctionService,
protected logService: LogService, protected logService: LogService,
@@ -31,7 +38,7 @@ export class EncryptServiceImplementation implements EncryptService {
// Handle updating private properties to turn on/off feature flags. // Handle updating private properties to turn on/off feature flags.
onServerConfigChange(newConfig: ServerConfig): void { onServerConfigChange(newConfig: ServerConfig): void {
return; this.blockType0 = getFeatureFlagValue(newConfig, FeatureFlag.PM17987_BlockType0);
} }
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> { async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
@@ -39,6 +46,12 @@ export class EncryptServiceImplementation implements EncryptService {
throw new Error("No encryption key provided."); 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) { if (plainValue == null) {
return Promise.resolve(null); return Promise.resolve(null);
} }
@@ -70,6 +83,12 @@ export class EncryptServiceImplementation implements EncryptService {
throw new Error("No encryption key provided."); 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(); const innerKey = key.inner();
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const encValue = await this.aesEncrypt(plainValue, innerKey); const encValue = await this.aesEncrypt(plainValue, innerKey);

View File

@@ -10,6 +10,8 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
import { CsprngArray } from "@bitwarden/common/types/csprng"; import { CsprngArray } from "@bitwarden/common/types/csprng";
import { makeStaticByteArray } from "../../../../spec"; 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"; import { EncryptServiceImplementation } from "./encrypt.service.implementation";
@@ -26,17 +28,65 @@ describe("EncryptService", () => {
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true); encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
}); });
describe("onServerConfigChange", () => {
const newConfig = mock<ServerConfig>();
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", () => { describe("encrypt", () => {
it("throws if no key is provided", () => { it("throws if no key is provided", () => {
return expect(encryptService.encrypt(null, null)).rejects.toThrow( return expect(encryptService.encrypt(null, null)).rejects.toThrow(
"No encryption key provided.", "No encryption key provided.",
); );
}); });
it("returns null if no data is provided", async () => {
const key = mock<SymmetricCryptoKey>(); 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<SymmetricCryptoKey>();
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); const actual = await encryptService.encrypt(null, key);
expect(actual).toBeNull(); expect(actual).toBeNull();
}); });
it("creates an EncString for Aes256Cbc", async () => { it("creates an EncString for Aes256Cbc", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const plainValue = "data"; const plainValue = "data";
@@ -53,6 +103,7 @@ describe("EncryptService", () => {
expect(Utils.fromB64ToArray(result.data).length).toEqual(4); expect(Utils.fromB64ToArray(result.data).length).toEqual(4);
expect(Utils.fromB64ToArray(result.iv).length).toEqual(16); expect(Utils.fromB64ToArray(result.iv).length).toEqual(16);
}); });
it("creates an EncString for Aes256Cbc_HmacSha256_B64", async () => { it("creates an EncString for Aes256Cbc_HmacSha256_B64", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64)); const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = "data"; 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<SymmetricCryptoKey>();
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 () => { it("encrypts data with provided Aes256Cbc key and returns correct encbuffer", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0)); const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const iv = makeStaticByteArray(16, 80); const iv = makeStaticByteArray(16, 80);