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 f5d58e3fc6b..cf6cdaefd85 100644 --- a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts @@ -38,4 +38,10 @@ export abstract class DevicesApiServiceAbstraction { * @param deviceId - The device ID */ deactivateDevice: (deviceId: string) => Promise; + + /** + * Removes trust from a list of devices + * @param deviceIds - The device IDs to be untrusted + */ + untrustDevices: (deviceIds: string[]) => Promise; } diff --git a/libs/common/src/auth/models/request/untrust-devices.request.ts b/libs/common/src/auth/models/request/untrust-devices.request.ts new file mode 100644 index 00000000000..ea1db101482 --- /dev/null +++ b/libs/common/src/auth/models/request/untrust-devices.request.ts @@ -0,0 +1,3 @@ +export class UntrustDevicesRequestModel { + constructor(public devices: string[]) {} +} diff --git a/libs/common/src/auth/services/devices-api.service.implementation.spec.ts b/libs/common/src/auth/services/devices-api.service.implementation.spec.ts index 7aea36c7bd4..3cbb9c20e74 100644 --- a/libs/common/src/auth/services/devices-api.service.implementation.spec.ts +++ b/libs/common/src/auth/services/devices-api.service.implementation.spec.ts @@ -89,6 +89,24 @@ describe("DevicesApiServiceImplementation", () => { }); }); + describe("untrustDevices", () => { + it("calls api with correct parameters", async () => { + const deviceIds = ["device1", "device2"]; + apiService.send.mockResolvedValue(true); + + await devicesApiService.untrustDevices(deviceIds); + expect(apiService.send).toHaveBeenCalledWith( + "POST", + "/devices/untrust", + { + devices: deviceIds, + }, + true, + false, + ); + }); + }); + describe("error handling", () => { it("propagates api errors", async () => { const error = new Error("API Error"); 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 9830a8c1ffd..5f587241244 100644 --- a/libs/common/src/auth/services/devices-api.service.implementation.ts +++ b/libs/common/src/auth/services/devices-api.service.implementation.ts @@ -5,6 +5,7 @@ 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 { UntrustDevicesRequestModel } from "../models/request/untrust-devices.request"; import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request"; import { ProtectedDeviceResponse } from "../models/response/protected-device.response"; @@ -117,4 +118,14 @@ export class DevicesApiServiceImplementation implements DevicesApiServiceAbstrac async deactivateDevice(deviceId: string): Promise { await this.apiService.send("POST", `/devices/${deviceId}/deactivate`, null, true, false); } + + async untrustDevices(deviceIds: string[]): Promise { + await this.apiService.send( + "POST", + "/devices/untrust", + new UntrustDevicesRequestModel(deviceIds), + true, + false, + ); + } } 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 501c5cfabc8..e681cc3e88c 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 @@ -204,7 +204,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { } const devices = await this.devicesApiService.getDevices(); - return await Promise.all( + const devicesToUntrust: string[] = []; + const rotatedData = await Promise.all( devices.data .filter((device) => device.isTrusted) .map(async (device) => { @@ -213,6 +214,13 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { deviceWithKeys.encryptedPublicKey, oldUserKey, ); + + if (!publicKey) { + // Device was trusted but encryption is broken. This should be untrusted + devicesToUntrust.push(device.id); + return null; + } + const newEncryptedPublicKey = await this.encryptService.encrypt(publicKey, newUserKey); const newEncryptedUserKey = await this.encryptService.rsaEncrypt( newUserKey.key, @@ -229,8 +237,14 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { request.encryptedUserKey = newRotateableKeySet.encryptedUserKey.encryptedString; request.deviceId = device.id; return request; - }), + }) + .filter((otherDeviceKeysUpdateRequest) => otherDeviceKeysUpdateRequest != null), ); + if (rotatedData.length > 0) { + this.logService.info("[Device trust rotation] Distrusting devices that failed to decrypt."); + await this.devicesApiService.untrustDevices(devicesToUntrust); + } + return rotatedData; } async rotateDevicesTrust( 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 2f3034e67ba..aab6114be3d 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 @@ -679,6 +679,52 @@ describe("deviceTrustService", () => { ).rejects.toThrow("New user key is required. Cannot get rotated data."); }); + it("untrusts devices that failed to decrypt", async () => { + const deviceResponse = { + id: "id", + userId: "", + name: "", + identifier: "", + type: DeviceType.Android, + creationDate: "", + revisionDate: "", + isTrusted: true, + }; + devicesApiService.getDevices.mockResolvedValue( + new ListResponse( + { + data: [deviceResponse], + }, + DeviceResponse, + ), + ); + encryptService.decryptToBytes.mockResolvedValue(null); + encryptService.encrypt.mockResolvedValue(new EncString("test_encrypted_data")); + encryptService.rsaEncrypt.mockResolvedValue(new EncString("test_encrypted_data")); + + const protectedDeviceResponse = new ProtectedDeviceResponse({ + id: "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; + + await deviceTrustService.getRotatedData(fakeOldUserKey, fakeNewUserKey, userId); + + expect(devicesApiService.untrustDevices).toHaveBeenCalledWith(["id"]); + }); + it("returns the expected data when all required parameters are provided", async () => { const deviceResponse = { id: "",