mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01: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:
parent
020018085a
commit
8c06508435
@ -771,8 +771,8 @@
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Premium membership"
|
||||
|
@ -209,6 +209,11 @@ export class LoginCommand {
|
||||
new PasswordLogInCredentials(email, password, null, twoFactor)
|
||||
);
|
||||
}
|
||||
if (response.requiresEncryptionKeyMigration) {
|
||||
return Response.error(
|
||||
"Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
);
|
||||
}
|
||||
if (response.captchaSiteKey) {
|
||||
const credentials = new PasswordLogInCredentials(email, password);
|
||||
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);
|
||||
|
@ -475,8 +475,8 @@
|
||||
"maxFileSize": {
|
||||
"message": "Maximum file size is 500 MB."
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
"encryptionKeyMigrationRequired": {
|
||||
"message": "Encryption key migration required. Please login through the web vault to update your encryption key."
|
||||
},
|
||||
"editedFolder": {
|
||||
"message": "Folder saved"
|
||||
|
@ -15,6 +15,7 @@ import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
@ -210,4 +211,12 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
|
||||
}
|
||||
await super.submit(false);
|
||||
}
|
||||
|
||||
protected override handleMigrateEncryptionKey(result: AuthResult): boolean {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
||||
}
|
||||
this.router.navigate(["migrate-legacy-encryption"]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<div class="tw-mt-12 tw-flex tw-justify-center">
|
||||
<div class="tw-max-w-xl">
|
||||
<h1 bitTypography="h1" class="tw-mb-4 tw-text-center">{{ "updateEncryptionKey" | i18n }}</h1>
|
||||
<div
|
||||
class="tw-block tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
|
||||
>
|
||||
<p>
|
||||
{{ "updateEncryptionSchemeDesc" | i18n }}
|
||||
<a
|
||||
href="https://bitwarden.com/help/account-encryption-key/#rotate-your-encryption-key"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ "learnMore" | i18n }}</a
|
||||
>
|
||||
</p>
|
||||
<bit-callout type="warning">{{ "updateEncryptionKeyWarning" | i18n }}</bit-callout>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="masterPassword"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="masterPassword"
|
||||
appAutofocus
|
||||
/>
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" block>
|
||||
{{ "updateEncryptionKey" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -0,0 +1,82 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption.service";
|
||||
|
||||
// The master key was originally used to encrypt user data, before the user key was introduced.
|
||||
// This component is used to migrate from the old encryption scheme to the new one.
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [SharedModule],
|
||||
providers: [MigrateFromLegacyEncryptionService],
|
||||
templateUrl: "migrate-legacy-encryption.component.html",
|
||||
})
|
||||
export class MigrateFromLegacyEncryptionComponent {
|
||||
protected formGroup = new FormGroup({
|
||||
masterPassword: new FormControl("", [Validators.required]),
|
||||
});
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private migrationService: MigrateFromLegacyEncryptionService,
|
||||
private cryptoService: CryptoService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAsTouched();
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasUserKey = await this.cryptoService.hasUserKey();
|
||||
if (hasUserKey) {
|
||||
this.messagingService.send("logout");
|
||||
throw new Error("User key already exists, cannot migrate legacy encryption.");
|
||||
}
|
||||
|
||||
const masterPassword = this.formGroup.value.masterPassword;
|
||||
|
||||
try {
|
||||
// Create new user key
|
||||
const [newUserKey, masterKeyEncUserKey] = await this.migrationService.createNewUserKey(
|
||||
masterPassword
|
||||
);
|
||||
|
||||
// Update admin recover keys
|
||||
await this.migrationService.updateAllAdminRecoveryKeys(masterPassword, newUserKey);
|
||||
|
||||
// Update emergency access
|
||||
await this.migrationService.updateEmergencyAccesses(newUserKey);
|
||||
|
||||
// Update keys, folders, ciphers, and sends
|
||||
await this.migrationService.updateKeysAndEncryptedData(
|
||||
masterPassword,
|
||||
newUserKey,
|
||||
masterKeyEncUserKey
|
||||
);
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("keyUpdated"),
|
||||
this.i18nService.t("logBackInOthersToo"),
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
this.messagingService.send("logout");
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,345 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
|
||||
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
|
||||
import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type";
|
||||
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request";
|
||||
import { EmergencyAccessGranteeDetailsResponse } from "@bitwarden/common/auth/models/response/emergency-access.response";
|
||||
import { EncryptionType, KdfType } from "@bitwarden/common/enums";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import {
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserKey,
|
||||
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { Send } from "@bitwarden/common/tools/send/models/domain/send";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { Folder } from "@bitwarden/common/vault/models/domain/folder";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption.service";
|
||||
|
||||
describe("migrateFromLegacyEncryptionService", () => {
|
||||
let migrateFromLegacyEncryptionService: MigrateFromLegacyEncryptionService;
|
||||
|
||||
const organizationService = mock<OrganizationService>();
|
||||
const organizationApiService = mock<OrganizationApiService>();
|
||||
const organizationUserService = mock<OrganizationUserService>();
|
||||
const apiService = mock<ApiService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const cryptoService = mock<CryptoService>();
|
||||
const syncService = mock<SyncService>();
|
||||
const cipherService = mock<CipherService>();
|
||||
const folderService = mock<FolderService>();
|
||||
const sendService = mock<SendService>();
|
||||
const stateService = mock<StateService>();
|
||||
let folderViews: BehaviorSubject<FolderView[]>;
|
||||
let sends: BehaviorSubject<Send[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
migrateFromLegacyEncryptionService = new MigrateFromLegacyEncryptionService(
|
||||
organizationService,
|
||||
organizationApiService,
|
||||
organizationUserService,
|
||||
apiService,
|
||||
cryptoService,
|
||||
encryptService,
|
||||
syncService,
|
||||
cipherService,
|
||||
folderService,
|
||||
sendService,
|
||||
stateService
|
||||
);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(migrateFromLegacyEncryptionService).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("createNewUserKey", () => {
|
||||
it("validates master password and legacy user", async () => {
|
||||
const mockMasterPassword = "mockMasterPassword";
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
const mockMasterKey = new SymmetricCryptoKey(mockRandomBytes) as MasterKey;
|
||||
stateService.getEmail.mockResolvedValue("mockEmail");
|
||||
stateService.getKdfType.mockResolvedValue(KdfType.PBKDF2_SHA256);
|
||||
stateService.getKdfConfig.mockResolvedValue({ iterations: 100000 });
|
||||
cryptoService.makeMasterKey.mockResolvedValue(mockMasterKey);
|
||||
cryptoService.isLegacyUser.mockResolvedValue(false);
|
||||
|
||||
await expect(
|
||||
migrateFromLegacyEncryptionService.createNewUserKey(mockMasterPassword)
|
||||
).rejects.toThrowError("Invalid master password or user may not be legacy");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateKeysAndEncryptedData", () => {
|
||||
let mockMasterPassword: string;
|
||||
let mockUserKey: UserKey;
|
||||
let mockEncUserKey: EncString;
|
||||
|
||||
beforeEach(() => {
|
||||
mockMasterPassword = "mockMasterPassword";
|
||||
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
||||
mockEncUserKey = new EncString("mockEncUserKey");
|
||||
|
||||
const mockFolders = [createMockFolder("1", "Folder 1"), createMockFolder("2", "Folder 2")];
|
||||
const mockCiphers = [createMockCipher("1", "Cipher 1"), createMockCipher("2", "Cipher 2")];
|
||||
const mockSends = [createMockSend("1", "Send 1"), createMockSend("2", "Send 2")];
|
||||
|
||||
cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64) as CsprngArray);
|
||||
|
||||
folderViews = new BehaviorSubject<FolderView[]>(mockFolders);
|
||||
folderService.folderViews$ = folderViews;
|
||||
|
||||
cipherService.getAllDecrypted.mockResolvedValue(mockCiphers);
|
||||
|
||||
sends = new BehaviorSubject<Send[]>(mockSends);
|
||||
sendService.sends$ = sends;
|
||||
|
||||
encryptService.encrypt.mockImplementation((plainValue, userKey) => {
|
||||
return Promise.resolve(
|
||||
new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "Encrypted: " + plainValue)
|
||||
);
|
||||
});
|
||||
|
||||
folderService.encrypt.mockImplementation((folder, userKey) => {
|
||||
const encryptedFolder = new Folder();
|
||||
encryptedFolder.id = folder.id;
|
||||
encryptedFolder.name = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"Encrypted: " + folder.name
|
||||
);
|
||||
return Promise.resolve(encryptedFolder);
|
||||
});
|
||||
|
||||
cipherService.encrypt.mockImplementation((cipher, userKey) => {
|
||||
const encryptedCipher = new Cipher();
|
||||
encryptedCipher.id = cipher.id;
|
||||
encryptedCipher.name = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"Encrypted: " + cipher.name
|
||||
);
|
||||
return Promise.resolve(encryptedCipher);
|
||||
});
|
||||
});
|
||||
|
||||
it("derives the master key in case it hasn't been set", async () => {
|
||||
await migrateFromLegacyEncryptionService.updateKeysAndEncryptedData(
|
||||
mockMasterPassword,
|
||||
mockUserKey,
|
||||
mockEncUserKey
|
||||
);
|
||||
|
||||
expect(cryptoService.getOrDeriveMasterKey).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("syncs latest data", async () => {
|
||||
await migrateFromLegacyEncryptionService.updateKeysAndEncryptedData(
|
||||
mockMasterPassword,
|
||||
mockUserKey,
|
||||
mockEncUserKey
|
||||
);
|
||||
expect(syncService.fullSync).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("does not post new account data if sync fails", async () => {
|
||||
syncService.fullSync.mockRejectedValueOnce(new Error("sync failed"));
|
||||
|
||||
await expect(
|
||||
migrateFromLegacyEncryptionService.updateKeysAndEncryptedData(
|
||||
mockMasterPassword,
|
||||
mockUserKey,
|
||||
mockEncUserKey
|
||||
)
|
||||
).rejects.toThrowError("sync failed");
|
||||
|
||||
expect(apiService.postAccountKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not post new account data if data retrieval fails", async () => {
|
||||
(migrateFromLegacyEncryptionService as any).encryptCiphers = async () => {
|
||||
throw new Error("Ciphers failed to be retrieved");
|
||||
};
|
||||
|
||||
await expect(
|
||||
migrateFromLegacyEncryptionService.updateKeysAndEncryptedData(
|
||||
mockMasterPassword,
|
||||
mockUserKey,
|
||||
mockEncUserKey
|
||||
)
|
||||
).rejects.toThrowError("Ciphers failed to be retrieved");
|
||||
|
||||
expect(apiService.postAccountKey).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateEmergencyAccesses", () => {
|
||||
let mockUserKey: UserKey;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
||||
|
||||
const mockEmergencyAccess = {
|
||||
data: [
|
||||
createMockEmergencyAccess("0", "EA 0", EmergencyAccessStatusType.Invited),
|
||||
createMockEmergencyAccess("1", "EA 1", EmergencyAccessStatusType.Accepted),
|
||||
createMockEmergencyAccess("2", "EA 2", EmergencyAccessStatusType.Confirmed),
|
||||
createMockEmergencyAccess("3", "EA 3", EmergencyAccessStatusType.RecoveryInitiated),
|
||||
createMockEmergencyAccess("4", "EA 4", EmergencyAccessStatusType.RecoveryApproved),
|
||||
],
|
||||
} as ListResponse<EmergencyAccessGranteeDetailsResponse>;
|
||||
apiService.getEmergencyAccessTrusted.mockResolvedValue(mockEmergencyAccess);
|
||||
apiService.getUserPublicKey.mockResolvedValue({
|
||||
userId: "mockUserId",
|
||||
publicKey: "mockPublicKey",
|
||||
} as UserKeyResponse);
|
||||
|
||||
cryptoService.rsaEncrypt.mockImplementation((plainValue, publicKey) => {
|
||||
return Promise.resolve(
|
||||
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "Encrypted: " + plainValue)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("Only updates emergency accesses with allowed statuses", async () => {
|
||||
await migrateFromLegacyEncryptionService.updateEmergencyAccesses(mockUserKey);
|
||||
|
||||
expect(apiService.putEmergencyAccess).not.toHaveBeenCalledWith(
|
||||
"0",
|
||||
expect.any(EmergencyAccessUpdateRequest)
|
||||
);
|
||||
expect(apiService.putEmergencyAccess).not.toHaveBeenCalledWith(
|
||||
"1",
|
||||
expect.any(EmergencyAccessUpdateRequest)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateAllAdminRecoveryKeys", () => {
|
||||
let mockMasterPassword: string;
|
||||
let mockUserKey: UserKey;
|
||||
|
||||
beforeEach(() => {
|
||||
mockMasterPassword = "mockMasterPassword";
|
||||
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
||||
|
||||
organizationService.getAll.mockResolvedValue([
|
||||
createOrganization("1", "Org 1", true),
|
||||
createOrganization("2", "Org 2", true),
|
||||
createOrganization("3", "Org 3", false),
|
||||
createOrganization("4", "Org 4", false),
|
||||
]);
|
||||
|
||||
organizationApiService.getKeys.mockImplementation((orgId) => {
|
||||
return Promise.resolve({
|
||||
publicKey: orgId + "mockPublicKey",
|
||||
privateKey: orgId + "mockPrivateKey",
|
||||
} as OrganizationKeysResponse);
|
||||
});
|
||||
});
|
||||
|
||||
it("Only updates organizations that are enrolled in admin recovery", async () => {
|
||||
await migrateFromLegacyEncryptionService.updateAllAdminRecoveryKeys(
|
||||
mockMasterPassword,
|
||||
mockUserKey
|
||||
);
|
||||
|
||||
expect(
|
||||
organizationUserService.putOrganizationUserResetPasswordEnrollment
|
||||
).toHaveBeenCalledWith(
|
||||
"1",
|
||||
expect.any(String),
|
||||
expect.any(OrganizationUserResetPasswordEnrollmentRequest)
|
||||
);
|
||||
expect(
|
||||
organizationUserService.putOrganizationUserResetPasswordEnrollment
|
||||
).toHaveBeenCalledWith(
|
||||
"2",
|
||||
expect.any(String),
|
||||
expect.any(OrganizationUserResetPasswordEnrollmentRequest)
|
||||
);
|
||||
expect(
|
||||
organizationUserService.putOrganizationUserResetPasswordEnrollment
|
||||
).not.toHaveBeenCalledWith(
|
||||
"3",
|
||||
expect.any(String),
|
||||
expect.any(OrganizationUserResetPasswordEnrollmentRequest)
|
||||
);
|
||||
expect(
|
||||
organizationUserService.putOrganizationUserResetPasswordEnrollment
|
||||
).not.toHaveBeenCalledWith(
|
||||
"4",
|
||||
expect.any(String),
|
||||
expect.any(OrganizationUserResetPasswordEnrollmentRequest)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createMockFolder(id: string, name: string): FolderView {
|
||||
const folder = new FolderView();
|
||||
folder.id = id;
|
||||
folder.name = name;
|
||||
return folder;
|
||||
}
|
||||
|
||||
function createMockCipher(id: string, name: string): CipherView {
|
||||
const cipher = new CipherView();
|
||||
cipher.id = id;
|
||||
cipher.name = name;
|
||||
return cipher;
|
||||
}
|
||||
|
||||
function createMockSend(id: string, name: string): Send {
|
||||
const send = new Send();
|
||||
send.id = id;
|
||||
send.name = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, name);
|
||||
return send;
|
||||
}
|
||||
|
||||
function createMockEmergencyAccess(
|
||||
id: string,
|
||||
name: string,
|
||||
status: EmergencyAccessStatusType
|
||||
): EmergencyAccessGranteeDetailsResponse {
|
||||
const emergencyAccess = new EmergencyAccessGranteeDetailsResponse({});
|
||||
emergencyAccess.id = id;
|
||||
emergencyAccess.name = name;
|
||||
emergencyAccess.type = 0;
|
||||
emergencyAccess.status = status;
|
||||
return emergencyAccess;
|
||||
}
|
||||
|
||||
function createOrganization(id: string, name: string, resetPasswordEnrolled: boolean) {
|
||||
const org = new Organization();
|
||||
org.id = id;
|
||||
org.name = name;
|
||||
org.resetPasswordEnrolled = resetPasswordEnrolled;
|
||||
org.userId = "mockUserID";
|
||||
return org;
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type";
|
||||
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request";
|
||||
import { UpdateKeyRequest } from "@bitwarden/common/models/request/update-key.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request";
|
||||
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
|
||||
|
||||
// TODO: PM-3797 - This service should be expanded and used for user key rotations in change-password.component.ts
|
||||
@Injectable()
|
||||
export class MigrateFromLegacyEncryptionService {
|
||||
constructor(
|
||||
private organizationService: OrganizationService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private apiService: ApiService,
|
||||
private cryptoService: CryptoService,
|
||||
private encryptService: EncryptService,
|
||||
private syncService: SyncService,
|
||||
private cipherService: CipherService,
|
||||
private folderService: FolderService,
|
||||
private sendService: SendService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the master password and creates a new user key.
|
||||
* @returns A new user key along with the encrypted version
|
||||
*/
|
||||
async createNewUserKey(masterPassword: string): Promise<[UserKey, EncString]> {
|
||||
// Create master key to validate the master password
|
||||
const masterKey = await this.cryptoService.makeMasterKey(
|
||||
masterPassword,
|
||||
await this.stateService.getEmail(),
|
||||
await this.stateService.getKdfType(),
|
||||
await this.stateService.getKdfConfig()
|
||||
);
|
||||
|
||||
if (!masterKey) {
|
||||
throw new Error("Invalid master password");
|
||||
}
|
||||
|
||||
if (!(await this.cryptoService.isLegacyUser(masterKey))) {
|
||||
throw new Error("Invalid master password or user may not be legacy");
|
||||
}
|
||||
|
||||
// Set master key again in case it was lost (could be lost on refresh)
|
||||
await this.cryptoService.setMasterKey(masterKey);
|
||||
return await this.cryptoService.makeUserKey(masterKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user key, master key hash, private key, folders, ciphers, and sends
|
||||
* on the server.
|
||||
* @param masterPassword The master password
|
||||
* @param newUserKey The new user key
|
||||
* @param newEncUserKey The new encrypted user key
|
||||
*/
|
||||
async updateKeysAndEncryptedData(
|
||||
masterPassword: string,
|
||||
newUserKey: UserKey,
|
||||
newEncUserKey: EncString
|
||||
): Promise<void> {
|
||||
// Create new request and add master key and hash
|
||||
const request = new UpdateKeyRequest();
|
||||
request.key = newEncUserKey.encryptedString;
|
||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||
masterPassword,
|
||||
await this.cryptoService.getOrDeriveMasterKey(masterPassword)
|
||||
);
|
||||
|
||||
// Sync before encrypting to make sure we have latest data
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
request.privateKey = await this.encryptPrivateKey(newUserKey);
|
||||
request.folders = await this.encryptFolders(newUserKey);
|
||||
request.ciphers = await this.encryptCiphers(newUserKey);
|
||||
request.sends = await this.encryptSends(newUserKey);
|
||||
|
||||
return this.apiService.postAccountKey(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets user's emergency access details from server and encrypts with new user key
|
||||
* on the server.
|
||||
* @param newUserKey The new user key
|
||||
*/
|
||||
async updateEmergencyAccesses(newUserKey: UserKey) {
|
||||
const emergencyAccess = await this.apiService.getEmergencyAccessTrusted();
|
||||
// Any Invited or Accepted requests won't have the key yet, so we don't need to update them
|
||||
const allowedStatuses = new Set([
|
||||
EmergencyAccessStatusType.Confirmed,
|
||||
EmergencyAccessStatusType.RecoveryInitiated,
|
||||
EmergencyAccessStatusType.RecoveryApproved,
|
||||
]);
|
||||
const filteredAccesses = emergencyAccess.data.filter((d) => allowedStatuses.has(d.status));
|
||||
|
||||
for (const details of filteredAccesses) {
|
||||
// Get public key of grantee
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
|
||||
// Encrypt new user key with public key
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(newUserKey.key, publicKey);
|
||||
|
||||
const updateRequest = new EmergencyAccessUpdateRequest();
|
||||
updateRequest.type = details.type;
|
||||
updateRequest.waitTimeDays = details.waitTimeDays;
|
||||
updateRequest.keyEncrypted = encryptedKey.encryptedString;
|
||||
|
||||
await this.apiService.putEmergencyAccess(details.id, updateRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates all admin recovery keys on the server with the new user key
|
||||
* @param masterPassword The user's master password
|
||||
* @param newUserKey The new user key
|
||||
*/
|
||||
async updateAllAdminRecoveryKeys(masterPassword: string, newUserKey: UserKey) {
|
||||
const allOrgs = await this.organizationService.getAll();
|
||||
|
||||
for (const org of allOrgs) {
|
||||
// If not already enrolled, skip
|
||||
if (!org.resetPasswordEnrolled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Retrieve public key
|
||||
const response = await this.organizationApiService.getKeys(org.id);
|
||||
const publicKey = Utils.fromB64ToArray(response?.publicKey);
|
||||
|
||||
// Re-enroll - encrypt user key with organization public key
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(newUserKey.key, publicKey);
|
||||
|
||||
// Create/Execute request
|
||||
const request = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
request.resetPasswordKey = encryptedKey.encryptedString;
|
||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||
masterPassword,
|
||||
await this.cryptoService.getOrDeriveMasterKey(masterPassword)
|
||||
);
|
||||
|
||||
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
|
||||
org.id,
|
||||
org.userId,
|
||||
request
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async encryptPrivateKey(newUserKey: UserKey): Promise<EncryptedString | null> {
|
||||
const privateKey = await this.cryptoService.getPrivateKey();
|
||||
if (!privateKey) {
|
||||
return;
|
||||
}
|
||||
return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString;
|
||||
}
|
||||
|
||||
private async encryptFolders(newUserKey: UserKey): Promise<FolderWithIdRequest[] | null> {
|
||||
const folders = await firstValueFrom(this.folderService.folderViews$);
|
||||
if (!folders) {
|
||||
return;
|
||||
}
|
||||
return await Promise.all(
|
||||
folders.map(async (folder) => {
|
||||
const encryptedFolder = await this.folderService.encrypt(folder, newUserKey);
|
||||
return new FolderWithIdRequest(encryptedFolder);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async encryptCiphers(newUserKey: UserKey): Promise<CipherWithIdRequest[] | null> {
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
if (!ciphers) {
|
||||
return;
|
||||
}
|
||||
return await Promise.all(
|
||||
ciphers.map(async (cipher) => {
|
||||
const encryptedCipher = await this.cipherService.encrypt(cipher, newUserKey);
|
||||
return new CipherWithIdRequest(encryptedCipher);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async encryptSends(newUserKey: UserKey): Promise<SendWithIdRequest[] | null> {
|
||||
const sends = await firstValueFrom(this.sendService.sends$);
|
||||
if (!sends) {
|
||||
return;
|
||||
}
|
||||
return await Promise.all(
|
||||
sends.map(async (send) => {
|
||||
const sendKey = await this.encryptService.decryptToBytes(send.key, null);
|
||||
send.key = (await this.encryptService.encrypt(sendKey, newUserKey)) ?? send.key;
|
||||
return new SendWithIdRequest(send);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -145,12 +145,6 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const hasUserKey = await this.cryptoService.hasUserKey();
|
||||
if (!hasUserKey) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("updateKey"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.masterPasswordHint != null && this.masterPasswordHint == this.masterPassword) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
|
@ -9,6 +9,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@ -86,6 +87,14 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
);
|
||||
}
|
||||
|
||||
protected override handleMigrateEncryptionKey(result: AuthResult): boolean {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
||||
}
|
||||
this.router.navigate(["migrate-legacy-encryption"]);
|
||||
return true;
|
||||
}
|
||||
|
||||
goAfterLogIn = async () => {
|
||||
this.loginService.clearValues();
|
||||
const previousUrl = this.routerService.getPreviousUrl();
|
||||
|
@ -175,6 +175,13 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
data: { titleId: "removeMasterPassword" },
|
||||
},
|
||||
{
|
||||
path: "migrate-legacy-encryption",
|
||||
loadComponent: () =>
|
||||
import("./auth/migrate-encryption/migrate-legacy-encryption.component").then(
|
||||
(mod) => mod.MigrateFromLegacyEncryptionComponent
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -42,12 +42,6 @@ export class ChangeEmailComponent implements OnInit {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const hasUserKey = await this.cryptoService.hasUserKey();
|
||||
if (!hasUserKey) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("updateKey"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.newEmail = this.newEmail.trim().toLowerCase();
|
||||
if (!this.tokenSent) {
|
||||
const request = new EmailTokenRequest();
|
||||
|
@ -46,11 +46,6 @@ export class ChangeKdfConfirmationComponent {
|
||||
|
||||
async submit() {
|
||||
this.loading = true;
|
||||
const hasUserKey = await this.cryptoService.hasUserKey();
|
||||
if (!hasUserKey) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("updateKey"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.makeKeyAndSaveAsync();
|
||||
|
@ -1,55 +0,0 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="updateUserKeyTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="updateUserKeyTitle">{{ "updateEncryptionKey" | i18n }}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
{{ "updateEncryptionKeyShortDesc" | i18n }} {{ "updateEncryptionKeyDesc" | i18n }}
|
||||
<a
|
||||
href="https://bitwarden.com/help/account-encryption-key/#rotate-your-encryption-key"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ "learnMore" | i18n }}</a
|
||||
>
|
||||
</p>
|
||||
<app-callout type="warning">{{ "updateEncryptionKeyWarning" | i18n }}</app-callout>
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="password"
|
||||
name="MasterPasswordHash"
|
||||
class="form-control"
|
||||
[(ngModel)]="masterPassword"
|
||||
required
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "updateEncryptionKey" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,108 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UpdateKeyRequest } from "@bitwarden/common/models/request/update-key.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { CipherWithIdRequest } from "@bitwarden/common/vault//models/request/cipher-with-id.request";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
|
||||
|
||||
@Component({
|
||||
selector: "app-update-key",
|
||||
templateUrl: "update-key.component.html",
|
||||
})
|
||||
export class UpdateKeyComponent {
|
||||
masterPassword: string;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private cryptoService: CryptoService,
|
||||
private messagingService: MessagingService,
|
||||
private syncService: SyncService,
|
||||
private folderService: FolderService,
|
||||
private cipherService: CipherService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
const hasUserKey = await this.cryptoService.hasUserKey();
|
||||
if (hasUserKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.masterPassword == null || this.masterPassword === "") {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("masterPasswordRequired")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.makeRequest().then((request) => {
|
||||
return this.apiService.postAccountKey(request);
|
||||
});
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("keyUpdated"),
|
||||
this.i18nService.t("logBackInOthersToo"),
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
this.messagingService.send("logout");
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async makeRequest(): Promise<UpdateKeyRequest> {
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
const newUserKey = await this.cryptoService.makeUserKey(masterKey);
|
||||
const privateKey = await this.cryptoService.getPrivateKey();
|
||||
let encPrivateKey: EncString = null;
|
||||
if (privateKey != null) {
|
||||
encPrivateKey = await this.cryptoService.encrypt(privateKey, newUserKey[0]);
|
||||
}
|
||||
const request = new UpdateKeyRequest();
|
||||
request.privateKey = encPrivateKey != null ? encPrivateKey.encryptedString : null;
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
await this.cryptoService.getOrDeriveMasterKey(this.masterPassword)
|
||||
);
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
const folders = await firstValueFrom(this.folderService.folderViews$);
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
if (folders[i].id == null) {
|
||||
continue;
|
||||
}
|
||||
const folder = await this.folderService.encrypt(folders[i], newUserKey[0]);
|
||||
request.folders.push(new FolderWithIdRequest(folder));
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
for (let i = 0; i < ciphers.length; i++) {
|
||||
if (ciphers[i].organizationId != null) {
|
||||
continue;
|
||||
}
|
||||
const cipher = await this.cipherService.encrypt(ciphers[i], newUserKey[0]);
|
||||
request.ciphers.push(new CipherWithIdRequest(cipher));
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
@ -86,7 +86,6 @@ import { PurgeVaultComponent } from "../settings/purge-vault.component";
|
||||
import { SecurityKeysComponent } from "../settings/security-keys.component";
|
||||
import { SecurityComponent } from "../settings/security.component";
|
||||
import { SettingsComponent } from "../settings/settings.component";
|
||||
import { UpdateKeyComponent } from "../settings/update-key.component";
|
||||
import { UpdateLicenseComponent } from "../settings/update-license.component";
|
||||
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
||||
import { GeneratorComponent } from "../tools/generator.component";
|
||||
@ -216,7 +215,6 @@ import { SharedModule } from "./shared.module";
|
||||
TwoFactorVerifyComponent,
|
||||
TwoFactorWebAuthnComponent,
|
||||
TwoFactorYubiKeyComponent,
|
||||
UpdateKeyComponent,
|
||||
UpdateLicenseComponent,
|
||||
UpdatePasswordComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
@ -319,7 +317,6 @@ import { SharedModule } from "./shared.module";
|
||||
TwoFactorVerifyComponent,
|
||||
TwoFactorWebAuthnComponent,
|
||||
TwoFactorYubiKeyComponent,
|
||||
UpdateKeyComponent,
|
||||
UpdateLicenseComponent,
|
||||
UpdatePasswordComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
|
@ -77,19 +77,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="card border-warning mb-4" *ngIf="showUpdateKey">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i>
|
||||
{{ "updateKeyTitle" | i18n }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ "updateEncryptionKeyShortDesc" | i18n }}</p>
|
||||
<button class="btn btn-block btn-outline-secondary" type="button" (click)="updateKey()">
|
||||
{{ "updateEncryptionKey" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-low-kdf class="d-block mb-4" *ngIf="showLowKdf"> </app-low-kdf>
|
||||
|
||||
<app-verify-email
|
||||
|
@ -60,7 +60,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { DialogService, Icons } from "@bitwarden/components";
|
||||
|
||||
import { UpdateKeyComponent } from "../../settings/update-key.component";
|
||||
import { CollectionDialogAction, openCollectionDialog } from "../components/collection-dialog";
|
||||
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
||||
import { getNestedCollectionTree } from "../utils/collection-utils";
|
||||
@ -114,12 +113,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("share", { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef;
|
||||
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
|
||||
collectionsModalRef: ViewContainerRef;
|
||||
@ViewChild("updateKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
updateKeyModalRef: ViewContainerRef;
|
||||
|
||||
showVerifyEmail = false;
|
||||
showBrowserOutdated = false;
|
||||
showUpdateKey = false;
|
||||
showPremiumCallout = false;
|
||||
showLowKdf = false;
|
||||
trashCleanupWarning: string = null;
|
||||
@ -197,7 +193,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||
this.showPremiumCallout =
|
||||
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
|
||||
this.showUpdateKey = !(await this.cryptoService.hasUserKey());
|
||||
|
||||
const cipherId = getCipherIdFromParams(params);
|
||||
if (!cipherId) {
|
||||
@ -405,11 +400,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
get isShowingCards() {
|
||||
return (
|
||||
this.showBrowserOutdated ||
|
||||
this.showPremiumCallout ||
|
||||
this.showUpdateKey ||
|
||||
this.showVerifyEmail ||
|
||||
this.showLowKdf
|
||||
this.showBrowserOutdated || this.showPremiumCallout || this.showVerifyEmail || this.showLowKdf
|
||||
);
|
||||
}
|
||||
|
||||
@ -865,10 +856,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
: this.cipherService.softDeleteWithServer(id);
|
||||
}
|
||||
|
||||
async updateKey() {
|
||||
await this.modalService.openViewRef(UpdateKeyComponent, this.updateKeyModalRef);
|
||||
}
|
||||
|
||||
async isLowKdfIteration() {
|
||||
const kdfType = await this.stateService.getKdfType();
|
||||
const kdfOptions = await this.stateService.getKdfConfig();
|
||||
|
@ -508,9 +508,6 @@
|
||||
"maxFileSize": {
|
||||
"message": "Maximum file size is 500 MB."
|
||||
},
|
||||
"updateKey": {
|
||||
"message": "You cannot use this feature until you update your encryption key."
|
||||
},
|
||||
"addedItem": {
|
||||
"message": "Item added"
|
||||
},
|
||||
@ -3476,17 +3473,11 @@
|
||||
"keyUpdated": {
|
||||
"message": "Key updated"
|
||||
},
|
||||
"updateKeyTitle": {
|
||||
"message": "Update key"
|
||||
},
|
||||
"updateEncryptionKey": {
|
||||
"message": "Update encryption key"
|
||||
},
|
||||
"updateEncryptionKeyShortDesc": {
|
||||
"message": "You are currently using an outdated encryption scheme."
|
||||
},
|
||||
"updateEncryptionKeyDesc": {
|
||||
"message": "We've moved to larger encryption keys that provide better security and access to newer features. Updating your encryption key is quick and easy. Just type your master password below. This update will eventually become mandatory."
|
||||
"updateEncryptionSchemeDesc": {
|
||||
"message": "We've changed the encryption scheme to provide better security. Update your encryption key now by entering your master password below."
|
||||
},
|
||||
"updateEncryptionKeyWarning": {
|
||||
"message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed."
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user