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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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", "");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user