[PM-5963] Fix tde offboarding vault corruption (#9480)

* Fix tde offboarding

* Add tde offboarding password request

* Add event for tde offboarding

* Update libs/auth/src/common/models/domain/user-decryption-options.ts

Co-authored-by: Jake Fink <jfink@bitwarden.com>

* Update libs/common/src/services/api.service.ts

Co-authored-by: Jake Fink <jfink@bitwarden.com>

* Make tde offboarding take priority

* Update tde offboarding message

* Fix unit tests

* Fix unit tests

* Fix typo

* Fix unit tests

---------

Co-authored-by: Jake Fink <jfink@bitwarden.com>
This commit is contained in:
Bernd Schoolmann 2024-08-02 01:48:09 +02:00 committed by GitHub
parent d26ea1be5f
commit c6229abd12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 94 additions and 17 deletions

View File

@ -2327,6 +2327,9 @@
"updateWeakMasterPasswordWarning": {
"message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"tdeDisabledMasterPasswordRequired": {
"message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault."
},
"resetPasswordPolicyAutoEnroll": {
"message": "Automatic enrollment"
},

View File

@ -2015,6 +2015,9 @@
"updateWeakMasterPasswordWarning": {
"message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"tdeDisabledMasterPasswordRequired": {
"message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault."
},
"tryAgain": {
"message": "Try again"
},

View File

@ -86,6 +86,9 @@ export class EventService {
case EventType.User_RequestedDeviceApproval:
msg = humanReadableMsg = this.i18nService.t("requestedDeviceApproval");
break;
case EventType.User_TdeOffboardingPasswordSet:
msg = humanReadableMsg = this.i18nService.t("tdeOffboardingPasswordSet");
break;
// Cipher
case EventType.Cipher_Created:
msg = this.i18nService.t("createdItemId", this.formatCipherId(ev, options));

View File

@ -5322,6 +5322,9 @@
"updateWeakMasterPasswordWarning": {
"message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"tdeDisabledMasterPasswordRequired": {
"message": "Your organization has updated your decryption options. Please set a master password to access your vault."
},
"maximumVaultTimeout": {
"message": "Vault timeout"
},
@ -7674,6 +7677,9 @@
"requestedDeviceApproval": {
"message": "Requested device approval."
},
"tdeOffboardingPasswordSet": {
"message": "User set a master password during TDE offboarding."
},
"startYour7DayFreeTrialOfBitwardenFor": {
"message": "Start your 7-Day free trial of Bitwarden for $ORG$",
"placeholders": {

View File

@ -154,12 +154,12 @@ describe("SsoComponent", () => {
}),
withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false, false),
keyConnectorOption: undefined,
}),
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true, false),
keyConnectorOption: undefined,
}),
withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
@ -169,12 +169,12 @@ describe("SsoComponent", () => {
}),
noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false, false),
keyConnectorOption: undefined,
}),
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true, false),
keyConnectorOption: undefined,
}),
noMasterPasswordWithKeyConnector: new UserDecryptionOptions({

View File

@ -287,8 +287,18 @@ export class SsoComponent {
orgIdentifier: string,
userDecryptionOpts: UserDecryptionOptions,
): Promise<void> {
// If user doesn't have a MP, but has reset password permission, they must set a MP
// Tde offboarding takes precedence
if (
!userDecryptionOpts.hasMasterPassword &&
userDecryptionOpts.trustedDeviceOption.isTdeOffboarding
) {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.TdeOffboarding,
userId,
);
} else if (
// If user doesn't have a MP, but has reset password permission, they must set a MP
!userDecryptionOpts.hasMasterPassword &&
userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
) {

View File

@ -127,12 +127,12 @@ describe("TwoFactorComponent", () => {
}),
withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false, false),
keyConnectorOption: undefined,
}),
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true, false),
keyConnectorOption: undefined,
}),
withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
@ -142,12 +142,12 @@ describe("TwoFactorComponent", () => {
}),
noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false, false),
keyConnectorOption: undefined,
}),
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true, false),
keyConnectorOption: undefined,
}),
noMasterPasswordWithKeyConnector: new UserDecryptionOptions({

View File

@ -120,12 +120,12 @@ describe("TwoFactorComponent", () => {
}),
withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false, false),
keyConnectorOption: undefined,
}),
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: true,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true, false),
keyConnectorOption: undefined,
}),
withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
@ -135,12 +135,12 @@ describe("TwoFactorComponent", () => {
}),
noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false, false),
keyConnectorOption: undefined,
}),
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
hasMasterPassword: false,
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true, false),
keyConnectorOption: undefined,
}),
noMasterPasswordWithKeyConnector: new UserDecryptionOptions({

View File

@ -12,6 +12,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -97,9 +98,13 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
}
get masterPasswordWarningText(): string {
return this.reason == ForceSetPasswordReason.WeakMasterPassword
? this.i18nService.t("updateWeakMasterPasswordWarning")
: this.i18nService.t("updateMasterPasswordWarning");
if (this.reason == ForceSetPasswordReason.WeakMasterPassword) {
return this.i18nService.t("weakMasterPasswordWarning");
} else if (this.reason == ForceSetPasswordReason.TdeOffboarding) {
return this.i18nService.t("tdeDisabledMasterPasswordRequired");
} else {
return this.i18nService.t("masterPasswordWarning");
}
}
togglePassword(confirmField: boolean) {
@ -165,6 +170,9 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
case ForceSetPasswordReason.WeakMasterPassword:
this.formPromise = this.updatePassword(masterPasswordHash, userKey);
break;
case ForceSetPasswordReason.TdeOffboarding:
this.formPromise = this.updateTdeOffboardingPassword(masterPasswordHash, userKey);
break;
}
await this.formPromise;
@ -211,4 +219,16 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
return this.apiService.postPassword(request);
}
private async updateTdeOffboardingPassword(
masterPasswordHash: string,
userKey: [UserKey, EncString],
) {
const request = new UpdateTdeOffboardingPasswordRequest();
request.key = userKey[1].encryptedString;
request.newMasterPasswordHash = masterPasswordHash;
request.masterPasswordHint = this.hint;
return this.apiService.putUpdateTdeOffboardingPassword(request);
}
}

View File

@ -211,6 +211,7 @@ describe("SsoLoginStrategy", () => {
HasAdminApproval: true,
HasLoginApprovingDevice: true,
HasManageResetPasswordPermission: false,
IsTdeOffboarding: false,
EncryptedPrivateKey: mockEncDevicePrivateKey,
EncryptedUserKey: mockEncUserKey,
},
@ -343,6 +344,7 @@ describe("SsoLoginStrategy", () => {
HasAdminApproval: true,
HasLoginApprovingDevice: false,
HasManageResetPasswordPermission: false,
IsTdeOffboarding: false,
EncryptedPrivateKey: mockEncDevicePrivateKey,
EncryptedUserKey: mockEncUserKey,
},

View File

@ -54,6 +54,8 @@ export class TrustedDeviceUserDecryptionOption {
hasLoginApprovingDevice: boolean;
/** True if the user has manage reset password permission, as these users must be forced to have a master password. */
hasManageResetPasswordPermission: boolean;
/** True if tde is disabled but user has not set a master password yet. */
isTdeOffboarding: boolean;
/**
* Initializes a new instance of the TrustedDeviceUserDecryptionOption from a response object.
@ -70,6 +72,7 @@ export class TrustedDeviceUserDecryptionOption {
options.hasAdminApproval = response?.hasAdminApproval ?? false;
options.hasLoginApprovingDevice = response?.hasLoginApprovingDevice ?? false;
options.hasManageResetPasswordPermission = response?.hasManageResetPasswordPermission ?? false;
options.isTdeOffboarding = response?.isTdeOffboarding ?? false;
return options;
}

View File

@ -29,10 +29,12 @@ export class FakeTrustedDeviceUserDecryptionOption extends TrustedDeviceUserDecr
hasAdminApproval: boolean,
hasLoginApprovingDevice: boolean,
hasManageResetPasswordPermission: boolean,
isTdeOffboarding: boolean,
) {
super();
this.hasAdminApproval = hasAdminApproval;
this.hasLoginApprovingDevice = hasLoginApprovingDevice;
this.hasManageResetPasswordPermission = hasManageResetPasswordPermission;
this.isTdeOffboarding = isTdeOffboarding;
}
}

View File

@ -8,6 +8,8 @@ import {
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { UserDecryptionOptions } from "../../models/domain/user-decryption-options";
import {
USER_DECRYPTION_OPTIONS,
UserDecryptionOptionsService,
@ -27,12 +29,13 @@ describe("UserDecryptionOptionsService", () => {
sut = new UserDecryptionOptionsService(fakeStateProvider);
});
const userDecryptionOptions = {
const userDecryptionOptions: UserDecryptionOptions = {
hasMasterPassword: true,
trustedDeviceOption: {
hasAdminApproval: false,
hasLoginApprovingDevice: false,
hasManageResetPasswordPermission: true,
isTdeOffboarding: false,
},
keyConnectorOption: {
keyConnectorUrl: "https://keyconnector.bitwarden.com",

View File

@ -48,6 +48,7 @@ import { TwoFactorEmailRequest } from "../auth/models/request/two-factor-email.r
import { TwoFactorProviderRequest } from "../auth/models/request/two-factor-provider.request";
import { TwoFactorRecoveryRequest } from "../auth/models/request/two-factor-recovery.request";
import { UpdateProfileRequest } from "../auth/models/request/update-profile.request";
import { UpdateTdeOffboardingPasswordRequest } from "../auth/models/request/update-tde-offboarding-password.request";
import { UpdateTempPasswordRequest } from "../auth/models/request/update-temp-password.request";
import { UpdateTwoFactorAuthenticatorRequest } from "../auth/models/request/update-two-factor-authenticator.request";
import { UpdateTwoFactorDuoRequest } from "../auth/models/request/update-two-factor-duo.request";
@ -181,6 +182,7 @@ export abstract class ApiService {
postUserApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise<any>;
putUpdateTdeOffboardingPassword: (request: UpdateTdeOffboardingPasswordRequest) => Promise<any>;
postConvertToKeyConnector: () => Promise<void>;
//passwordless
postAuthRequest: (request: CreateAuthRequest) => Promise<AuthRequestResponse>;

View File

@ -26,4 +26,9 @@ export enum ForceSetPasswordReason {
* Set post login & decryption client side and by server in sync (to catch logged in users).
*/
TdeUserWithoutPasswordHasPasswordResetPermission,
/**
* Occurs when TDE is disabled and master password has to be set.
*/
TdeOffboarding,
}

View File

@ -0,0 +1,5 @@
import { OrganizationUserResetPasswordRequest } from "../../../admin-console/abstractions/organization-user/requests";
export class UpdateTdeOffboardingPasswordRequest extends OrganizationUserResetPasswordRequest {
masterPasswordHint: string;
}

View File

@ -5,6 +5,7 @@ export interface ITrustedDeviceUserDecryptionOptionServerResponse {
HasAdminApproval: boolean;
HasLoginApprovingDevice: boolean;
HasManageResetPasswordPermission: boolean;
IsTdeOffboarding: boolean;
EncryptedPrivateKey?: string;
EncryptedUserKey?: string;
}
@ -13,6 +14,7 @@ export class TrustedDeviceUserDecryptionOptionResponse extends BaseResponse {
hasAdminApproval: boolean;
hasLoginApprovingDevice: boolean;
hasManageResetPasswordPermission: boolean;
isTdeOffboarding: boolean;
encryptedPrivateKey: EncString;
encryptedUserKey: EncString;
@ -25,6 +27,8 @@ export class TrustedDeviceUserDecryptionOptionResponse extends BaseResponse {
"HasManageResetPasswordPermission",
);
this.isTdeOffboarding = this.getResponseProperty("IsTdeOffboarding");
if (response.EncryptedPrivateKey) {
this.encryptedPrivateKey = new EncString(this.getResponseProperty("EncryptedPrivateKey"));
}

View File

@ -11,6 +11,7 @@ export enum EventType {
User_UpdatedTempPassword = 1008,
User_MigratedKeyToKeyConnector = 1009,
User_RequestedDeviceApproval = 1010,
User_TdeOffboardingPasswordSet = 1011,
Cipher_Created = 1100,
Cipher_Updated = 1101,

View File

@ -57,6 +57,7 @@ import { TwoFactorEmailRequest } from "../auth/models/request/two-factor-email.r
import { TwoFactorProviderRequest } from "../auth/models/request/two-factor-provider.request";
import { TwoFactorRecoveryRequest } from "../auth/models/request/two-factor-recovery.request";
import { UpdateProfileRequest } from "../auth/models/request/update-profile.request";
import { UpdateTdeOffboardingPasswordRequest } from "../auth/models/request/update-tde-offboarding-password.request";
import { UpdateTempPasswordRequest } from "../auth/models/request/update-temp-password.request";
import { UpdateTwoFactorAuthenticatorRequest } from "../auth/models/request/update-two-factor-authenticator.request";
import { UpdateTwoFactorDuoRequest } from "../auth/models/request/update-two-factor-duo.request";
@ -461,6 +462,10 @@ export class ApiService implements ApiServiceAbstraction {
return this.send("PUT", "/accounts/update-temp-password", request, true, false);
}
putUpdateTdeOffboardingPassword(request: UpdateTdeOffboardingPasswordRequest): Promise<void> {
return this.send("PUT", "/accounts/update-tde-offboarding-password", request, true, false);
}
postConvertToKeyConnector(): Promise<void> {
return this.send("POST", "/accounts/convert-to-key-connector", null, true, false);
}