mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[PM-3726] Force migration of legacy user's encryption key (#6195)
* [PM-3726] migrate legacy user's encryption key * [PM-3726] add 2fa support and pr feedback * [PM-3726] revert launch.json & webpack.config changes * [PM-3726] remove update key component - also remove card in vault since legacy users can't login * [PM-3726] Fix i18n & PR feedback * [PM-3726] make standalone component * [PM-3726] linter * [PM-3726] missing await * [PM-3726] logout legacy users with vault timeout to never * [PM-3726] add await * [PM-3726] skip auto key migration for legacy users * [PM-3726] pr feedback * [PM-3726] move check for web into migrate method --------- Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
@@ -141,6 +141,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||
await this.loginService.saveEmailSettings();
|
||||
if (this.handleCaptchaRequired(response)) {
|
||||
return;
|
||||
} else if (this.handleMigrateEncryptionKey(response)) {
|
||||
return;
|
||||
} else if (response.requiresTwoFactor) {
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||
this.onSuccessfulLoginTwoFactorNavigate();
|
||||
@@ -272,6 +274,21 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||
await this.loginService.saveEmailSettings();
|
||||
}
|
||||
|
||||
// Legacy accounts used the master key to encrypt data. Migration is required
|
||||
// but only performed on web
|
||||
protected handleMigrateEncryptionKey(result: AuthResult): boolean {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccured"),
|
||||
this.i18nService.t("encryptionKeyMigrationRequired")
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private getErrorToastMessage() {
|
||||
const error: AllValidationErrors = this.formValidationErrorService
|
||||
.getFormValidationErrors(this.formGroup.controls)
|
||||
|
||||
@@ -215,9 +215,24 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
await this.handleLoginResponse(authResult);
|
||||
}
|
||||
|
||||
protected handleMigrateEncryptionKey(result: AuthResult): boolean {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccured"),
|
||||
this.i18nService.t("encryptionKeyMigrationRequired")
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async handleLoginResponse(authResult: AuthResult) {
|
||||
if (this.handleCaptchaRequired(authResult)) {
|
||||
return;
|
||||
} else if (this.handleMigrateEncryptionKey(authResult)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loginService.clearValues();
|
||||
|
||||
@@ -10,7 +10,10 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
/**
|
||||
* Only allow access to this route if the vault is locked.
|
||||
@@ -25,9 +28,21 @@ export function lockGuard(): CanActivateFn {
|
||||
const authService = inject(AuthService);
|
||||
const cryptoService = inject(CryptoService);
|
||||
const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction);
|
||||
const platformUtilService = inject(PlatformUtilsService);
|
||||
const messagingService = inject(MessagingService);
|
||||
const router = inject(Router);
|
||||
const userVerificationService = inject(UserVerificationService);
|
||||
|
||||
// If legacy user on web, redirect to migration page
|
||||
if (await cryptoService.isLegacyUser()) {
|
||||
if (platformUtilService.getClientType() === ClientType.Web) {
|
||||
return router.createUrlTree(["migrate-legacy-encryption"]);
|
||||
}
|
||||
// Log out legacy users on other clients
|
||||
messagingService.send("logout");
|
||||
return false;
|
||||
}
|
||||
|
||||
const authStatus = await authService.getAuthStatus();
|
||||
if (authStatus !== AuthenticationStatus.Locked) {
|
||||
return router.createUrlTree(["/"]);
|
||||
|
||||
@@ -24,7 +24,6 @@ export class AttachmentsComponent implements OnInit {
|
||||
|
||||
cipher: CipherView;
|
||||
cipherDomain: Cipher;
|
||||
hasUpdatedKey: boolean;
|
||||
canAccessAttachments: boolean;
|
||||
formPromise: Promise<any>;
|
||||
deletePromises: { [id: string]: Promise<any> } = {};
|
||||
@@ -50,15 +49,6 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.hasUpdatedKey) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("updateKey")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileEl = document.getElementById("file") as HTMLInputElement;
|
||||
const files = fileEl.files;
|
||||
if (files == null || files.length === 0) {
|
||||
@@ -191,7 +181,6 @@ export class AttachmentsComponent implements OnInit {
|
||||
this.cipherDomain = await this.loadCipher();
|
||||
this.cipher = await this.cipherDomain.decrypt();
|
||||
|
||||
this.hasUpdatedKey = await this.cryptoService.hasUserKey();
|
||||
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||
this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null;
|
||||
|
||||
@@ -206,19 +195,6 @@ export class AttachmentsComponent implements OnInit {
|
||||
if (confirmed) {
|
||||
this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/?premium=purchase");
|
||||
}
|
||||
} else if (!this.hasUpdatedKey) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "featureUnavailable" },
|
||||
content: { key: "updateKey" },
|
||||
acceptButtonText: { key: "learnMore" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.platformUtilsService.launchUri(
|
||||
"https://bitwarden.com/help/account-encryption-key/#rotate-your-encryption-key"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { ClientType } from "../../enums";
|
||||
import { KeysRequest } from "../../models/request/keys.request";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
@@ -151,6 +152,16 @@ export abstract class LogInStrategy {
|
||||
|
||||
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
|
||||
const result = new AuthResult();
|
||||
|
||||
// Old encryption keys must be migrated, but is currently only available on web.
|
||||
// Other clients shouldn't continue the login process.
|
||||
if (this.encryptionKeyMigrationRequired(response)) {
|
||||
result.requiresEncryptionKeyMigration = true;
|
||||
if (this.platformUtilsService.getClientType() !== ClientType.Web) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result.resetMasterPassword = response.resetMasterPassword;
|
||||
|
||||
// Convert boolean to enum
|
||||
@@ -166,9 +177,7 @@ export abstract class LogInStrategy {
|
||||
}
|
||||
|
||||
await this.setMasterKey(response);
|
||||
|
||||
await this.setUserKey(response);
|
||||
|
||||
await this.setPrivateKey(response);
|
||||
|
||||
this.messagingService.send("loggedIn");
|
||||
@@ -183,6 +192,12 @@ export abstract class LogInStrategy {
|
||||
|
||||
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
// Old accounts used master key for encryption. We are forcing migrations but only need to
|
||||
// check on password logins
|
||||
protected encryptionKeyMigrationRequired(response: IdentityTokenResponse): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async createKeyPairForOldAccount() {
|
||||
try {
|
||||
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
|
||||
|
||||
@@ -147,6 +147,10 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
}
|
||||
|
||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
||||
// If migration is required, we won't have a user key to set yet.
|
||||
if (this.encryptionKeyMigrationRequired(response)) {
|
||||
return;
|
||||
}
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
@@ -162,6 +166,10 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
protected override encryptionKeyMigrationRequired(response: IdentityTokenResponse): boolean {
|
||||
return !response.key;
|
||||
}
|
||||
|
||||
private getMasterPasswordPolicyOptionsFromResponse(
|
||||
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse
|
||||
): MasterPasswordPolicyOptions {
|
||||
|
||||
@@ -17,6 +17,7 @@ export class AuthResult {
|
||||
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
|
||||
ssoEmail2FaSessionToken?: string;
|
||||
email: string;
|
||||
requiresEncryptionKeyMigration: boolean;
|
||||
|
||||
get requiresCaptcha() {
|
||||
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
||||
|
||||
@@ -42,6 +42,12 @@ export abstract class CryptoService {
|
||||
* @returns The user key
|
||||
*/
|
||||
getUserKey: (userId?: string) => Promise<UserKey>;
|
||||
|
||||
/**
|
||||
* Checks if the user is using an old encryption scheme that used the master key
|
||||
* for encryption of data instead of the user key.
|
||||
*/
|
||||
isLegacyUser: (masterKey?: MasterKey, userId?: string) => Promise<boolean>;
|
||||
/**
|
||||
* Use for encryption/decryption of data in order to support legacy
|
||||
* encryption models. It will return the user key if available,
|
||||
|
||||
@@ -77,6 +77,12 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
async isLegacyUser(masterKey?: MasterKey, userId?: string): Promise<boolean> {
|
||||
return await this.validateUserKey(
|
||||
(masterKey ?? (await this.getMasterKey(userId))) as unknown as UserKey
|
||||
);
|
||||
}
|
||||
|
||||
async getUserKeyWithLegacySupport(userId?: string): Promise<UserKey> {
|
||||
const userKey = await this.getUserKey(userId);
|
||||
if (userKey) {
|
||||
@@ -510,7 +516,8 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]> {
|
||||
key ||= await this.getUserKey();
|
||||
// Default to user key
|
||||
key ||= await this.getUserKeyWithLegacySupport();
|
||||
|
||||
const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
||||
const publicB64 = Utils.fromBufferToB64(keyPair[0]);
|
||||
@@ -943,23 +950,30 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
|
||||
async migrateAutoKeyIfNeeded(userId?: string) {
|
||||
const oldAutoKey = await this.stateService.getCryptoMasterKeyAuto({ userId: userId });
|
||||
if (oldAutoKey) {
|
||||
// decrypt
|
||||
const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldAutoKey)) as MasterKey;
|
||||
const encryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({
|
||||
userId: userId,
|
||||
});
|
||||
const userKey = await this.decryptUserKeyWithMasterKey(
|
||||
masterKey,
|
||||
new EncString(encryptedUserKey),
|
||||
userId
|
||||
);
|
||||
// migrate
|
||||
await this.stateService.setUserKeyAutoUnlock(userKey.keyB64, { userId: userId });
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
// set encrypted user key in case user immediately locks without syncing
|
||||
await this.setMasterKeyEncryptedUserKey(encryptedUserKey);
|
||||
if (!oldAutoKey) {
|
||||
return;
|
||||
}
|
||||
// Decrypt
|
||||
const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldAutoKey)) as MasterKey;
|
||||
if (await this.isLegacyUser(masterKey, userId)) {
|
||||
// Legacy users don't have a user key, so no need to migrate.
|
||||
// Instead, set the master key for additional isLegacyUser checks that will log the user out.
|
||||
await this.setMasterKey(masterKey, userId);
|
||||
return;
|
||||
}
|
||||
const encryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({
|
||||
userId: userId,
|
||||
});
|
||||
const userKey = await this.decryptUserKeyWithMasterKey(
|
||||
masterKey,
|
||||
new EncString(encryptedUserKey),
|
||||
userId
|
||||
);
|
||||
// Migrate
|
||||
await this.stateService.setUserKeyAutoUnlock(userKey.keyB64, { userId: userId });
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
// Set encrypted user key in case user immediately locks without syncing
|
||||
await this.setMasterKeyEncryptedUserKey(encryptedUserKey);
|
||||
}
|
||||
|
||||
async decryptAndMigrateOldPinKey(
|
||||
|
||||
@@ -5,6 +5,7 @@ import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/va
|
||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout.service";
|
||||
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
import { ClientType } from "../../enums";
|
||||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
@@ -141,10 +142,18 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
}
|
||||
|
||||
private async migrateKeyForNeverLockIfNeeded(): Promise<void> {
|
||||
// Web can't set vault timeout to never
|
||||
if (this.platformUtilsService.getClientType() == ClientType.Web) {
|
||||
return;
|
||||
}
|
||||
const accounts = await firstValueFrom(this.stateService.accounts$);
|
||||
for (const userId in accounts) {
|
||||
if (userId != null) {
|
||||
await this.cryptoService.migrateAutoKeyIfNeeded(userId);
|
||||
// Legacy users should be logged out since we're not on the web vault and can't migrate.
|
||||
if (await this.cryptoService.isLegacyUser(null, userId)) {
|
||||
await this.logOut(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,11 +329,6 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return await this.getDecryptedCipherCache();
|
||||
}
|
||||
|
||||
const hasKey = await this.cryptoService.hasUserKey();
|
||||
if (!hasKey) {
|
||||
throw new Error("No user key found.");
|
||||
}
|
||||
|
||||
const ciphers = await this.getAll();
|
||||
const orgKeys = await this.cryptoService.getOrgKeys();
|
||||
const userKey = await this.cryptoService.getUserKeyWithLegacySupport();
|
||||
|
||||
Reference in New Issue
Block a user