1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 14:53:33 +00:00

[PM-10059] alert server if device trust is lost (#10235)

* alert server if device trust is lost

* add test

* add tests for extra errors

* fix build

---------

Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
Jake Fink
2024-07-24 10:25:57 -04:00
committed by GitHub
parent 768b5393e9
commit 4c26ab5a9e
11 changed files with 109 additions and 3 deletions

View File

@@ -697,6 +697,7 @@ export default class MainBackground {
this.secureStorageService, this.secureStorageService,
this.userDecryptionOptionsService, this.userDecryptionOptionsService,
this.logService, this.logService,
this.configService,
); );
this.devicesService = new DevicesServiceImplementation(this.devicesApiService); this.devicesService = new DevicesServiceImplementation(this.devicesApiService);

View File

@@ -534,6 +534,7 @@ export class ServiceContainer {
this.secureStorageService, this.secureStorageService,
this.userDecryptionOptionsService, this.userDecryptionOptionsService,
this.logService, this.logService,
this.configService,
); );
this.authRequestService = new AuthRequestService( this.authRequestService = new AuthRequestService(

View File

@@ -1050,6 +1050,7 @@ const safeProviders: SafeProvider[] = [
SECURE_STORAGE, SECURE_STORAGE,
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
LogService, LogService,
ConfigService,
], ],
}), }),
safeProvider({ safeProvider({

View File

@@ -312,6 +312,27 @@ describe("SsoLoginStrategy", () => {
expect(cryptoService.setUserKey).not.toHaveBeenCalled(); expect(cryptoService.setUserKey).not.toHaveBeenCalled();
}); });
it("logs when a device key is found but no decryption keys were recieved in token response", async () => {
// Arrange
const userDecryptionOpts = userDecryptionOptsServerResponseWithTdeOption;
userDecryptionOpts.TrustedDeviceOption.EncryptedPrivateKey = null;
userDecryptionOpts.TrustedDeviceOption.EncryptedUserKey = null;
const idTokenResponse: IdentityTokenResponse = identityTokenResponseFactory(
null,
userDecryptionOpts,
);
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
deviceTrustService.getDeviceKey.mockResolvedValue(mockDeviceKey);
// Act
await ssoLoginStrategy.logIn(credentials);
// Assert
expect(deviceTrustService.recordDeviceTrustLoss).toHaveBeenCalledTimes(1);
});
describe("AdminAuthRequest", () => { describe("AdminAuthRequest", () => {
let tokenResponse: IdentityTokenResponse; let tokenResponse: IdentityTokenResponse;

View File

@@ -296,16 +296,20 @@ export class SsoLoginStrategy extends LoginStrategy {
if (!deviceKey || !encDevicePrivateKey || !encUserKey) { if (!deviceKey || !encDevicePrivateKey || !encUserKey) {
if (!deviceKey) { if (!deviceKey) {
await this.logService.warning("Unable to set user key due to missing device key."); this.logService.warning("Unable to set user key due to missing device key.");
} else if (!encDevicePrivateKey || !encUserKey) {
// Tell the server that we have a device key, but received no decryption keys
await this.deviceTrustService.recordDeviceTrustLoss();
} }
if (!encDevicePrivateKey) { if (!encDevicePrivateKey) {
await this.logService.warning( this.logService.warning(
"Unable to set user key due to missing encrypted device private key.", "Unable to set user key due to missing encrypted device private key.",
); );
} }
if (!encUserKey) { if (!encUserKey) {
await this.logService.warning("Unable to set user key due to missing encrypted user key."); this.logService.warning("Unable to set user key due to missing encrypted user key.");
} }
return; return;
} }

View File

@@ -32,4 +32,9 @@ export abstract class DeviceTrustServiceAbstraction {
newUserKey: UserKey, newUserKey: UserKey,
masterPasswordHash: string, masterPasswordHash: string,
) => Promise<void>; ) => Promise<void>;
/**
* Notifies the server that the device has a device key, but didn't receive any associated decryption keys.
* Note: For debugging purposes only.
*/
recordDeviceTrustLoss: () => Promise<void>;
} }

View File

@@ -27,4 +27,11 @@ export abstract class DevicesApiServiceAbstraction {
deviceIdentifier: string, deviceIdentifier: string,
secretVerificationRequest: SecretVerificationRequest, secretVerificationRequest: SecretVerificationRequest,
) => Promise<ProtectedDeviceResponse>; ) => Promise<ProtectedDeviceResponse>;
/**
* Notifies the server that the device has a device key, but didn't receive any associated decryption keys.
* Note: For debugging purposes only.
* @param deviceIdentifier - current device identifier
*/
postDeviceTrustLoss: (deviceIdentifier: string) => Promise<void>;
} }

View File

@@ -2,7 +2,9 @@ import { firstValueFrom, map, Observable } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { FeatureFlag } from "../../enums/feature-flag.enum";
import { AppIdService } from "../../platform/abstractions/app-id.service"; import { AppIdService } from "../../platform/abstractions/app-id.service";
import { ConfigService } from "../../platform/abstractions/config/config.service";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
import { CryptoService } from "../../platform/abstractions/crypto.service"; import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service";
@@ -68,6 +70,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
private secureStorageService: AbstractStorageService, private secureStorageService: AbstractStorageService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private logService: LogService, private logService: LogService,
private configService: ConfigService,
) { ) {
this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe( this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe(
map((options) => options?.trustedDeviceOption != null ?? false), map((options) => options?.trustedDeviceOption != null ?? false),
@@ -287,6 +290,16 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
throw new Error("UserId is required. Cannot decrypt user key with device key."); throw new Error("UserId is required. Cannot decrypt user key with device key.");
} }
if (!encryptedDevicePrivateKey) {
throw new Error(
"Encrypted device private key is required. Cannot decrypt user key with device key.",
);
}
if (!encryptedUserKey) {
throw new Error("Encrypted user key is required. Cannot decrypt user key with device key.");
}
if (!deviceKey) { if (!deviceKey) {
// User doesn't have a device key anymore so device is untrusted // User doesn't have a device key anymore so device is untrusted
return null; return null;
@@ -315,6 +328,14 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
} }
} }
async recordDeviceTrustLoss(): Promise<void> {
if (!(await this.configService.getFeatureFlag(FeatureFlag.DeviceTrustLogging))) {
return;
}
const deviceIdentifier = await this.appIdService.getAppId();
await this.devicesApiService.postDeviceTrustLoss(deviceIdentifier);
}
private getSecureStorageOptions(userId: UserId): StorageOptions { private getSecureStorageOptions(userId: UserId): StorageOptions {
return { return {
storageLocation: StorageLocation.Disk, storageLocation: StorageLocation.Disk,

View File

@@ -9,6 +9,7 @@ import { FakeActiveUserState } from "../../../spec/fake-state";
import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { FakeStateProvider } from "../../../spec/fake-state-provider";
import { DeviceType } from "../../enums"; import { DeviceType } from "../../enums";
import { AppIdService } from "../../platform/abstractions/app-id.service"; import { AppIdService } from "../../platform/abstractions/app-id.service";
import { ConfigService } from "../../platform/abstractions/config/config.service";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
import { CryptoService } from "../../platform/abstractions/crypto.service"; import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service";
@@ -50,6 +51,7 @@ describe("deviceTrustService", () => {
const platformUtilsService = mock<PlatformUtilsService>(); const platformUtilsService = mock<PlatformUtilsService>();
const secureStorageService = mock<AbstractStorageService>(); const secureStorageService = mock<AbstractStorageService>();
const logService = mock<LogService>(); const logService = mock<LogService>();
const configService = mock<ConfigService>();
const userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>(); const userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
const decryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null); const decryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
@@ -533,6 +535,32 @@ describe("deviceTrustService", () => {
).rejects.toThrow("UserId is required. Cannot decrypt user key with device key."); ).rejects.toThrow("UserId is required. Cannot decrypt user key with device key.");
}); });
it("throws an error when a nullish encrypted device private key is passed in", async () => {
await expect(
deviceTrustService.decryptUserKeyWithDeviceKey(
mockUserId,
null,
mockEncryptedUserKey,
mockDeviceKey,
),
).rejects.toThrow(
"Encrypted device private key is required. Cannot decrypt user key with device key.",
);
});
it("throws an error when a nullish encrypted user key is passed in", async () => {
await expect(
deviceTrustService.decryptUserKeyWithDeviceKey(
mockUserId,
mockEncryptedDevicePrivateKey,
null,
mockDeviceKey,
),
).rejects.toThrow(
"Encrypted user key is required. Cannot decrypt user key with device key.",
);
});
it("returns null when device key isn't provided", async () => { it("returns null when device key isn't provided", async () => {
const result = await deviceTrustService.decryptUserKeyWithDeviceKey( const result = await deviceTrustService.decryptUserKeyWithDeviceKey(
mockUserId, mockUserId,
@@ -731,6 +759,7 @@ describe("deviceTrustService", () => {
secureStorageService, secureStorageService,
userDecryptionOptionsService, userDecryptionOptionsService,
logService, logService,
configService,
); );
} }
}); });

View File

@@ -101,4 +101,18 @@ export class DevicesApiServiceImplementation implements DevicesApiServiceAbstrac
); );
return new ProtectedDeviceResponse(result); return new ProtectedDeviceResponse(result);
} }
async postDeviceTrustLoss(deviceIdentifier: string): Promise<void> {
await this.apiService.send(
"POST",
"/devices/lost-trust",
null,
true,
false,
null,
(headers) => {
headers.set("Device-Identifier", deviceIdentifier);
},
);
}
} }

View File

@@ -26,6 +26,7 @@ export enum FeatureFlag {
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
VaultBulkManagementAction = "vault-bulk-management-action", VaultBulkManagementAction = "vault-bulk-management-action",
AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page", AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page",
DeviceTrustLogging = "pm-8285-device-trust-logging",
} }
export type AllowedFeatureFlagTypes = boolean | number | string; export type AllowedFeatureFlagTypes = boolean | number | string;
@@ -62,6 +63,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
[FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.VaultBulkManagementAction]: FALSE,
[FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE, [FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE,
[FeatureFlag.DeviceTrustLogging]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>; } satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;