From 8c06508435ebc92aa34f574edcefa984bb9a10f3 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 20 Sep 2023 15:57:01 -0400 Subject: [PATCH] [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> --- apps/browser/src/_locales/en/messages.json | 4 +- apps/cli/src/auth/commands/login.command.ts | 5 + apps/desktop/src/locales/en/messages.json | 4 +- .../web/src/app/auth/login/login.component.ts | 9 + .../migrate-legacy-encryption.component.html | 36 ++ .../migrate-legacy-encryption.component.ts | 82 +++++ .../migrate-legacy-encryption.service.spec.ts | 345 ++++++++++++++++++ .../migrate-legacy-encryption.service.ts | 215 +++++++++++ .../settings/change-password.component.ts | 6 - apps/web/src/app/auth/two-factor.component.ts | 9 + apps/web/src/app/oss-routing.module.ts | 7 + .../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 | 13 +- .../src/auth/components/login.component.ts | 17 + .../auth/components/two-factor.component.ts | 15 + libs/angular/src/auth/guards/lock.guard.ts | 15 + .../vault/components/attachments.component.ts | 24 -- .../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 | 48 ++- .../vault-timeout/vault-timeout.service.ts | 9 + .../src/vault/services/cipher.service.ts | 5 - 30 files changed, 834 insertions(+), 273 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.service.spec.ts create mode 100644 apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts 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 6e95df17b0..bf1eedd826 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -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" diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index c6e16ccecd..5cef79a09f 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 15606aa220..09d848c363 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -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" diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index 3bc65542b7..72d8e3766e 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"; @@ -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; + } } 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 0000000000..2fdb4711cd --- /dev/null +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html @@ -0,0 +1,36 @@ +
+
+
+

{{ "updateEncryptionKey" | i18n }}

+
+

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

+ {{ "updateEncryptionKeyWarning" | 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 0000000000..d1bb74c066 --- /dev/null +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts @@ -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; + } + }; +} 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 0000000000..88bbdc4e5b --- /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 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(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 0000000000..35005fdce4 --- /dev/null +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.service.ts @@ -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 { + // 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 { + 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/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 00baee44d0..9ed8227316 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/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index 2990331761..2dbb650874 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/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index e6f4caa112..3a08a5863a 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -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 + ), + }, ], }, { diff --git a/apps/web/src/app/settings/change-email.component.ts b/apps/web/src/app/settings/change-email.component.ts index 76e78fcba9..3321187af8 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 dd2a8c45d9..169b1bbdfa 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 7b94a6dca0..0000000000 --- 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 8c7d7cf882..0000000000 --- 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 4552162cc8..b9c220ab5e 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"; @@ -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, 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 dece0ea10f..265bdec2ce 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" - ); - } } } diff --git a/libs/common/src/auth/login-strategies/login.strategy.ts b/libs/common/src/auth/login-strategies/login.strategy.ts index 96855a3410..d8b0f5ca89 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; // 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; + // 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 7f7ec58569..0bcc679ae9 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 c0a6f034ae..6900cba1c4 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 42f60bde84..a868484bd0 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 1f5cd10edc..3b4090ef34 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]); @@ -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( 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 0fbbf51bd6..9e5a78834f 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"; @@ -141,10 +142,18 @@ 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) { 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); + } } } } diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 0bae9a607a..f03cb88e6e 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();