1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-6789] finish key rotation distribution and fix legacy user (#9498)

* finish key rotation distribution and fix legacy user

* add ticket to TODO

* PR feedback: docs and renaming

* fix webauthn tests

* add test for send service

* add await to test
This commit is contained in:
Jake Fink
2024-06-20 11:36:24 -04:00
committed by GitHub
parent eadb1fa4ef
commit b306554675
23 changed files with 516 additions and 196 deletions

View File

@@ -13,6 +13,7 @@ import { EncryptionType, KdfType } from "@bitwarden/common/platform/enums";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey, OrgKey, MasterKey } from "@bitwarden/common/types/key";
import { OrganizationUserResetPasswordService } from "./organization-user-reset-password.service";
@@ -157,7 +158,7 @@ describe("OrganizationUserResetPasswordService", () => {
});
});
describe("getRotatedKeys", () => {
describe("getRotatedData", () => {
beforeEach(() => {
organizationService.getAll.mockResolvedValue([
createOrganization("1", "org1"),
@@ -175,12 +176,24 @@ describe("OrganizationUserResetPasswordService", () => {
});
it("should return all re-encrypted account recovery keys", async () => {
const result = await sut.getRotatedKeys(
const result = await sut.getRotatedData(
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
"mockUserId" as UserId,
);
expect(result).toHaveLength(2);
});
it("throws if the new user key is null", async () => {
await expect(
sut.getRotatedData(
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
null,
"mockUserId" as UserId,
),
).rejects.toThrow("New user key is required for rotation.");
});
});
});

View File

@@ -1,5 +1,6 @@
import { Injectable } from "@angular/core";
import { UserKeyRotationDataProvider } from "@bitwarden/auth/common";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
@@ -19,12 +20,15 @@ import { KdfType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
@Injectable({
providedIn: "root",
})
export class OrganizationUserResetPasswordService {
export class OrganizationUserResetPasswordService
implements UserKeyRotationDataProvider<OrganizationUserResetPasswordWithIdRequest>
{
constructor(
private cryptoService: CryptoService,
private encryptService: EncryptService,
@@ -129,11 +133,16 @@ export class OrganizationUserResetPasswordService {
/**
* Returns existing account recovery keys re-encrypted with the new user key.
* @param originalUserKey the original user key
* @param newUserKey the new user key
* @param userId the user id
* @throws Error if new user key is null
* @returns a list of account recovery keys that have been re-encrypted with the new user key
*/
async getRotatedKeys(
async getRotatedData(
originalUserKey: UserKey,
newUserKey: UserKey,
userId: UserId,
): Promise<OrganizationUserResetPasswordWithIdRequest[] | null> {
if (newUserKey == null) {
throw new Error("New user key is required for rotation.");

View File

@@ -190,7 +190,7 @@ describe("WebauthnAdminService", () => {
it("should throw when old userkey is null", async () => {
const newUserKey = makeSymmetricCryptoKey(64) as UserKey;
try {
await service.rotateWebAuthnKeys(null, newUserKey);
await service.getRotatedData(null, newUserKey, null);
} catch (error) {
expect(error).toEqual(new Error("oldUserKey is required"));
}
@@ -198,7 +198,7 @@ describe("WebauthnAdminService", () => {
it("should throw when new userkey is null", async () => {
const oldUserKey = makeSymmetricCryptoKey(64) as UserKey;
try {
await service.rotateWebAuthnKeys(oldUserKey, null);
await service.getRotatedData(oldUserKey, null, null);
} catch (error) {
expect(error).toEqual(new Error("newUserKey is required"));
}
@@ -222,7 +222,7 @@ describe("WebauthnAdminService", () => {
.mockResolvedValue(
new RotateableKeySet<PrfKey>(mockEncryptedUserKey, mockEncryptedPublicKey),
);
await service.rotateWebAuthnKeys(oldUserKey, newUserKey);
await service.getRotatedData(oldUserKey, newUserKey, null);
expect(rotateKeySetMock).toHaveBeenCalledWith(
expect.any(RotateableKeySet),
oldUserKey,
@@ -242,7 +242,7 @@ describe("WebauthnAdminService", () => {
],
} as any);
const rotateKeySetMock = jest.spyOn(rotateableKeySetService, "rotateKeySet");
await service.rotateWebAuthnKeys(oldUserKey, newUserKey);
await service.getRotatedData(oldUserKey, newUserKey, null);
expect(rotateKeySetMock).not.toHaveBeenCalled();
});
});

View File

@@ -1,7 +1,7 @@
import { Injectable, Optional } from "@angular/core";
import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap, tap } from "rxjs";
import { PrfKeySet } from "@bitwarden/auth/common";
import { PrfKeySet, UserKeyRotationDataProvider } from "@bitwarden/auth/common";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { WebAuthnLoginPrfCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction";
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";
@@ -9,6 +9,7 @@ import { WebAuthnLoginCredentialAssertionOptionsView } from "@bitwarden/common/a
import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view";
import { Verification } from "@bitwarden/common/auth/types/verification";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import { CredentialCreateOptionsView } from "../../views/credential-create-options.view";
@@ -25,7 +26,9 @@ import { WebAuthnLoginAdminApiService } from "./webauthn-login-admin-api.service
/**
* Service for managing WebAuthnLogin credentials.
*/
export class WebauthnLoginAdminService {
export class WebauthnLoginAdminService
implements UserKeyRotationDataProvider<WebauthnRotateCredentialRequest>
{
static readonly MaxCredentialCount = 5;
private navigatorCredentials: CredentialsContainer;
@@ -283,11 +286,13 @@ export class WebauthnLoginAdminService {
*
* @param oldUserKey The old user key
* @param newUserKey The new user key
* @param userId The user id
* @returns A promise that returns an array of rotate credential requests when resolved.
*/
async rotateWebAuthnKeys(
async getRotatedData(
oldUserKey: UserKey,
newUserKey: UserKey,
userId: UserId,
): Promise<WebauthnRotateCredentialRequest[]> {
if (!oldUserKey) {
throw new Error("oldUserKey is required");

View File

@@ -11,6 +11,7 @@ import { EncryptionType, KdfType } from "@bitwarden/common/platform/enums";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -223,8 +224,11 @@ describe("EmergencyAccessService", () => {
});
});
describe("getRotatedKeys", () => {
let mockUserKey: UserKey;
describe("getRotatedData", () => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
const mockOriginalUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
const mockNewUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
const allowedStatuses = [
EmergencyAccessStatusType.Confirmed,
EmergencyAccessStatusType.RecoveryInitiated,
@@ -242,9 +246,6 @@ describe("EmergencyAccessService", () => {
} as ListResponse<EmergencyAccessGranteeDetailsResponse>;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
emergencyAccessApiService.getEmergencyAccessTrusted.mockResolvedValue(mockEmergencyAccess);
apiService.getUserPublicKey.mockResolvedValue({
userId: "mockUserId",
@@ -259,10 +260,20 @@ describe("EmergencyAccessService", () => {
});
it("Only returns emergency accesses with allowed statuses", async () => {
const result = await emergencyAccessService.getRotatedKeys(mockUserKey);
const result = await emergencyAccessService.getRotatedData(
mockOriginalUserKey,
mockNewUserKey,
"mockUserId" as UserId,
);
expect(result).toHaveLength(allowedStatuses.length);
});
it("throws if new user key is null", async () => {
await expect(
emergencyAccessService.getRotatedData(mockOriginalUserKey, null, "mockUserId" as UserId),
).rejects.toThrow("New user key is required for rotation.");
});
});
});

View File

@@ -1,5 +1,6 @@
import { Injectable } from "@angular/core";
import { UserKeyRotationDataProvider } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
@@ -15,6 +16,7 @@ import { KdfType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@@ -35,7 +37,9 @@ import {
import { EmergencyAccessApiService } from "./emergency-access-api.service";
@Injectable()
export class EmergencyAccessService {
export class EmergencyAccessService
implements UserKeyRotationDataProvider<EmergencyAccessWithIdRequest>
{
constructor(
private emergencyAccessApiService: EmergencyAccessApiService,
private apiService: ApiService,
@@ -286,9 +290,21 @@ export class EmergencyAccessService {
/**
* Returns existing emergency access keys re-encrypted with new user key.
* Intended for grantor.
* @param originalUserKey the original user key
* @param newUserKey the new user key
* @param userId the user id
* @throws Error if newUserKey is nullish
* @returns an array of re-encrypted emergency access requests or an empty array if there are no requests
*/
async getRotatedKeys(newUserKey: UserKey): Promise<EmergencyAccessWithIdRequest[]> {
async getRotatedData(
originalUserKey: UserKey,
newUserKey: UserKey,
userId: UserId,
): Promise<EmergencyAccessWithIdRequest[]> {
if (newUserKey == null) {
throw new Error("New user key is required for rotation.");
}
const requests: EmergencyAccessWithIdRequest[] = [];
const existingEmergencyAccess =
await this.emergencyAccessApiService.getEmergencyAccessTrusted();

View File

@@ -1,37 +1,30 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { BehaviorSubject } from "rxjs";
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { Send } from "@bitwarden/common/tools/send/models/domain/send";
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import { UserKey, UserPrivateKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { Folder } from "@bitwarden/common/vault/models/domain/folder";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request";
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
import {
FakeAccountService,
mockAccountServiceWith,
} from "../../../../../../libs/common/spec/fake-account-service";
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
import { StateService } from "../../core";
import { WebauthnLoginAdminService } from "../core";
import { EmergencyAccessService } from "../emergency-access";
import { EmergencyAccessWithIdRequest } from "../emergency-access/request/emergency-access-update.request";
import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
import { UserKeyRotationService } from "./user-key-rotation.service";
@@ -48,16 +41,19 @@ describe("KeyRotationService", () => {
let mockDeviceTrustService: MockProxy<DeviceTrustServiceAbstraction>;
let mockCryptoService: MockProxy<CryptoService>;
let mockEncryptService: MockProxy<EncryptService>;
let mockStateService: MockProxy<StateService>;
let mockConfigService: MockProxy<ConfigService>;
let mockKdfConfigService: MockProxy<KdfConfigService>;
let mockSyncService: MockProxy<SyncService>;
let mockWebauthnLoginAdminService: MockProxy<WebauthnLoginAdminService>;
const mockUserId = Utils.newGuid() as UserId;
const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId);
let mockMasterPasswordService: FakeMasterPasswordService = new FakeMasterPasswordService();
const mockUser = {
id: "mockUserId" as UserId,
email: "mockEmail",
emailVerified: true,
name: "mockName",
};
beforeAll(() => {
mockMasterPasswordService = new FakeMasterPasswordService();
mockApiService = mock<UserKeyRotationApiService>();
@@ -69,7 +65,6 @@ describe("KeyRotationService", () => {
mockDeviceTrustService = mock<DeviceTrustServiceAbstraction>();
mockCryptoService = mock<CryptoService>();
mockEncryptService = mock<EncryptService>();
mockStateService = mock<StateService>();
mockConfigService = mock<ConfigService>();
mockKdfConfigService = mock<KdfConfigService>();
mockSyncService = mock<SyncService>();
@@ -86,8 +81,6 @@ describe("KeyRotationService", () => {
mockDeviceTrustService,
mockCryptoService,
mockEncryptService,
mockStateService,
mockAccountService,
mockKdfConfigService,
mockSyncService,
mockWebauthnLoginAdminService,
@@ -98,91 +91,82 @@ describe("KeyRotationService", () => {
jest.clearAllMocks();
});
it("instantiates", () => {
expect(keyRotationService).not.toBeFalsy();
});
describe("rotateUserKeyAndEncryptedData", () => {
let folderViews: BehaviorSubject<FolderView[]>;
let sends: BehaviorSubject<Send[]>;
let privateKey: BehaviorSubject<UserPrivateKey>;
beforeAll(() => {
beforeEach(() => {
mockCryptoService.makeMasterKey.mockResolvedValue("mockMasterKey" as any);
mockCryptoService.makeUserKey.mockResolvedValue([
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
{
encryptedString: "mockEncryptedUserKey",
encryptedString: "mockNewUserKey",
} as any,
]);
mockCryptoService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash");
mockConfigService.getFeatureFlag.mockResolvedValue(true);
// Mock private key
mockCryptoService.getPrivateKey.mockResolvedValue("MockPrivateKey" as any);
mockCryptoService.userKey$.mockReturnValue(
of(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey),
);
// Mock ciphers
const mockCiphers = [createMockCipher("1", "Cipher 1"), createMockCipher("2", "Cipher 2")];
mockCipherService.getAllDecrypted.mockResolvedValue(mockCiphers);
// Mock folders
const mockFolders = [createMockFolder("1", "Folder 1"), createMockFolder("2", "Folder 2")];
folderViews = new BehaviorSubject<FolderView[]>(mockFolders);
mockFolderService.folderViews$ = folderViews;
// Mock sends
const mockSends = [createMockSend("1", "Send 1"), createMockSend("2", "Send 2")];
sends = new BehaviorSubject<Send[]>(mockSends);
mockSendService.sends$ = sends;
mockWebauthnLoginAdminService.rotateWebAuthnKeys.mockResolvedValue([]);
// Mock encryption methods
mockEncryptService.encrypt.mockResolvedValue({
encryptedString: "mockEncryptedData",
} as any);
mockFolderService.encrypt.mockImplementation((folder, userKey) => {
const encryptedFolder = new Folder();
encryptedFolder.id = folder.id;
encryptedFolder.name = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"Encrypted: " + folder.name,
);
return Promise.resolve(encryptedFolder);
});
// Mock user key
mockCryptoService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any));
mockCipherService.encrypt.mockImplementation((cipher, userKey) => {
const encryptedCipher = new Cipher();
encryptedCipher.id = cipher.id;
encryptedCipher.name = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"Encrypted: " + cipher.name,
);
return Promise.resolve(encryptedCipher);
});
// Mock private key
privateKey = new BehaviorSubject("mockPrivateKey" as any);
mockCryptoService.userPrivateKeyWithLegacySupport$.mockReturnValue(privateKey);
// Mock ciphers
const mockCiphers = [createMockCipher("1", "Cipher 1"), createMockCipher("2", "Cipher 2")];
mockCipherService.getRotatedData.mockResolvedValue(mockCiphers);
// Mock folders
const mockFolders = [createMockFolder("1", "Folder 1"), createMockFolder("2", "Folder 2")];
mockFolderService.getRotatedData.mockResolvedValue(mockFolders);
// Mock sends
const mockSends = [createMockSend("1", "Send 1"), createMockSend("2", "Send 2")];
mockSendService.getRotatedData.mockResolvedValue(mockSends);
// Mock emergency access
const emergencyAccess = [createMockEmergencyAccess("13")];
mockEmergencyAccessService.getRotatedData.mockResolvedValue(emergencyAccess);
// Mock reset password
const resetPassword = [createMockResetPassword("12")];
mockResetPasswordService.getRotatedData.mockResolvedValue(resetPassword);
// Mock Webauthn
const webauthn = [createMockWebauthn("13"), createMockWebauthn("14")];
mockWebauthnLoginAdminService.getRotatedData.mockResolvedValue(webauthn);
});
it("rotates the user key and encrypted data", async () => {
await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword");
await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser);
expect(mockApiService.postUserKeyUpdate).toHaveBeenCalled();
const arg = mockApiService.postUserKeyUpdate.mock.calls[0][0];
expect(arg.key).toBe("mockNewUserKey");
expect(arg.privateKey).toBe("mockEncryptedData");
expect(arg.ciphers.length).toBe(2);
expect(arg.folders.length).toBe(2);
expect(arg.sends.length).toBe(2);
expect(arg.emergencyAccessKeys.length).toBe(1);
expect(arg.resetPasswordKeys.length).toBe(1);
expect(arg.webauthnKeys.length).toBe(2);
});
it("throws if master password provided is falsey", async () => {
await expect(keyRotationService.rotateUserKeyAndEncryptedData("")).rejects.toThrow();
await expect(
keyRotationService.rotateUserKeyAndEncryptedData("", mockUser),
).rejects.toThrow();
});
it("throws if master key creation fails", async () => {
mockCryptoService.makeMasterKey.mockResolvedValueOnce(null);
await expect(
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"),
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
).rejects.toThrow();
});
@@ -190,16 +174,24 @@ describe("KeyRotationService", () => {
mockCryptoService.makeUserKey.mockResolvedValueOnce([null, null]);
await expect(
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"),
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
).rejects.toThrow();
});
it("throws if no private key is found", async () => {
privateKey.next(null);
await expect(
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
).rejects.toThrow();
});
it("saves the master key in state after creation", async () => {
await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword");
await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser);
expect(mockMasterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(
"mockMasterKey" as any,
mockUserId,
mockUser.id,
);
});
@@ -207,30 +199,51 @@ describe("KeyRotationService", () => {
mockApiService.postUserKeyUpdate.mockRejectedValueOnce(new Error("mockError"));
await expect(
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"),
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
).rejects.toThrow();
});
});
});
function createMockFolder(id: string, name: string): FolderView {
const folder = new FolderView();
folder.id = id;
folder.name = name;
return folder;
function createMockFolder(id: string, name: string): FolderWithIdRequest {
return {
id: id,
name: name,
} as FolderWithIdRequest;
}
function createMockCipher(id: string, name: string): CipherView {
const cipher = new CipherView();
cipher.id = id;
cipher.name = name;
cipher.type = CipherType.Login;
return cipher;
function createMockCipher(id: string, name: string): CipherWithIdRequest {
return {
id: id,
name: name,
type: CipherType.Login,
} as CipherWithIdRequest;
}
function createMockSend(id: string, name: string): Send {
const send = new Send();
send.id = id;
send.name = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, name);
return send;
function createMockSend(id: string, name: string): SendWithIdRequest {
return {
id: id,
name: name,
} as SendWithIdRequest;
}
function createMockEmergencyAccess(id: string): EmergencyAccessWithIdRequest {
return {
id: id,
type: 0,
waitTimeDays: 5,
} as EmergencyAccessWithIdRequest;
}
function createMockResetPassword(id: string): OrganizationUserResetPasswordWithIdRequest {
return {
organizationId: id,
resetPasswordKey: "mockResetPasswordKey",
} as OrganizationUserResetPasswordWithIdRequest;
}
function createMockWebauthn(id: string): any {
return {
id: id,
} as WebauthnRotateCredentialRequest;
}

View File

@@ -1,21 +1,19 @@
import { Injectable } from "@angular/core";
import { firstValueFrom, map } from "rxjs";
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AccountInfo } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request";
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
import { WebauthnLoginAdminService } from "../core";
@@ -37,8 +35,6 @@ export class UserKeyRotationService {
private deviceTrustService: DeviceTrustServiceAbstraction,
private cryptoService: CryptoService,
private encryptService: EncryptService,
private stateService: StateService,
private accountService: AccountService,
private kdfConfigService: KdfConfigService,
private syncService: SyncService,
private webauthnLoginAdminService: WebauthnLoginAdminService,
@@ -48,7 +44,10 @@ export class UserKeyRotationService {
* Creates a new user key and re-encrypts all required data with the it.
* @param masterPassword current master password (used for validation)
*/
async rotateUserKeyAndEncryptedData(masterPassword: string): Promise<void> {
async rotateUserKeyAndEncryptedData(
masterPassword: string,
user: { id: UserId } & AccountInfo,
): Promise<void> {
if (!masterPassword) {
throw new Error("Invalid master password");
}
@@ -62,7 +61,7 @@ export class UserKeyRotationService {
// Create master key to validate the master password
const masterKey = await this.cryptoService.makeMasterKey(
masterPassword,
await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.email))),
user.email,
await this.kdfConfigService.getKdfConfig(),
);
@@ -71,9 +70,7 @@ export class UserKeyRotationService {
}
// Set master key again in case it was lost (could be lost on refresh)
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const oldUserKey = await firstValueFrom(this.cryptoService.userKey$(userId));
await this.masterPasswordService.setMasterKey(masterKey, userId);
await this.masterPasswordService.setMasterKey(masterKey, user.id);
const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey(masterKey);
if (!newUserKey || !newEncUserKey) {
@@ -90,61 +87,86 @@ export class UserKeyRotationService {
const masterPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey);
request.masterPasswordHash = masterPasswordHash;
// Get original user key
// Note: We distribute the legacy key, but not all domains actually use it. If any of those
// domains break their legacy support it will break the migration process for legacy users.
const originalUserKey = await this.cryptoService.getUserKeyWithLegacySupport(user.id);
// Add re-encrypted data
request.privateKey = await this.encryptPrivateKey(newUserKey);
request.ciphers = await this.encryptCiphers(newUserKey);
request.folders = await this.encryptFolders(newUserKey);
request.sends = await this.sendService.getRotatedKeys(newUserKey);
request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey);
request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey);
request.webauthnKeys = await this.webauthnLoginAdminService.rotateWebAuthnKeys(
oldUserKey,
request.privateKey = await this.encryptPrivateKey(newUserKey, user.id);
const rotatedCiphers = await this.cipherService.getRotatedData(
originalUserKey,
newUserKey,
user.id,
);
if (rotatedCiphers != null) {
request.ciphers = rotatedCiphers;
}
const rotatedFolders = await this.folderService.getRotatedData(
originalUserKey,
newUserKey,
user.id,
);
if (rotatedFolders != null) {
request.folders = rotatedFolders;
}
const rotatedSends = await this.sendService.getRotatedData(
originalUserKey,
newUserKey,
user.id,
);
if (rotatedSends != null) {
request.sends = rotatedSends;
}
const rotatedEmergencyAccessKeys = await this.emergencyAccessService.getRotatedData(
originalUserKey,
newUserKey,
user.id,
);
if (rotatedEmergencyAccessKeys != null) {
request.emergencyAccessKeys = rotatedEmergencyAccessKeys;
}
// Note: Reset password keys request model has user verification
// properties, but the rotation endpoint uses its own MP hash.
const rotatedResetPasswordKeys = await this.resetPasswordService.getRotatedData(
originalUserKey,
newUserKey,
user.id,
);
if (rotatedResetPasswordKeys != null) {
request.resetPasswordKeys = rotatedResetPasswordKeys;
}
const rotatedWebauthnKeys = await this.webauthnLoginAdminService.getRotatedData(
originalUserKey,
newUserKey,
user.id,
);
if (rotatedWebauthnKeys != null) {
request.webauthnKeys = rotatedWebauthnKeys;
}
await this.apiService.postUserKeyUpdate(request);
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.deviceTrustService.rotateDevicesTrust(
activeAccount.id,
newUserKey,
masterPasswordHash,
);
// TODO PM-2199: Add device trust rotation support to the user key rotation endpoint
await this.deviceTrustService.rotateDevicesTrust(user.id, newUserKey, masterPasswordHash);
}
private async encryptPrivateKey(newUserKey: UserKey): Promise<EncryptedString | null> {
const privateKey = await this.cryptoService.getPrivateKey();
private async encryptPrivateKey(
newUserKey: UserKey,
userId: UserId,
): Promise<EncryptedString | null> {
const privateKey = await firstValueFrom(
this.cryptoService.userPrivateKeyWithLegacySupport$(userId),
);
if (!privateKey) {
return;
throw new Error("No private key found for user key rotation");
}
return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString;
}
private async encryptCiphers(newUserKey: UserKey): Promise<CipherWithIdRequest[]> {
const ciphers = await this.cipherService.getAllDecrypted();
if (!ciphers) {
// Must return an empty array for backwards compatibility
return [];
}
return await Promise.all(
ciphers.map(async (cipher) => {
const encryptedCipher = await this.cipherService.encrypt(cipher, newUserKey);
return new CipherWithIdRequest(encryptedCipher);
}),
);
}
private async encryptFolders(newUserKey: UserKey): Promise<FolderWithIdRequest[]> {
const folders = await firstValueFrom(this.folderService.folderViews$);
if (!folders) {
// Must return an empty array for backwards compatibility
return [];
}
return await Promise.all(
folders.map(async (folder) => {
const encryptedFolder = await this.folderService.encrypt(folder, newUserKey);
return new FolderWithIdRequest(encryptedFolder);
}),
);
}
}

View File

@@ -1,6 +1,8 @@
import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -25,6 +27,7 @@ export class MigrateFromLegacyEncryptionComponent {
});
constructor(
private accountService: AccountService,
private keyRotationService: UserKeyRotationService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
@@ -41,7 +44,9 @@ export class MigrateFromLegacyEncryptionComponent {
return;
}
const hasUserKey = await this.cryptoService.hasUserKey();
const activeUser = await firstValueFrom(this.accountService.activeAccount$);
const hasUserKey = await this.cryptoService.hasUserKey(activeUser.id);
if (hasUserKey) {
this.messagingService.send("logout");
throw new Error("User key already exists, cannot migrate legacy encryption.");
@@ -52,7 +57,7 @@ export class MigrateFromLegacyEncryptionComponent {
try {
await this.syncService.fullSync(false, true);
await this.keyRotationService.rotateUserKeyAndEncryptedData(masterPassword);
await this.keyRotationService.rotateUserKeyAndEncryptedData(masterPassword, activeUser);
this.platformUtilsService.showToast(
"success",

View File

@@ -220,6 +220,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
}
private async updateKey() {
await this.keyRotationService.rotateUserKeyAndEncryptedData(this.masterPassword);
const user = await firstValueFrom(this.accountService.activeAccount$);
await this.keyRotationService.rotateUserKeyAndEncryptedData(this.masterPassword, user);
}
}