1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 13:10:17 +00:00

Add new encrypt service functions

This commit is contained in:
Bernd Schoolmann
2025-04-24 13:49:51 +02:00
parent fe3e6fd198
commit 6624d78a04
3 changed files with 276 additions and 61 deletions

View File

@@ -8,17 +8,99 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
export abstract class EncryptService {
/**
* @deprecated
* Encrypts a string or Uint8Array to an EncString
* @param plainValue - The value to encrypt
* @param key - The key to encrypt the value with
*/
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
/**
* @deprecated
* Encrypts a value to a Uint8Array
* @param plainValue - The value to encrypt
* @param key - The key to encrypt the value with
*/
abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
/**
* @deprecated
* Decrypts an EncString to a string
* @param encString - The EncString to decrypt
* @param key - The key to decrypt the EncString with
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
* @returns The decrypted string
*/
abstract decryptToUtf8(
encString: EncString,
key: SymmetricCryptoKey,
decryptTrace?: string,
): Promise<string>;
/**
* @deprecated
* Decrypts an Encrypted object to a Uint8Array
* @param encThing - The Encrypted object to decrypt
* @param key - The key to decrypt the Encrypted object with
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
* @deprecated
* @returns The decrypted Uint8Array
*/
abstract decryptToBytes(
encThing: Encrypted,
key: SymmetricCryptoKey,
decryptTrace?: string,
): Promise<Uint8Array | null>;
/**
* @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed
* @param items The items to decrypt
* @param key The key to decrypt the items with
*/
abstract decryptItems<T extends InitializerMetadata>(
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]>;
/**
* Encrypts a string to an EncString
* @param plainValue - The value to encrypt
* @param key - The key to encrypt the value with
*/
abstract encryptString(plainValue: string, key: SymmetricCryptoKey): Promise<EncString>;
/**
* Encrypts bytes to an EncString
* @param plainValue - The value to encrypt
* @param key - The key to encrypt the value with
*/
abstract encryptBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
/**
* Encrypts a value to a Uint8Array
* @param plainValue - The value to encrypt
* @param key - The key to encrypt the value with
*/
abstract encryptFileData(
plainValue: Uint8Array,
key: SymmetricCryptoKey,
): Promise<EncArrayBuffer>;
/**
* Decrypts an EncString to a string
* @param plainValue - The value to encrypt
* @param key - The key to encrypt the value with
*/
abstract decryptString(encString: EncString, key: SymmetricCryptoKey): Promise<string>;
/**
* Decrypts an EncString to a Uint8Array
* @param plainValue - The value to encrypt
* @param key - The key to encrypt the value with
* @deprecated Bytes are not the right abstraction to encrypt in. Use e.g. key wrapping or file encryption instead
*/
abstract decryptBytes(encString: EncString, key: SymmetricCryptoKey): Promise<Uint8Array>;
/**
* Decrypts an encarraybuffer to a Uint8Array
* @param encBuffer - The buffer to decrypt
* @param key - The key to encrypt the value with
*/
abstract decryptFileData(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<Uint8Array>;
/**
* Wraps a decapsulation key (Private key) with a symmetric key
@@ -52,31 +134,35 @@ export abstract class EncryptService {
): Promise<EncString>;
/**
* Decrypts an EncString to a string
* @param encString - The EncString to decrypt
* @param key - The key to decrypt the EncString with
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
* @returns The decrypted string
* Unwraps a decapsulation key (Private key) with a symmetric key
* @see {@link https://en.wikipedia.org/wiki/Key_wrap}
* @param decapsulationKeyPcks8 - The private key in PKCS8 format
* @param wrappingKey - The symmetric key to wrap the private key with
*/
abstract decryptToUtf8(
encString: EncString,
key: SymmetricCryptoKey,
decryptTrace?: string,
): Promise<string>;
abstract unwrapDecapsulationKey(
wrappedDecapsulationKey: EncString,
wrappingKey: SymmetricCryptoKey,
): Promise<Uint8Array>;
/**
* Decrypts an Encrypted object to a Uint8Array
* @param encThing - The Encrypted object to decrypt
* @param key - The key to decrypt the Encrypted object with
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
* @returns The decrypted Uint8Array
* Wraps an encapsulation key (Public key) with a symmetric key
* @see {@link https://en.wikipedia.org/wiki/Key_wrap}
* @param encapsulationKeySpki - The public key in SPKI format
* @param wrappingKey - The symmetric key to wrap the public key with
*/
abstract decryptToBytes(
encThing: Encrypted,
key: SymmetricCryptoKey,
decryptTrace?: string,
): Promise<Uint8Array | null>;
abstract unwrapEncapsulationKey(
wrappedEncapsulationKey: EncString,
wrappingKey: SymmetricCryptoKey,
): Promise<Uint8Array>;
/**
* Unwraps a symmetric key with another symmetric key
* @see {@link https://en.wikipedia.org/wiki/Key_wrap}
* @param keyToBeWrapped - The symmetric key to wrap
* @param wrappingKey - The symmetric key to wrap the encapsulated key with
*/
abstract unwrapSymmetricKey(
keyToBeUnwrapped: EncString,
wrappingKey: SymmetricCryptoKey,
): Promise<SymmetricCryptoKey>;
/**
* Encapsulates a symmetric key with an asymmetric public key
@@ -100,6 +186,7 @@ export abstract class EncryptService {
encryptedSharedKey: EncString,
decapsulationKey: Uint8Array,
): Promise<SymmetricCryptoKey>;
/**
* @deprecated Use @see {@link encapsulateKeyUnsigned} instead
* @param data - The data to encrypt
@@ -112,15 +199,7 @@ export abstract class EncryptService {
* @param privateKey - The privateKey to decrypt with
*/
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
/**
* @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed
* @param items The items to decrypt
* @param key The key to decrypt the items with
*/
abstract decryptItems<T extends InitializerMetadata>(
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]>;
/**
* Generates a base64-encoded hash of the given value
* @param value The value to hash

View File

@@ -36,31 +36,29 @@ export class EncryptServiceImplementation implements EncryptService {
protected logMacFailures: boolean,
) {}
// Handle updating private properties to turn on/off feature flags.
onServerConfigChange(newConfig: ServerConfig): void {
this.blockType0 = getFeatureFlagValue(newConfig, FeatureFlag.PM17987_BlockType0);
// Proxy functions; Their implementation are temporary before moving at this level to the SDK
async encryptString(plainValue: string, key: SymmetricCryptoKey): Promise<EncString> {
return this.encrypt(plainValue, key);
}
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
if (key == null) {
throw new Error("No encryption key provided.");
}
async encryptBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
return this.encrypt(plainValue, key);
}
if (this.blockType0) {
if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) {
throw new Error("Type 0 encryption is not supported.");
}
}
async encryptFileData(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
return this.encryptToBytes(plainValue, key);
}
if (plainValue == null) {
return Promise.resolve(null);
}
async decryptString(encString: EncString, key: SymmetricCryptoKey): Promise<string> {
return this.decryptToUtf8(encString, key);
}
if (typeof plainValue === "string") {
return this.encryptUint8Array(Utils.fromUtf8ToArray(plainValue), key);
} else {
return this.encryptUint8Array(plainValue, key);
}
async decryptBytes(encString: EncString, key: SymmetricCryptoKey): Promise<Uint8Array> {
return this.decryptToBytes(encString, key);
}
async decryptFileData(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<Uint8Array> {
return this.decryptToBytes(encBuffer, key);
}
async wrapDecapsulationKey(
@@ -108,6 +106,57 @@ export class EncryptServiceImplementation implements EncryptService {
return await this.encryptUint8Array(keyToBeWrapped.key, wrappingKey);
}
async unwrapDecapsulationKey(
wrappedDecapsulationKey: EncString,
wrappingKey: SymmetricCryptoKey,
): Promise<Uint8Array> {
return this.decryptBytes(wrappedDecapsulationKey, wrappingKey);
}
async unwrapEncapsulationKey(
wrappedEncapsulationKey: EncString,
wrappingKey: SymmetricCryptoKey,
): Promise<Uint8Array> {
return this.decryptBytes(wrappedEncapsulationKey, wrappingKey);
}
async unwrapSymmetricKey(
keyToBeUnwrapped: EncString,
wrappingKey: SymmetricCryptoKey,
): Promise<SymmetricCryptoKey> {
return new SymmetricCryptoKey(await this.decryptBytes(keyToBeUnwrapped, wrappingKey));
}
async hash(value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512"): Promise<string> {
const hashArray = await this.cryptoFunctionService.hash(value, algorithm);
return Utils.fromBufferToB64(hashArray);
}
// Handle updating private properties to turn on/off feature flags.
onServerConfigChange(newConfig: ServerConfig): void {
this.blockType0 = getFeatureFlagValue(newConfig, FeatureFlag.PM17987_BlockType0);
}
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (this.blockType0) {
if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) {
throw new Error("Type 0 encryption is not supported.");
}
}
if (plainValue == null) {
return Promise.resolve(null);
}
if (typeof plainValue === "string") {
return this.encryptUint8Array(Utils.fromUtf8ToArray(plainValue), key);
} else {
return this.encryptUint8Array(plainValue, key);
}
}
private async encryptUint8Array(
plainValue: Uint8Array,
key: SymmetricCryptoKey,
@@ -339,11 +388,6 @@ export class EncryptServiceImplementation implements EncryptService {
return results;
}
async hash(value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512"): Promise<string> {
const hashArray = await this.cryptoFunctionService.hash(value, algorithm);
return Utils.fromBufferToB64(hashArray);
}
private async aesEncrypt(data: Uint8Array, key: Aes256CbcHmacKey): Promise<EncryptedObject> {
const obj = new EncryptedObject();
obj.iv = await this.cryptoFunctionService.randomBytes(16);

View File

@@ -152,6 +152,8 @@ describe("EncryptService", () => {
});
});
describe("unwrapSymmetricKey", () => {});
describe("onServerConfigChange", () => {
const newConfig = mock<ServerConfig>();
@@ -264,7 +266,7 @@ describe("EncryptService", () => {
const plainValue = makeStaticByteArray(16, 1);
it("throws if no key is provided", () => {
return expect(encryptService.encryptToBytes(plainValue, null)).rejects.toThrow(
return expect(encryptService.encryptFileData(plainValue, null)).rejects.toThrow(
"No encryption key",
);
});
@@ -279,11 +281,11 @@ describe("EncryptService", () => {
encryptionKey: mock32Key.key,
});
await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow(
await expect(encryptService.encryptFileData(plainValue, key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
await expect(encryptService.encryptToBytes(plainValue, mock32Key)).rejects.toThrow(
await expect(encryptService.encryptFileData(plainValue, mock32Key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
});
@@ -295,7 +297,7 @@ describe("EncryptService", () => {
cryptoFunctionService.randomBytes.mockResolvedValue(iv as CsprngArray);
cryptoFunctionService.aesEncrypt.mockResolvedValue(cipherText);
const actual = await encryptService.encryptToBytes(plainValue, key);
const actual = await encryptService.encryptFileData(plainValue, key);
const expectedBytes = new Uint8Array(1 + iv.byteLength + cipherText.byteLength);
expectedBytes.set([EncryptionType.AesCbc256_B64]);
expectedBytes.set(iv, 1);
@@ -313,7 +315,7 @@ describe("EncryptService", () => {
cryptoFunctionService.aesEncrypt.mockResolvedValue(cipherText);
cryptoFunctionService.hmac.mockResolvedValue(mac);
const actual = await encryptService.encryptToBytes(plainValue, key);
const actual = await encryptService.encryptFileData(plainValue, key);
const expectedBytes = new Uint8Array(
1 + iv.byteLength + mac.byteLength + cipherText.byteLength,
);
@@ -543,6 +545,96 @@ describe("EncryptService", () => {
});
});
describe("encryptString", () => {
it("is a proxy to encrypt", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = "data";
encryptService.encrypt = jest.fn();
await encryptService.encryptString(plainValue, key);
expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key);
});
});
describe("encryptBytes", () => {
it("is a proxy to encrypt", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = makeStaticByteArray(16, 1);
encryptService.encrypt = jest.fn();
await encryptService.encryptBytes(plainValue, key);
expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key);
});
});
describe("encryptFileData", () => {
it("is a proxy to encryptToBytes", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = makeStaticByteArray(16, 1);
encryptService.encryptToBytes = jest.fn();
await encryptService.encryptFileData(plainValue, key);
expect(encryptService.encryptToBytes).toHaveBeenCalledWith(plainValue, key);
});
});
describe("decryptString", () => {
it("is a proxy to decryptToUtf8", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptToUtf8 = jest.fn();
await encryptService.decryptString(encString, key);
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key);
});
});
describe("decryptBytes", () => {
it("is a proxy to decryptToBytes", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptToBytes = jest.fn();
await encryptService.decryptBytes(encString, key);
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key);
});
});
describe("decryptFileData", () => {
it("is a proxy to decrypt", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncArrayBuffer(makeStaticByteArray(60, EncryptionType.AesCbc256_B64));
encryptService.decryptToBytes = jest.fn();
await encryptService.decryptFileData(encString, key);
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key);
});
});
describe("unwrapDecapsulationKey", () => {
it("is a proxy to decryptBytes", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptBytes = jest.fn();
await encryptService.unwrapDecapsulationKey(encString, key);
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
});
});
describe("unwrapEncapsulationKey", () => {
it("is a proxy to decryptBytes", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptBytes = jest.fn();
await encryptService.unwrapEncapsulationKey(encString, key);
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
});
});
describe("unwrapSymmetricKey", () => {
it("is a proxy to decryptBytes", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptBytes = jest.fn();
await encryptService.unwrapSymmetricKey(encString, key);
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
});
});
describe("rsa", () => {
const data = makeStaticByteArray(64, 100);
const testKey = new SymmetricCryptoKey(data);