1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-19731] Refactor encrypt service to expose key wrapping (#14080)

* Refactor encrypt service to expose key wrapping

* Fix build

* Undo ts strict removal

* Fix wrong method being used to encrypt key material

* Rename parameters and remove todo

* Add summary to encrypt

* 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/services/encrypt.service.implementation.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/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>

* Update libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Add tests for unhappy paths

* Add test coverage

* Add links

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
Bernd Schoolmann
2025-04-22 15:56:39 +02:00
committed by GitHub
parent 2aeca29b20
commit e231286f37
21 changed files with 272 additions and 55 deletions

View File

@@ -1,5 +1,5 @@
{ {
"cSpell.words": ["Csprng", "decryptable", "Popout", "Reprompt", "takeuntil"], "cSpell.words": ["Csprng", "Decapsulation", "decryptable", "Popout", "Reprompt", "takeuntil"],
"search.exclude": { "search.exclude": {
"**/locales/[^e]*/messages.json": true, "**/locales/[^e]*/messages.json": true,
"**/locales/*[^n]/messages.json": true, "**/locales/*[^n]/messages.json": true,

View File

@@ -36,7 +36,7 @@ describe("RotateableKeySetService", () => {
keyService.makeKeyPair.mockResolvedValue(["publicKey", encryptedPrivateKey as any]); keyService.makeKeyPair.mockResolvedValue(["publicKey", encryptedPrivateKey as any]);
keyService.getUserKey.mockResolvedValue({ key: userKey.key } as any); keyService.getUserKey.mockResolvedValue({ key: userKey.key } as any);
encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedUserKey as any); encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedUserKey as any);
encryptService.encrypt.mockResolvedValue(encryptedPublicKey as any); encryptService.wrapEncapsulationKey.mockResolvedValue(encryptedPublicKey as any);
const result = await service.createKeySet(externalKey as any); const result = await service.createKeySet(externalKey as any);

View File

@@ -29,7 +29,10 @@ export class RotateableKeySetService {
userKey, userKey,
rawPublicKey, rawPublicKey,
); );
const encryptedPublicKey = await this.encryptService.encrypt(rawPublicKey, userKey); const encryptedPublicKey = await this.encryptService.wrapEncapsulationKey(
rawPublicKey,
userKey,
);
return new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey); return new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey);
} }
@@ -62,7 +65,10 @@ export class RotateableKeySetService {
if (publicKey == null) { if (publicKey == null) {
throw new Error("failed to rotate key set: could not decrypt public key"); throw new Error("failed to rotate key set: could not decrypt public key");
} }
const newEncryptedPublicKey = await this.encryptService.encrypt(publicKey, newUserKey); const newEncryptedPublicKey = await this.encryptService.wrapEncapsulationKey(
publicKey,
newUserKey,
);
const newEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned( const newEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
newUserKey, newUserKey,
publicKey, publicKey,

View File

@@ -92,6 +92,9 @@ describe("AcceptOrganizationInviteService", () => {
"orgPublicKey", "orgPublicKey",
{ encryptedString: "string" } as EncString, { encryptedString: "string" } as EncString,
]); ]);
encryptService.wrapDecapsulationKey.mockResolvedValue({
encryptedString: "string",
} as EncString);
encryptService.encrypt.mockResolvedValue({ encryptedString: "string" } as EncString); encryptService.encrypt.mockResolvedValue({ encryptedString: "string" } as EncString);
const invite = createOrgInvite({ initOrganization: true }); const invite = createOrgInvite({ initOrganization: true });

View File

@@ -812,7 +812,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
); );
const providerKey = await this.keyService.getProviderKey(this.providerId); const providerKey = await this.keyService.getProviderKey(this.providerId);
providerRequest.organizationCreateRequest.key = ( providerRequest.organizationCreateRequest.key = (
await this.encryptService.encrypt(orgKey.key, providerKey) await this.encryptService.wrapSymmetricKey(orgKey, providerKey)
).encryptedString; ).encryptedString;
const orgId = ( const orgId = (
await this.apiService.postProviderCreateOrganization(this.providerId, providerRequest) await this.apiService.postProviderCreateOrganization(this.providerId, providerRequest)

View File

@@ -183,7 +183,10 @@ describe("KeyRotationService", () => {
mockKeyService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash"); mockKeyService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash");
mockConfigService.getFeatureFlag.mockResolvedValue(true); mockConfigService.getFeatureFlag.mockResolvedValue(true);
mockEncryptService.encrypt.mockResolvedValue({ mockEncryptService.wrapSymmetricKey.mockResolvedValue({
encryptedString: "mockEncryptedData",
} as any);
mockEncryptService.wrapDecapsulationKey.mockResolvedValue({
encryptedString: "mockEncryptedData", encryptedString: "mockEncryptedData",
} as any); } as any);

View File

@@ -145,7 +145,9 @@ export class UserKeyRotationService {
const { privateKey, publicKey } = keyPair; const { privateKey, publicKey } = keyPair;
const accountKeysRequest = new AccountKeysRequest( const accountKeysRequest = new AccountKeysRequest(
(await this.encryptService.encrypt(privateKey, newUnencryptedUserKey)).encryptedString!, (
await this.encryptService.wrapDecapsulationKey(privateKey, newUnencryptedUserKey)
).encryptedString!,
Utils.fromBufferToB64(publicKey), Utils.fromBufferToB64(publicKey),
); );
@@ -427,6 +429,6 @@ export class UserKeyRotationService {
if (privateKey == null) { if (privateKey == null) {
throw new Error("No private key found for user key rotation"); throw new Error("No private key found for user key rotation");
} }
return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString; return (await this.encryptService.wrapDecapsulationKey(privateKey, newUserKey)).encryptedString;
} }
} }

View File

@@ -36,7 +36,7 @@ export class WebProviderService {
const orgKey = await this.keyService.getOrgKey(organizationId); const orgKey = await this.keyService.getOrgKey(organizationId);
const providerKey = await this.keyService.getProviderKey(providerId); const providerKey = await this.keyService.getProviderKey(providerId);
const encryptedOrgKey = await this.encryptService.encrypt(orgKey.key, providerKey); const encryptedOrgKey = await this.encryptService.wrapSymmetricKey(orgKey, providerKey);
const request = new ProviderAddOrganizationRequest(); const request = new ProviderAddOrganizationRequest();
request.organizationId = organizationId; request.organizationId = organizationId;
@@ -55,7 +55,7 @@ export class WebProviderService {
), ),
); );
const providerKey = await this.keyService.getProviderKey(providerId); const providerKey = await this.keyService.getProviderKey(providerId);
const encryptedOrgKey = await this.encryptService.encrypt(orgKey.key, providerKey); const encryptedOrgKey = await this.encryptService.wrapSymmetricKey(orgKey, providerKey);
await this.providerApiService.addOrganizationToProvider(providerId, { await this.providerApiService.addOrganizationToProvider(providerId, {
key: encryptedOrgKey.encryptedString, key: encryptedOrgKey.encryptedString,
organizationId, organizationId,
@@ -81,8 +81,8 @@ export class WebProviderService {
const providerKey = await this.keyService.getProviderKey(providerId); const providerKey = await this.keyService.getProviderKey(providerId);
const encryptedProviderKey = await this.encryptService.encrypt( const encryptedProviderKey = await this.encryptService.wrapSymmetricKey(
organizationKey.key, organizationKey,
providerKey, providerKey,
); );

View File

@@ -178,7 +178,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey); const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
newKeyPair = [ newKeyPair = [
existingUserPublicKeyB64, existingUserPublicKeyB64,
await this.encryptService.encrypt(existingUserPrivateKey, userKey[0]), await this.encryptService.wrapDecapsulationKey(existingUserPrivateKey, userKey[0]),
]; ];
} else { } else {
newKeyPair = await this.keyService.makeKeyPair(userKey[0]); newKeyPair = await this.keyService.makeKeyPair(userKey[0]);

View File

@@ -174,7 +174,8 @@ export class PinService implements PinServiceAbstraction {
); );
const kdfConfig = await this.kdfConfigService.getKdfConfig(); const kdfConfig = await this.kdfConfigService.getKdfConfig();
const pinKey = await this.makePinKey(pin, email, kdfConfig); const pinKey = await this.makePinKey(pin, email, kdfConfig);
return await this.encryptService.encrypt(userKey.key, pinKey);
return await this.encryptService.wrapSymmetricKey(userKey, pinKey);
} }
async storePinKeyEncryptedUserKey( async storePinKeyEncryptedUserKey(

View File

@@ -170,7 +170,7 @@ describe("PinService", () => {
await sut.createPinKeyEncryptedUserKey(mockPin, mockUserKey, mockUserId); await sut.createPinKeyEncryptedUserKey(mockPin, mockUserKey, mockUserId);
// Assert // Assert
expect(encryptService.encrypt).toHaveBeenCalledWith(mockUserKey.key, mockPinKey); expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockUserKey, mockPinKey);
}); });
}); });

View File

@@ -7,8 +7,50 @@ import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
export abstract class EncryptService { export abstract class EncryptService {
/**
* 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>; abstract encrypt(plainValue: string | 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 encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>; abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
/**
* Wraps 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 wrapDecapsulationKey(
decapsulationKeyPcks8: Uint8Array,
wrappingKey: SymmetricCryptoKey,
): Promise<EncString>;
/**
* 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 wrapEncapsulationKey(
encapsulationKeySpki: Uint8Array,
wrappingKey: SymmetricCryptoKey,
): Promise<EncString>;
/**
* Wraps 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 wrapSymmetricKey(
keyToBeWrapped: SymmetricCryptoKey,
wrappingKey: SymmetricCryptoKey,
): Promise<EncString>;
/** /**
* Decrypts an EncString to a string * Decrypts an EncString to a string
* @param encString - The EncString to decrypt * @param encString - The EncString to decrypt
@@ -39,6 +81,7 @@ export abstract class EncryptService {
/** /**
* Encapsulates a symmetric key with an asymmetric public key * Encapsulates a symmetric key with an asymmetric public key
* Note: This does not establish sender authenticity * Note: This does not establish sender authenticity
* @see {@link https://en.wikipedia.org/wiki/Key_encapsulation_mechanism}
* @param sharedKey - The symmetric key that is to be shared * @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 * @param encapsulationKey - The encapsulation key (public key) of the receiver that the key is shared with
*/ */
@@ -49,6 +92,7 @@ export abstract class EncryptService {
/** /**
* Decapsulates a shared symmetric key with an asymmetric private key * Decapsulates a shared symmetric key with an asymmetric private key
* Note: This does not establish sender authenticity * Note: This does not establish sender authenticity
* @see {@link https://en.wikipedia.org/wiki/Key_encapsulation_mechanism}
* @param encryptedSharedKey - The encrypted shared symmetric key * @param encryptedSharedKey - The encrypted shared symmetric key
* @param decapsulationKey - The key to decapsulate with (private key) * @param decapsulationKey - The key to decapsulate with (private key)
*/ */
@@ -57,13 +101,13 @@ export abstract class EncryptService {
decapsulationKey: Uint8Array, decapsulationKey: Uint8Array,
): Promise<SymmetricCryptoKey>; ): Promise<SymmetricCryptoKey>;
/** /**
* @deprecated Use encapsulateKeyUnsigned instead * @deprecated Use @see {@link encapsulateKeyUnsigned} instead
* @param data - The data to encrypt * @param data - The data to encrypt
* @param publicKey - The public key to encrypt with * @param publicKey - The public key to encrypt with
*/ */
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>; abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
/** /**
* @deprecated Use decapsulateKeyUnsigned instead * @deprecated Use @see {@link decapsulateKeyUnsigned} instead
* @param data - The ciphertext to decrypt * @param data - The ciphertext to decrypt
* @param privateKey - The privateKey to decrypt with * @param privateKey - The privateKey to decrypt with
*/ */

View File

@@ -56,22 +56,79 @@ export class EncryptServiceImplementation implements EncryptService {
return Promise.resolve(null); return Promise.resolve(null);
} }
let plainBuf: Uint8Array;
if (typeof plainValue === "string") { if (typeof plainValue === "string") {
plainBuf = Utils.fromUtf8ToArray(plainValue); return this.encryptUint8Array(Utils.fromUtf8ToArray(plainValue), key);
} else { } else {
plainBuf = plainValue; return this.encryptUint8Array(plainValue, key);
}
}
async wrapDecapsulationKey(
decapsulationKeyPkcs8: Uint8Array,
wrappingKey: SymmetricCryptoKey,
): Promise<EncString> {
if (decapsulationKeyPkcs8 == null) {
throw new Error("No decapsulation key provided for wrapping.");
}
if (wrappingKey == null) {
throw new Error("No wrappingKey provided for wrapping.");
}
return await this.encryptUint8Array(decapsulationKeyPkcs8, wrappingKey);
}
async wrapEncapsulationKey(
encapsulationKeySpki: Uint8Array,
wrappingKey: SymmetricCryptoKey,
): Promise<EncString> {
if (encapsulationKeySpki == null) {
throw new Error("No encapsulation key provided for wrapping.");
}
if (wrappingKey == null) {
throw new Error("No wrappingKey provided for wrapping.");
}
return await this.encryptUint8Array(encapsulationKeySpki, wrappingKey);
}
async wrapSymmetricKey(
keyToBeWrapped: SymmetricCryptoKey,
wrappingKey: SymmetricCryptoKey,
): Promise<EncString> {
if (keyToBeWrapped == null) {
throw new Error("No keyToBeWrapped provided for wrapping.");
}
if (wrappingKey == null) {
throw new Error("No wrappingKey provided for wrapping.");
}
return await this.encryptUint8Array(keyToBeWrapped.key, wrappingKey);
}
private async encryptUint8Array(
plainValue: Uint8Array,
key: SymmetricCryptoKey,
): Promise<EncString> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (plainValue == null) {
return Promise.resolve(null);
} }
const innerKey = key.inner(); const innerKey = key.inner();
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const encObj = await this.aesEncrypt(plainBuf, innerKey); const encObj = await this.aesEncrypt(plainValue, innerKey);
const iv = Utils.fromBufferToB64(encObj.iv); const iv = Utils.fromBufferToB64(encObj.iv);
const data = Utils.fromBufferToB64(encObj.data); const data = Utils.fromBufferToB64(encObj.data);
const mac = Utils.fromBufferToB64(encObj.mac); const mac = Utils.fromBufferToB64(encObj.mac);
return new EncString(innerKey.type, data, iv, mac); return new EncString(innerKey.type, data, iv, mac);
} else if (innerKey.type === EncryptionType.AesCbc256_B64) { } else if (innerKey.type === EncryptionType.AesCbc256_B64) {
const encObj = await this.aesEncryptLegacy(plainBuf, innerKey); const encObj = await this.aesEncryptLegacy(plainValue, innerKey);
const iv = Utils.fromBufferToB64(encObj.iv); const iv = Utils.fromBufferToB64(encObj.iv);
const data = Utils.fromBufferToB64(encObj.data); const data = Utils.fromBufferToB64(encObj.data);
return new EncString(innerKey.type, data, iv); return new EncString(innerKey.type, data, iv);

View File

@@ -31,6 +31,88 @@ describe("EncryptService", () => {
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true); encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
}); });
describe("wrapSymmetricKey", () => {
it("roundtrip encrypts and decrypts a symmetric key", async () => {
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = await encryptService.wrapSymmetricKey(key, wrappingKey);
expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0)));
});
it("fails if key toBeWrapped is null", async () => {
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
await expect(encryptService.wrapSymmetricKey(null, wrappingKey)).rejects.toThrow(
"No keyToBeWrapped provided for wrapping.",
);
});
it("fails if wrapping key is null", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
await expect(encryptService.wrapSymmetricKey(key, null)).rejects.toThrow(
"No wrappingKey provided for wrapping.",
);
});
});
describe("wrapDecapsulationKey", () => {
it("roundtrip encrypts and decrypts a decapsulation key", async () => {
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = await encryptService.wrapDecapsulationKey(
makeStaticByteArray(64),
wrappingKey,
);
expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0)));
});
it("fails if decapsulation key is null", async () => {
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
await expect(encryptService.wrapDecapsulationKey(null, wrappingKey)).rejects.toThrow(
"No decapsulation key provided for wrapping.",
);
});
it("fails if wrapping key is null", async () => {
const decapsulationKey = makeStaticByteArray(64);
await expect(encryptService.wrapDecapsulationKey(decapsulationKey, null)).rejects.toThrow(
"No wrappingKey provided for wrapping.",
);
});
});
describe("wrapEncapsulationKey", () => {
it("roundtrip encrypts and decrypts an encapsulationKey key", async () => {
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = await encryptService.wrapEncapsulationKey(
makeStaticByteArray(64),
wrappingKey,
);
expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0)));
});
it("fails if encapsulation key is null", async () => {
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
await expect(encryptService.wrapEncapsulationKey(null, wrappingKey)).rejects.toThrow(
"No encapsulation key provided for wrapping.",
);
});
it("fails if wrapping key is null", async () => {
const encapsulationKey = makeStaticByteArray(64);
await expect(encryptService.wrapEncapsulationKey(encapsulationKey, null)).rejects.toThrow(
"No wrappingKey provided for wrapping.",
);
});
});
describe("onServerConfigChange", () => { describe("onServerConfigChange", () => {
const newConfig = mock<ServerConfig>(); const newConfig = mock<ServerConfig>();
@@ -461,6 +543,12 @@ describe("EncryptService", () => {
expect(actual).toEqual(encString); expect(actual).toEqual(encString);
expect(actual.dataBytes).toEqualBuffer(encryptedData); expect(actual.dataBytes).toEqualBuffer(encryptedData);
}); });
it("throws if no data was provided", () => {
return expect(encryptService.rsaEncrypt(null, new Uint8Array(32))).rejects.toThrow(
"No data provided for encryption",
);
});
}); });
describe("decapsulateKeyUnsigned", () => { describe("decapsulateKeyUnsigned", () => {

View File

@@ -164,10 +164,10 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
this.encryptService.encapsulateKeyUnsigned(userKey, devicePublicKey), this.encryptService.encapsulateKeyUnsigned(userKey, devicePublicKey),
// Encrypt devicePublicKey with user key // Encrypt devicePublicKey with user key
this.encryptService.encrypt(devicePublicKey, userKey), this.encryptService.wrapEncapsulationKey(devicePublicKey, userKey),
// Encrypt devicePrivateKey with deviceKey // Encrypt devicePrivateKey with deviceKey
this.encryptService.encrypt(devicePrivateKey, deviceKey), this.encryptService.wrapDecapsulationKey(devicePrivateKey, deviceKey),
]); ]);
// Send encrypted keys to server // Send encrypted keys to server
@@ -290,7 +290,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
); );
// Re-encrypt the device public key with the new user key // Re-encrypt the device public key with the new user key
const encryptedDevicePublicKey = await this.encryptService.encrypt( const encryptedDevicePublicKey = await this.encryptService.wrapEncapsulationKey(
decryptedDevicePublicKey, decryptedDevicePublicKey,
newUserKey, newUserKey,
); );

View File

@@ -346,8 +346,6 @@ describe("deviceTrustService", () => {
const deviceRsaKeyLength = 2048; const deviceRsaKeyLength = 2048;
let mockDeviceRsaKeyPair: [Uint8Array, Uint8Array]; let mockDeviceRsaKeyPair: [Uint8Array, Uint8Array];
let mockDevicePrivateKey: Uint8Array;
let mockDevicePublicKey: Uint8Array;
let mockDevicePublicKeyEncryptedUserKey: EncString; let mockDevicePublicKeyEncryptedUserKey: EncString;
let mockUserKeyEncryptedDevicePublicKey: EncString; let mockUserKeyEncryptedDevicePublicKey: EncString;
let mockDeviceKeyEncryptedDevicePrivateKey: EncString; let mockDeviceKeyEncryptedDevicePrivateKey: EncString;
@@ -366,7 +364,8 @@ describe("deviceTrustService", () => {
let rsaGenerateKeyPairSpy: jest.SpyInstance; let rsaGenerateKeyPairSpy: jest.SpyInstance;
let cryptoSvcGetUserKeySpy: jest.SpyInstance; let cryptoSvcGetUserKeySpy: jest.SpyInstance;
let cryptoSvcRsaEncryptSpy: jest.SpyInstance; let cryptoSvcRsaEncryptSpy: jest.SpyInstance;
let encryptServiceEncryptSpy: jest.SpyInstance; let encryptServiceWrapDecapsulationKeySpy: jest.SpyInstance;
let encryptServiceWrapEncapsulationKeySpy: jest.SpyInstance;
let appIdServiceGetAppIdSpy: jest.SpyInstance; let appIdServiceGetAppIdSpy: jest.SpyInstance;
let devicesApiServiceUpdateTrustedDeviceKeysSpy: jest.SpyInstance; let devicesApiServiceUpdateTrustedDeviceKeysSpy: jest.SpyInstance;
@@ -384,9 +383,6 @@ describe("deviceTrustService", () => {
new Uint8Array(deviceRsaKeyLength), new Uint8Array(deviceRsaKeyLength),
]; ];
mockDevicePublicKey = mockDeviceRsaKeyPair[0];
mockDevicePrivateKey = mockDeviceRsaKeyPair[1];
mockDevicePublicKeyEncryptedUserKey = new EncString( mockDevicePublicKeyEncryptedUserKey = new EncString(
EncryptionType.Rsa2048_OaepSha1_B64, EncryptionType.Rsa2048_OaepSha1_B64,
"mockDevicePublicKeyEncryptedUserKey", "mockDevicePublicKeyEncryptedUserKey",
@@ -419,13 +415,17 @@ describe("deviceTrustService", () => {
.spyOn(encryptService, "encapsulateKeyUnsigned") .spyOn(encryptService, "encapsulateKeyUnsigned")
.mockResolvedValue(mockDevicePublicKeyEncryptedUserKey); .mockResolvedValue(mockDevicePublicKeyEncryptedUserKey);
encryptServiceEncryptSpy = jest encryptServiceWrapEncapsulationKeySpy = jest
.spyOn(encryptService, "encrypt") .spyOn(encryptService, "wrapEncapsulationKey")
.mockImplementation((plainValue, key) => { .mockImplementation((plainValue, key) => {
if (plainValue === mockDevicePublicKey && key === mockUserKey) { if (plainValue instanceof Uint8Array && key instanceof SymmetricCryptoKey) {
return Promise.resolve(mockUserKeyEncryptedDevicePublicKey); return Promise.resolve(mockUserKeyEncryptedDevicePublicKey);
} }
if (plainValue === mockDevicePrivateKey && key === mockDeviceKey) { });
encryptServiceWrapDecapsulationKeySpy = jest
.spyOn(encryptService, "wrapDecapsulationKey")
.mockImplementation((plainValue, key) => {
if (plainValue instanceof Uint8Array && key instanceof SymmetricCryptoKey) {
return Promise.resolve(mockDeviceKeyEncryptedDevicePrivateKey); return Promise.resolve(mockDeviceKeyEncryptedDevicePrivateKey);
} }
}); });
@@ -452,7 +452,8 @@ describe("deviceTrustService", () => {
const userKey = cryptoSvcRsaEncryptSpy.mock.calls[0][0]; const userKey = cryptoSvcRsaEncryptSpy.mock.calls[0][0];
expect(userKey.key.byteLength).toBe(64); expect(userKey.key.byteLength).toBe(64);
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2); expect(encryptServiceWrapDecapsulationKeySpy).toHaveBeenCalledTimes(1);
expect(encryptServiceWrapEncapsulationKeySpy).toHaveBeenCalledTimes(1);
expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1); expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1);
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledTimes(1); expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledTimes(1);
@@ -508,9 +509,14 @@ describe("deviceTrustService", () => {
errorText: "rsaEncrypt error", errorText: "rsaEncrypt error",
}, },
{ {
method: "encryptService.encrypt", method: "encryptService.wrapEncapsulationKey",
spy: () => encryptServiceEncryptSpy, spy: () => encryptServiceWrapEncapsulationKeySpy,
errorText: "encryptService.encrypt error", errorText: "encryptService.wrapEncapsulationKey error",
},
{
method: "encryptService.wrapDecapsulationKey",
spy: () => encryptServiceWrapDecapsulationKeySpy,
errorText: "encryptService.wrapDecapsulationKey error",
}, },
]; ];
@@ -872,7 +878,7 @@ describe("deviceTrustService", () => {
}); });
// Mock the reencryption of the device public key with the new user key // Mock the reencryption of the device public key with the new user key
encryptService.encrypt.mockImplementationOnce((plainValue, key) => { encryptService.wrapEncapsulationKey.mockImplementationOnce((plainValue, key) => {
expect(plainValue).toBeInstanceOf(Uint8Array); expect(plainValue).toBeInstanceOf(Uint8Array);
expect(new Uint8Array(plainValue as Uint8Array)[0]).toBe(FakeDecryptedPublicKeyMarker); expect(new Uint8Array(plainValue as Uint8Array)[0]).toBe(FakeDecryptedPublicKeyMarker);

View File

@@ -479,7 +479,7 @@ describe("SendService", () => {
beforeEach(() => { beforeEach(() => {
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
encryptedKey = new EncString("Re-encrypted Send Key"); encryptedKey = new EncString("Re-encrypted Send Key");
encryptService.encrypt.mockResolvedValue(encryptedKey); encryptService.wrapSymmetricKey.mockResolvedValue(encryptedKey);
}); });
it("returns re-encrypted user sends", async () => { it("returns re-encrypted user sends", async () => {

View File

@@ -81,6 +81,7 @@ export class SendService implements InternalSendServiceAbstraction {
if (key == null) { if (key == null) {
key = await this.keyService.getUserKey(); key = await this.keyService.getUserKey();
} }
// Key is not a SymmetricCryptoKey, but key material used to derive the cryptoKey
send.key = await this.encryptService.encrypt(model.key, key); send.key = await this.encryptService.encrypt(model.key, key);
send.name = await this.encryptService.encrypt(model.name, model.cryptoKey); send.name = await this.encryptService.encrypt(model.name, model.cryptoKey);
send.notes = await this.encryptService.encrypt(model.notes, model.cryptoKey); send.notes = await this.encryptService.encrypt(model.notes, model.cryptoKey);
@@ -287,8 +288,10 @@ export class SendService implements InternalSendServiceAbstraction {
) { ) {
const requests = await Promise.all( const requests = await Promise.all(
sends.map(async (send) => { sends.map(async (send) => {
const sendKey = await this.encryptService.decryptToBytes(send.key, originalUserKey); const sendKey = new SymmetricCryptoKey(
send.key = await this.encryptService.encrypt(sendKey, rotateUserKey); await this.encryptService.decryptToBytes(send.key, originalUserKey),
);
send.key = await this.encryptService.wrapSymmetricKey(sendKey, rotateUserKey);
return new SendWithIdRequest(send); return new SendWithIdRequest(send);
}), }),
); );

View File

@@ -280,6 +280,7 @@ describe("Cipher Service", () => {
Promise.resolve(new SymmetricCryptoKey(makeStaticByteArray(64)) as CipherKey), Promise.resolve(new SymmetricCryptoKey(makeStaticByteArray(64)) as CipherKey),
); );
encryptService.encrypt.mockImplementation(encryptText); encryptService.encrypt.mockImplementation(encryptText);
encryptService.wrapSymmetricKey.mockResolvedValue(new EncString("Re-encrypted Cipher Key"));
jest.spyOn(cipherService as any, "getAutofillOnPageLoadDefault").mockResolvedValue(true); jest.spyOn(cipherService as any, "getAutofillOnPageLoadDefault").mockResolvedValue(true);
}); });
@@ -436,7 +437,7 @@ describe("Cipher Service", () => {
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
encryptedKey = new EncString("Re-encrypted Cipher Key"); encryptedKey = new EncString("Re-encrypted Cipher Key");
encryptService.encrypt.mockResolvedValue(encryptedKey); encryptService.wrapSymmetricKey.mockResolvedValue(encryptedKey);
keyService.makeCipherKey.mockResolvedValue( keyService.makeCipherKey.mockResolvedValue(
new SymmetricCryptoKey(new Uint8Array(32)) as CipherKey, new SymmetricCryptoKey(new Uint8Array(32)) as CipherKey,

View File

@@ -266,7 +266,7 @@ export class CipherService implements CipherServiceAbstraction {
key, key,
).then(async () => { ).then(async () => {
if (model.key != null) { if (model.key != null) {
attachment.key = await this.encryptService.encrypt(model.key.key, key); attachment.key = await this.encryptService.wrapSymmetricKey(model.key, key);
} }
encAttachments.push(attachment); encAttachments.push(attachment);
}); });
@@ -1820,8 +1820,8 @@ export class CipherService implements CipherServiceAbstraction {
} }
// Then, we have to encrypt the cipher key with the proper key. // Then, we have to encrypt the cipher key with the proper key.
cipher.key = await this.encryptService.encrypt( cipher.key = await this.encryptService.wrapSymmetricKey(
decryptedCipherKey.key, decryptedCipherKey,
keyForCipherKeyEncryption, keyForCipherKeyEncryption,
); );

View File

@@ -232,7 +232,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
} }
const newUserKey = await this.keyGenerationService.createKey(512); const newUserKey = await this.keyGenerationService.createKey(512);
return this.buildProtectedSymmetricKey(masterKey, newUserKey.key); return this.buildProtectedSymmetricKey(masterKey, newUserKey);
} }
/** /**
@@ -323,7 +323,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
userKey?: UserKey, userKey?: UserKey,
): Promise<[UserKey, EncString]> { ): Promise<[UserKey, EncString]> {
userKey ||= await this.getUserKey(); userKey ||= await this.getUserKey();
return await this.buildProtectedSymmetricKey(masterKey, userKey.key); return await this.buildProtectedSymmetricKey(masterKey, userKey);
} }
// TODO: move to MasterPasswordService // TODO: move to MasterPasswordService
@@ -433,7 +433,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
} }
const newSymKey = await this.keyGenerationService.createKey(512); const newSymKey = await this.keyGenerationService.createKey(512);
return this.buildProtectedSymmetricKey(key, newSymKey.key); return this.buildProtectedSymmetricKey(key, newSymKey);
} }
private async clearOrgKeys(userId: UserId): Promise<void> { private async clearOrgKeys(userId: UserId): Promise<void> {
@@ -547,7 +547,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
const publicB64 = Utils.fromBufferToB64(keyPair[0]); const publicB64 = Utils.fromBufferToB64(keyPair[0]);
const privateEnc = await this.encryptService.encrypt(keyPair[1], key); const privateEnc = await this.encryptService.wrapDecapsulationKey(keyPair[1], key);
return [publicB64, privateEnc]; return [publicB64, privateEnc];
} }
@@ -820,18 +820,21 @@ export class DefaultKeyService implements KeyServiceAbstraction {
private async buildProtectedSymmetricKey<T extends SymmetricCryptoKey>( private async buildProtectedSymmetricKey<T extends SymmetricCryptoKey>(
encryptionKey: SymmetricCryptoKey, encryptionKey: SymmetricCryptoKey,
newSymKey: Uint8Array, newSymKey: SymmetricCryptoKey,
): Promise<[T, EncString]> { ): Promise<[T, EncString]> {
let protectedSymKey: EncString; let protectedSymKey: EncString;
if (encryptionKey.key.byteLength === 32) { if (encryptionKey.key.byteLength === 32) {
const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey); const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey);
protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey); protectedSymKey = await this.encryptService.wrapSymmetricKey(
newSymKey,
stretchedEncryptionKey,
);
} else if (encryptionKey.key.byteLength === 64) { } else if (encryptionKey.key.byteLength === 64) {
protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey); protectedSymKey = await this.encryptService.wrapSymmetricKey(newSymKey, encryptionKey);
} else { } else {
throw new Error("Invalid key size."); throw new Error("Invalid key size.");
} }
return [new SymmetricCryptoKey(newSymKey) as T, protectedSymKey]; return [newSymKey as T, protectedSymKey];
} }
userKey$(userId: UserId): Observable<UserKey | null> { userKey$(userId: UserId): Observable<UserKey | null> {