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:
@@ -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);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -1050,6 +1050,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
SECURE_STORAGE,
|
SECURE_STORAGE,
|
||||||
UserDecryptionOptionsServiceAbstraction,
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
LogService,
|
LogService,
|
||||||
|
ConfigService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user