Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-16603] Implement userkey rotation v2 #12646

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ <h1>{{ "changeMasterPassword" | i18n }}</h1>
[(ngModel)]="masterPasswordHint"
/>
</div>
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
<button type="submit" buttonType="primary" bitButton [loading]="loading">
{{ "changeMasterPassword" | i18n }}
</button>
</form>
Expand Down
135 changes: 133 additions & 2 deletions apps/web/src/app/auth/settings/change-password.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
Expand All @@ -35,11 +37,13 @@
extends BaseChangePasswordComponent
implements OnInit, OnDestroy
{
loading = false;

Check warning on line 40 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L40

Added line #L40 was not covered by tests
rotateUserKey = false;
currentMasterPassword: string;
masterPasswordHint: string;
checkForBreaches = true;
characterMinimumMessage = "";
userkeyRotationV2 = false;

Check warning on line 46 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L46

Added line #L46 was not covered by tests

constructor(
i18nService: I18nService,
Expand All @@ -56,9 +60,10 @@
private userVerificationService: UserVerificationService,
private keyRotationService: UserKeyRotationService,
kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
toastService: ToastService,
private configService: ConfigService,

Check warning on line 66 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L66

Added line #L66 was not covered by tests
) {
super(
i18nService,
Expand All @@ -75,6 +80,8 @@
}

async ngOnInit() {
this.userkeyRotationV2 = await this.configService.getFeatureFlag(FeatureFlag.UserKeyRotationV2);

Check warning on line 83 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L83

Added line #L83 was not covered by tests

if (!(await this.userVerificationService.hasMasterPassword())) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Expand Down Expand Up @@ -135,6 +142,130 @@
}

async submit() {
if (this.userkeyRotationV2) {
this.loading = true;
await this.submitNew();
this.loading = false;

Check warning on line 148 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L146-L148

Added lines #L146 - L148 were not covered by tests
} else {
await this.submitOld();

Check warning on line 150 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L150

Added line #L150 was not covered by tests
}
}

async submitNew() {
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
this.toastService.showToast({

Check warning on line 156 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L156

Added line #L156 was not covered by tests
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordRequired"),
});
return;

Check warning on line 161 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L161

Added line #L161 was not covered by tests
}

if (
this.masterPasswordHint != null &&
this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase()
) {
this.toastService.showToast({

Check warning on line 168 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L168

Added line #L168 was not covered by tests
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("hintEqualsPassword"),
});
return;

Check warning on line 173 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L173

Added line #L173 was not covered by tests
}

this.leakedPassword = false;

Check warning on line 176 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L176

Added line #L176 was not covered by tests
if (this.checkForBreaches) {
this.leakedPassword = (await this.auditService.passwordLeaked(this.masterPassword)) > 0;

Check warning on line 178 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L178

Added line #L178 was not covered by tests
}

if (!(await this.strongPassword())) {
return;

Check warning on line 182 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L182

Added line #L182 was not covered by tests
}

try {

Check warning on line 185 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L185

Added line #L185 was not covered by tests
if (this.rotateUserKey) {
await this.syncService.fullSync(true);
const user = await firstValueFrom(this.accountService.activeAccount$);
await this.keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(

Check warning on line 189 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L187-L189

Added lines #L187 - L189 were not covered by tests
this.currentMasterPassword,
this.masterPassword,
user,
this.masterPasswordHint,
);
} else {
await this.updatePassword(this.masterPassword);

Check warning on line 196 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L196

Added line #L196 was not covered by tests
}
} catch (e) {
this.toastService.showToast({

Check warning on line 199 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L199

Added line #L199 was not covered by tests
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: e.message,
});
}
}

// todo: move this to a service
// https://bitwarden.atlassian.net/browse/PM-17108
private async updatePassword(newMasterPassword: string) {
const currentMasterPassword = this.currentMasterPassword;
const { userId, email } = await firstValueFrom(

Check warning on line 211 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L210-L211

Added lines #L210 - L211 were not covered by tests
this.accountService.activeAccount$.pipe(map((a) => ({ userId: a?.id, email: a?.email }))),
);
const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId));

Check warning on line 214 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L214

Added line #L214 was not covered by tests

const currentMasterKey = await this.keyService.makeMasterKey(

Check warning on line 216 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L216

Added line #L216 was not covered by tests
currentMasterPassword,
email,
kdfConfig,
);
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(

Check warning on line 221 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L221

Added line #L221 was not covered by tests
currentMasterKey,
userId,
);
if (decryptedUserKey == null) {
this.toastService.showToast({

Check warning on line 226 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L226

Added line #L226 was not covered by tests
variant: "error",
title: null,
message: this.i18nService.t("invalidMasterPassword"),
});
return;

Check warning on line 231 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L231

Added line #L231 was not covered by tests
}

const newMasterKey = await this.keyService.makeMasterKey(newMasterPassword, email, kdfConfig);
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(

Check warning on line 235 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L234-L235

Added lines #L234 - L235 were not covered by tests
newMasterKey,
decryptedUserKey,
);

const request = new PasswordRequest();
request.masterPasswordHash = await this.keyService.hashMasterKey(

Check warning on line 241 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L240-L241

Added lines #L240 - L241 were not covered by tests
this.currentMasterPassword,
currentMasterKey,
);
request.masterPasswordHint = this.masterPasswordHint;
request.newMasterPasswordHash = await this.keyService.hashMasterKey(

Check warning on line 246 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L245-L246

Added lines #L245 - L246 were not covered by tests
newMasterPassword,
newMasterKey,
);
request.key = newMasterKeyEncryptedUserKey[1].encryptedString;
try {
await this.apiService.postPassword(request);
this.toastService.showToast({

Check warning on line 253 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L250-L253

Added lines #L250 - L253 were not covered by tests
variant: "success",
title: this.i18nService.t("masterPasswordChanged"),
message: this.i18nService.t("masterPasswordChangedDesc"),
});
this.messagingService.send("logout");

Check warning on line 258 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L258

Added line #L258 was not covered by tests
} catch {
this.toastService.showToast({

Check warning on line 260 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L260

Added line #L260 was not covered by tests
variant: "error",
title: null,
message: this.i18nService.t("errorOccurred"),
});
}
}

async submitOld() {
if (
this.masterPasswordHint != null &&
this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase()
Expand Down Expand Up @@ -240,6 +371,6 @@

private async updateKey() {
const user = await firstValueFrom(this.accountService.activeAccount$);
await this.keyRotationService.rotateUserKeyAndEncryptedData(this.masterPassword, user);
await this.keyRotationService.rotateUserKeyAndEncryptedDataLegacy(this.masterPassword, user);

Check warning on line 374 in apps/web/src/app/auth/settings/change-password.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/change-password.component.ts#L374

Added line #L374 was not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export class AccountKeysRequest {
// Other keys encrypted by the userkey
userKeyEncryptedAccountPrivateKey: string;
accountPublicKey: string;

constructor(userKeyEncryptedAccountPrivateKey: string, accountPublicKey: string) {
this.userKeyEncryptedAccountPrivateKey = userKeyEncryptedAccountPrivateKey;
this.accountPublicKey = accountPublicKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Argon2KdfConfig, KdfConfig, KdfType } from "@bitwarden/key-management";

export class MasterPasswordUnlockDataRequest {
kdfType: KdfType = KdfType.PBKDF2_SHA256;
kdfIterations: number = 0;
kdfMemory?: number;
kdfParallelism?: number;

email: string;
masterKeyAuthenticationHash: string;

masterKeyEncryptedUserKey: string;

masterPasswordHint?: string;

constructor(
kdfConfig: KdfConfig,
email: string,
masterKeyAuthenticationHash: string,
masterKeyEncryptedUserKey: string,
masterPasswordHash?: string,
) {
this.kdfType = kdfConfig.kdfType;
this.kdfIterations = kdfConfig.iterations;
if (kdfConfig.kdfType === KdfType.Argon2id) {
this.kdfMemory = (kdfConfig as Argon2KdfConfig).memory;
this.kdfParallelism = (kdfConfig as Argon2KdfConfig).parallelism;

Check warning on line 27 in apps/web/src/app/key-management/key-rotation/request/master-password-unlock-data.request.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/key-management/key-rotation/request/master-password-unlock-data.request.ts#L26-L27

Added lines #L26 - L27 were not covered by tests
}

this.email = email;
this.masterKeyAuthenticationHash = masterKeyAuthenticationHash;
this.masterKeyEncryptedUserKey = masterKeyEncryptedUserKey;
this.masterPasswordHint = masterPasswordHash;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { AccountKeysRequest } from "./account-keys.request";
import { UnlockDataRequest } from "./unlock-data.request";
import { UserDataRequest as AccountDataRequest } from "./userdata.request";

export class RotateUserAccountKeysRequest {
constructor(
accountUnlockData: UnlockDataRequest,
accountKeys: AccountKeysRequest,
accountData: AccountDataRequest,
oldMasterKeyAuthenticationHash: string,
) {
this.accountUnlockData = accountUnlockData;
this.accountKeys = accountKeys;
this.accountData = accountData;
this.oldMasterKeyAuthenticationHash = oldMasterKeyAuthenticationHash;
}

// Authentication for the request
oldMasterKeyAuthenticationHash: string;

// All methods to get to the userkey
accountUnlockData: UnlockDataRequest;

// Other keys encrypted by the userkey
accountKeys: AccountKeysRequest;

// User vault data encrypted by the userkey
accountData: AccountDataRequest;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common";
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";

import { EmergencyAccessWithIdRequest } from "../../../auth/emergency-access/request/emergency-access-update.request";

import { MasterPasswordUnlockDataRequest } from "./master-password-unlock-data.request";

export class UnlockDataRequest {
// All methods to get to the userkey
masterPasswordUnlockData: MasterPasswordUnlockDataRequest;
emergencyAccessUnlockData: EmergencyAccessWithIdRequest[];
organizationAccountRecoveryUnlockData: OrganizationUserResetPasswordWithIdRequest[];
passkeyUnlockData: WebauthnRotateCredentialRequest[];

constructor(
masterPasswordUnlockData: MasterPasswordUnlockDataRequest,
emergencyAccessUnlockData: EmergencyAccessWithIdRequest[],
organizationAccountRecoveryUnlockData: OrganizationUserResetPasswordWithIdRequest[],
passkeyUnlockData: WebauthnRotateCredentialRequest[],
) {
this.masterPasswordUnlockData = masterPasswordUnlockData;
this.emergencyAccessUnlockData = emergencyAccessUnlockData;
this.organizationAccountRecoveryUnlockData = organizationAccountRecoveryUnlockData;
this.passkeyUnlockData = passkeyUnlockData;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request";
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";

export class UserDataRequest {
ciphers: CipherWithIdRequest[];
folders: FolderWithIdRequest[];
sends: SendWithIdRequest[];

constructor(
ciphers: CipherWithIdRequest[],
folders: FolderWithIdRequest[],
sends: SendWithIdRequest[],
) {
this.ciphers = ciphers;
this.folders = folders;
this.sends = sends;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { ApiService } from "@bitwarden/common/abstractions/api.service";

import { RotateUserAccountKeysRequest } from "./request/rotate-user-account-keys.request";
import { UpdateKeyRequest } from "./request/update-key.request";

@Injectable()
Expand All @@ -11,4 +12,14 @@
postUserKeyUpdate(request: UpdateKeyRequest): Promise<any> {
return this.apiService.send("POST", "/accounts/key", request, true, false);
}

postUserKeyUpdateV2(request: RotateUserAccountKeysRequest): Promise<any> {
return this.apiService.send(

Check warning on line 17 in apps/web/src/app/key-management/key-rotation/user-key-rotation-api.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/key-management/key-rotation/user-key-rotation-api.service.ts#L17

Added line #L17 was not covered by tests
"POST",
"/accounts/key-management/rotate-user-account-keys",
request,
true,
false,
);
}
}
Loading
Loading