mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 00:33:44 +00:00
[PM-19603] Change asymmetric interface to only allow key encapsulation (#14046)
* Change asymmetric interface to only allow key encapsulation * Fix naming * Clean up naming * Update libs/common/src/key-management/crypto/abstractions/encrypt.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/crypto/abstractions/encrypt.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Fix test --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
@@ -31,8 +31,10 @@ export class EncryptedOrganizationKey implements BaseEncryptedOrganizationKey {
|
||||
constructor(private key: string) {}
|
||||
|
||||
async decrypt(encryptService: EncryptService, privateKey: UserPrivateKey) {
|
||||
const decValue = await encryptService.rsaDecrypt(this.encryptedOrganizationKey, privateKey);
|
||||
return new SymmetricCryptoKey(decValue) as OrgKey;
|
||||
return (await encryptService.decapsulateKeyUnsigned(
|
||||
this.encryptedOrganizationKey,
|
||||
privateKey,
|
||||
)) as OrgKey;
|
||||
}
|
||||
|
||||
get encryptedOrganizationKey() {
|
||||
|
||||
@@ -100,7 +100,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
||||
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
|
||||
|
||||
keyService.getUserKey.mockResolvedValue({ key: "key" } as any);
|
||||
encryptService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedKey as any);
|
||||
|
||||
await service.enroll("orgId");
|
||||
|
||||
@@ -122,7 +122,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
||||
};
|
||||
const encryptedKey = { encryptedString: "encryptedString" };
|
||||
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
|
||||
encryptService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedKey as any);
|
||||
|
||||
await service.enroll("orgId", "userId", { key: "key" } as any);
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ export class PasswordResetEnrollmentServiceImplementation
|
||||
userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
|
||||
userKey = userKey ?? (await this.keyService.getUserKey(userId));
|
||||
// RSA Encrypt user's userKey.key with organization public key
|
||||
const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, orgPublicKey);
|
||||
const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(userKey, orgPublicKey);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
|
||||
|
||||
@@ -35,7 +35,38 @@ export abstract class EncryptService {
|
||||
key: SymmetricCryptoKey,
|
||||
decryptTrace?: string,
|
||||
): Promise<Uint8Array | null>;
|
||||
|
||||
/**
|
||||
* Encapsulates a symmetric key with an asymmetric public key
|
||||
* Note: This does not establish sender authenticity
|
||||
* @param sharedKey - The symmetric key that is to be shared
|
||||
* @param encapsulationKey - The encapsulation key (public key) of the receiver that the key is shared with
|
||||
*/
|
||||
abstract encapsulateKeyUnsigned(
|
||||
sharedKey: SymmetricCryptoKey,
|
||||
encapsulationKey: Uint8Array,
|
||||
): Promise<EncString>;
|
||||
/**
|
||||
* Decapsulates a shared symmetric key with an asymmetric private key
|
||||
* Note: This does not establish sender authenticity
|
||||
* @param encryptedSharedKey - The encrypted shared symmetric key
|
||||
* @param decapsulationKey - The key to decapsulate with (private key)
|
||||
*/
|
||||
abstract decapsulateKeyUnsigned(
|
||||
encryptedSharedKey: EncString,
|
||||
decapsulationKey: Uint8Array,
|
||||
): Promise<SymmetricCryptoKey>;
|
||||
/**
|
||||
* @deprecated Use encapsulateKeyUnsigned instead
|
||||
* @param data - The data to encrypt
|
||||
* @param publicKey - The public key to encrypt with
|
||||
*/
|
||||
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
|
||||
/**
|
||||
* @deprecated Use decapsulateKeyUnsigned instead
|
||||
* @param data - The ciphertext to decrypt
|
||||
* @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
|
||||
|
||||
@@ -235,42 +235,22 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
}
|
||||
}
|
||||
|
||||
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
|
||||
if (data == null) {
|
||||
throw new Error("No data provided for encryption.");
|
||||
async encapsulateKeyUnsigned(
|
||||
sharedKey: SymmetricCryptoKey,
|
||||
encapsulationKey: Uint8Array,
|
||||
): Promise<EncString> {
|
||||
if (sharedKey == null) {
|
||||
throw new Error("No sharedKey provided for encapsulation");
|
||||
}
|
||||
|
||||
if (publicKey == null) {
|
||||
throw new Error("No public key provided for encryption.");
|
||||
}
|
||||
const encrypted = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1");
|
||||
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encrypted));
|
||||
return await this.rsaEncrypt(sharedKey.toEncoded(), encapsulationKey);
|
||||
}
|
||||
|
||||
async rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array> {
|
||||
if (data == null) {
|
||||
throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption.");
|
||||
}
|
||||
|
||||
let algorithm: "sha1" | "sha256";
|
||||
switch (data.encryptionType) {
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
algorithm = "sha1";
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
||||
algorithm = "sha256";
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid encryption type.");
|
||||
}
|
||||
|
||||
if (privateKey == null) {
|
||||
throw new Error("[Encrypt service] rsaDecrypt: No private key provided for decryption.");
|
||||
}
|
||||
|
||||
return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm);
|
||||
async decapsulateKeyUnsigned(
|
||||
encryptedSharedKey: EncString,
|
||||
decapsulationKey: Uint8Array,
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
const keyBytes = await this.rsaDecrypt(encryptedSharedKey, decapsulationKey);
|
||||
return new SymmetricCryptoKey(keyBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -341,4 +321,42 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
this.logDecryptError(msg, keyEncType, dataEncType, decryptContext);
|
||||
}
|
||||
}
|
||||
|
||||
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
|
||||
if (data == null) {
|
||||
throw new Error("No data provided for encryption.");
|
||||
}
|
||||
|
||||
if (publicKey == null) {
|
||||
throw new Error("No public key provided for encryption.");
|
||||
}
|
||||
const encrypted = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1");
|
||||
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encrypted));
|
||||
}
|
||||
|
||||
async rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array> {
|
||||
if (data == null) {
|
||||
throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption.");
|
||||
}
|
||||
|
||||
let algorithm: "sha1" | "sha256";
|
||||
switch (data.encryptionType) {
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
algorithm = "sha1";
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
||||
algorithm = "sha256";
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid encryption type.");
|
||||
}
|
||||
|
||||
if (privateKey == null) {
|
||||
throw new Error("[Encrypt service] rsaDecrypt: No private key provided for decryption.");
|
||||
}
|
||||
|
||||
return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,7 +412,8 @@ describe("EncryptService", () => {
|
||||
});
|
||||
|
||||
describe("rsa", () => {
|
||||
const data = makeStaticByteArray(10, 100);
|
||||
const data = makeStaticByteArray(64, 100);
|
||||
const testKey = new SymmetricCryptoKey(data);
|
||||
const encryptedData = makeStaticByteArray(10, 150);
|
||||
const publicKey = makeStaticByteArray(10, 200);
|
||||
const privateKey = makeStaticByteArray(10, 250);
|
||||
@@ -422,22 +423,26 @@ describe("EncryptService", () => {
|
||||
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(data));
|
||||
}
|
||||
|
||||
describe("rsaEncrypt", () => {
|
||||
describe("encapsulateKeyUnsigned", () => {
|
||||
it("throws if no data is provided", () => {
|
||||
return expect(encryptService.rsaEncrypt(null, publicKey)).rejects.toThrow("No data");
|
||||
return expect(encryptService.encapsulateKeyUnsigned(null, publicKey)).rejects.toThrow(
|
||||
"No sharedKey provided for encapsulation",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if no public key is provided", () => {
|
||||
return expect(encryptService.rsaEncrypt(data, null)).rejects.toThrow("No public key");
|
||||
return expect(encryptService.encapsulateKeyUnsigned(testKey, null)).rejects.toThrow(
|
||||
"No public key",
|
||||
);
|
||||
});
|
||||
|
||||
it("encrypts data with provided key", async () => {
|
||||
cryptoFunctionService.rsaEncrypt.mockResolvedValue(encryptedData);
|
||||
|
||||
const actual = await encryptService.rsaEncrypt(data, publicKey);
|
||||
const actual = await encryptService.encapsulateKeyUnsigned(testKey, publicKey);
|
||||
|
||||
expect(cryptoFunctionService.rsaEncrypt).toBeCalledWith(
|
||||
expect.toEqualBuffer(data),
|
||||
expect.toEqualBuffer(testKey.key),
|
||||
expect.toEqualBuffer(publicKey),
|
||||
"sha1",
|
||||
);
|
||||
@@ -447,13 +452,17 @@ describe("EncryptService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("rsaDecrypt", () => {
|
||||
describe("decapsulateKeyUnsigned", () => {
|
||||
it("throws if no data is provided", () => {
|
||||
return expect(encryptService.rsaDecrypt(null, privateKey)).rejects.toThrow("No data");
|
||||
return expect(encryptService.decapsulateKeyUnsigned(null, privateKey)).rejects.toThrow(
|
||||
"No data",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if no private key is provided", () => {
|
||||
return expect(encryptService.rsaDecrypt(encString, null)).rejects.toThrow("No private key");
|
||||
return expect(encryptService.decapsulateKeyUnsigned(encString, null)).rejects.toThrow(
|
||||
"No private key",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([EncryptionType.AesCbc256_B64, EncryptionType.AesCbc256_HmacSha256_B64])(
|
||||
@@ -461,16 +470,16 @@ describe("EncryptService", () => {
|
||||
async (encType) => {
|
||||
encString.encryptionType = encType;
|
||||
|
||||
await expect(encryptService.rsaDecrypt(encString, privateKey)).rejects.toThrow(
|
||||
"Invalid encryption type",
|
||||
);
|
||||
await expect(
|
||||
encryptService.decapsulateKeyUnsigned(encString, privateKey),
|
||||
).rejects.toThrow("Invalid encryption type");
|
||||
},
|
||||
);
|
||||
|
||||
it("decrypts data with provided key", async () => {
|
||||
cryptoFunctionService.rsaDecrypt.mockResolvedValue(data);
|
||||
|
||||
const actual = await encryptService.rsaDecrypt(makeEncString(data), privateKey);
|
||||
const actual = await encryptService.decapsulateKeyUnsigned(makeEncString(data), privateKey);
|
||||
|
||||
expect(cryptoFunctionService.rsaDecrypt).toBeCalledWith(
|
||||
expect.toEqualBuffer(data),
|
||||
@@ -478,7 +487,7 @@ describe("EncryptService", () => {
|
||||
"sha1",
|
||||
);
|
||||
|
||||
expect(actual).toEqualBuffer(data);
|
||||
expect(actual.key).toEqualBuffer(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,7 +161,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
deviceKeyEncryptedDevicePrivateKey,
|
||||
] = await Promise.all([
|
||||
// Encrypt user key with the DevicePublicKey
|
||||
this.encryptService.rsaEncrypt(userKey.key, devicePublicKey),
|
||||
this.encryptService.encapsulateKeyUnsigned(userKey, devicePublicKey),
|
||||
|
||||
// Encrypt devicePublicKey with user key
|
||||
this.encryptService.encrypt(devicePublicKey, userKey),
|
||||
@@ -285,8 +285,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
);
|
||||
|
||||
// Encrypt the brand new user key with the now-decrypted public key for the device
|
||||
const encryptedNewUserKey = await this.encryptService.rsaEncrypt(
|
||||
newUserKey.key,
|
||||
const encryptedNewUserKey = await this.encryptService.encapsulateKeyUnsigned(
|
||||
newUserKey,
|
||||
decryptedDevicePublicKey,
|
||||
);
|
||||
|
||||
@@ -401,12 +401,12 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
);
|
||||
|
||||
// Attempt to decrypt encryptedUserDataKey with devicePrivateKey
|
||||
const userKey = await this.encryptService.rsaDecrypt(
|
||||
const userKey = await this.encryptService.decapsulateKeyUnsigned(
|
||||
new EncString(encryptedUserKey.encryptedString),
|
||||
devicePrivateKey,
|
||||
);
|
||||
|
||||
return new SymmetricCryptoKey(userKey) as UserKey;
|
||||
return userKey as UserKey;
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
|
||||
@@ -416,7 +416,7 @@ describe("deviceTrustService", () => {
|
||||
.mockResolvedValue(mockUserKey);
|
||||
|
||||
cryptoSvcRsaEncryptSpy = jest
|
||||
.spyOn(encryptService, "rsaEncrypt")
|
||||
.spyOn(encryptService, "encapsulateKeyUnsigned")
|
||||
.mockResolvedValue(mockDevicePublicKeyEncryptedUserKey);
|
||||
|
||||
encryptServiceEncryptSpy = jest
|
||||
@@ -449,8 +449,8 @@ describe("deviceTrustService", () => {
|
||||
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// RsaEncrypt must be called w/ a user key array buffer of 64 bytes
|
||||
const userKeyKey: Uint8Array = cryptoSvcRsaEncryptSpy.mock.calls[0][0];
|
||||
expect(userKeyKey.byteLength).toBe(64);
|
||||
const userKey = cryptoSvcRsaEncryptSpy.mock.calls[0][0];
|
||||
expect(userKey.key.byteLength).toBe(64);
|
||||
|
||||
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -610,7 +610,7 @@ describe("deviceTrustService", () => {
|
||||
mockUserId,
|
||||
mockEncryptedDevicePrivateKey,
|
||||
mockEncryptedUserKey,
|
||||
mockDeviceKey,
|
||||
null,
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
@@ -621,8 +621,8 @@ describe("deviceTrustService", () => {
|
||||
.spyOn(encryptService, "decryptToBytes")
|
||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
|
||||
const rsaDecryptSpy = jest
|
||||
.spyOn(encryptService, "rsaDecrypt")
|
||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
|
||||
.spyOn(encryptService, "decapsulateKeyUnsigned")
|
||||
.mockResolvedValue(new SymmetricCryptoKey(new Uint8Array(userKeyBytesLength)));
|
||||
|
||||
const result = await deviceTrustService.decryptUserKeyWithDeviceKey(
|
||||
mockUserId,
|
||||
@@ -863,9 +863,9 @@ describe("deviceTrustService", () => {
|
||||
});
|
||||
|
||||
// Mock the encryption of the new user key with the decrypted public key
|
||||
encryptService.rsaEncrypt.mockImplementationOnce((data, publicKey) => {
|
||||
expect(data.byteLength).toBe(64); // New key should also be 64 bytes
|
||||
expect(new Uint8Array(data)[0]).toBe(FakeNewUserKeyMarker); // New key should have the first byte be '1';
|
||||
encryptService.encapsulateKeyUnsigned.mockImplementationOnce((data, publicKey) => {
|
||||
expect(data.key.byteLength).toBe(64); // New key should also be 64 bytes
|
||||
expect(new Uint8Array(data.key)[0]).toBe(FakeNewUserKeyMarker); // New key should have the first byte be '1';
|
||||
|
||||
expect(new Uint8Array(publicKey)[0]).toBe(FakeDecryptedPublicKeyMarker);
|
||||
return Promise.resolve(new EncString("4.ZW5jcnlwdGVkdXNlcg=="));
|
||||
|
||||
Reference in New Issue
Block a user