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:
@@ -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