mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-11 10:10:25 +01: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:
parent
768b5393e9
commit
4c26ab5a9e
@ -697,6 +697,7 @@ export default class MainBackground {
|
||||
this.secureStorageService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.logService,
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.devicesService = new DevicesServiceImplementation(this.devicesApiService);
|
||||
|
@ -534,6 +534,7 @@ export class ServiceContainer {
|
||||
this.secureStorageService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.logService,
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.authRequestService = new AuthRequestService(
|
||||
|
@ -1050,6 +1050,7 @@ const safeProviders: SafeProvider[] = [
|
||||
SECURE_STORAGE,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
LogService,
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
@ -312,6 +312,27 @@ describe("SsoLoginStrategy", () => {
|
||||
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", () => {
|
||||
let tokenResponse: IdentityTokenResponse;
|
||||
|
||||
|
@ -296,16 +296,20 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
|
||||
if (!deviceKey || !encDevicePrivateKey || !encUserKey) {
|
||||
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) {
|
||||
await this.logService.warning(
|
||||
this.logService.warning(
|
||||
"Unable to set user key due to missing encrypted device private key.",
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -32,4 +32,9 @@ export abstract class DeviceTrustServiceAbstraction {
|
||||
newUserKey: UserKey,
|
||||
masterPasswordHash: string,
|
||||
) => 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,
|
||||
secretVerificationRequest: SecretVerificationRequest,
|
||||
) => 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 { FeatureFlag } from "../../enums/feature-flag.enum";
|
||||
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 { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||
@ -68,6 +70,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
private secureStorageService: AbstractStorageService,
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe(
|
||||
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.");
|
||||
}
|
||||
|
||||
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) {
|
||||
// User doesn't have a device key anymore so device is untrusted
|
||||
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 {
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
|
@ -9,6 +9,7 @@ import { FakeActiveUserState } from "../../../spec/fake-state";
|
||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||
import { DeviceType } from "../../enums";
|
||||
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 { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||
@ -50,6 +51,7 @@ describe("deviceTrustService", () => {
|
||||
const platformUtilsService = mock<PlatformUtilsService>();
|
||||
const secureStorageService = mock<AbstractStorageService>();
|
||||
const logService = mock<LogService>();
|
||||
const configService = mock<ConfigService>();
|
||||
|
||||
const userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
const decryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
|
||||
@ -533,6 +535,32 @@ describe("deviceTrustService", () => {
|
||||
).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 () => {
|
||||
const result = await deviceTrustService.decryptUserKeyWithDeviceKey(
|
||||
mockUserId,
|
||||
@ -731,6 +759,7 @@ describe("deviceTrustService", () => {
|
||||
secureStorageService,
|
||||
userDecryptionOptionsService,
|
||||
logService,
|
||||
configService,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -101,4 +101,18 @@ export class DevicesApiServiceImplementation implements DevicesApiServiceAbstrac
|
||||
);
|
||||
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",
|
||||
VaultBulkManagementAction = "vault-bulk-management-action",
|
||||
AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page",
|
||||
DeviceTrustLogging = "pm-8285-device-trust-logging",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@ -62,6 +63,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
|
||||
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
||||
[FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE,
|
||||
[FeatureFlag.DeviceTrustLogging]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
Loading…
Reference in New Issue
Block a user