diff --git a/apps/web/src/app/key-management/key-rotation/request/unlock-data.request.ts b/apps/web/src/app/key-management/key-rotation/request/unlock-data.request.ts index 5cdb56a3e23..6faf0c29401 100644 --- a/apps/web/src/app/key-management/key-rotation/request/unlock-data.request.ts +++ b/apps/web/src/app/key-management/key-rotation/request/unlock-data.request.ts @@ -1,4 +1,5 @@ import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; +import { DeviceKeysUpdateRequest } from "@bitwarden/common/auth/models/request/update-devices-trust.request"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; import { EmergencyAccessWithIdRequest } from "../../../auth/emergency-access/request/emergency-access-update.request"; @@ -11,16 +12,19 @@ export class UnlockDataRequest { emergencyAccessUnlockData: EmergencyAccessWithIdRequest[]; organizationAccountRecoveryUnlockData: OrganizationUserResetPasswordWithIdRequest[]; passkeyUnlockData: WebauthnRotateCredentialRequest[]; + deviceKeyUnlockData: DeviceKeysUpdateRequest[]; constructor( masterPasswordUnlockData: MasterPasswordUnlockDataRequest, emergencyAccessUnlockData: EmergencyAccessWithIdRequest[], organizationAccountRecoveryUnlockData: OrganizationUserResetPasswordWithIdRequest[], passkeyUnlockData: WebauthnRotateCredentialRequest[], + deviceTrustUnlockData: DeviceKeysUpdateRequest[], ) { this.masterPasswordUnlockData = masterPasswordUnlockData; this.emergencyAccessUnlockData = emergencyAccessUnlockData; this.organizationAccountRecoveryUnlockData = organizationAccountRecoveryUnlockData; this.passkeyUnlockData = passkeyUnlockData; + this.deviceKeyUnlockData = deviceTrustUnlockData; } } diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index c1b7a04d62b..9dc844c0104 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -180,11 +180,19 @@ export class UserKeyRotationService { newUnencryptedUserKey, user.id, ); + + const trustedDeviceUnlockData = await this.deviceTrustService.getRotatedData( + originalUserKey, + newUnencryptedUserKey, + user.id, + ); + const unlockDataRequest = new UnlockDataRequest( masterPasswordUnlockData, emergencyAccessUnlockData, organizationAccountRecoveryUnlockData, passkeyUnlockData, + trustedDeviceUnlockData, ); const request = new RotateUserAccountKeysRequest( @@ -198,14 +206,6 @@ export class UserKeyRotationService { await this.apiService.postUserKeyUpdateV2(request); this.logService.info("[Userkey rotation] Userkey rotation request posted to server"); - // TODO PM-2199: Add device trust rotation support to the user key rotation endpoint - this.logService.info("[Userkey rotation] Rotating device trust..."); - await this.deviceTrustService.rotateDevicesTrust( - user.id, - newUnencryptedUserKey, - newMasterKeyAuthenticationHash, - ); - this.logService.info("[Userkey rotation] Device trust rotation completed"); this.toastService.showToast({ variant: "success", title: this.i18nService.t("rotationCompletedTitle"), diff --git a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts index 92f0ebf1667..f5d58e3fc6b 100644 --- a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts @@ -2,7 +2,6 @@ // @ts-strict-ignore import { ListResponse } from "../../models/response/list.response"; import { DeviceResponse } from "../abstractions/devices/responses/device.response"; -import { SecretVerificationRequest } from "../models/request/secret-verification.request"; import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request"; import { ProtectedDeviceResponse } from "../models/response/protected-device.response"; @@ -25,10 +24,7 @@ export abstract class DevicesApiServiceAbstraction { deviceIdentifier: string, ) => Promise; - getDeviceKeys: ( - deviceIdentifier: string, - secretVerificationRequest: SecretVerificationRequest, - ) => Promise; + getDeviceKeys: (deviceIdentifier: string) => Promise; /** * Notifies the server that the device has a device key, but didn't receive any associated decryption keys. diff --git a/libs/common/src/auth/models/request/update-devices-trust.request.ts b/libs/common/src/auth/models/request/update-devices-trust.request.ts index 21fe0f600dc..f3cee00f948 100644 --- a/libs/common/src/auth/models/request/update-devices-trust.request.ts +++ b/libs/common/src/auth/models/request/update-devices-trust.request.ts @@ -13,5 +13,5 @@ export class DeviceKeysUpdateRequest { } export class OtherDeviceKeysUpdateRequest extends DeviceKeysUpdateRequest { - id: string; + deviceId: string; } diff --git a/libs/common/src/auth/models/response/protected-device.response.ts b/libs/common/src/auth/models/response/protected-device.response.ts index 14662236197..3cc3a3e0792 100644 --- a/libs/common/src/auth/models/response/protected-device.response.ts +++ b/libs/common/src/auth/models/response/protected-device.response.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { RotateableKeySet } from "@bitwarden/auth/common"; + import { DeviceType } from "../../../enums"; import { BaseResponse } from "../../../models/response/base.response"; import { EncString } from "../../../platform/models/domain/enc-string"; @@ -38,4 +40,12 @@ export class ProtectedDeviceResponse extends BaseResponse { * This enabled a user to rotate the keys for all of their devices. */ encryptedPublicKey: EncString; + + getRotateableKeyset(): RotateableKeySet { + return new RotateableKeySet(this.encryptedUserKey, this.encryptedPublicKey); + } + + isTrusted(): boolean { + return this.encryptedUserKey != null && this.encryptedPublicKey != null; + } } diff --git a/libs/common/src/auth/services/devices-api.service.implementation.ts b/libs/common/src/auth/services/devices-api.service.implementation.ts index cf760effbdf..9830a8c1ffd 100644 --- a/libs/common/src/auth/services/devices-api.service.implementation.ts +++ b/libs/common/src/auth/services/devices-api.service.implementation.ts @@ -5,7 +5,6 @@ import { ListResponse } from "../../models/response/list.response"; import { Utils } from "../../platform/misc/utils"; import { DeviceResponse } from "../abstractions/devices/responses/device.response"; import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction"; -import { SecretVerificationRequest } from "../models/request/secret-verification.request"; import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request"; import { ProtectedDeviceResponse } from "../models/response/protected-device.response"; @@ -90,14 +89,11 @@ export class DevicesApiServiceImplementation implements DevicesApiServiceAbstrac ); } - async getDeviceKeys( - deviceIdentifier: string, - secretVerificationRequest: SecretVerificationRequest, - ): Promise { + async getDeviceKeys(deviceIdentifier: string): Promise { const result = await this.apiService.send( "POST", `/devices/${deviceIdentifier}/retrieve-keys`, - secretVerificationRequest, + null, true, true, ); diff --git a/libs/common/src/key-management/device-trust/abstractions/device-trust.service.abstraction.ts b/libs/common/src/key-management/device-trust/abstractions/device-trust.service.abstraction.ts index 882625fa231..407ae007622 100644 --- a/libs/common/src/key-management/device-trust/abstractions/device-trust.service.abstraction.ts +++ b/libs/common/src/key-management/device-trust/abstractions/device-trust.service.abstraction.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Observable } from "rxjs"; +import { DeviceKeysUpdateRequest } from "@bitwarden/common/auth/models/request/update-devices-trust.request"; + import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response"; import { EncString } from "../../../platform/models/domain/enc-string"; import { UserId } from "../../../types/guid"; @@ -55,4 +57,9 @@ export abstract class DeviceTrustServiceAbstraction { * Note: For debugging purposes only. */ recordDeviceTrustLoss: () => Promise; + getRotatedData: ( + oldUserKey: UserKey, + newUserKey: UserKey, + userId: UserId, + ) => Promise; } diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts index 579fe9360a6..501c5cfabc8 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { firstValueFrom, map, Observable, Subject } from "rxjs"; -import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { RotateableKeySet, UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { KeyService } from "@bitwarden/key-management"; import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response"; @@ -10,6 +10,7 @@ import { DevicesApiServiceAbstraction } from "../../../auth/abstractions/devices import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; import { DeviceKeysUpdateRequest, + OtherDeviceKeysUpdateRequest, UpdateDevicesTrustRequest, } from "../../../auth/models/request/update-devices-trust.request"; import { AppIdService } from "../../../platform/abstractions/app-id.service"; @@ -187,6 +188,51 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { return deviceResponse; } + async getRotatedData( + oldUserKey: UserKey, + newUserKey: UserKey, + userId: UserId, + ): Promise { + if (!userId) { + throw new Error("UserId is required. Cannot get rotated data."); + } + if (!oldUserKey) { + throw new Error("Old user key is required. Cannot get rotated data."); + } + if (!newUserKey) { + throw new Error("New user key is required. Cannot get rotated data."); + } + + const devices = await this.devicesApiService.getDevices(); + return await Promise.all( + devices.data + .filter((device) => device.isTrusted) + .map(async (device) => { + const deviceWithKeys = await this.devicesApiService.getDeviceKeys(device.identifier); + const publicKey = await this.encryptService.decryptToBytes( + deviceWithKeys.encryptedPublicKey, + oldUserKey, + ); + const newEncryptedPublicKey = await this.encryptService.encrypt(publicKey, newUserKey); + const newEncryptedUserKey = await this.encryptService.rsaEncrypt( + newUserKey.key, + publicKey, + ); + + const newRotateableKeySet = new RotateableKeySet( + newEncryptedUserKey, + newEncryptedPublicKey, + ); + + const request = new OtherDeviceKeysUpdateRequest(); + request.encryptedPublicKey = newRotateableKeySet.encryptedPublicKey.encryptedString; + request.encryptedUserKey = newRotateableKeySet.encryptedUserKey.encryptedString; + request.deviceId = device.id; + return request; + }), + ); + } + async rotateDevicesTrust( userId: UserId, newUserKey: UserKey, @@ -216,10 +262,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { secretVerificationRequest.masterPasswordHash = masterPasswordHash; // Get the keys that are used in rotating a devices keys from the server - const currentDeviceKeys = await this.devicesApiService.getDeviceKeys( - deviceIdentifier, - secretVerificationRequest, - ); + const currentDeviceKeys = await this.devicesApiService.getDeviceKeys(deviceIdentifier); // Decrypt the existing device public key with the old user key const decryptedDevicePublicKey = await this.encryptService.decryptToBytes( diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts index 1893b097ec6..2f3034e67ba 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts @@ -7,6 +7,7 @@ import { UserDecryptionOptionsServiceAbstraction, UserDecryptionOptions, } from "@bitwarden/auth/common"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { KeyService } from "@bitwarden/key-management"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service"; @@ -655,6 +656,86 @@ describe("deviceTrustService", () => { }); }); + describe("getRotatedData", () => { + let fakeNewUserKey: UserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; + let fakeOldUserKey: UserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; + const userId: UserId = Utils.newGuid() as UserId; + + it("throws an error when a null user id is passed in", async () => { + await expect( + deviceTrustService.getRotatedData(fakeOldUserKey, fakeNewUserKey, null), + ).rejects.toThrow("UserId is required. Cannot get rotated data."); + }); + + it("throws an error when a null old user key is passed in", async () => { + await expect( + deviceTrustService.getRotatedData(null, fakeNewUserKey, userId), + ).rejects.toThrow("Old user key is required. Cannot get rotated data."); + }); + + it("throws an error when a null new user key is passed in", async () => { + await expect( + deviceTrustService.getRotatedData(fakeOldUserKey, null, userId), + ).rejects.toThrow("New user key is required. Cannot get rotated data."); + }); + + it("returns the expected data when all required parameters are provided", async () => { + const deviceResponse = { + id: "", + userId: "", + name: "", + identifier: "", + type: DeviceType.Android, + creationDate: "", + revisionDate: "", + isTrusted: true, + }; + devicesApiService.getDevices.mockResolvedValue( + new ListResponse( + { + data: [deviceResponse], + }, + DeviceResponse, + ), + ); + encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(64)); + encryptService.encrypt.mockResolvedValue(new EncString("test_encrypted_data")); + encryptService.rsaEncrypt.mockResolvedValue(new EncString("test_encrypted_data")); + + const protectedDeviceResponse = new ProtectedDeviceResponse({ + id: "", + creationDate: "", + identifier: "test_device_identifier", + name: "Firefox", + type: DeviceType.FirefoxBrowser, + encryptedPublicKey: "", + encryptedUserKey: "", + }); + devicesApiService.getDeviceKeys.mockResolvedValue(protectedDeviceResponse); + const fakeOldUserKeyData = new Uint8Array(64); + fakeOldUserKeyData.fill(5, 0, 1); + fakeOldUserKey = new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey; + + const fakeNewUserKeyData = new Uint8Array(64); + fakeNewUserKeyData.fill(1, 0, 1); + fakeNewUserKey = new SymmetricCryptoKey(fakeNewUserKeyData) as UserKey; + + const result = await deviceTrustService.getRotatedData( + fakeOldUserKey, + fakeNewUserKey, + userId, + ); + + expect(result).toEqual([ + { + deviceId: "", + encryptedUserKey: "test_encrypted_data", + encryptedPublicKey: "test_encrypted_data", + }, + ]); + }); + }); + describe("rotateDevicesTrust", () => { let fakeNewUserKey: UserKey = null; @@ -708,11 +789,8 @@ describe("deviceTrustService", () => { appIdService.getAppId.mockResolvedValue("test_device_identifier"); - devicesApiService.getDeviceKeys.mockImplementation((deviceIdentifier, secretRequest) => { - if ( - deviceIdentifier !== "test_device_identifier" || - secretRequest.masterPasswordHash !== "my_password_hash" - ) { + devicesApiService.getDeviceKeys.mockImplementation((deviceIdentifier) => { + if (deviceIdentifier !== "test_device_identifier") { return Promise.resolve(null); }