diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 6e95df17b01..bf1eedd8261 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 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 15606aa220a..09d848c3638 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 3bc65542b73..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"; @@ -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 00000000000..2fdb4711cdd --- /dev/null +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.html @@ -0,0 +1,36 @@ +
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..d1bb74c066c --- /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 00000000000..88bbdc4e5b2 --- /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{{ "updateEncryptionKeyShortDesc" | i18n }}
- -