1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +00:00

[PM-24107] Migrate KM's usage of getUserKey from the key service (#17117)

* Remove internal use of getUserKey in the key service

* Move ownership of RotateableKeySet and remove usage of getUserKey

* Add input validation to createKeySet
This commit is contained in:
Thomas Avery
2025-11-13 10:07:13 -06:00
committed by GitHub
parent 42a79e65cf
commit cfe2458935
23 changed files with 488 additions and 237 deletions

View File

@@ -1,57 +0,0 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { KeyService } from "@bitwarden/key-management";
import { RotateableKeySetService } from "./rotateable-key-set.service";
describe("RotateableKeySetService", () => {
let testBed!: TestBed;
let keyService!: MockProxy<KeyService>;
let encryptService!: MockProxy<EncryptService>;
let service!: RotateableKeySetService;
beforeEach(() => {
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
testBed = TestBed.configureTestingModule({
providers: [
{ provide: KeyService, useValue: keyService },
{ provide: EncryptService, useValue: encryptService },
],
});
service = testBed.inject(RotateableKeySetService);
});
describe("createKeySet", () => {
it("should create a new key set", async () => {
const externalKey = createSymmetricKey();
const userKey = createSymmetricKey();
const encryptedUserKey = Symbol();
const encryptedPublicKey = Symbol();
const encryptedPrivateKey = Symbol();
keyService.makeKeyPair.mockResolvedValue(["publicKey", encryptedPrivateKey as any]);
keyService.getUserKey.mockResolvedValue({ key: userKey.key } as any);
encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedUserKey as any);
encryptService.wrapEncapsulationKey.mockResolvedValue(encryptedPublicKey as any);
const result = await service.createKeySet(externalKey as any);
expect(result).toEqual({
encryptedUserKey,
encryptedPublicKey,
encryptedPrivateKey,
});
});
});
});
function createSymmetricKey() {
const key = Utils.fromB64ToArray(
"1h-TuPwSbX5qoX0aVgjmda_Lfq85qAcKssBlXZnPIsQC3HNDGIecunYqXhJnp55QpdXRh-egJiLH3a0wqlVQsQ",
);
return new SymmetricCryptoKey(key);
}

View File

@@ -1,85 +0,0 @@
import { inject, Injectable } from "@angular/core";
import { RotateableKeySet } from "@bitwarden/auth/common";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { KeyService } from "@bitwarden/key-management";
@Injectable({ providedIn: "root" })
export class RotateableKeySetService {
private readonly keyService = inject(KeyService);
private readonly encryptService = inject(EncryptService);
/**
* Create a new rotateable key set for the current user, using the provided external key.
* For more information on rotateable key sets, see {@link RotateableKeySet}
*
* @param externalKey The `ExternalKey` used to encrypt {@link RotateableKeySet.encryptedPrivateKey}
* @returns RotateableKeySet containing the current users `UserKey`
*/
async createKeySet<ExternalKey extends SymmetricCryptoKey>(
externalKey: ExternalKey,
): Promise<RotateableKeySet<ExternalKey>> {
const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(externalKey);
const userKey = await this.keyService.getUserKey();
const rawPublicKey = Utils.fromB64ToArray(publicKey);
const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
userKey,
rawPublicKey,
);
const encryptedPublicKey = await this.encryptService.wrapEncapsulationKey(
rawPublicKey,
userKey,
);
return new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey);
}
/**
* Rotates the current user's `UserKey` and updates the provided `RotateableKeySet` with the new keys.
*
* @param keySet The current `RotateableKeySet` for the user
* @returns The updated `RotateableKeySet` with the new `UserKey`
*/
async rotateKeySet<ExternalKey extends SymmetricCryptoKey>(
keySet: RotateableKeySet<ExternalKey>,
oldUserKey: SymmetricCryptoKey,
newUserKey: SymmetricCryptoKey,
): Promise<RotateableKeySet<ExternalKey>> {
// validate parameters
if (!keySet) {
throw new Error("failed to rotate key set: keySet is required");
}
if (!oldUserKey) {
throw new Error("failed to rotate key set: oldUserKey is required");
}
if (!newUserKey) {
throw new Error("failed to rotate key set: newUserKey is required");
}
const publicKey = await this.encryptService.unwrapEncapsulationKey(
keySet.encryptedPublicKey,
oldUserKey,
);
if (publicKey == null) {
throw new Error("failed to rotate key set: could not decrypt public key");
}
const newEncryptedPublicKey = await this.encryptService.wrapEncapsulationKey(
publicKey,
newUserKey,
);
const newEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
newUserKey,
publicKey,
);
const newRotateableKeySet = new RotateableKeySet<ExternalKey>(
newEncryptedUserKey,
newEncryptedPublicKey,
keySet.encryptedPrivateKey,
);
return newRotateableKeySet;
}
}

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { RotateableKeySet } from "@bitwarden/auth/common";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { RotateableKeySet } from "@bitwarden/common/key-management/keys/models/rotateable-key-set";
import { BaseResponse } from "@bitwarden/common/models/response/base.response"; import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { WebauthnLoginCredentialPrfStatus } from "../../../enums/webauthn-login-credential-prf-status.enum"; import { WebauthnLoginCredentialPrfStatus } from "../../../enums/webauthn-login-credential-prf-status.enum";

View File

@@ -3,23 +3,26 @@
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
import { RotateableKeySet } from "@bitwarden/auth/common";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction"; import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction";
import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view"; import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view";
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { RotateableKeySet } from "@bitwarden/common/key-management/keys/models/rotateable-key-set";
import { RotateableKeySetService } from "@bitwarden/common/key-management/keys/services/abstractions/rotateable-key-set.service";
import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { makeEncString, makeSymmetricCryptoKey } from "@bitwarden/common/spec"; import { makeEncString, makeSymmetricCryptoKey } from "@bitwarden/common/spec";
import { PrfKey, UserKey } from "@bitwarden/common/types/key"; import { PrfKey, UserKey } from "@bitwarden/common/types/key";
import { newGuid } from "@bitwarden/guid";
import { KeyService } from "@bitwarden/key-management";
import { UserId } from "@bitwarden/user-core"; import { UserId } from "@bitwarden/user-core";
import { WebauthnLoginCredentialPrfStatus } from "../../enums/webauthn-login-credential-prf-status.enum"; import { WebauthnLoginCredentialPrfStatus } from "../../enums/webauthn-login-credential-prf-status.enum";
import { CredentialCreateOptionsView } from "../../views/credential-create-options.view"; import { CredentialCreateOptionsView } from "../../views/credential-create-options.view";
import { PendingWebauthnLoginCredentialView } from "../../views/pending-webauthn-login-credential.view"; import { PendingWebauthnLoginCredentialView } from "../../views/pending-webauthn-login-credential.view";
import { RotateableKeySetService } from "../rotateable-key-set.service";
import { EnableCredentialEncryptionRequest } from "./request/enable-credential-encryption.request"; import { EnableCredentialEncryptionRequest } from "./request/enable-credential-encryption.request";
import { WebauthnLoginCredentialResponse } from "./response/webauthn-login-credential.response"; import { WebauthnLoginCredentialResponse } from "./response/webauthn-login-credential.response";
@@ -32,9 +35,12 @@ describe("WebauthnAdminService", () => {
let rotateableKeySetService!: MockProxy<RotateableKeySetService>; let rotateableKeySetService!: MockProxy<RotateableKeySetService>;
let webAuthnLoginPrfKeyService!: MockProxy<WebAuthnLoginPrfKeyServiceAbstraction>; let webAuthnLoginPrfKeyService!: MockProxy<WebAuthnLoginPrfKeyServiceAbstraction>;
let credentials: MockProxy<CredentialsContainer>; let credentials: MockProxy<CredentialsContainer>;
let keyService: MockProxy<KeyService>;
let service!: WebauthnLoginAdminService; let service!: WebauthnLoginAdminService;
let originalAuthenticatorAssertionResponse!: AuthenticatorAssertionResponse | any; let originalAuthenticatorAssertionResponse!: AuthenticatorAssertionResponse | any;
const mockUserId = newGuid() as UserId;
const mockUserKey = makeSymmetricCryptoKey(64) as UserKey;
beforeAll(() => { beforeAll(() => {
// Polyfill missing class // Polyfill missing class
@@ -45,12 +51,14 @@ describe("WebauthnAdminService", () => {
userVerificationService = mock<UserVerificationService>(); userVerificationService = mock<UserVerificationService>();
rotateableKeySetService = mock<RotateableKeySetService>(); rotateableKeySetService = mock<RotateableKeySetService>();
webAuthnLoginPrfKeyService = mock<WebAuthnLoginPrfKeyServiceAbstraction>(); webAuthnLoginPrfKeyService = mock<WebAuthnLoginPrfKeyServiceAbstraction>();
keyService = mock<KeyService>();
credentials = mock<CredentialsContainer>(); credentials = mock<CredentialsContainer>();
service = new WebauthnLoginAdminService( service = new WebauthnLoginAdminService(
apiService, apiService,
userVerificationService, userVerificationService,
rotateableKeySetService, rotateableKeySetService,
webAuthnLoginPrfKeyService, webAuthnLoginPrfKeyService,
keyService,
credentials, credentials,
); );
@@ -58,6 +66,8 @@ describe("WebauthnAdminService", () => {
originalAuthenticatorAssertionResponse = global.AuthenticatorAssertionResponse; originalAuthenticatorAssertionResponse = global.AuthenticatorAssertionResponse;
// Mock the global AuthenticatorAssertionResponse class b/c the class is only available in secure contexts // Mock the global AuthenticatorAssertionResponse class b/c the class is only available in secure contexts
global.AuthenticatorAssertionResponse = MockAuthenticatorAssertionResponse; global.AuthenticatorAssertionResponse = MockAuthenticatorAssertionResponse;
keyService.userKey$.mockReturnValue(of(mockUserKey));
}); });
beforeEach(() => { beforeEach(() => {
@@ -124,7 +134,7 @@ describe("WebauthnAdminService", () => {
const request = new EnableCredentialEncryptionRequest(); const request = new EnableCredentialEncryptionRequest();
request.token = assertionOptions.token; request.token = assertionOptions.token;
request.deviceResponse = assertionOptions.deviceResponse; request.deviceResponse = assertionOptions.deviceResponse;
request.encryptedUserKey = prfKeySet.encryptedUserKey.encryptedString; request.encryptedUserKey = prfKeySet.encapsulatedDownstreamKey.encryptedString;
request.encryptedPublicKey = prfKeySet.encryptedPublicKey.encryptedString; request.encryptedPublicKey = prfKeySet.encryptedPublicKey.encryptedString;
request.encryptedPrivateKey = prfKeySet.encryptedPrivateKey.encryptedString; request.encryptedPrivateKey = prfKeySet.encryptedPrivateKey.encryptedString;
@@ -135,10 +145,10 @@ describe("WebauthnAdminService", () => {
const updateCredentialMock = jest.spyOn(apiService, "updateCredential").mockResolvedValue(); const updateCredentialMock = jest.spyOn(apiService, "updateCredential").mockResolvedValue();
// Act // Act
await service.enableCredentialEncryption(assertionOptions); await service.enableCredentialEncryption(assertionOptions, mockUserId);
// Assert // Assert
expect(createKeySetMock).toHaveBeenCalledWith(assertionOptions.prfKey); expect(createKeySetMock).toHaveBeenCalledWith(assertionOptions.prfKey, mockUserKey);
expect(updateCredentialMock).toHaveBeenCalledWith(request); expect(updateCredentialMock).toHaveBeenCalledWith(request);
}); });
@@ -161,7 +171,7 @@ describe("WebauthnAdminService", () => {
// Act // Act
try { try {
await service.enableCredentialEncryption(assertionOptions); await service.enableCredentialEncryption(assertionOptions, mockUserId);
} catch (error) { } catch (error) {
// Assert // Assert
expect(error).toEqual(new Error("invalid credential")); expect(error).toEqual(new Error("invalid credential"));
@@ -170,6 +180,19 @@ describe("WebauthnAdminService", () => {
} }
}); });
test.each([null, undefined, ""])("should throw an error when userId is %p", async (userId) => {
const response = new MockPublicKeyCredential();
const assertionOptions: WebAuthnLoginCredentialAssertionView =
new WebAuthnLoginCredentialAssertionView(
"enable_credential_encryption_test_token",
new WebAuthnLoginAssertionResponseRequest(response),
{} as PrfKey,
);
await expect(
service.enableCredentialEncryption(assertionOptions, userId as any),
).rejects.toThrow("userId is required");
});
it("should throw error when WehAuthnLoginCredentialAssertionView is undefined", async () => { it("should throw error when WehAuthnLoginCredentialAssertionView is undefined", async () => {
// Arrange // Arrange
const assertionOptions: WebAuthnLoginCredentialAssertionView = undefined; const assertionOptions: WebAuthnLoginCredentialAssertionView = undefined;
@@ -182,7 +205,7 @@ describe("WebauthnAdminService", () => {
// Act // Act
try { try {
await service.enableCredentialEncryption(assertionOptions); await service.enableCredentialEncryption(assertionOptions, mockUserId);
} catch (error) { } catch (error) {
// Assert // Assert
expect(error).toEqual(new Error("invalid credential")); expect(error).toEqual(new Error("invalid credential"));

View File

@@ -1,24 +1,34 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Injectable, Optional } from "@angular/core"; import { Injectable, Optional } from "@angular/core";
import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap, tap } from "rxjs"; import {
BehaviorSubject,
filter,
firstValueFrom,
from,
map,
Observable,
shareReplay,
switchMap,
tap,
} from "rxjs";
import { PrfKeySet } from "@bitwarden/auth/common";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction"; import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction";
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";
import { WebAuthnLoginCredentialAssertionOptionsView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion-options.view"; import { WebAuthnLoginCredentialAssertionOptionsView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion-options.view";
import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view"; import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view";
import { Verification } from "@bitwarden/common/auth/types/verification"; import { Verification } from "@bitwarden/common/auth/types/verification";
import { PrfKeySet } from "@bitwarden/common/key-management/keys/models/rotateable-key-set";
import { RotateableKeySetService } from "@bitwarden/common/key-management/keys/services/abstractions/rotateable-key-set.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key"; import { UserKey } from "@bitwarden/common/types/key";
import { UserKeyRotationDataProvider } from "@bitwarden/key-management"; import { KeyService, UserKeyRotationDataProvider } from "@bitwarden/key-management";
import { CredentialCreateOptionsView } from "../../views/credential-create-options.view"; import { CredentialCreateOptionsView } from "../../views/credential-create-options.view";
import { PendingWebauthnLoginCredentialView } from "../../views/pending-webauthn-login-credential.view"; import { PendingWebauthnLoginCredentialView } from "../../views/pending-webauthn-login-credential.view";
import { WebauthnLoginCredentialView } from "../../views/webauthn-login-credential.view"; import { WebauthnLoginCredentialView } from "../../views/webauthn-login-credential.view";
import { RotateableKeySetService } from "../rotateable-key-set.service";
import { EnableCredentialEncryptionRequest } from "./request/enable-credential-encryption.request"; import { EnableCredentialEncryptionRequest } from "./request/enable-credential-encryption.request";
import { SaveCredentialRequest } from "./request/save-credential.request"; import { SaveCredentialRequest } from "./request/save-credential.request";
@@ -55,6 +65,7 @@ export class WebauthnLoginAdminService
private userVerificationService: UserVerificationService, private userVerificationService: UserVerificationService,
private rotateableKeySetService: RotateableKeySetService, private rotateableKeySetService: RotateableKeySetService,
private webAuthnLoginPrfKeyService: WebAuthnLoginPrfKeyServiceAbstraction, private webAuthnLoginPrfKeyService: WebAuthnLoginPrfKeyServiceAbstraction,
private keyService: KeyService,
@Optional() navigatorCredentials?: CredentialsContainer, @Optional() navigatorCredentials?: CredentialsContainer,
@Optional() private logService?: LogService, @Optional() private logService?: LogService,
) { ) {
@@ -131,10 +142,12 @@ export class WebauthnLoginAdminService
* This will trigger the browsers WebAuthn API to generate a PRF-output. * This will trigger the browsers WebAuthn API to generate a PRF-output.
* *
* @param pendingCredential A credential created using `createCredential`. * @param pendingCredential A credential created using `createCredential`.
* @param userId The target users id.
* @returns A key set that can be saved to the server. Undefined is returned if the credential doesn't support PRF. * @returns A key set that can be saved to the server. Undefined is returned if the credential doesn't support PRF.
*/ */
async createKeySet( async createKeySet(
pendingCredential: PendingWebauthnLoginCredentialView, pendingCredential: PendingWebauthnLoginCredentialView,
userId: UserId,
): Promise<PrfKeySet | undefined> { ): Promise<PrfKeySet | undefined> {
const nativeOptions: CredentialRequestOptions = { const nativeOptions: CredentialRequestOptions = {
publicKey: { publicKey: {
@@ -166,7 +179,8 @@ export class WebauthnLoginAdminService
const symmetricPrfKey = const symmetricPrfKey =
await this.webAuthnLoginPrfKeyService.createSymmetricKeyFromPrf(prfResult); await this.webAuthnLoginPrfKeyService.createSymmetricKeyFromPrf(prfResult);
return await this.rotateableKeySetService.createKeySet(symmetricPrfKey); const userKey = await firstValueFrom(this.keyService.userKey$(userId));
return await this.rotateableKeySetService.createKeySet(symmetricPrfKey, userKey);
} catch (error) { } catch (error) {
this.logService?.error(error); this.logService?.error(error);
return undefined; return undefined;
@@ -190,7 +204,7 @@ export class WebauthnLoginAdminService
request.token = credential.createOptions.token; request.token = credential.createOptions.token;
request.name = name; request.name = name;
request.supportsPrf = credential.supportsPrf; request.supportsPrf = credential.supportsPrf;
request.encryptedUserKey = prfKeySet?.encryptedUserKey.encryptedString; request.encryptedUserKey = prfKeySet?.encapsulatedDownstreamKey.encryptedString;
request.encryptedPublicKey = prfKeySet?.encryptedPublicKey.encryptedString; request.encryptedPublicKey = prfKeySet?.encryptedPublicKey.encryptedString;
request.encryptedPrivateKey = prfKeySet?.encryptedPrivateKey.encryptedString; request.encryptedPrivateKey = prfKeySet?.encryptedPrivateKey.encryptedString;
await this.apiService.saveCredential(request); await this.apiService.saveCredential(request);
@@ -204,23 +218,31 @@ export class WebauthnLoginAdminService
* if there was a problem with the Credential Assertion. * if there was a problem with the Credential Assertion.
* *
* @param assertionOptions Options received from the server using `getCredentialAssertOptions`. * @param assertionOptions Options received from the server using `getCredentialAssertOptions`.
* @param userId The target users id.
* @returns void * @returns void
*/ */
async enableCredentialEncryption( async enableCredentialEncryption(
assertionOptions: WebAuthnLoginCredentialAssertionView, assertionOptions: WebAuthnLoginCredentialAssertionView,
userId: UserId,
): Promise<void> { ): Promise<void> {
if (assertionOptions === undefined || assertionOptions?.prfKey === undefined) { if (assertionOptions === undefined || assertionOptions?.prfKey === undefined) {
throw new Error("invalid credential"); throw new Error("invalid credential");
} }
if (!userId) {
throw new Error("userId is required");
}
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
const prfKeySet: PrfKeySet = await this.rotateableKeySetService.createKeySet( const prfKeySet: PrfKeySet = await this.rotateableKeySetService.createKeySet(
assertionOptions.prfKey, assertionOptions.prfKey,
userKey,
); );
const request = new EnableCredentialEncryptionRequest(); const request = new EnableCredentialEncryptionRequest();
request.token = assertionOptions.token; request.token = assertionOptions.token;
request.deviceResponse = assertionOptions.deviceResponse; request.deviceResponse = assertionOptions.deviceResponse;
request.encryptedUserKey = prfKeySet.encryptedUserKey.encryptedString; request.encryptedUserKey = prfKeySet.encapsulatedDownstreamKey.encryptedString;
request.encryptedPublicKey = prfKeySet.encryptedPublicKey.encryptedString; request.encryptedPublicKey = prfKeySet.encryptedPublicKey.encryptedString;
request.encryptedPrivateKey = prfKeySet.encryptedPrivateKey.encryptedString; request.encryptedPrivateKey = prfKeySet.encryptedPrivateKey.encryptedString;
await this.apiService.updateCredential(request); await this.apiService.updateCredential(request);
@@ -317,7 +339,7 @@ export class WebauthnLoginAdminService
const request = new WebauthnRotateCredentialRequest( const request = new WebauthnRotateCredentialRequest(
response.id, response.id,
rotatedKeyset.encryptedPublicKey, rotatedKeyset.encryptedPublicKey,
rotatedKeyset.encryptedUserKey, rotatedKeyset.encapsulatedDownstreamKey,
); );
return request; return request;
}), }),

View File

@@ -8,12 +8,13 @@ import {
TwoFactorAuthSecurityKeyFailedIcon, TwoFactorAuthSecurityKeyFailedIcon,
TwoFactorAuthSecurityKeyIcon, TwoFactorAuthSecurityKeyIcon,
} from "@bitwarden/assets/svg"; } from "@bitwarden/assets/svg";
import { PrfKeySet } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { Verification } from "@bitwarden/common/auth/types/verification"; import { Verification } from "@bitwarden/common/auth/types/verification";
import { PrfKeySet } from "@bitwarden/common/key-management/keys/models/rotateable-key-set";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogConfig, DialogRef, DialogService, ToastService } from "@bitwarden/components"; import { DialogConfig, DialogRef, DialogService, ToastService } from "@bitwarden/components";
import { WebauthnLoginAdminService } from "../../../core"; import { WebauthnLoginAdminService } from "../../../core";
@@ -67,10 +68,10 @@ export class CreateCredentialDialogComponent implements OnInit {
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private dialogRef: DialogRef, private dialogRef: DialogRef,
private webauthnService: WebauthnLoginAdminService, private webauthnService: WebauthnLoginAdminService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private i18nService: I18nService,
private logService: LogService, private logService: LogService,
private toastService: ToastService, private toastService: ToastService,
private accountService: AccountService,
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@@ -146,13 +147,14 @@ export class CreateCredentialDialogComponent implements OnInit {
if (this.formGroup.controls.credentialNaming.controls.name.invalid) { if (this.formGroup.controls.credentialNaming.controls.name.invalid) {
return; return;
} }
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
let keySet: PrfKeySet | undefined; let keySet: PrfKeySet | undefined;
if ( if (
this.pendingCredential.supportsPrf && this.pendingCredential.supportsPrf &&
this.formGroup.value.credentialNaming.useForEncryption this.formGroup.value.credentialNaming.useForEncryption
) { ) {
keySet = await this.webauthnService.createKeySet(this.pendingCredential); keySet = await this.webauthnService.createKeySet(this.pendingCredential, userId);
if (keySet === undefined) { if (keySet === undefined) {
this.formGroup.controls.credentialNaming.controls.useForEncryption?.setErrors({ this.formGroup.controls.credentialNaming.controls.useForEncryption?.setErrors({

View File

@@ -2,11 +2,13 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; import { FormBuilder, Validators } from "@angular/forms";
import { Subject } from "rxjs"; import { firstValueFrom, Subject } from "rxjs";
import { takeUntil } from "rxjs/operators"; import { takeUntil } from "rxjs/operators";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { WebAuthnLoginCredentialAssertionOptionsView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion-options.view"; import { WebAuthnLoginCredentialAssertionOptionsView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion-options.view";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { Verification } from "@bitwarden/common/auth/types/verification"; import { Verification } from "@bitwarden/common/auth/types/verification";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { DIALOG_DATA, DialogConfig, DialogRef } from "@bitwarden/components"; import { DIALOG_DATA, DialogConfig, DialogRef } from "@bitwarden/components";
@@ -47,6 +49,7 @@ export class EnableEncryptionDialogComponent implements OnInit, OnDestroy {
private dialogRef: DialogRef, private dialogRef: DialogRef,
private webauthnService: WebauthnLoginAdminService, private webauthnService: WebauthnLoginAdminService,
private webauthnLoginService: WebAuthnLoginServiceAbstraction, private webauthnLoginService: WebAuthnLoginServiceAbstraction,
private accountService: AccountService,
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@@ -60,6 +63,7 @@ export class EnableEncryptionDialogComponent implements OnInit, OnDestroy {
if (this.credential === undefined) { if (this.credential === undefined) {
return; return;
} }
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.dialogRef.disableClose = true; this.dialogRef.disableClose = true;
try { try {
@@ -68,6 +72,7 @@ export class EnableEncryptionDialogComponent implements OnInit, OnDestroy {
); );
await this.webauthnService.enableCredentialEncryption( await this.webauthnService.enableCredentialEncryption(
await this.webauthnLoginService.assertCredential(this.credentialOptions), await this.webauthnLoginService.assertCredential(this.credentialOptions),
userId,
); );
} catch (error) { } catch (error) {
if (error instanceof ErrorResponse && error.statusCode === 400) { if (error instanceof ErrorResponse && error.statusCode === 400) {

View File

@@ -182,7 +182,10 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
if (userKey == null) { if (userKey == null) {
masterKeyEncryptedUserKey = await this.keyService.makeUserKey(masterKey); masterKeyEncryptedUserKey = await this.keyService.makeUserKey(masterKey);
} else { } else {
masterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey); masterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
masterKey,
userKey,
);
} }
return masterKeyEncryptedUserKey; return masterKeyEncryptedUserKey;

View File

@@ -181,7 +181,9 @@ import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kd
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
import { KeyApiService } from "@bitwarden/common/key-management/keys/services/abstractions/key-api-service.abstraction"; import { KeyApiService } from "@bitwarden/common/key-management/keys/services/abstractions/key-api-service.abstraction";
import { RotateableKeySetService } from "@bitwarden/common/key-management/keys/services/abstractions/rotateable-key-set.service";
import { DefaultKeyApiService } from "@bitwarden/common/key-management/keys/services/default-key-api-service.service"; import { DefaultKeyApiService } from "@bitwarden/common/key-management/keys/services/default-key-api-service.service";
import { DefaultRotateableKeySetService } from "@bitwarden/common/key-management/keys/services/default-rotateable-key-set.service";
import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service"; import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
import { import {
InternalMasterPasswordServiceAbstraction, InternalMasterPasswordServiceAbstraction,
@@ -1738,6 +1740,11 @@ const safeProviders: SafeProvider[] = [
ConfigService, ConfigService,
], ],
}), }),
safeProvider({
provide: RotateableKeySetService,
useClass: DefaultRotateableKeySetService,
deps: [KeyService, EncryptService],
}),
safeProvider({ safeProvider({
provide: NewDeviceVerificationComponentService, provide: NewDeviceVerificationComponentService,
useClass: DefaultNewDeviceVerificationComponentService, useClass: DefaultNewDeviceVerificationComponentService,

View File

@@ -1,3 +1,2 @@
export * from "./rotateable-key-set";
export * from "./login-credentials"; export * from "./login-credentials";
export * from "./user-decryption-options"; export * from "./user-decryption-options";

View File

@@ -1,36 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PrfKey } from "@bitwarden/common/types/key";
declare const tag: unique symbol;
/**
* A set of keys where a `UserKey` is protected by an encrypted public/private key-pair.
* The `UserKey` is used to encrypt/decrypt data, while the public/private key-pair is
* used to rotate the `UserKey`.
*
* The `PrivateKey` is protected by an `ExternalKey`, such as a `DeviceKey`, or `PrfKey`,
* and the `PublicKey` is protected by the `UserKey`. This setup allows:
*
* - Access to `UserKey` by knowing the `ExternalKey`
* - Rotation to a `NewUserKey` by knowing the current `UserKey`,
* without needing access to the `ExternalKey`
*/
export class RotateableKeySet<ExternalKey extends SymmetricCryptoKey = SymmetricCryptoKey> {
private readonly [tag]: ExternalKey;
constructor(
/** PublicKey encrypted UserKey */
readonly encryptedUserKey: EncString,
/** UserKey encrypted PublicKey */
readonly encryptedPublicKey: EncString,
/** ExternalKey encrypted PrivateKey */
readonly encryptedPrivateKey?: EncString,
) {}
}
export type PrfKeySet = RotateableKeySet<PrfKey>;

View File

@@ -11,7 +11,7 @@ export abstract class WebAuthnLoginPrfKeyServiceAbstraction {
/** /**
* Create a symmetric key from the PRF-output by stretching it. * Create a symmetric key from the PRF-output by stretching it.
* This should be used as `ExternalKey` with `RotateableKeySet`. * This should be used as `UpstreamKey` with `RotateableKeySet`.
*/ */
abstract createSymmetricKeyFromPrf(prf: ArrayBuffer): Promise<PrfKey>; abstract createSymmetricKeyFromPrf(prf: ArrayBuffer): Promise<PrfKey>;
} }

View File

@@ -1,10 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { RotateableKeySet } from "../../../../../auth/src/common/models";
import { EncString } from "../../../key-management/crypto/models/enc-string"; import { EncString } from "../../../key-management/crypto/models/enc-string";
import { RotateableKeySet } from "../../../key-management/keys/models/rotateable-key-set";
export class WebauthnRotateCredentialRequest { export class WebauthnRotateCredentialRequest {
id: string; id: string;

View File

@@ -2,12 +2,9 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { RotateableKeySet } from "@bitwarden/auth/common";
import { DeviceType } from "../../../enums"; import { DeviceType } from "../../../enums";
import { EncString } from "../../../key-management/crypto/models/enc-string"; import { EncString } from "../../../key-management/crypto/models/enc-string";
import { RotateableKeySet } from "../../../key-management/keys/models/rotateable-key-set";
import { BaseResponse } from "../../../models/response/base.response"; import { BaseResponse } from "../../../models/response/base.response";
export class ProtectedDeviceResponse extends BaseResponse { export class ProtectedDeviceResponse extends BaseResponse {

View File

@@ -4,7 +4,7 @@ import { firstValueFrom, map, Observable, Subject } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { RotateableKeySet, UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
@@ -33,6 +33,7 @@ import { KeyGenerationService } from "../../crypto";
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service"; import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../crypto/abstractions/encrypt.service"; import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { EncString } from "../../crypto/models/enc-string"; import { EncString } from "../../crypto/models/enc-string";
import { RotateableKeySet } from "../../keys/models/rotateable-key-set";
import { DeviceTrustServiceAbstraction } from "../abstractions/device-trust.service.abstraction"; import { DeviceTrustServiceAbstraction } from "../abstractions/device-trust.service.abstraction";
/** Uses disk storage so that the device key can persist after log out and tab removal. */ /** Uses disk storage so that the device key can persist after log out and tab removal. */
@@ -145,7 +146,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
} }
// Attempt to get user key // Attempt to get user key
const userKey: UserKey = await this.keyService.getUserKey(userId); const userKey = await firstValueFrom(this.keyService.userKey$(userId));
// If user key is not found, throw error // If user key is not found, throw error
if (!userKey) { if (!userKey) {
@@ -240,7 +241,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
const request = new OtherDeviceKeysUpdateRequest(); const request = new OtherDeviceKeysUpdateRequest();
request.encryptedPublicKey = newRotateableKeySet.encryptedPublicKey.encryptedString; request.encryptedPublicKey = newRotateableKeySet.encryptedPublicKey.encryptedString;
request.encryptedUserKey = newRotateableKeySet.encryptedUserKey.encryptedString; request.encryptedUserKey = newRotateableKeySet.encapsulatedDownstreamKey.encryptedString;
request.deviceId = device.id; request.deviceId = device.id;
return request; return request;
}) })

View File

@@ -366,7 +366,6 @@ describe("deviceTrustService", () => {
let makeDeviceKeySpy: jest.SpyInstance; let makeDeviceKeySpy: jest.SpyInstance;
let rsaGenerateKeyPairSpy: jest.SpyInstance; let rsaGenerateKeyPairSpy: jest.SpyInstance;
let cryptoSvcGetUserKeySpy: jest.SpyInstance;
let cryptoSvcRsaEncryptSpy: jest.SpyInstance; let cryptoSvcRsaEncryptSpy: jest.SpyInstance;
let encryptServiceWrapDecapsulationKeySpy: jest.SpyInstance; let encryptServiceWrapDecapsulationKeySpy: jest.SpyInstance;
let encryptServiceWrapEncapsulationKeySpy: jest.SpyInstance; let encryptServiceWrapEncapsulationKeySpy: jest.SpyInstance;
@@ -402,6 +401,8 @@ describe("deviceTrustService", () => {
"mockDeviceKeyEncryptedDevicePrivateKey", "mockDeviceKeyEncryptedDevicePrivateKey",
); );
keyService.userKey$.mockReturnValue(of(mockUserKey));
// TypeScript will allow calling private methods if the object is of type 'any' // TypeScript will allow calling private methods if the object is of type 'any'
makeDeviceKeySpy = jest makeDeviceKeySpy = jest
.spyOn(deviceTrustService as any, "makeDeviceKey") .spyOn(deviceTrustService as any, "makeDeviceKey")
@@ -411,10 +412,6 @@ describe("deviceTrustService", () => {
.spyOn(cryptoFunctionService, "rsaGenerateKeyPair") .spyOn(cryptoFunctionService, "rsaGenerateKeyPair")
.mockResolvedValue(mockDeviceRsaKeyPair); .mockResolvedValue(mockDeviceRsaKeyPair);
cryptoSvcGetUserKeySpy = jest
.spyOn(keyService, "getUserKey")
.mockResolvedValue(mockUserKey);
cryptoSvcRsaEncryptSpy = jest cryptoSvcRsaEncryptSpy = jest
.spyOn(encryptService, "encapsulateKeyUnsigned") .spyOn(encryptService, "encapsulateKeyUnsigned")
.mockResolvedValue(mockDevicePublicKeyEncryptedUserKey); .mockResolvedValue(mockDevicePublicKeyEncryptedUserKey);
@@ -448,7 +445,7 @@ describe("deviceTrustService", () => {
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1); expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1); expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcGetUserKeySpy).toHaveBeenCalledTimes(1); expect(keyService.userKey$).toHaveBeenCalledTimes(1);
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1); expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
@@ -473,18 +470,13 @@ describe("deviceTrustService", () => {
}); });
it("throws specific error if user key is not found", async () => { it("throws specific error if user key is not found", async () => {
// setup the spy to return null keyService.userKey$.mockReturnValueOnce(of(null));
cryptoSvcGetUserKeySpy.mockResolvedValue(null);
// check if the expected error is thrown // check if the expected error is thrown
await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow( await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow(
"User symmetric key not found", "User symmetric key not found",
); );
// reset the spy keyService.userKey$.mockReturnValueOnce(of(undefined));
cryptoSvcGetUserKeySpy.mockReset();
// setup the spy to return undefined
cryptoSvcGetUserKeySpy.mockResolvedValue(undefined);
// check if the expected error is thrown // check if the expected error is thrown
await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow( await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow(
"User symmetric key not found", "User symmetric key not found",
@@ -502,11 +494,6 @@ describe("deviceTrustService", () => {
spy: () => rsaGenerateKeyPairSpy, spy: () => rsaGenerateKeyPairSpy,
errorText: "rsaGenerateKeyPair error", errorText: "rsaGenerateKeyPair error",
}, },
{
method: "getUserKey",
spy: () => cryptoSvcGetUserKeySpy,
errorText: "getUserKey error",
},
{ {
method: "rsaEncrypt", method: "rsaEncrypt",
spy: () => cryptoSvcRsaEncryptSpy, spy: () => cryptoSvcRsaEncryptSpy,

View File

@@ -0,0 +1,34 @@
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { PrfKey } from "../../../types/key";
import { EncString } from "../../crypto/models/enc-string";
declare const tag: unique symbol;
/**
* A set of keys where a symmetric `DownstreamKey` is protected by an encrypted public/private key-pair.
* The `DownstreamKey` is used to encrypt/decrypt data, while the public/private key-pair is
* used to rotate the `DownstreamKey`.
*
* The `PrivateKey` is protected by an `UpstreamKey`, such as a `DeviceKey`, or `PrfKey`,
* and the `PublicKey` is protected by the `DownstreamKey`. This setup allows:
*
* - Access to `DownstreamKey` by knowing the `UpstreamKey`
* - Rotation to a new `DownstreamKey` by knowing the current `DownstreamKey`,
* without needing access to the `UpstreamKey`
*/
export class RotateableKeySet<UpstreamKey extends SymmetricCryptoKey = SymmetricCryptoKey> {
private readonly [tag]!: UpstreamKey;
constructor(
/** `DownstreamKey` protected by publicKey */
readonly encapsulatedDownstreamKey: EncString,
/** DownstreamKey encrypted PublicKey */
readonly encryptedPublicKey: EncString,
/** UpstreamKey encrypted PrivateKey */
readonly encryptedPrivateKey?: EncString,
) {}
}
export type PrfKeySet = RotateableKeySet<PrfKey>;

View File

@@ -0,0 +1,30 @@
import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key";
import { RotateableKeySet } from "../../models/rotateable-key-set";
export abstract class RotateableKeySetService {
/**
* Create a new rotatable key set for the provided downstreamKey, using the provided upstream key.
* For more information on rotatable key sets, see {@link RotateableKeySet}
* @param upstreamKey The `UpstreamKey` used to encrypt {@link RotateableKeySet.encryptedPrivateKey}
* @param downstreamKey The symmetric key to be contained within the `RotateableKeySet`.
* @returns RotateableKeySet containing the provided symmetric downstreamKey.
*/
abstract createKeySet<UpstreamKey extends SymmetricCryptoKey>(
upstreamKey: UpstreamKey,
downstreamKey: SymmetricCryptoKey,
): Promise<RotateableKeySet<UpstreamKey>>;
/**
* Rotates the provided `RotateableKeySet` with the new key.
*
* @param keySet The current `RotateableKeySet` to be rotated.
* @param oldDownstreamKey The current downstreamKey used to decrypt the `PublicKey`.
* @param newDownstreamKey The new downstreamKey to encrypt the `PublicKey`.
* @returns The updated `RotateableKeySet` that contains the new downstreamKey.
*/
abstract rotateKeySet<UpstreamKey extends SymmetricCryptoKey>(
keySet: RotateableKeySet<UpstreamKey>,
oldDownstreamKey: SymmetricCryptoKey,
newDownstreamKey: SymmetricCryptoKey,
): Promise<RotateableKeySet<UpstreamKey>>;
}

View File

@@ -0,0 +1,185 @@
import { mock, MockProxy } from "jest-mock-extended";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { EncString } from "../../crypto/models/enc-string";
import { RotateableKeySet } from "../models/rotateable-key-set";
import { DefaultRotateableKeySetService } from "./default-rotateable-key-set.service";
describe("DefaultRotateableKeySetService", () => {
let keyService!: MockProxy<KeyService>;
let encryptService!: MockProxy<EncryptService>;
let service!: DefaultRotateableKeySetService;
beforeEach(() => {
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
service = new DefaultRotateableKeySetService(keyService, encryptService);
});
describe("createKeySet", () => {
test.each([null, undefined])(
"throws error when downstreamKey parameter is %s",
async (downstreamKey) => {
const externalKey = createSymmetricKey();
await expect(service.createKeySet(externalKey, downstreamKey as any)).rejects.toThrow(
"failed to create key set: downstreamKey is required",
);
},
);
test.each([null, undefined])(
"throws error when upstreamKey parameter is %s",
async (upstreamKey) => {
const userKey = createSymmetricKey();
await expect(service.createKeySet(upstreamKey as any, userKey)).rejects.toThrow(
"failed to create key set: upstreamKey is required",
);
},
);
it("should create a new key set", async () => {
const externalKey = createSymmetricKey();
const userKey = createSymmetricKey();
const encryptedUserKey = new EncString("encryptedUserKey");
const encryptedPublicKey = new EncString("encryptedPublicKey");
const encryptedPrivateKey = new EncString("encryptedPrivateKey");
keyService.makeKeyPair.mockResolvedValue(["publicKey", encryptedPrivateKey]);
encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedUserKey);
encryptService.wrapEncapsulationKey.mockResolvedValue(encryptedPublicKey);
const result = await service.createKeySet(externalKey, userKey);
expect(result).toEqual(
new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey),
);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(externalKey);
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
userKey,
Utils.fromB64ToArray("publicKey"),
);
expect(encryptService.wrapEncapsulationKey).toHaveBeenCalledWith(
Utils.fromB64ToArray("publicKey"),
userKey,
);
});
});
describe("rotateKeySet", () => {
const keySet = new RotateableKeySet(
new EncString("encUserKey"),
new EncString("encPublicKey"),
new EncString("encPrivateKey"),
);
const dataValidationTests = [
{
keySet: null as any as RotateableKeySet,
oldDownstreamKey: createSymmetricKey(),
newDownstreamKey: createSymmetricKey(),
expectedError: "failed to rotate key set: keySet is required",
},
{
keySet: undefined as any as RotateableKeySet,
oldDownstreamKey: createSymmetricKey(),
newDownstreamKey: createSymmetricKey(),
expectedError: "failed to rotate key set: keySet is required",
},
{
keySet: keySet,
oldDownstreamKey: null,
newDownstreamKey: createSymmetricKey(),
expectedError: "failed to rotate key set: oldDownstreamKey is required",
},
{
keySet: keySet,
oldDownstreamKey: undefined,
newDownstreamKey: createSymmetricKey(),
expectedError: "failed to rotate key set: oldDownstreamKey is required",
},
{
keySet: keySet,
oldDownstreamKey: createSymmetricKey(),
newDownstreamKey: null,
expectedError: "failed to rotate key set: newDownstreamKey is required",
},
{
keySet: keySet,
oldDownstreamKey: createSymmetricKey(),
newDownstreamKey: undefined,
expectedError: "failed to rotate key set: newDownstreamKey is required",
},
];
test.each(dataValidationTests)(
"should throw error when required parameter is missing",
async ({ keySet, oldDownstreamKey, newDownstreamKey, expectedError }) => {
await expect(
service.rotateKeySet(keySet, oldDownstreamKey as any, newDownstreamKey as any),
).rejects.toThrow(expectedError);
},
);
it("throws an error if the public key cannot be decrypted", async () => {
const oldDownstreamKey = createSymmetricKey();
const newDownstreamKey = createSymmetricKey();
encryptService.unwrapEncapsulationKey.mockResolvedValue(null as any);
await expect(
service.rotateKeySet(keySet, oldDownstreamKey, newDownstreamKey),
).rejects.toThrow("failed to rotate key set: could not decrypt public key");
expect(encryptService.unwrapEncapsulationKey).toHaveBeenCalledWith(
keySet.encryptedPublicKey,
oldDownstreamKey,
);
expect(encryptService.wrapEncapsulationKey).not.toHaveBeenCalled();
expect(encryptService.encapsulateKeyUnsigned).not.toHaveBeenCalled();
});
it("rotates the key set", async () => {
const oldDownstreamKey = createSymmetricKey();
const newDownstreamKey = new SymmetricCryptoKey(new Uint8Array(64));
const publicKey = Utils.fromB64ToArray("decryptedPublicKey");
const newEncryptedPublicKey = new EncString("newEncPublicKey");
const newEncryptedRotateableKey = new EncString("newEncUserKey");
encryptService.unwrapEncapsulationKey.mockResolvedValue(publicKey);
encryptService.wrapEncapsulationKey.mockResolvedValue(newEncryptedPublicKey);
encryptService.encapsulateKeyUnsigned.mockResolvedValue(newEncryptedRotateableKey);
const result = await service.rotateKeySet(keySet, oldDownstreamKey, newDownstreamKey);
expect(result).toEqual(
new RotateableKeySet(
newEncryptedRotateableKey,
newEncryptedPublicKey,
keySet.encryptedPrivateKey,
),
);
expect(encryptService.unwrapEncapsulationKey).toHaveBeenCalledWith(
keySet.encryptedPublicKey,
oldDownstreamKey,
);
expect(encryptService.wrapEncapsulationKey).toHaveBeenCalledWith(publicKey, newDownstreamKey);
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
newDownstreamKey,
publicKey,
);
});
});
});
function createSymmetricKey() {
const key = Utils.fromB64ToArray(
"1h-TuPwSbX5qoX0aVgjmda_Lfq85qAcKssBlXZnPIsQC3HNDGIecunYqXhJnp55QpdXRh-egJiLH3a0wqlVQsQ",
);
return new SymmetricCryptoKey(key);
}

View File

@@ -0,0 +1,83 @@
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { RotateableKeySet } from "../models/rotateable-key-set";
import { RotateableKeySetService } from "./abstractions/rotateable-key-set.service";
export class DefaultRotateableKeySetService implements RotateableKeySetService {
constructor(
private keyService: KeyService,
private encryptService: EncryptService,
) {}
async createKeySet<UpstreamKey extends SymmetricCryptoKey>(
upstreamKey: UpstreamKey,
downstreamKey: SymmetricCryptoKey,
): Promise<RotateableKeySet<UpstreamKey>> {
if (!upstreamKey) {
throw new Error("failed to create key set: upstreamKey is required");
}
if (!downstreamKey) {
throw new Error("failed to create key set: downstreamKey is required");
}
const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(upstreamKey);
const rawPublicKey = Utils.fromB64ToArray(publicKey);
const encryptedRotateableKey = await this.encryptService.encapsulateKeyUnsigned(
downstreamKey,
rawPublicKey,
);
const encryptedPublicKey = await this.encryptService.wrapEncapsulationKey(
rawPublicKey,
downstreamKey,
);
return new RotateableKeySet(encryptedRotateableKey, encryptedPublicKey, encryptedPrivateKey);
}
async rotateKeySet<UpstreamKey extends SymmetricCryptoKey>(
keySet: RotateableKeySet<UpstreamKey>,
oldDownstreamKey: SymmetricCryptoKey,
newDownstreamKey: SymmetricCryptoKey,
): Promise<RotateableKeySet<UpstreamKey>> {
// validate parameters
if (!keySet) {
throw new Error("failed to rotate key set: keySet is required");
}
if (!oldDownstreamKey) {
throw new Error("failed to rotate key set: oldDownstreamKey is required");
}
if (!newDownstreamKey) {
throw new Error("failed to rotate key set: newDownstreamKey is required");
}
const publicKey = await this.encryptService.unwrapEncapsulationKey(
keySet.encryptedPublicKey,
oldDownstreamKey,
);
if (publicKey == null) {
throw new Error("failed to rotate key set: could not decrypt public key");
}
const newEncryptedPublicKey = await this.encryptService.wrapEncapsulationKey(
publicKey,
newDownstreamKey,
);
const newEncryptedRotateableKey = await this.encryptService.encapsulateKeyUnsigned(
newDownstreamKey,
publicKey,
);
const newRotateableKeySet = new RotateableKeySet<UpstreamKey>(
newEncryptedRotateableKey,
newEncryptedPublicKey,
keySet.encryptedPrivateKey,
);
return newRotateableKeySet;
}
}

View File

@@ -166,16 +166,16 @@ export abstract class KeyService {
*/ */
abstract makeMasterKey(password: string, email: string, kdfConfig: KdfConfig): Promise<MasterKey>; abstract makeMasterKey(password: string, email: string, kdfConfig: KdfConfig): Promise<MasterKey>;
/** /**
* Encrypts the existing (or provided) user key with the * Encrypts the provided user key with the provided master key.
* provided master key
* @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead. * @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead.
* @param masterKey The user's master key * @param masterKey The user's master key
* @param userKey The user key * @param userKey The user key
* @throws Error when userKey or masterKey is null/undefined.
* @returns The user key and the master key protected version of it * @returns The user key and the master key protected version of it
*/ */
abstract encryptUserKeyWithMasterKey( abstract encryptUserKeyWithMasterKey(
masterKey: MasterKey, masterKey: MasterKey,
userKey?: UserKey, userKey: UserKey,
): Promise<[UserKey, EncString]>; ): Promise<[UserKey, EncString]>;
/** /**
* Creates a master password hash from the user's master password. Can * Creates a master password hash from the user's master password. Can

View File

@@ -1357,6 +1357,51 @@ describe("keyService", () => {
}); });
}); });
describe("encryptUserKeyWithMasterKey", () => {
const mockMasterKey = makeSymmetricCryptoKey<MasterKey>(32);
const mockUserKey = makeSymmetricCryptoKey<UserKey>(64);
test.each([null as unknown as MasterKey, undefined as unknown as MasterKey])(
"throws when the provided master key is %s",
async (key) => {
await expect(keyService.encryptUserKeyWithMasterKey(key, mockUserKey)).rejects.toThrow(
"masterKey is required.",
);
},
);
test.each([null as unknown as UserKey, undefined as unknown as UserKey])(
"throws when the provided userKey key is %s",
async (key) => {
await expect(keyService.encryptUserKeyWithMasterKey(mockMasterKey, key)).rejects.toThrow(
"userKey is required.",
);
},
);
it("throws with invalid master key size", async () => {
const invalidMasterKey = new SymmetricCryptoKey(new Uint8Array(78)) as MasterKey;
await expect(
keyService.encryptUserKeyWithMasterKey(invalidMasterKey, mockUserKey),
).rejects.toThrow("Invalid key size.");
});
it("encrypts the user key with the master key", async () => {
const mockEncryptedUserKey = makeEncString("encryptedUserKey");
encryptService.wrapSymmetricKey.mockResolvedValue(mockEncryptedUserKey);
const stretchedMasterKey = new SymmetricCryptoKey(new Uint8Array(64));
keyGenerationService.stretchKey.mockResolvedValue(stretchedMasterKey);
const result = await keyService.encryptUserKeyWithMasterKey(mockMasterKey, mockUserKey);
expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockUserKey, stretchedMasterKey);
expect(result[0]).toBe(mockUserKey);
expect(result[1]).toBe(mockEncryptedUserKey);
});
});
describe("makeKeyPair", () => { describe("makeKeyPair", () => {
test.each([null as unknown as SymmetricCryptoKey, undefined as unknown as SymmetricCryptoKey])( test.each([null as unknown as SymmetricCryptoKey, undefined as unknown as SymmetricCryptoKey])(
"throws when the provided key is %s", "throws when the provided key is %s",

View File

@@ -166,6 +166,9 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return this.stateProvider.getUserState$(USER_KEY, userId); return this.stateProvider.getUserState$(USER_KEY, userId);
} }
/**
* @deprecated Use {@link userKey$} with a required {@link UserId} instead.
*/
async getUserKey(userId?: UserId): Promise<UserKey> { async getUserKey(userId?: UserId): Promise<UserKey> {
const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId));
return userKey; return userKey;
@@ -298,9 +301,15 @@ export class DefaultKeyService implements KeyServiceAbstraction {
*/ */
async encryptUserKeyWithMasterKey( async encryptUserKeyWithMasterKey(
masterKey: MasterKey, masterKey: MasterKey,
userKey?: UserKey, userKey: UserKey,
): Promise<[UserKey, EncString]> { ): Promise<[UserKey, EncString]> {
userKey ||= await this.getUserKey(); if (masterKey == null) {
throw new Error("masterKey is required.");
}
if (userKey == null) {
throw new Error("userKey is required.");
}
return await this.buildProtectedSymmetricKey(masterKey, userKey); return await this.buildProtectedSymmetricKey(masterKey, userKey);
} }
@@ -630,7 +639,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
} }
// Verify user key doesn't exist // Verify user key doesn't exist
const existingUserKey = await this.getUserKey(userId); const existingUserKey = await firstValueFrom(this.userKey$(userId));
if (existingUserKey != null) { if (existingUserKey != null) {
this.logService.error("Tried to initialize account with existing user key."); this.logService.error("Tried to initialize account with existing user key.");