1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-21 03:43:58 +00:00

Merge branch 'main' into vault/PM-12423

This commit is contained in:
gbubemismith
2025-04-14 22:44:40 -04:00
200 changed files with 2072 additions and 2488 deletions

View File

@@ -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",
@@ -56,6 +57,9 @@ export enum FeatureFlag {
SecurityTasks = "security-tasks",
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
CipherKeyEncryption = "cipher-key-encryption",
/* Platform */
IpcChannelFramework = "ipc-channel-framework",
}
export type AllowedFeatureFlagTypes = boolean | number | string;
@@ -120,6 +124,10 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
[FeatureFlag.UserKeyRotationV2]: FALSE,
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
[FeatureFlag.PM17987_BlockType0]: FALSE,
/* Platform */
[FeatureFlag.IpcChannelFramework]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;

View File

@@ -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<EncString> {
@@ -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);

View File

@@ -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<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", () => {
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<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);
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<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 () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const iv = makeStaticByteArray(16, 80);

View File

@@ -189,6 +189,33 @@ describe("IntegrationContext", () => {
expect(result).toBe("");
});
it("extracts the hostname when extractHostname is true", () => {
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
const result = context.website(
{ website: "https://www.example.com/path" },
{ extractHostname: true },
);
expect(result).toBe("www.example.com");
});
it("falls back to the full URL when Utils.getHost cannot extract the hostname", () => {
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
const result = context.website({ website: "invalid-url" }, { extractHostname: true });
expect(result).toBe("invalid-url");
});
it("truncates the website to maxLength", () => {
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
const result = context.website({ website: "www.example.com" }, { maxLength: 3 });
expect(result).toBe("www");
});
});
describe("generatedBy", () => {
@@ -211,5 +238,15 @@ describe("IntegrationContext", () => {
expect(result).toBe("result");
expect(i18n.t).toHaveBeenCalledWith("forwarderGeneratedByWithWebsite", "www.example.com");
});
it("truncates generated text to maxLength", () => {
const context = new IntegrationContext(EXAMPLE_META, null, i18n);
i18n.t.mockReturnValue("This is the result text");
const result = context.generatedBy({ website: null }, { maxLength: 4 });
expect(result).toBe("This");
expect(i18n.t).toHaveBeenCalledWith("forwarderGeneratedBy", "");
});
});
});

View File

@@ -79,24 +79,40 @@ export class IntegrationContext<Settings extends object> {
/** look up the website the integration is working with.
* @param request supplies information about the state of the extension site
* @param options optional parameters
* @param options.extractHostname when `true`, tries to extract the hostname from the website URL, returns full URL otherwise
* @param options.maxLength limits the length of the return value
* @returns The website or an empty string if a website isn't available
* @remarks `website` is usually supplied when generating a credential from the vault
*/
website(request: IntegrationRequest) {
return request.website ?? "";
website(
request: IntegrationRequest,
options?: { extractHostname?: boolean; maxLength?: number },
) {
let url = request.website ?? "";
if (options?.extractHostname) {
url = Utils.getHost(url) ?? url;
}
return url.slice(0, options?.maxLength);
}
/** look up localized text indicating Bitwarden requested the forwarding address.
* @param request supplies information about the state of the extension site
* @param options optional parameters
* @param options.extractHostname when `true`, extracts the hostname from the website URL
* @param options.maxLength limits the length of the return value
* @returns localized text describing a generated forwarding address
*/
generatedBy(request: IntegrationRequest) {
const website = this.website(request);
generatedBy(
request: IntegrationRequest,
options?: { extractHostname?: boolean; maxLength?: number },
) {
const website = this.website(request, { extractHostname: options?.extractHostname ?? false });
const descriptionId =
website === "" ? "forwarderGeneratedBy" : "forwarderGeneratedByWithWebsite";
const description = this.i18n.t(descriptionId, website);
return description;
return description.slice(0, options?.maxLength);
}
}