From 1367f041eb34b9bc75f267d8fa5334dffda4d6af Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Tue, 5 Sep 2023 17:38:23 -0400 Subject: [PATCH 01/13] [PM-3726] migrate legacy user's encryption key --- apps/browser/src/_locales/en/messages.json | 3 + apps/cli/src/auth/commands/login.command.ts | 5 + apps/desktop/src/locales/en/messages.json | 3 + .../web/src/app/auth/login/login.component.ts | 4 + .../migrate-legacy-encryption.component.html | 35 ++ .../migrate-legacy-encryption.component.ts | 77 ++++ .../migrate-legacy-encryption.module.ts | 13 + .../migrate-legacy-encryption.service.spec.ts | 345 ++++++++++++++++++ .../migrate-legacy-encryption.service.ts | 216 +++++++++++ apps/web/src/app/oss-routing.module.ts | 5 + apps/web/src/app/oss.module.ts | 3 + apps/web/src/locales/en/messages.json | 9 +- .../src/auth/components/login.component.ts | 12 + libs/angular/src/auth/guards/lock.guard.ts | 5 + .../auth/login-strategies/login.strategy.ts | 19 +- .../password-login.strategy.ts | 8 + .../src/auth/models/domain/auth-result.ts | 1 + .../platform/abstractions/crypto.service.ts | 6 + .../src/platform/services/crypto.service.ts | 9 +- .../src/vault/services/cipher.service.ts | 5 - 20 files changed, 769 insertions(+), 14 deletions(-) create mode 100644 apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html create mode 100644 apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts create mode 100644 apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.module.ts create mode 100644 apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts create mode 100644 apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 6aea5876eac..987006498d9 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -774,6 +774,9 @@ "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" }, diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index c6e16ccecd8..5cef79a09fd 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -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); diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index dbcb70d5e0c..01fa74e3905 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -478,6 +478,9 @@ "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" }, diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index 3bc65542b73..e912c68fc35 100644 --- a/apps/web/src/app/auth/login/login.component.ts +++ b/apps/web/src/app/auth/login/login.component.ts @@ -210,4 +210,8 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest } await super.submit(false); } + + protected override handleMigrateEncryptionKeyError() { + this.router.navigate(["migrate-legacy-encryption"]); + } } diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html new file mode 100644 index 00000000000..e6431a9543f --- /dev/null +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html @@ -0,0 +1,35 @@ +
+
+
+

{{ "updateEncryptionKey" | i18n }}

+
+

+ {{ "updateEncryptionSchemeDesc" | i18n }} {{ "updateEncryptionKeyDesc" | i18n }} + {{ "learnMore" | i18n }} +

+ {{ "updateEncryptionSchemeWarning" | i18n }} + + + {{ "masterPass" | i18n }} + + + +
+
+
+
diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts new file mode 100644 index 00000000000..5367aff255a --- /dev/null +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts @@ -0,0 +1,77 @@ +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 { 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({ + selector: "migreate-legacy-encryption", + 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) { + 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 keys, folders, ciphers, and sends + await this.migrationService.updateKeysAndEncryptedData( + masterPassword, + newUserKey, + masterKeyEncUserKey + ); + + // Update emergency access + await this.migrationService.updateEmergencyAccesses(newUserKey); + + // Update admin recover keys + await this.migrationService.updateAllAdminRecoveryKeys(masterPassword, newUserKey); + + 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; + } + }; +} diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.module.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.module.ts new file mode 100644 index 00000000000..5b3e47aae47 --- /dev/null +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from "@angular/core"; + +import { SharedModule } from "../../shared"; + +import { MigrateFromLegacyEncryptionComponent } from "./migrate-legacy-encryption.component"; +import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption.service"; + +@NgModule({ + imports: [SharedModule], + declarations: [MigrateFromLegacyEncryptionComponent], + providers: [MigrateFromLegacyEncryptionService], +}) +export class MigrateLegacyEncryptionModule {} diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts new file mode 100644 index 00000000000..bb2e5599c43 --- /dev/null +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts @@ -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(); + const organizationApiService = mock(); + const organizationUserService = mock(); + const apiService = mock(); + const encryptService = mock(); + const cryptoService = mock(); + const syncService = mock(); + const cipherService = mock(); + const folderService = mock(); + const sendService = mock(); + const stateService = mock(); + let folderViews: BehaviorSubject; + let sends: BehaviorSubject; + + 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 is not 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(mockFolders); + folderService.folderViews$ = folderViews; + + cipherService.getAllDecrypted.mockResolvedValue(mockCiphers); + + sends = new BehaviorSubject(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; + 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; +} diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts new file mode 100644 index 00000000000..9d4ac8c1b26 --- /dev/null +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts @@ -0,0 +1,216 @@ +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: 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 { + // 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 = [ + EmergencyAccessStatusType.Confirmed, + EmergencyAccessStatusType.RecoveryInitiated, + EmergencyAccessStatusType.RecoveryApproved, + ]; + + const filteredAccesses = emergencyAccess.data.filter((d) => allowedStatuses.includes(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 { + const privateKey = await this.cryptoService.getPrivateKey(); + if (!privateKey) { + return; + } + return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString; + } + + private async encryptFolders(newUserKey: UserKey): Promise { + 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 { + 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 { + 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); + }) + ); + } +} diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index e6f4caa112b..c7beb4d2b0a 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -25,6 +25,7 @@ import { LockComponent } from "./auth/lock.component"; import { LoginDecryptionOptionsComponent } from "./auth/login/login-decryption-options/login-decryption-options.component"; import { LoginWithDeviceComponent } from "./auth/login/login-with-device.component"; import { LoginComponent } from "./auth/login/login.component"; +import { MigrateFromLegacyEncryptionComponent } from "./auth/migrate-encryption/migrate-legacy-encryption.component"; import { RecoverDeleteComponent } from "./auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component"; import { RemovePasswordComponent } from "./auth/remove-password.component"; @@ -175,6 +176,10 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { titleId: "removeMasterPassword" }, }, + { + path: "migrate-legacy-encryption", + component: MigrateFromLegacyEncryptionComponent, + }, ], }, { diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index 1428aaea193..690b77f7f64 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -4,6 +4,7 @@ import { OrganizationCreateModule } from "./admin-console/organizations/create/o import { OrganizationManageModule } from "./admin-console/organizations/manage/organization-manage.module"; import { OrganizationUserModule } from "./admin-console/organizations/users/organization-user.module"; import { LoginModule } from "./auth/login/login.module"; +import { MigrateLegacyEncryptionModule } from "./auth/migrate-encryption/migrate-legacy-encryption.module"; import { TrialInitiationModule } from "./auth/trial-initiation/trial-initiation.module"; import { LooseComponentsModule, SharedModule } from "./shared"; import { OrganizationBadgeModule } from "./vault/individual-vault/organization-badge/organization-badge.module"; @@ -20,6 +21,7 @@ import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-f OrganizationUserModule, OrganizationCreateModule, LoginModule, + MigrateLegacyEncryptionModule, ], exports: [ SharedModule, @@ -28,6 +30,7 @@ import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-f VaultFilterModule, OrganizationBadgeModule, LoginModule, + MigrateLegacyEncryptionModule, ], bootstrap: [], }) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 2e747f3cefd..72d8e4095dd 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3479,13 +3479,10 @@ "updateEncryptionKey": { "message": "Update encryption key" }, - "updateEncryptionKeyShortDesc": { - "message": "You are currently using an outdated encryption scheme." + "updateEncryptionSchemeDesc": { + "message": "We've changed the encryption scheme to provide better security. Update your encryption key now by entering your master password below." }, - "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." - }, - "updateEncryptionKeyWarning": { + "updateEncryptionSchemeWarning": { "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." }, "updateEncryptionKeyExportWarning": { diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index 33b3dc2364e..2f3f52ff30c 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -136,6 +136,10 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit const response = await this.formPromise; this.setFormValues(); await this.loginService.saveEmailSettings(); + if (response.requiresEncryptionKeyMigration) { + this.handleMigrateEncryptionKeyError(); + return; + } if (this.handleCaptchaRequired(response)) { return; } else if (response.requiresTwoFactor) { @@ -269,6 +273,14 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit await this.loginService.saveEmailSettings(); } + protected handleMigrateEncryptionKeyError() { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccured"), + this.i18nService.t("encryptionKeyMigrationRequired") + ); + } + private getErrorToastMessage() { const error: AllValidationErrors = this.formValidationErrorService .getFormValidationErrors(this.formGroup.controls) diff --git a/libs/angular/src/auth/guards/lock.guard.ts b/libs/angular/src/auth/guards/lock.guard.ts index ea0f38d7742..25f079257ea 100644 --- a/libs/angular/src/auth/guards/lock.guard.ts +++ b/libs/angular/src/auth/guards/lock.guard.ts @@ -28,6 +28,11 @@ export function lockGuard(): CanActivateFn { const router = inject(Router); const userVerificationService = inject(UserVerificationService); + // If legacy user, redirect to migration page + if (cryptoService.isLegacyUser()) { + return router.createUrlTree(["migrate-legacy-encryption"]); + } + const authStatus = await authService.getAuthStatus(); if (authStatus !== AuthenticationStatus.Locked) { return router.createUrlTree(["/"]); diff --git a/libs/common/src/auth/login-strategies/login.strategy.ts b/libs/common/src/auth/login-strategies/login.strategy.ts index 6e51f215012..5c0e9f2244e 100644 --- a/libs/common/src/auth/login-strategies/login.strategy.ts +++ b/libs/common/src/auth/login-strategies/login.strategy.ts @@ -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 { 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; if (response.forcePasswordReset) { @@ -165,9 +176,7 @@ export abstract class LogInStrategy { } await this.setMasterKey(response); - await this.setUserKey(response); - await this.setPrivateKey(response); this.messagingService.send("loggedIn"); @@ -182,6 +191,12 @@ export abstract class LogInStrategy { protected abstract setPrivateKey(response: IdentityTokenResponse): Promise; + // 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(); diff --git a/libs/common/src/auth/login-strategies/password-login.strategy.ts b/libs/common/src/auth/login-strategies/password-login.strategy.ts index 7f7ec585699..0bcc679ae9a 100644 --- a/libs/common/src/auth/login-strategies/password-login.strategy.ts +++ b/libs/common/src/auth/login-strategies/password-login.strategy.ts @@ -147,6 +147,10 @@ export class PasswordLogInStrategy extends LogInStrategy { } protected override async setUserKey(response: IdentityTokenResponse): Promise { + // 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 { diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index c0a6f034aef..6900cba1c48 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -17,6 +17,7 @@ export class AuthResult { twoFactorProviders: Map = null; ssoEmail2FaSessionToken?: string; email: string; + requiresEncryptionKeyMigration: boolean; get requiresCaptcha() { return !Utils.isNullOrWhitespace(this.captchaSiteKey); diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 42f60bde845..a868484bd04 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -42,6 +42,12 @@ export abstract class CryptoService { * @returns The user key */ getUserKey: (userId?: string) => Promise; + + /** + * 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; /** * Use for encryption/decryption of data in order to support legacy * encryption models. It will return the user key if available, diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 7be0738f68a..4dafc80644d 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -77,6 +77,12 @@ export class CryptoService implements CryptoServiceAbstraction { } } + async isLegacyUser(masterKey?: MasterKey, userId?: string): Promise { + return await this.validateUserKey( + (masterKey ?? (await this.getMasterKey(userId))) as unknown as UserKey + ); + } + async getUserKeyWithLegacySupport(userId?: string): Promise { 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]); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 52ca151dc5f..2433cdc3bfc 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -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(); From 42fff3898f6781449a3bd77228e2896026121583 Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Wed, 6 Sep 2023 14:54:13 -0400 Subject: [PATCH 02/13] [PM-3726] add 2fa support and pr feedback --- .vscode/launch.json | 2 +- apps/web/src/app/auth/login/login.component.ts | 7 ++++++- .../migrate-legacy-encryption.component.ts | 15 ++++++++------- .../migrate-legacy-encryption.service.spec.ts | 2 +- .../migrate-legacy-encryption.service.ts | 2 +- apps/web/src/app/auth/two-factor.component.ts | 9 +++++++++ apps/web/webpack.config.js | 1 + .../src/auth/components/login.component.ts | 15 ++++++++++----- .../src/auth/components/two-factor.component.ts | 15 +++++++++++++++ libs/angular/src/auth/guards/lock.guard.ts | 13 +++++++++++-- 10 files changed, 63 insertions(+), 18 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 95b961d530e..98ff43a4456 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,7 +19,7 @@ "request": "launch", "name": "Jest Current File", "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["${fileBasenameNoExtension}", "--config", "jest.config.js"], + "args": ["--runTestsByPath", "${relativeFile}", "--config", "jest.config.js"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "disableOptimisticBPs": true, diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index e912c68fc35..72d8e3766ec 100644 --- a/apps/web/src/app/auth/login/login.component.ts +++ b/apps/web/src/app/auth/login/login.component.ts @@ -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"; @@ -211,7 +212,11 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest await super.submit(false); } - protected override handleMigrateEncryptionKeyError() { + protected override handleMigrateEncryptionKey(result: AuthResult): boolean { + if (!result.requiresEncryptionKeyMigration) { + return false; + } this.router.navigate(["migrate-legacy-encryption"]); + return true; } } diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts index 5367aff255a..878685e2ce8 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts @@ -12,7 +12,7 @@ import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption. // 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({ - selector: "migreate-legacy-encryption", + selector: "migrate-legacy-encryption", templateUrl: "migrate-legacy-encryption.component.html", }) export class MigrateFromLegacyEncryptionComponent { @@ -38,6 +38,7 @@ export class MigrateFromLegacyEncryptionComponent { const hasUserKey = await this.cryptoService.hasUserKey(); if (hasUserKey) { + this.messagingService.send("logout"); throw new Error("User key already exists, cannot migrate legacy encryption."); } @@ -49,6 +50,12 @@ export class MigrateFromLegacyEncryptionComponent { 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, @@ -56,12 +63,6 @@ export class MigrateFromLegacyEncryptionComponent { masterKeyEncUserKey ); - // Update emergency access - await this.migrationService.updateEmergencyAccesses(newUserKey); - - // Update admin recover keys - await this.migrationService.updateAllAdminRecoveryKeys(masterPassword, newUserKey); - this.platformUtilsService.showToast( "success", this.i18nService.t("keyUpdated"), diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts index bb2e5599c43..88bbdc4e5b2 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.spec.ts @@ -88,7 +88,7 @@ describe("migrateFromLegacyEncryptionService", () => { await expect( migrateFromLegacyEncryptionService.createNewUserKey(mockMasterPassword) - ).rejects.toThrowError("Invalid master password or user is not legacy"); + ).rejects.toThrowError("Invalid master password or user may not be legacy"); }); }); diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts index 9d4ac8c1b26..7804f3eacfa 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts @@ -23,7 +23,7 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv 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: This service should be expanded and used for user key rotations in change-password.component.ts +// TODO: PM-3797 - This service should be expanded and used for user key rotations in change-password.component.ts @Injectable() export class MigrateFromLegacyEncryptionService { constructor( diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index 29903317619..2dbb650874a 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -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(); diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index bfdc8ae90ae..1312c6cd7ed 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -283,6 +283,7 @@ const devServer = https://api.fastmail.com https://api.forwardemail.net http://localhost:5000 + ws://localhost:61840 ;object-src 'self' blob: diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index 2f3f52ff30c..737f45314a5 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -136,12 +136,10 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit const response = await this.formPromise; this.setFormValues(); await this.loginService.saveEmailSettings(); - if (response.requiresEncryptionKeyMigration) { - this.handleMigrateEncryptionKeyError(); - return; - } if (this.handleCaptchaRequired(response)) { return; + } else if (this.handleMigrateEncryptionKey(response)) { + return; } else if (response.requiresTwoFactor) { if (this.onSuccessfulLoginTwoFactorNavigate != null) { this.onSuccessfulLoginTwoFactorNavigate(); @@ -273,12 +271,19 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit await this.loginService.saveEmailSettings(); } - protected handleMigrateEncryptionKeyError() { + // 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() { diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 0a06d393158..57db2618f79 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -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(); diff --git a/libs/angular/src/auth/guards/lock.guard.ts b/libs/angular/src/auth/guards/lock.guard.ts index 25f079257ea..264597d806a 100644 --- a/libs/angular/src/auth/guards/lock.guard.ts +++ b/libs/angular/src/auth/guards/lock.guard.ts @@ -11,6 +11,8 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; 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,12 +27,19 @@ 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, redirect to migration page + // If legacy user on web, redirect to migration page if (cryptoService.isLegacyUser()) { - return router.createUrlTree(["migrate-legacy-encryption"]); + if (platformUtilService.getClientType() === "web") { + return router.createUrlTree(["migrate-legacy-encryption"]); + } + // Log out legacy users on other clients + messagingService.send("logout"); + return false; } const authStatus = await authService.getAuthStatus(); From ade6610e3a193c0ace1bc2cd2dbf8fbe33d4dc75 Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Wed, 6 Sep 2023 15:23:39 -0400 Subject: [PATCH 03/13] [PM-3726] revert launch.json & webpack.config changes --- .vscode/launch.json | 2 +- apps/web/webpack.config.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 98ff43a4456..95b961d530e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,7 +19,7 @@ "request": "launch", "name": "Jest Current File", "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["--runTestsByPath", "${relativeFile}", "--config", "jest.config.js"], + "args": ["${fileBasenameNoExtension}", "--config", "jest.config.js"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "disableOptimisticBPs": true, diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index 1312c6cd7ed..bfdc8ae90ae 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -283,7 +283,6 @@ const devServer = https://api.fastmail.com https://api.forwardemail.net http://localhost:5000 - ws://localhost:61840 ;object-src 'self' blob: From 2d4c5cb581a05885646fd4b902837cdec6a08296 Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Wed, 6 Sep 2023 15:48:24 -0400 Subject: [PATCH 04/13] [PM-3726] remove update key component - also remove card in vault since legacy users can't login --- apps/browser/src/_locales/en/messages.json | 3 - apps/desktop/src/locales/en/messages.json | 3 - .../app/settings/change-email.component.ts | 6 - .../change-kdf-confirmation.component.ts | 5 - .../app/settings/update-key.component.html | 55 --------- .../src/app/settings/update-key.component.ts | 108 ------------------ .../src/app/shared/loose-components.module.ts | 3 - .../individual-vault/vault.component.html | 13 --- .../vault/individual-vault/vault.component.ts | 15 +-- apps/web/src/locales/en/messages.json | 6 - .../vault/components/attachments.component.ts | 24 ---- 11 files changed, 1 insertion(+), 240 deletions(-) delete mode 100644 apps/web/src/app/settings/update-key.component.html delete mode 100644 apps/web/src/app/settings/update-key.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 987006498d9..e537fd35331 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -771,9 +771,6 @@ "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." }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 01fa74e3905..03a4ca136e8 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -475,9 +475,6 @@ "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." }, diff --git a/apps/web/src/app/settings/change-email.component.ts b/apps/web/src/app/settings/change-email.component.ts index 76e78fcba9d..3321187af83 100644 --- a/apps/web/src/app/settings/change-email.component.ts +++ b/apps/web/src/app/settings/change-email.component.ts @@ -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(); diff --git a/apps/web/src/app/settings/change-kdf/change-kdf-confirmation.component.ts b/apps/web/src/app/settings/change-kdf/change-kdf-confirmation.component.ts index dd2a8c45d95..169b1bbdfa6 100644 --- a/apps/web/src/app/settings/change-kdf/change-kdf-confirmation.component.ts +++ b/apps/web/src/app/settings/change-kdf/change-kdf-confirmation.component.ts @@ -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(); diff --git a/apps/web/src/app/settings/update-key.component.html b/apps/web/src/app/settings/update-key.component.html deleted file mode 100644 index 7b94a6dca04..00000000000 --- a/apps/web/src/app/settings/update-key.component.html +++ /dev/null @@ -1,55 +0,0 @@ - diff --git a/apps/web/src/app/settings/update-key.component.ts b/apps/web/src/app/settings/update-key.component.ts deleted file mode 100644 index 8c7d7cf882f..00000000000 --- a/apps/web/src/app/settings/update-key.component.ts +++ /dev/null @@ -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; - - 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 { - 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; - } -} diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 62702349f95..c250af89957 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -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"; @@ -218,7 +217,6 @@ import { SharedModule } from "./shared.module"; TwoFactorVerifyComponent, TwoFactorWebAuthnComponent, TwoFactorYubiKeyComponent, - UpdateKeyComponent, UpdateLicenseComponent, UpdatePasswordComponent, UpdateTempPasswordComponent, @@ -322,7 +320,6 @@ import { SharedModule } from "./shared.module"; TwoFactorVerifyComponent, TwoFactorWebAuthnComponent, TwoFactorYubiKeyComponent, - UpdateKeyComponent, UpdateLicenseComponent, UpdatePasswordComponent, UpdateTempPasswordComponent, diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index dece0ea10fb..265bdec2cee 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -77,19 +77,6 @@
-
-
- - {{ "updateKeyTitle" | i18n }} -
-
-

{{ "updateEncryptionKeyShortDesc" | i18n }}

- -
-
- ; deletePromises: { [id: string]: Promise } = {}; @@ -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" - ); - } } } From cdbe93c56cb99ce16aaa18fb3b8336cd53351162 Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Wed, 6 Sep 2023 17:33:55 -0400 Subject: [PATCH 05/13] [PM-3726] Fix i18n & PR feedback --- .../migrate-legacy-encryption.component.html | 6 +++--- .../migrate-legacy-encryption.service.ts | 7 +++---- .../web/src/app/auth/settings/change-password.component.ts | 6 ------ apps/web/src/locales/en/messages.json | 2 +- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html index e6431a9543f..547913758b6 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html @@ -6,7 +6,7 @@

{{ "updateEncryptionKey" | class="tw-block tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8" >

- {{ "updateEncryptionSchemeDesc" | i18n }} {{ "updateEncryptionKeyDesc" | i18n }} + {{ "updateEncryptionSchemeDesc" | i18n }} {{ "updateEncryptionKey" | >{{ "learnMore" | i18n }}

- {{ "updateEncryptionSchemeWarning" | i18n }} + {{ "updateEncryptionKeyWarning" | i18n }} - {{ "masterPass" | i18n }} + {{ "masterPass" | i18n }} allowedStatuses.includes(d.status)); + ]); + const filteredAccesses = emergencyAccess.data.filter((d) => allowedStatuses.has(d.status)); for (const details of filteredAccesses) { // Get public key of grantee diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 00baee44d0a..9ed8227316c 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -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", diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 9c040425753..f7a72e61240 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3476,7 +3476,7 @@ "updateEncryptionSchemeDesc": { "message": "We've changed the encryption scheme to provide better security. Update your encryption key now by entering your master password below." }, - "updateEncryptionSchemeWarning": { + "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." }, "updateEncryptionKeyExportWarning": { From e1e41b047b3faf54cbc0a48f589c139c8915bbe1 Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Thu, 7 Sep 2023 10:40:18 -0400 Subject: [PATCH 06/13] [PM-3726] make standalone component --- .../migrate-legacy-encryption.component.html | 7 ++++--- .../migrate-legacy-encryption.component.ts | 5 ++++- .../migrate-legacy-encryption.module.ts | 13 ------------- apps/web/src/app/oss-routing.module.ts | 6 ++++-- apps/web/src/app/oss.module.ts | 3 --- libs/angular/src/auth/guards/lock.guard.ts | 3 ++- 6 files changed, 14 insertions(+), 23 deletions(-) delete mode 100644 apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.module.ts diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html index 547913758b6..2fdb4711cdd 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html @@ -14,10 +14,10 @@

{{ "updateEncryptionKey" | >{{ "learnMore" | i18n }}

- {{ "updateEncryptionKeyWarning" | i18n }} + {{ "updateEncryptionKeyWarning" | i18n }} - {{ "masterPass" | i18n }} + {{ "masterPass" | i18n }} {{ "updateEncryptionKey" | formControlName="masterPassword" appAutofocus /> + -

diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts index 878685e2ce8..73805f7a722 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts @@ -6,13 +6,16 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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({ - selector: "migrate-legacy-encryption", + standalone: true, + imports: [SharedModule], + providers: [MigrateFromLegacyEncryptionService], templateUrl: "migrate-legacy-encryption.component.html", }) export class MigrateFromLegacyEncryptionComponent { diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.module.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.module.ts deleted file mode 100644 index 5b3e47aae47..00000000000 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NgModule } from "@angular/core"; - -import { SharedModule } from "../../shared"; - -import { MigrateFromLegacyEncryptionComponent } from "./migrate-legacy-encryption.component"; -import { MigrateFromLegacyEncryptionService } from "./migrate-legacy-encryption.service"; - -@NgModule({ - imports: [SharedModule], - declarations: [MigrateFromLegacyEncryptionComponent], - providers: [MigrateFromLegacyEncryptionService], -}) -export class MigrateLegacyEncryptionModule {} diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index c7beb4d2b0a..3a08a5863a5 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -25,7 +25,6 @@ import { LockComponent } from "./auth/lock.component"; import { LoginDecryptionOptionsComponent } from "./auth/login/login-decryption-options/login-decryption-options.component"; import { LoginWithDeviceComponent } from "./auth/login/login-with-device.component"; import { LoginComponent } from "./auth/login/login.component"; -import { MigrateFromLegacyEncryptionComponent } from "./auth/migrate-encryption/migrate-legacy-encryption.component"; import { RecoverDeleteComponent } from "./auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component"; import { RemovePasswordComponent } from "./auth/remove-password.component"; @@ -178,7 +177,10 @@ const routes: Routes = [ }, { path: "migrate-legacy-encryption", - component: MigrateFromLegacyEncryptionComponent, + loadComponent: () => + import("./auth/migrate-encryption/migrate-legacy-encryption.component").then( + (mod) => mod.MigrateFromLegacyEncryptionComponent + ), }, ], }, diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index 690b77f7f64..1428aaea193 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -4,7 +4,6 @@ import { OrganizationCreateModule } from "./admin-console/organizations/create/o import { OrganizationManageModule } from "./admin-console/organizations/manage/organization-manage.module"; import { OrganizationUserModule } from "./admin-console/organizations/users/organization-user.module"; import { LoginModule } from "./auth/login/login.module"; -import { MigrateLegacyEncryptionModule } from "./auth/migrate-encryption/migrate-legacy-encryption.module"; import { TrialInitiationModule } from "./auth/trial-initiation/trial-initiation.module"; import { LooseComponentsModule, SharedModule } from "./shared"; import { OrganizationBadgeModule } from "./vault/individual-vault/organization-badge/organization-badge.module"; @@ -21,7 +20,6 @@ import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-f OrganizationUserModule, OrganizationCreateModule, LoginModule, - MigrateLegacyEncryptionModule, ], exports: [ SharedModule, @@ -30,7 +28,6 @@ import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-f VaultFilterModule, OrganizationBadgeModule, LoginModule, - MigrateLegacyEncryptionModule, ], bootstrap: [], }) diff --git a/libs/angular/src/auth/guards/lock.guard.ts b/libs/angular/src/auth/guards/lock.guard.ts index 264597d806a..76847cb0be6 100644 --- a/libs/angular/src/auth/guards/lock.guard.ts +++ b/libs/angular/src/auth/guards/lock.guard.ts @@ -10,6 +10,7 @@ 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"; @@ -34,7 +35,7 @@ export function lockGuard(): CanActivateFn { // If legacy user on web, redirect to migration page if (cryptoService.isLegacyUser()) { - if (platformUtilService.getClientType() === "web") { + if (platformUtilService.getClientType() === ClientType.Web) { return router.createUrlTree(["migrate-legacy-encryption"]); } // Log out legacy users on other clients From 4ad300fbc43f0bdaae3b3f3407025513990a580b Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Thu, 7 Sep 2023 12:00:48 -0400 Subject: [PATCH 07/13] [PM-3726] linter --- .../migrate-encryption/migrate-legacy-encryption.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts index 73805f7a722..d1bb74c066c 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts @@ -6,6 +6,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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"; From d2233e0dc85d3ca0bbb585bfd7baf6c9af88cbec Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Tue, 12 Sep 2023 08:19:28 -0400 Subject: [PATCH 08/13] [PM-3726] missing await --- libs/angular/src/auth/guards/lock.guard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/angular/src/auth/guards/lock.guard.ts b/libs/angular/src/auth/guards/lock.guard.ts index 76847cb0be6..551391b8c25 100644 --- a/libs/angular/src/auth/guards/lock.guard.ts +++ b/libs/angular/src/auth/guards/lock.guard.ts @@ -34,7 +34,7 @@ export function lockGuard(): CanActivateFn { const userVerificationService = inject(UserVerificationService); // If legacy user on web, redirect to migration page - if (cryptoService.isLegacyUser()) { + if (await cryptoService.isLegacyUser()) { if (platformUtilService.getClientType() === ClientType.Web) { return router.createUrlTree(["migrate-legacy-encryption"]); } From d74462438aa20875d29da0f33da178cd306aa3a4 Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Tue, 12 Sep 2023 15:57:01 -0400 Subject: [PATCH 09/13] [PM-3726] logout legacy users with vault timeout to never --- .../common/src/services/vault-timeout/vault-timeout.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 0fbbf51bd60..094f43d13ed 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -144,6 +144,9 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { const accounts = await firstValueFrom(this.stateService.accounts$); for (const userId in accounts) { if (userId != null) { + if (this.cryptoService.isLegacyUser(null, userId)) { + await this.logOut(userId); + } await this.cryptoService.migrateAutoKeyIfNeeded(userId); } } From 5ac27eb6f36af7ae7e9209d05726ce847a7bb120 Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Tue, 12 Sep 2023 15:59:05 -0400 Subject: [PATCH 10/13] [PM-3726] add await --- libs/common/src/services/vault-timeout/vault-timeout.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 094f43d13ed..f9561f41734 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -144,7 +144,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { const accounts = await firstValueFrom(this.stateService.accounts$); for (const userId in accounts) { if (userId != null) { - if (this.cryptoService.isLegacyUser(null, userId)) { + if (await this.cryptoService.isLegacyUser(null, userId)) { await this.logOut(userId); } await this.cryptoService.migrateAutoKeyIfNeeded(userId); From f38f85ad94a784cc6770566f138aaa399655f3c9 Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Tue, 12 Sep 2023 22:28:19 -0400 Subject: [PATCH 11/13] [PM-3726] skip auto key migration for legacy users --- .../src/platform/services/crypto.service.ts | 39 +++++++++++-------- .../vault-timeout/vault-timeout.service.ts | 3 +- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 4dafc80644d..19fdedb5603 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -950,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( diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index f9561f41734..78468a3ee6a 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -144,10 +144,11 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { 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); } - await this.cryptoService.migrateAutoKeyIfNeeded(userId); } } } From 42db3bb3b665afdea83186f420ce7eef183cc675 Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Sun, 17 Sep 2023 18:45:14 -0400 Subject: [PATCH 12/13] [PM-3726] pr feedback --- .../src/services/vault-timeout/vault-timeout.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 78468a3ee6a..8f807d9666c 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -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"; @@ -37,7 +38,9 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return; } // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3483) - await this.migrateKeyForNeverLockIfNeeded(); + if (this.platformUtilsService.getClientType() != ClientType.Web) { + await this.migrateKeyForNeverLockIfNeeded(); + } this.inited = true; if (checkOnInterval) { From 8a77b8047858b54691ee74e4af5b0a2f2790e86a Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Mon, 18 Sep 2023 08:28:10 -0400 Subject: [PATCH 13/13] [PM-3726] move check for web into migrate method --- .../src/services/vault-timeout/vault-timeout.service.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 8f807d9666c..9e5a78834f7 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -38,9 +38,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return; } // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3483) - if (this.platformUtilsService.getClientType() != ClientType.Web) { - await this.migrateKeyForNeverLockIfNeeded(); - } + await this.migrateKeyForNeverLockIfNeeded(); this.inited = true; if (checkOnInterval) { @@ -144,6 +142,10 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { } private async migrateKeyForNeverLockIfNeeded(): Promise { + // 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) {