diff --git a/.github/whitelist-capital-letters.txt b/.github/whitelist-capital-letters.txt index 17047f4333..a2ed46ce99 100644 --- a/.github/whitelist-capital-letters.txt +++ b/.github/whitelist-capital-letters.txt @@ -2,10 +2,7 @@ ./apps/browser/src/safari/desktop/Assets.xcassets/AccentColor.colorset ./apps/browser/src/safari/desktop/Assets.xcassets/AppIcon.appiconset ./apps/browser/src/safari/desktop/Base.lproj -./apps/browser/src/services/vaultTimeout ./apps/browser/store/windows/Assets -./libs/common/src/abstractions/vaultTimeout -./libs/common/src/services/vaultTimeout ./bitwarden_license/README.md ./libs/angular/src/directives/cipherListVirtualScroll.directive.ts ./libs/angular/src/scss/webfonts/Open_Sans-italic-700.woff @@ -25,11 +22,7 @@ ./libs/common/src/misc/linkedFieldOption.decorator.ts ./libs/common/src/misc/serviceUtils.ts ./libs/common/src/misc/serviceUtils.spec.ts -./libs/common/src/abstractions/vaultTimeout/vaultTimeoutSettings.service.ts -./libs/common/src/abstractions/vaultTimeout/vaultTimeout.service.ts ./libs/common/src/abstractions/anonymousHub.service.ts -./libs/common/src/services/vaultTimeout/vaultTimeoutSettings.service.ts -./libs/common/src/services/vaultTimeout/vaultTimeout.service.ts ./libs/common/src/services/anonymousHub.service.ts ./libs/auth/README.md ./README.md @@ -78,5 +71,4 @@ ./apps/browser/src/safari/safari/SafariWebExtensionHandler.swift ./apps/browser/src/safari/safari/Info.plist ./apps/browser/src/safari/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist -./apps/browser/src/services/vaultTimeout/vaultTimeout.service.ts ./SECURITY.md diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 400e75dd1b..6de09d355a 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -338,6 +338,9 @@ "other": { "message": "Other" }, + "unlockMethodNeededToChangeTimeoutActionDesc": { + "message": "Set up an unlock method to change your vault timeout action." + }, "rateExtension": { "message": "Rate the extension" }, @@ -1602,6 +1605,12 @@ "biometricsNotSupportedDesc": { "message": "Browser biometrics is not supported on this device." }, + "biometricsFailedTitle": { + "message": "Biometrics failed" + }, + "biometricsFailedDesc": { + "message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support." + }, "nativeMessaginPermissionErrorTitle": { "message": "Permission not provided" }, @@ -2143,8 +2152,8 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, - "logInInitiated": { - "message": "Log in initiated" + "loginInitiated": { + "message": "Login initiated" }, "exposedMasterPassword": { "message": "Exposed Master Password" @@ -2230,6 +2239,31 @@ "opensInANewWindow": { "message": "Opens in a new window" }, + "deviceApprovalRequired": { + "message": "Device approval required. Select an approval option below:" + }, + "rememberThisDevice": { + "message": "Remember this device" + }, + "uncheckIfPublicDevice": { + "message": "Uncheck if using a public device" + }, + "approveFromYourOtherDevice": { + "message": "Approve from your other device" + }, + "requestAdminApproval": { + "message": "Request admin approval" + }, + "approveWithMasterPassword": { + "message": "Approve with master password" + }, + "ssoIdentifierRequired": { + "message": "Organization SSO identifier is required." + }, + "eu": { + "message": "EU", + "description": "European Union" + }, "usDomain": { "message": "bitwarden.com" }, @@ -2244,5 +2278,29 @@ }, "display": { "message": "Display" + }, + "accountSuccessfullyCreated": { + "message": "Account successfully created!" + }, + "adminApprovalRequested": { + "message": "Admin approval requested" + }, + "adminApprovalRequestSentToAdmins": { + "message": "Your request has been sent to your admin." + }, + "youWillBeNotifiedOnceApproved": { + "message": "You will be notified once approved." + }, + "troubleLoggingIn": { + "message": "Trouble logging in?" + }, + "loginApproved": { + "message": "Login approved" + }, + "userEmailMissing": { + "message": "User email missing" + }, + "deviceTrusted": { + "message": "Device trusted" } } diff --git a/apps/browser/src/auth/background/service-factories/auth-request-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/auth-request-crypto-service.factory.ts new file mode 100644 index 0000000000..e1757f9812 --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/auth-request-crypto-service.factory.ts @@ -0,0 +1,29 @@ +import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction"; +import { AuthRequestCryptoServiceImplementation } from "@bitwarden/common/auth/services/auth-request-crypto.service.implementation"; + +import { + CryptoServiceInitOptions, + cryptoServiceFactory, +} from "../../../platform/background/service-factories/crypto-service.factory"; +import { + CachedServices, + FactoryOptions, + factory, +} from "../../../platform/background/service-factories/factory-options"; + +type AuthRequestCryptoServiceFactoryOptions = FactoryOptions; + +export type AuthRequestCryptoServiceInitOptions = AuthRequestCryptoServiceFactoryOptions & + CryptoServiceInitOptions; + +export function authRequestCryptoServiceFactory( + cache: { authRequestCryptoService?: AuthRequestCryptoServiceAbstraction } & CachedServices, + opts: AuthRequestCryptoServiceInitOptions +): Promise { + return factory( + cache, + "authRequestCryptoService", + opts, + async () => new AuthRequestCryptoServiceImplementation(await cryptoServiceFactory(cache, opts)) + ); +} diff --git a/apps/browser/src/auth/background/service-factories/auth-service.factory.ts b/apps/browser/src/auth/background/service-factories/auth-service.factory.ts index 5612cedb91..6aaeb47636 100644 --- a/apps/browser/src/auth/background/service-factories/auth-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/auth-service.factory.ts @@ -52,6 +52,14 @@ import { PasswordStrengthServiceInitOptions, } from "../../../tools/background/service_factories/password-strength-service.factory"; +import { + authRequestCryptoServiceFactory, + AuthRequestCryptoServiceInitOptions, +} from "./auth-request-crypto-service.factory"; +import { + deviceTrustCryptoServiceFactory, + DeviceTrustCryptoServiceInitOptions, +} from "./device-trust-crypto-service.factory"; import { keyConnectorServiceFactory, KeyConnectorServiceInitOptions, @@ -75,7 +83,9 @@ export type AuthServiceInitOptions = AuthServiceFactoyOptions & I18nServiceInitOptions & EncryptServiceInitOptions & PolicyServiceInitOptions & - PasswordStrengthServiceInitOptions; + PasswordStrengthServiceInitOptions & + DeviceTrustCryptoServiceInitOptions & + AuthRequestCryptoServiceInitOptions; export function authServiceFactory( cache: { authService?: AbstractAuthService } & CachedServices, @@ -101,7 +111,9 @@ export function authServiceFactory( await i18nServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), await passwordStrengthServiceFactory(cache, opts), - await policyServiceFactory(cache, opts) + await policyServiceFactory(cache, opts), + await deviceTrustCryptoServiceFactory(cache, opts), + await authRequestCryptoServiceFactory(cache, opts) ) ); } diff --git a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts new file mode 100644 index 0000000000..430d50fea7 --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts @@ -0,0 +1,74 @@ +import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; + +import { + DevicesApiServiceInitOptions, + devicesApiServiceFactory, +} from "../../../background/service-factories/devices-api-service.factory"; +import { + AppIdServiceInitOptions, + appIdServiceFactory, +} from "../../../platform/background/service-factories/app-id-service.factory"; +import { + CryptoFunctionServiceInitOptions, + cryptoFunctionServiceFactory, +} from "../../../platform/background/service-factories/crypto-function-service.factory"; +import { + CryptoServiceInitOptions, + cryptoServiceFactory, +} from "../../../platform/background/service-factories/crypto-service.factory"; +import { + EncryptServiceInitOptions, + encryptServiceFactory, +} from "../../../platform/background/service-factories/encrypt-service.factory"; +import { + CachedServices, + FactoryOptions, + factory, +} from "../../../platform/background/service-factories/factory-options"; +import { + I18nServiceInitOptions, + i18nServiceFactory, +} from "../../../platform/background/service-factories/i18n-service.factory"; +import { + PlatformUtilsServiceInitOptions, + platformUtilsServiceFactory, +} from "../../../platform/background/service-factories/platform-utils-service.factory"; +import { + StateServiceInitOptions, + stateServiceFactory, +} from "../../../platform/background/service-factories/state-service.factory"; + +type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions; + +export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions & + CryptoFunctionServiceInitOptions & + CryptoServiceInitOptions & + EncryptServiceInitOptions & + StateServiceInitOptions & + AppIdServiceInitOptions & + DevicesApiServiceInitOptions & + I18nServiceInitOptions & + PlatformUtilsServiceInitOptions; + +export function deviceTrustCryptoServiceFactory( + cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices, + opts: DeviceTrustCryptoServiceInitOptions +): Promise { + return factory( + cache, + "deviceTrustCryptoService", + opts, + async () => + new DeviceTrustCryptoService( + await cryptoFunctionServiceFactory(cache, opts), + await cryptoServiceFactory(cache, opts), + await encryptServiceFactory(cache, opts), + await stateServiceFactory(cache, opts), + await appIdServiceFactory(cache, opts), + await devicesApiServiceFactory(cache, opts), + await i18nServiceFactory(cache, opts), + await platformUtilsServiceFactory(cache, opts) + ) + ); +} diff --git a/apps/browser/src/auth/background/service-factories/user-verification-api-service.factory.ts b/apps/browser/src/auth/background/service-factories/user-verification-api-service.factory.ts new file mode 100644 index 0000000000..01bfb0f13c --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/user-verification-api-service.factory.ts @@ -0,0 +1,29 @@ +import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction"; +import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; + +import { + ApiServiceInitOptions, + apiServiceFactory, +} from "../../../platform/background/service-factories/api-service.factory"; +import { + FactoryOptions, + CachedServices, + factory, +} from "../../../platform/background/service-factories/factory-options"; + +type UserVerificationApiServiceFactoryOptions = FactoryOptions; + +export type UserVerificationApiServiceInitOptions = UserVerificationApiServiceFactoryOptions & + ApiServiceInitOptions; + +export function userVerificationApiServiceFactory( + cache: { userVerificationApiService?: UserVerificationApiServiceAbstraction } & CachedServices, + opts: UserVerificationApiServiceInitOptions +): Promise { + return factory( + cache, + "userVerificationApiService", + opts, + async () => new UserVerificationApiService(await apiServiceFactory(cache, opts)) + ); +} diff --git a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts new file mode 100644 index 0000000000..79d327c948 --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts @@ -0,0 +1,51 @@ +import { UserVerificationService as AbstractUserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; + +import { + CryptoServiceInitOptions, + cryptoServiceFactory, +} from "../../../platform/background/service-factories/crypto-service.factory"; +import { + FactoryOptions, + CachedServices, + factory, +} from "../../../platform/background/service-factories/factory-options"; +import { + I18nServiceInitOptions, + i18nServiceFactory, +} from "../../../platform/background/service-factories/i18n-service.factory"; +import { + StateServiceInitOptions, + stateServiceFactory, +} from "../../../platform/background/service-factories/state-service.factory"; + +import { + UserVerificationApiServiceInitOptions, + userVerificationApiServiceFactory, +} from "./user-verification-api-service.factory"; + +type UserVerificationServiceFactoryOptions = FactoryOptions; + +export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryOptions & + StateServiceInitOptions & + CryptoServiceInitOptions & + I18nServiceInitOptions & + UserVerificationApiServiceInitOptions; + +export function userVerificationServiceFactory( + cache: { userVerificationService?: AbstractUserVerificationService } & CachedServices, + opts: UserVerificationServiceInitOptions +): Promise { + return factory( + cache, + "userVerificationService", + opts, + async () => + new UserVerificationService( + await stateServiceFactory(cache, opts), + await cryptoServiceFactory(cache, opts), + await i18nServiceFactory(cache, opts), + await userVerificationApiServiceFactory(cache, opts) + ) + ); +} diff --git a/apps/browser/src/auth/popup/lock.component.html b/apps/browser/src/auth/popup/lock.component.html index cf964c1564..e787e0106d 100644 --- a/apps/browser/src/auth/popup/lock.component.html +++ b/apps/browser/src/auth/popup/lock.component.html @@ -5,14 +5,20 @@ {{ "verifyIdentity" | i18n }}
- +
-
-
+
+
-
+
{ - document.getElementById(this.pinLock ? "pin" : "masterPassword").focus(); + document.getElementById(this.pinEnabled ? "pin" : "masterPassword")?.focus(); if ( this.biometricLock && !disableAutoBiometricsPrompt && @@ -93,7 +96,7 @@ export class LockComponent extends BaseLockComponent { }, 100); } - async unlockBiometric(): Promise { + override async unlockBiometric(): Promise { if (!this.biometricLock) { return; } diff --git a/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html b/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html new file mode 100644 index 0000000000..32e3ea0c59 --- /dev/null +++ b/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html @@ -0,0 +1,108 @@ +
+
+

+ {{ "loginInitiated" | i18n }} +

+
+ + +
diff --git a/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.ts b/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.ts new file mode 100644 index 0000000000..7fac9a42b0 --- /dev/null +++ b/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.ts @@ -0,0 +1,18 @@ +import { Component } from "@angular/core"; + +import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component"; + +@Component({ + selector: "browser-login-decryption-options", + templateUrl: "login-decryption-options.component.html", +}) +export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent { + override async createUser(): Promise { + try { + await super.createUser(); + await this.router.navigate(["/tabs/vault"]); + } catch (error) { + this.validationService.showError(error); + } + } +} diff --git a/apps/browser/src/auth/popup/login-with-device.component.html b/apps/browser/src/auth/popup/login-with-device.component.html index d794b7d212..127f7ec96f 100644 --- a/apps/browser/src/auth/popup/login-with-device.component.html +++ b/apps/browser/src/auth/popup/login-with-device.component.html @@ -5,32 +5,57 @@
diff --git a/apps/browser/src/auth/popup/login-with-device.component.ts b/apps/browser/src/auth/popup/login-with-device.component.ts index cf0e57b5ee..f3a1dfffaa 100644 --- a/apps/browser/src/auth/popup/login-with-device.component.ts +++ b/apps/browser/src/auth/popup/login-with-device.component.ts @@ -1,10 +1,13 @@ +import { Location } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-with-device.component"; import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -42,7 +45,10 @@ export class LoginWithDeviceComponent validationService: ValidationService, stateService: StateService, loginService: LoginService, - syncService: SyncService + syncService: SyncService, + deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + authReqCryptoService: AuthRequestCryptoServiceAbstraction, + private location: Location ) { super( router, @@ -59,10 +65,16 @@ export class LoginWithDeviceComponent anonymousHubService, validationService, stateService, - loginService + loginService, + deviceTrustCryptoService, + authReqCryptoService ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); }; } + + protected back() { + this.location.back(); + } } diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index 776b792fa1..d9b789e4d8 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -4,8 +4,8 @@ import { ActivatedRoute, Router } from "@angular/router"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; -import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction"; 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 { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; diff --git a/apps/browser/src/auth/popup/services/index.ts b/apps/browser/src/auth/popup/services/index.ts index 06bfe0009b..63563f61fd 100644 --- a/apps/browser/src/auth/popup/services/index.ts +++ b/apps/browser/src/auth/popup/services/index.ts @@ -1,2 +1 @@ -export { LockGuardService } from "./lock-guard.service"; export { UnauthGuardService } from "./unauth-guard.service"; diff --git a/apps/browser/src/auth/popup/services/lock-guard.service.ts b/apps/browser/src/auth/popup/services/lock-guard.service.ts deleted file mode 100644 index ef6ebc73ac..0000000000 --- a/apps/browser/src/auth/popup/services/lock-guard.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { LockGuard as BaseLockGuardService } from "@bitwarden/angular/auth/guards/lock.guard"; - -@Injectable() -export class LockGuardService extends BaseLockGuardService { - protected homepage = "tabs/current"; -} diff --git a/apps/browser/src/auth/popup/services/unauth-guard.service.ts b/apps/browser/src/auth/popup/services/unauth-guard.service.ts index 4aa700b556..062239a7d3 100644 --- a/apps/browser/src/auth/popup/services/unauth-guard.service.ts +++ b/apps/browser/src/auth/popup/services/unauth-guard.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; -import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards/unauth.guard"; +import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; @Injectable() export class UnauthGuardService extends BaseUnauthGuardService { diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 2214e91687..9d04701680 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -1,11 +1,12 @@ -import { Component } from "@angular/core"; +import { Component, Inject } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component"; +import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -35,7 +36,8 @@ export class SsoComponent extends BaseSsoComponent { syncService: SyncService, environmentService: EnvironmentService, logService: LogService, - private vaultTimeoutService: VaultTimeoutService + configService: ConfigServiceAbstraction, + @Inject(WINDOW) private win: Window ) { super( authService, @@ -48,7 +50,8 @@ export class SsoComponent extends BaseSsoComponent { cryptoFunctionService, environmentService, passwordGenerationService, - logService + logService, + configService ); const url = this.environmentService.getWebVaultUrl(); @@ -57,15 +60,22 @@ export class SsoComponent extends BaseSsoComponent { this.clientId = "browser"; super.onSuccessfulLogin = async () => { - await syncService.fullSync(true); + syncService.fullSync(true); // If the vault is unlocked then this will clear keys from memory, which we don't want to do if ((await this.authService.getAuthStatus()) !== AuthenticationStatus.Unlocked) { BrowserApi.reloadOpenWindows(); } - const thisWindow = window.open("", "_self"); - thisWindow.close(); + this.win.close(); + }; + + super.onSuccessfulLoginTde = async () => { + syncService.fullSync(true); + }; + + super.onSuccessfulLoginTdeNavigate = async () => { + this.win.close(); }; } } diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index 03821bb22f..c0af31d25e 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -1,8 +1,9 @@ -import { Component } from "@angular/core"; +import { Component, Inject } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; +import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; @@ -10,6 +11,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -48,7 +50,9 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { twoFactorService: TwoFactorService, appIdService: AppIdService, loginService: LoginService, - private dialogService: DialogService + configService: ConfigServiceAbstraction, + private dialogService: DialogService, + @Inject(WINDOW) protected win: Window ) { super( authService, @@ -56,19 +60,28 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { i18nService, apiService, platformUtilsService, - window, + win, environmentService, stateService, route, logService, twoFactorService, appIdService, - loginService + loginService, + configService ); - super.onSuccessfulLogin = () => { - this.loginService.clearValues(); - return syncService.fullSync(true); + super.onSuccessfulLogin = async () => { + syncService.fullSync(true); }; + + super.onSuccessfulLoginTde = async () => { + syncService.fullSync(true); + }; + + super.onSuccessfulLoginTdeNavigate = async () => { + this.win.close(); + }; + super.successRoute = "/tabs/vault"; // FIXME: Chromium 110 has broken WebAuthn support in extensions via an iframe this.webAuthnNewTab = true; @@ -117,11 +130,11 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.queryParams.pipe(first()).subscribe(async (qParams) => { if (qParams.sso === "true") { - super.onSuccessfulLogin = () => { + super.onSuccessfulLogin = async () => { // This is not awaited so we don't pause the application while the sync is happening. // This call is executed by the service that lives in the background script so it will continue // the sync even if this tab closes. - const syncPromise = this.syncService.fullSync(true); + this.syncService.fullSync(true); // Force sidebars (FF && Opera) to reload while exempting current window // because we are just going to close the current window. @@ -130,8 +143,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { // We don't need this window anymore because the intent is for the user to be left // on the web vault screen which tells them to continue in the browser extension (sidebar or popup) BrowserApi.closeBitwardenExtensionTab(); - - return syncPromise; }; } }); diff --git a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts index a802fd8cf1..efa5bdcffb 100644 --- a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts +++ b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts @@ -2,6 +2,10 @@ import { TotpServiceInitOptions, totpServiceFactory, } from "../../../auth/background/service-factories/totp-service.factory"; +import { + UserVerificationServiceInitOptions, + userVerificationServiceFactory, +} from "../../../auth/background/service-factories/user-verification-service.factory"; import { EventCollectionServiceInitOptions, eventCollectionServiceFactory, @@ -38,7 +42,8 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions & TotpServiceInitOptions & EventCollectionServiceInitOptions & LogServiceInitOptions & - SettingsServiceInitOptions; + SettingsServiceInitOptions & + UserVerificationServiceInitOptions; export function autofillServiceFactory( cache: { autofillService?: AbstractAutoFillService } & CachedServices, @@ -55,7 +60,8 @@ export function autofillServiceFactory( await totpServiceFactory(cache, opts), await eventCollectionServiceFactory(cache, opts), await logServiceFactory(cache, opts), - await settingsServiceFactory(cache, opts) + await settingsServiceFactory(cache, opts), + await userVerificationServiceFactory(cache, opts) ) ); } diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts index 9db6574aad..dbe391ce4a 100644 --- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts @@ -1,6 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; @@ -13,6 +14,7 @@ describe("CipherContextMenuHandler", () => { let mainContextMenuHandler: MockProxy; let authService: MockProxy; let cipherService: MockProxy; + let userVerificationService: MockProxy; let sut: CipherContextMenuHandler; @@ -20,10 +22,17 @@ describe("CipherContextMenuHandler", () => { mainContextMenuHandler = mock(); authService = mock(); cipherService = mock(); + userVerificationService = mock(); + userVerificationService.hasMasterPassword.mockResolvedValue(true); jest.spyOn(MainContextMenuHandler, "removeAll").mockResolvedValue(); - sut = new CipherContextMenuHandler(mainContextMenuHandler, authService, cipherService); + sut = new CipherContextMenuHandler( + mainContextMenuHandler, + authService, + cipherService, + userVerificationService + ); }); afterEach(() => jest.resetAllMocks()); @@ -83,11 +92,11 @@ describe("CipherContextMenuHandler", () => { }; cipherService.getAllDecryptedForUrl.mockResolvedValue([ - null, - undefined, - { type: CipherType.Card }, - { type: CipherType.Login, reprompt: CipherRepromptType.Password }, - realCipher, + null, // invalid cipher + undefined, // invalid cipher + { type: CipherType.Card }, // invalid cipher + { type: CipherType.Login, reprompt: CipherRepromptType.Password }, // invalid cipher + realCipher, // valid cipher ] as any[]); await sut.update("https://test.com"); @@ -105,5 +114,61 @@ describe("CipherContextMenuHandler", () => { realCipher ); }); + + it("adds ciphers with master password reprompt if the user does not have a master password", async () => { + authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked); + + // User does not have a master password, or has one but hasn't logged in with it (key connector user or TDE user) + userVerificationService.hasMasterPasswordAndMasterKeyHash.mockResolvedValue(false); + + mainContextMenuHandler.init.mockResolvedValue(true); + + const realCipher = { + id: "5", + type: CipherType.Login, + reprompt: CipherRepromptType.None, + name: "Test Cipher", + login: { username: "Test Username" }, + }; + + const repromptCipher = { + id: "6", + type: CipherType.Login, + reprompt: CipherRepromptType.Password, + name: "Test Reprompt Cipher", + login: { username: "Test Username" }, + }; + + cipherService.getAllDecryptedForUrl.mockResolvedValue([ + null, // invalid cipher + undefined, // invalid cipher + { type: CipherType.Card }, // invalid cipher + repromptCipher, // valid cipher + realCipher, // valid cipher + ] as any[]); + + await sut.update("https://test.com"); + + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1); + + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com"); + + // Should call this twice, once for each valid cipher + expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2); + + expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith( + "Test Cipher (Test Username)", + "5", + "https://test.com", + realCipher + ); + + expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith( + "Test Reprompt Cipher (Test Username)", + "6", + "https://test.com", + repromptCipher + ); + }); }); }); diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts index fe6479aae5..1d1be8f838 100644 --- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts @@ -1,4 +1,5 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -11,6 +12,7 @@ import { authServiceFactory, AuthServiceInitOptions, } from "../../auth/background/service-factories/auth-service.factory"; +import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory"; import { Account } from "../../models/account"; import { CachedServices } from "../../platform/background/service-factories/factory-options"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -37,7 +39,8 @@ export class CipherContextMenuHandler { constructor( private mainContextMenuHandler: MainContextMenuHandler, private authService: AuthService, - private cipherService: CipherService + private cipherService: CipherService, + private userVerificationService: UserVerificationService ) {} static async create(cachedServices: CachedServices) { @@ -76,7 +79,8 @@ export class CipherContextMenuHandler { return new CipherContextMenuHandler( await MainContextMenuHandler.mv3Create(cachedServices), await authServiceFactory(cachedServices, serviceOptions), - await cipherServiceFactory(cachedServices, serviceOptions) + await cipherServiceFactory(cachedServices, serviceOptions), + await userVerificationServiceFactory(cachedServices, serviceOptions) ); } @@ -176,7 +180,11 @@ export class CipherContextMenuHandler { } private async updateForCipher(url: string, cipher: CipherView) { - if (cipher == null || cipher.type !== CipherType.Login) { + if ( + cipher == null || + cipher.type !== CipherType.Login || + (await this.userVerificationService.hasMasterPasswordAndMasterKeyHash()) + ) { return; } diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 8b7ec91653..571b4af721 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -1,6 +1,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { TotpService } from "@bitwarden/common/abstractions/totp.service"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { EventType, FieldType, UriMatchType } from "@bitwarden/common/enums"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -45,7 +46,8 @@ export default class AutofillService implements AutofillServiceInterface { private totpService: TotpService, private eventCollectionService: EventCollectionService, private logService: LogService, - private settingsService: SettingsService + private settingsService: SettingsService, + private userVerificationService: UserVerificationService ) {} getFormsWithPasswordFields(pageDetails: AutofillPageDetails): FormData[] { @@ -238,7 +240,10 @@ export default class AutofillService implements AutofillServiceInterface { return null; } - if (cipher.reprompt !== CipherRepromptType.None) { + if ( + cipher.reprompt !== CipherRepromptType.None && + (await this.userVerificationService.hasMasterPasswordAndMasterKeyHash()) + ) { await BrowserApi.tabSendMessageData(tab, "passwordReprompt", { cipherId: cipher.id, action: "autofill", diff --git a/apps/browser/src/background/commands.background.ts b/apps/browser/src/background/commands.background.ts index 118953b9da..0cbf91c666 100644 --- a/apps/browser/src/background/commands.background.ts +++ b/apps/browser/src/background/commands.background.ts @@ -1,4 +1,4 @@ -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; +import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index 3854d2383b..7200301c79 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -1,5 +1,5 @@ import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; +import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 59a1b444e0..617acc2bf7 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,27 +1,33 @@ import { AvatarUpdateService as AvatarUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; +import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service"; -import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; -import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service"; +import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; +import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { AuthRequestCryptoServiceImplementation } from "@bitwarden/common/auth/services/auth-request-crypto.service.implementation"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; +import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; +import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; @@ -59,12 +65,13 @@ import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/we import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service"; import { ApiService } from "@bitwarden/common/services/api.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; +import { DevicesServiceImplementation } from "@bitwarden/common/services/devices/devices.service.implementation"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { SearchService } from "@bitwarden/common/services/search.service"; import { TotpService } from "@bitwarden/common/services/totp.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vaultTimeout/vaultTimeoutSettings.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { PasswordGenerationService, PasswordGenerationServiceAbstraction, @@ -128,7 +135,7 @@ import { KeyGenerationService } from "../platform/services/key-generation.servic import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { BrowserSendService } from "../services/browser-send.service"; import { BrowserSettingsService } from "../services/browser-settings.service"; -import VaultTimeoutService from "../services/vaultTimeout/vaultTimeout.service"; +import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import { BrowserFolderService } from "../vault/services/browser-folder.service"; import { VaultFilterService } from "../vault/services/vault-filter.service"; @@ -156,7 +163,7 @@ export default class MainBackground { cipherService: CipherServiceAbstraction; folderService: InternalFolderServiceAbstraction; collectionService: CollectionServiceAbstraction; - vaultTimeoutService: VaultTimeoutServiceAbstraction; + vaultTimeoutService: VaultTimeoutService; vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction; syncService: SyncServiceAbstraction; passwordGenerationService: PasswordGenerationServiceAbstraction; @@ -196,6 +203,10 @@ export default class MainBackground { cipherContextMenuHandler: CipherContextMenuHandler; configService: ConfigServiceAbstraction; configApiService: ConfigApiServiceAbstraction; + devicesApiService: DevicesApiServiceAbstraction; + devicesService: DevicesServiceAbstraction; + deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; + authRequestCryptoService: AuthRequestCryptoServiceAbstraction; browserPopoutWindowService: BrowserPopoutWindowService; // Passed to the popup for Safari to workaround issues with theming, downloading, etc. @@ -387,6 +398,23 @@ export default class MainBackground { that.runtimeBackground.processMessage(message, that as any, null); }; })(); + + this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); + this.deviceTrustCryptoService = new DeviceTrustCryptoService( + this.cryptoFunctionService, + this.cryptoService, + this.encryptService, + this.stateService, + this.appIdService, + this.devicesApiService, + this.i18nService, + this.platformUtilsService + ); + + this.devicesService = new DevicesServiceImplementation(this.devicesApiService); + + this.authRequestCryptoService = new AuthRequestCryptoServiceImplementation(this.cryptoService); + this.authService = new AuthService( this.cryptoService, this.apiService, @@ -402,14 +430,26 @@ export default class MainBackground { this.i18nService, this.encryptService, this.passwordStrengthService, - this.policyService + this.policyService, + this.deviceTrustCryptoService, + this.authRequestCryptoService + ); + + this.userVerificationApiService = new UserVerificationApiService(this.apiService); + + this.userVerificationService = new UserVerificationService( + this.stateService, + this.cryptoService, + this.i18nService, + this.userVerificationApiService ); this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( this.cryptoService, this.tokenService, this.policyService, - this.stateService + this.stateService, + this.userVerificationService ); this.vaultTimeoutService = new VaultTimeoutService( @@ -420,7 +460,6 @@ export default class MainBackground { this.platformUtilsService, this.messagingService, this.searchService, - this.keyConnectorService, this.stateService, this.authService, this.vaultTimeoutSettingsService, @@ -471,13 +510,15 @@ export default class MainBackground { this.eventUploadService ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); + this.autofillService = new AutofillService( this.cipherService, this.stateService, this.totpService, this.eventCollectionService, this.logService, - this.settingsService + this.settingsService, + this.userVerificationService ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); this.exportService = new VaultExportService( @@ -499,15 +540,6 @@ export default class MainBackground { this.authService, this.messagingService ); - - this.userVerificationApiService = new UserVerificationApiService(this.apiService); - - this.userVerificationService = new UserVerificationService( - this.cryptoService, - this.i18nService, - this.userVerificationApiService - ); - this.configService = new ConfigService( this.stateService, this.configApiService, @@ -638,7 +670,8 @@ export default class MainBackground { this.cipherContextMenuHandler = new CipherContextMenuHandler( this.mainContextMenuHandler, this.authService, - this.cipherService + this.cipherService, + this.userVerificationService ); } } @@ -648,7 +681,7 @@ export default class MainBackground { await this.stateService.init(); - await (this.vaultTimeoutService as VaultTimeoutService).init(true); + await this.vaultTimeoutService.init(true); await (this.i18nService as BrowserI18nService).init(); await (this.eventUploadService as EventUploadService).init(true); await this.runtimeBackground.init(); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index e482c7cc83..d393022b7b 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -10,7 +10,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { + MasterKey, + SymmetricCryptoKey, + UserKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { BrowserApi } from "../platform/browser/browser-api"; @@ -42,6 +46,7 @@ type ReceiveMessage = { // Unlock key keyB64?: string; + userKeyB64?: string; }; type ReceiveMessageOuter = { @@ -320,16 +325,55 @@ export class NativeMessagingBackground { } if (message.response === "unlocked") { - await this.cryptoService.setKey( - new SymmetricCryptoKey(Utils.fromB64ToArray(message.keyB64)) - ); + try { + if (message.userKeyB64) { + const userKey = new SymmetricCryptoKey( + Utils.fromB64ToArray(message.userKeyB64) + ) as UserKey; + await this.cryptoService.setUserKey(userKey); + } else if (message.keyB64) { + // Backwards compatibility to support cases in which the user hasn't updated their desktop app + // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) + let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); + encUserKey ||= await this.stateService.getMasterKeyEncryptedUserKey(); + if (!encUserKey) { + throw new Error("No encrypted user key found"); + } + const masterKey = new SymmetricCryptoKey( + Utils.fromB64ToArray(message.keyB64) + ) as MasterKey; + const userKey = await this.cryptoService.decryptUserKeyWithMasterKey( + masterKey, + new EncString(encUserKey) + ); + await this.cryptoService.setMasterKey(masterKey); + await this.cryptoService.setUserKey(userKey); + } else { + throw new Error("No key received"); + } + } catch (e) { + this.logService.error("Unable to set key: " + e); + this.messagingService.send("showDialog", { + title: { key: "biometricsFailedTitle" }, + content: { key: "biometricsFailedDesc" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "danger", + }); + + // Exit early + if (this.resolver) { + this.resolver(message); + } + return; + } // Verify key is correct by attempting to decrypt a secret try { await this.cryptoService.getFingerprint(await this.stateService.getUserId()); } catch (e) { this.logService.error("Unable to verify key: " + e); - await this.cryptoService.clearKey(); + await this.cryptoService.clearKeys(); this.showWrongUserDialog(); // Exit early diff --git a/apps/browser/src/background/service-factories/devices-api-service.factory.ts b/apps/browser/src/background/service-factories/devices-api-service.factory.ts new file mode 100644 index 0000000000..8999b7c2c7 --- /dev/null +++ b/apps/browser/src/background/service-factories/devices-api-service.factory.ts @@ -0,0 +1,28 @@ +import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; + +import { + ApiServiceInitOptions, + apiServiceFactory, +} from "../../platform/background/service-factories/api-service.factory"; +import { + FactoryOptions, + CachedServices, + factory, +} from "../../platform/background/service-factories/factory-options"; + +type DevicesApiServiceFactoryOptions = FactoryOptions; + +export type DevicesApiServiceInitOptions = DevicesApiServiceFactoryOptions & ApiServiceInitOptions; + +export function devicesApiServiceFactory( + cache: { devicesApiService?: DevicesApiServiceAbstraction } & CachedServices, + opts: DevicesApiServiceInitOptions +): Promise { + return factory( + cache, + "devicesApiService", + opts, + async () => new DevicesApiServiceImplementation(await apiServiceFactory(cache, opts)) + ); +} diff --git a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts index 601867ad38..b019db3297 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts @@ -1,13 +1,9 @@ -import { VaultTimeoutService as AbstractVaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; +import { VaultTimeoutService as AbstractVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { authServiceFactory, AuthServiceInitOptions, } from "../../auth/background/service-factories/auth-service.factory"; -import { - keyConnectorServiceFactory, - KeyConnectorServiceInitOptions, -} from "../../auth/background/service-factories/key-connector-service.factory"; import { CryptoServiceInitOptions, cryptoServiceFactory, @@ -29,7 +25,7 @@ import { StateServiceInitOptions, stateServiceFactory, } from "../../platform/background/service-factories/state-service.factory"; -import VaultTimeoutService from "../../services/vaultTimeout/vaultTimeout.service"; +import VaultTimeoutService from "../../services/vault-timeout/vault-timeout.service"; import { cipherServiceFactory, CipherServiceInitOptions, @@ -64,7 +60,6 @@ export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions & PlatformUtilsServiceInitOptions & MessagingServiceInitOptions & SearchServiceInitOptions & - KeyConnectorServiceInitOptions & StateServiceInitOptions & AuthServiceInitOptions & VaultTimeoutSettingsServiceInitOptions; @@ -86,7 +81,6 @@ export function vaultTimeoutServiceFactory( await platformUtilsServiceFactory(cache, opts), await messagingServiceFactory(cache, opts), await searchServiceFactory(cache, opts), - await keyConnectorServiceFactory(cache, opts), await stateServiceFactory(cache, opts), await authServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts), diff --git a/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts index 724993127b..eda86c0a15 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts @@ -1,5 +1,5 @@ -import { VaultTimeoutSettingsService as AbstractVaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vaultTimeout/vaultTimeoutSettings.service"; +import { VaultTimeoutSettingsService as AbstractVaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { policyServiceFactory, @@ -9,6 +9,10 @@ import { tokenServiceFactory, TokenServiceInitOptions, } from "../../auth/background/service-factories/token-service.factory"; +import { + userVerificationServiceFactory, + UserVerificationServiceInitOptions, +} from "../../auth/background/service-factories/user-verification-service.factory"; import { CryptoServiceInitOptions, cryptoServiceFactory, @@ -29,7 +33,8 @@ export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsService CryptoServiceInitOptions & TokenServiceInitOptions & PolicyServiceInitOptions & - StateServiceInitOptions; + StateServiceInitOptions & + UserVerificationServiceInitOptions; export function vaultTimeoutSettingsServiceFactory( cache: { vaultTimeoutSettingsService?: AbstractVaultTimeoutSettingsService } & CachedServices, @@ -44,7 +49,8 @@ export function vaultTimeoutSettingsServiceFactory( await cryptoServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), await policyServiceFactory(cache, opts), - await stateServiceFactory(cache, opts) + await stateServiceFactory(cache, opts), + await userVerificationServiceFactory(cache, opts) ) ); } diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index 1018c270cb..a6dce0f39e 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-crypto.service.ts @@ -1,13 +1,32 @@ import { KeySuffixOptions } from "@bitwarden/common/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { + SymmetricCryptoKey, + UserKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; export class BrowserCryptoService extends CryptoService { - protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) { - if (keySuffix === "biometric") { + override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise { + if (keySuffix === KeySuffixOptions.Biometric) { + return await this.stateService.getBiometricUnlock({ userId: userId }); + } + return super.hasUserKeyStored(keySuffix, userId); + } + + /** + * Browser doesn't store biometric keys, so we retrieve them from the desktop and return + * if we successfully saved it into memory as the User Key + */ + protected override async getKeyFromStorage(keySuffix: KeySuffixOptions): Promise { + if (keySuffix === KeySuffixOptions.Biometric) { await this.platformUtilService.authenticateBiometric(); - return (await this.getKey())?.keyB64; + const userKey = await this.stateService.getUserKey(); + if (userKey) { + return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey.keyB64)) as UserKey; + } } - return await super.retrieveKeyFromStorage(keySuffix); + return await super.getKeyFromStorage(keySuffix); } } diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index 874e13b7d8..d6bb83f7fb 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -41,7 +41,8 @@ describe("Browser State Service", () => { logService = mock(); stateMigrationService = mock(); stateFactory = mock(); - useAccountCache = true; + // turn off account cache for tests + useAccountCache = false; state = new State(new GlobalState()); state.accounts[userId] = new Account({ diff --git a/apps/browser/src/platform/services/browser-state.service.ts b/apps/browser/src/platform/services/browser-state.service.ts index 37f50d6dc7..34fa1a1d0f 100644 --- a/apps/browser/src/platform/services/browser-state.service.ts +++ b/apps/browser/src/platform/services/browser-state.service.ts @@ -1,5 +1,12 @@ import { BehaviorSubject } from "rxjs"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateMigrationService } from "@bitwarden/common/platform/abstractions/state-migration.service"; +import { + AbstractStorageService, + AbstractMemoryStorageService, +} from "@bitwarden/common/platform/abstractions/storage.service"; +import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; @@ -26,14 +33,44 @@ export class BrowserStateService protected activeAccountSubject: BehaviorSubject; @sessionSync({ initializer: (b: boolean) => b }) protected activeAccountUnlockedSubject: BehaviorSubject; - @sessionSync({ - initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account - initializeAs: "record", - }) - protected accountDiskCache: BehaviorSubject>; protected accountDeserializer = Account.fromJSON; + constructor( + storageService: AbstractStorageService, + secureStorageService: AbstractStorageService, + memoryStorageService: AbstractMemoryStorageService, + logService: LogService, + stateMigrationService: StateMigrationService, + stateFactory: StateFactory, + useAccountCache = true + ) { + super( + storageService, + secureStorageService, + memoryStorageService, + logService, + stateMigrationService, + stateFactory, + useAccountCache + ); + + // TODO: This is a hack to fix having a disk cache on both the popup and + // the background page that can get out of sync. We need to work out the + // best way to handle caching with multiple instances of the state service. + if (useAccountCache) { + chrome.storage.onChanged.addListener((changes, namespace) => { + if (namespace === "local") { + for (const key of Object.keys(changes)) { + if (key !== "accountActivity" && this.accountDiskCache.value[key]) { + this.deleteDiskCache(key); + } + } + } + }); + } + } + async addAccount(account: Account) { // Apply browser overrides to default account values account = new Account(account); @@ -132,4 +169,17 @@ export class BrowserStateService this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); } + + // Overriding the base class to prevent deleting the cache on save. We register a storage listener + // to delete the cache in the constructor above. + protected override async saveAccountToDisk( + account: Account, + options: StorageOptions + ): Promise { + const storageLocation = options.useSecureStorage + ? this.secureStorageService + : this.storageService; + + await storageLocation.save(`${options.userId}`, account, options); + } } diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 7da3d3049f..c15bf86f08 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -1,14 +1,21 @@ import { Injectable, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; -import { AuthGuard } from "@bitwarden/angular/auth/guards/auth.guard"; -import { LockGuard } from "@bitwarden/angular/auth/guards/lock.guard"; -import { UnauthGuard } from "@bitwarden/angular/auth/guards/unauth.guard"; +import { + redirectGuard, + AuthGuard, + lockGuard, + tdeDecryptionRequiredGuard, + UnauthGuard, +} from "@bitwarden/angular/auth/guards"; +import { canAccessFeature } from "@bitwarden/angular/guard/feature-flag.guard"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EnvironmentComponent } from "../auth/popup/environment.component"; import { HintComponent } from "../auth/popup/hint.component"; import { HomeComponent } from "../auth/popup/home.component"; import { LockComponent } from "../auth/popup/lock.component"; +import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component"; import { LoginWithDeviceComponent } from "../auth/popup/login-with-device.component"; import { LoginComponent } from "../auth/popup/login.component"; import { RegisterComponent } from "../auth/popup/register.component"; @@ -49,8 +56,9 @@ import { TabsComponent } from "./tabs.component"; const routes: Routes = [ { path: "", - redirectTo: "home", pathMatch: "full", + children: [], // Children lets us have an empty component. + canActivate: [redirectGuard({ loggedIn: "/tabs/vault", loggedOut: "/home", locked: "/lock" })], }, { path: "vault", @@ -72,13 +80,19 @@ const routes: Routes = [ { path: "login-with-device", component: LoginWithDeviceComponent, - canActivate: [UnauthGuard], + canActivate: [], + data: { state: "login-with-device" }, + }, + { + path: "admin-approval-requested", + component: LoginWithDeviceComponent, + canActivate: [], data: { state: "login-with-device" }, }, { path: "lock", component: LockComponent, - canActivate: [LockGuard], + canActivate: [lockGuard()], data: { state: "lock" }, }, { @@ -93,6 +107,14 @@ const routes: Routes = [ canActivate: [UnauthGuard], data: { state: "2fa-options" }, }, + { + path: "login-initiated", + component: LoginDecryptionOptionsComponent, + canActivate: [ + tdeDecryptionRequiredGuard(), + canAccessFeature(FeatureFlag.TrustedDeviceEncryption), + ], + }, { path: "sso", component: SsoComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 3665df7dec..f7539d6fa6 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -20,6 +20,7 @@ import { EnvironmentComponent } from "../auth/popup/environment.component"; import { HintComponent } from "../auth/popup/hint.component"; import { HomeComponent } from "../auth/popup/home.component"; import { LockComponent } from "../auth/popup/lock.component"; +import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component"; import { LoginWithDeviceComponent } from "../auth/popup/login-with-device.component"; import { LoginComponent } from "../auth/popup/login.component"; import { RegisterComponent } from "../auth/popup/register.component"; @@ -121,6 +122,7 @@ import "../platform/popup/locales"; LockComponent, LoginComponent, LoginWithDeviceComponent, + LoginDecryptionOptionsComponent, OptionsComponent, GeneratorComponent, PasswordGeneratorHistoryComponent, diff --git a/apps/browser/src/popup/components/user-verification.component.html b/apps/browser/src/popup/components/user-verification.component.html index 8d7f1ed870..25bd81cf39 100644 --- a/apps/browser/src/popup/components/user-verification.component.html +++ b/apps/browser/src/popup/components/user-verification.component.html @@ -1,4 +1,4 @@ - +
- +
- +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+

{{ "loggingInAs" | i18n }} {{ data.userEmail }}

+ {{ "notYou" | i18n }} +
+ +
+
diff --git a/apps/desktop/src/auth/login/login-decryption-options/login-decryption-options.component.ts b/apps/desktop/src/auth/login/login-decryption-options/login-decryption-options.component.ts new file mode 100644 index 0000000000..f64ec977ce --- /dev/null +++ b/apps/desktop/src/auth/login/login-decryption-options/login-decryption-options.component.ts @@ -0,0 +1,19 @@ +import { Component } from "@angular/core"; + +import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component"; + +@Component({ + selector: "desktop-login-decryption-options", + templateUrl: "login-decryption-options.component.html", +}) +export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent { + override async createUser(): Promise { + try { + await super.createUser(); + this.messagingService.send("redrawMenu"); + await this.router.navigate(["/vault"]); + } catch (error) { + this.validationService.showError(error); + } + } +} diff --git a/apps/desktop/src/auth/login/login-with-device.component.html b/apps/desktop/src/auth/login/login-with-device.component.html index 29fb103494..a81c92ad1d 100644 --- a/apps/desktop/src/auth/login/login-with-device.component.html +++ b/apps/desktop/src/auth/login/login-with-device.component.html @@ -1,40 +1,72 @@
Bitwarden -

{{ "logInInitiated" | i18n }}

-
-
-
-
-

{{ "notificationSentDevice" | i18n }}

-

- {{ "fingerprintMatchInfo" | i18n }} -

-
+ +

{{ "loginInitiated" | i18n }}

-
-

{{ "fingerprintPhraseHeader" | i18n }}

- {{ fingerprintPhrase }} -
+
+
+
+
+

{{ "notificationSentDevice" | i18n }}

+

+ {{ "fingerprintMatchInfo" | i18n }} +

+
- +
+

{{ "fingerprintPhraseHeader" | i18n }}

+ {{ fingerprintPhrase }} +
-
-

- {{ "needAnotherOption" | i18n }} - - {{ "viewAllLoginOptions" | i18n }} - -

+ + +
+

+ {{ "needAnotherOption" | i18n }} + + {{ "viewAllLoginOptions" | i18n }} + +

+
-
+
+ + +

{{ "adminApprovalRequested" | i18n }}

+ +
+
+
+
+

{{ "adminApprovalRequestSentToAdmins" | i18n }}

+

{{ "youWillBeNotifiedOnceApproved" | i18n }}

+
+ +
+

{{ "fingerprintPhraseHeader" | i18n }}

+ {{ fingerprintPhrase }} +
+ +
+

+ {{ "troubleLoggingIn" | i18n }} + + {{ "viewAllLoginOptions" | i18n }} + +

+
+
+
+
+
diff --git a/apps/desktop/src/auth/login/login-with-device.component.ts b/apps/desktop/src/auth/login/login-with-device.component.ts index 3b3ee83ae4..daca2d1993 100644 --- a/apps/desktop/src/auth/login/login-with-device.component.ts +++ b/apps/desktop/src/auth/login/login-with-device.component.ts @@ -1,3 +1,4 @@ +import { Location } from "@angular/common"; import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Router } from "@angular/router"; @@ -5,7 +6,9 @@ import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwa import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -50,7 +53,10 @@ export class LoginWithDeviceComponent private modalService: ModalService, syncService: SyncService, stateService: StateService, - loginService: LoginService + loginService: LoginService, + deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + authReqCryptoService: AuthRequestCryptoServiceAbstraction, + private location: Location ) { super( router, @@ -67,7 +73,9 @@ export class LoginWithDeviceComponent anonymousHubService, validationService, stateService, - loginService + loginService, + deviceTrustCryptoService, + authReqCryptoService ); super.onSuccessfulLogin = () => { @@ -100,7 +108,7 @@ export class LoginWithDeviceComponent super.ngOnDestroy(); } - goToLogin() { - this.router.navigate(["/login"]); + back() { + this.location.back(); } } diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index 00aa0300f6..45b330f6da 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -7,8 +7,8 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction"; 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 { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; diff --git a/apps/desktop/src/auth/login/login.module.ts b/apps/desktop/src/auth/login/login.module.ts index 35ad83e9db..9fb2b28915 100644 --- a/apps/desktop/src/auth/login/login.module.ts +++ b/apps/desktop/src/auth/login/login.module.ts @@ -5,12 +5,18 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components import { SharedModule } from "../../app/shared/shared.module"; +import { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component"; import { LoginWithDeviceComponent } from "./login-with-device.component"; import { LoginComponent } from "./login.component"; @NgModule({ imports: [SharedModule, RouterModule], - declarations: [LoginComponent, LoginWithDeviceComponent, EnvironmentSelectorComponent], + declarations: [ + LoginComponent, + LoginWithDeviceComponent, + EnvironmentSelectorComponent, + LoginDecryptionOptionsComponent, + ], exports: [LoginComponent, LoginWithDeviceComponent], }) export class LoginModule {} diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index 75f380b0ed..22badf9d69 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -30,7 +31,8 @@ export class SsoComponent extends BaseSsoComponent { cryptoFunctionService: CryptoFunctionService, environmentService: EnvironmentService, passwordGenerationService: PasswordGenerationServiceAbstraction, - logService: LogService + logService: LogService, + configService: ConfigServiceAbstraction ) { super( authService, @@ -43,11 +45,17 @@ export class SsoComponent extends BaseSsoComponent { cryptoFunctionService, environmentService, passwordGenerationService, - logService + logService, + configService ); - super.onSuccessfulLogin = () => { - return syncService.fullSync(true); + super.onSuccessfulLogin = async () => { + syncService.fullSync(true); }; + + super.onSuccessfulLoginTde = async () => { + syncService.fullSync(true); + }; + this.redirectUri = "bitwarden://sso-callback"; this.clientId = "desktop"; } diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index ee3764027a..bf3545820b 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -1,7 +1,8 @@ -import { Component, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, Inject, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; +import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -9,6 +10,7 @@ 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 { 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"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -43,7 +45,9 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService: LogService, twoFactorService: TwoFactorService, appIdService: AppIdService, - loginService: LoginService + loginService: LoginService, + configService: ConfigServiceAbstraction, + @Inject(WINDOW) protected win: Window ) { super( authService, @@ -51,18 +55,22 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { i18nService, apiService, platformUtilsService, - window, + win, environmentService, stateService, route, logService, twoFactorService, appIdService, - loginService + loginService, + configService ); - super.onSuccessfulLogin = () => { - this.loginService.clearValues(); - return syncService.fullSync(true); + super.onSuccessfulLogin = async () => { + syncService.fullSync(true); + }; + + super.onSuccessfulLoginTde = async () => { + syncService.fullSync(true); }; } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index ab8446ee46..eba53b45b3 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1492,6 +1492,9 @@ "vaultTimeoutActionLogOutDesc": { "message": "Re-authentication is required to access your vault again." }, + "unlockMethodNeededToChangeTimeoutActionDesc": { + "message": "Set up an unlock method to change your vault timeout action." + }, "lock": { "message": "Lock", "description": "Verb form: to make secure or inaccesible by" @@ -2106,8 +2109,8 @@ "logInWithAnotherDevice": { "message": "Log in with another device" }, - "logInInitiated": { - "message": "Log in initiated" + "loginInitiated": { + "message": "Login initiated" }, "notificationSentDevice": { "message": "A notification has been sent to your device." @@ -2246,6 +2249,34 @@ "windowsBiometricUpdateWarningTitle": { "message": "Recommended Settings Update" }, + "deviceApprovalRequired": { + "message": "Device approval required. Select an approval option below:" + }, + "rememberThisDevice": { + "message": "Remember this device" + }, + "uncheckIfPublicDevice": { + "message": "Uncheck if using a public device" + }, + "approveFromYourOtherDevice": { + "message": "Approve from your other device" + }, + "requestAdminApproval": { + "message": "Request admin approval" + }, + "approveWithMasterPassword": { + "message": "Approve with master password" + }, + "region": { + "message": "Region" + }, + "ssoIdentifierRequired": { + "message": "Organization SSO identifier is required." + }, + "eu": { + "message": "EU", + "description": "European Union" + }, "loggingInOn": { "message": "Logging in on" }, @@ -2260,5 +2291,29 @@ }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." + }, + "accountSuccessfullyCreated": { + "message": "Account successfully created!" + }, + "adminApprovalRequested": { + "message": "Admin approval requested" + }, + "adminApprovalRequestSentToAdmins": { + "message": "Your request has been sent to your admin." + }, + "youWillBeNotifiedOnceApproved": { + "message": "You will be notified once approved." + }, + "troubleLoggingIn": { + "message": "Trouble logging in?" + }, + "loginApproved": { + "message": "Login approved" + }, + "userEmailMissing": { + "message": "User email missing" + }, + "deviceTrusted": { + "message": "Device trusted" } } diff --git a/apps/desktop/src/main/menu/menu.account.ts b/apps/desktop/src/main/menu/menu.account.ts index 10a6d6d77d..5060a4934c 100644 --- a/apps/desktop/src/main/menu/menu.account.ts +++ b/apps/desktop/src/main/menu/menu.account.ts @@ -15,14 +15,15 @@ export class AccountMenu implements IMenubarMenu { } get items(): MenuItemConstructorOptions[] { - return [ - this.premiumMembership, - this.changeMasterPassword, - this.twoStepLogin, - this.fingerprintPhrase, - this.separator, - this.deleteAccount, - ]; + const items = [this.premiumMembership]; + if (this._hasMasterPassword) { + items.push(this.changeMasterPassword); + } + items.push(this.twoStepLogin); + items.push(this.fingerprintPhrase); + items.push(this.separator); + items.push(this.deleteAccount); + return items; } private readonly _i18nService: I18nService; @@ -30,19 +31,22 @@ export class AccountMenu implements IMenubarMenu { private readonly _webVaultUrl: string; private readonly _window: BrowserWindow; private readonly _isLocked: boolean; + private readonly _hasMasterPassword: boolean; constructor( i18nService: I18nService, messagingService: MessagingService, webVaultUrl: string, window: BrowserWindow, - isLocked: boolean + isLocked: boolean, + hasMasterPassword: boolean ) { this._i18nService = i18nService; this._messagingService = messagingService; this._webVaultUrl = webVaultUrl; this._window = window; this._isLocked = isLocked; + this._hasMasterPassword = hasMasterPassword; } private get premiumMembership(): MenuItemConstructorOptions { diff --git a/apps/desktop/src/main/menu/menu.bitwarden.ts b/apps/desktop/src/main/menu/menu.bitwarden.ts index 3c3a16702e..6873f679de 100644 --- a/apps/desktop/src/main/menu/menu.bitwarden.ts +++ b/apps/desktop/src/main/menu/menu.bitwarden.ts @@ -52,9 +52,10 @@ export class BitwardenMenu extends FirstMenu implements IMenubarMenu { updater: UpdaterMain, window: BrowserWindow, accounts: { [userId: string]: MenuAccount }, - isLocked: boolean + isLocked: boolean, + isLockable: boolean ) { - super(i18nService, messagingService, updater, window, accounts, isLocked); + super(i18nService, messagingService, updater, window, accounts, isLocked, isLockable); } private get aboutBitwarden(): MenuItemConstructorOptions { diff --git a/apps/desktop/src/main/menu/menu.file.ts b/apps/desktop/src/main/menu/menu.file.ts index 618acdc7fe..173b6066ab 100644 --- a/apps/desktop/src/main/menu/menu.file.ts +++ b/apps/desktop/src/main/menu/menu.file.ts @@ -51,9 +51,10 @@ export class FileMenu extends FirstMenu implements IMenubarMenu { updater: UpdaterMain, window: BrowserWindow, accounts: { [userId: string]: MenuAccount }, - isLocked: boolean + isLocked: boolean, + isLockable: boolean ) { - super(i18nService, messagingService, updater, window, accounts, isLocked); + super(i18nService, messagingService, updater, window, accounts, isLocked, isLockable); } private get addNewLogin(): MenuItemConstructorOptions { diff --git a/apps/desktop/src/main/menu/menu.first.ts b/apps/desktop/src/main/menu/menu.first.ts index 805956f263..b164e280d0 100644 --- a/apps/desktop/src/main/menu/menu.first.ts +++ b/apps/desktop/src/main/menu/menu.first.ts @@ -9,33 +9,24 @@ import { UpdaterMain } from "../updater.main"; import { MenuAccount } from "./menu.updater"; export class FirstMenu { - protected readonly _i18nService: I18nService; - protected readonly _updater: UpdaterMain; - protected readonly _messagingService: MessagingService; - protected readonly _accounts: { [userId: string]: MenuAccount }; - protected readonly _window: BrowserWindow; - protected readonly _isLocked: boolean; - constructor( - i18nService: I18nService, - messagingService: MessagingService, - updater: UpdaterMain, - window: BrowserWindow, - accounts: { [userId: string]: MenuAccount }, - isLocked: boolean - ) { - this._i18nService = i18nService; - this._updater = updater; - this._messagingService = messagingService; - this._window = window; - this._accounts = accounts; - this._isLocked = isLocked; - } + protected readonly _i18nService: I18nService, + protected readonly _messagingService: MessagingService, + protected readonly _updater: UpdaterMain, + protected readonly _window: BrowserWindow, + protected readonly _accounts: { [userId: string]: MenuAccount }, + protected readonly _isLocked: boolean, + protected readonly _isLockable: boolean + ) {} protected get hasAccounts(): boolean { return this._accounts != null && Object.keys(this._accounts).length > 0; } + protected get hasLockableAccounts(): boolean { + return this._accounts != null && Object.values(this._accounts).some((a) => a.isLockable); + } + protected get checkForUpdates(): MenuItemConstructorOptions { return { id: "checkForUpdates", @@ -66,23 +57,29 @@ export class FirstMenu { id: "lock", label: this.localize("lockVault"), submenu: this.lockSubmenu, - enabled: this.hasAccounts, + enabled: this.hasLockableAccounts, }; } protected get lockSubmenu(): MenuItemConstructorOptions[] { const value: MenuItemConstructorOptions[] = []; for (const userId in this._accounts) { - if (userId == null) { + if (!userId) { + continue; + } + + const account = this._accounts[userId]; + + if (account == null || !account.isLockable) { continue; } value.push({ - label: this._accounts[userId].email, - id: `lockNow_${this._accounts[userId].userId}`, - click: () => this.sendMessage("lockVault", { userId: this._accounts[userId].userId }), - enabled: !this._accounts[userId].isLocked, - visible: this._accounts[userId].isAuthenticated, + label: account.email, + id: `lockNow_${account.userId}`, + click: () => this.sendMessage("lockVault", { userId: account.userId }), + enabled: !account.isLocked, + visible: account.isAuthenticated, }); } return value; diff --git a/apps/desktop/src/main/menu/menu.updater.ts b/apps/desktop/src/main/menu/menu.updater.ts index 75454a56f8..170804cae2 100644 --- a/apps/desktop/src/main/menu/menu.updater.ts +++ b/apps/desktop/src/main/menu/menu.updater.ts @@ -1,5 +1,4 @@ export class MenuUpdateRequest { - hideChangeMasterPassword: boolean; activeUserId: string; accounts: { [userId: string]: MenuAccount }; } @@ -7,6 +6,8 @@ export class MenuUpdateRequest { export class MenuAccount { isAuthenticated: boolean; isLocked: boolean; + isLockable: boolean; userId: string; email: string; + hasMasterPassword: boolean; } diff --git a/apps/desktop/src/main/menu/menubar.ts b/apps/desktop/src/main/menu/menubar.ts index 6ac28a1c8d..c3f37ecbff 100644 --- a/apps/desktop/src/main/menu/menubar.ts +++ b/apps/desktop/src/main/menu/menubar.ts @@ -62,6 +62,10 @@ export class Menubar { isLocked = updateRequest.accounts[updateRequest.activeUserId]?.isLocked ?? true; } + const isLockable = !isLocked && updateRequest?.accounts[updateRequest.activeUserId]?.isLockable; + const hasMasterPassword = + updateRequest?.accounts[updateRequest.activeUserId]?.hasMasterPassword ?? false; + this.items = [ new FileMenu( i18nService, @@ -69,11 +73,19 @@ export class Menubar { updaterMain, windowMain.win, updateRequest?.accounts, - isLocked + isLocked, + isLockable ), new EditMenu(i18nService, messagingService, isLocked), new ViewMenu(i18nService, messagingService, isLocked), - new AccountMenu(i18nService, messagingService, webVaultUrl, windowMain.win, isLocked), + new AccountMenu( + i18nService, + messagingService, + webVaultUrl, + windowMain.win, + isLocked, + hasMasterPassword + ), new WindowMenu(i18nService, messagingService, windowMain), new HelpMenu( i18nService, @@ -91,7 +103,8 @@ export class Menubar { updaterMain, windowMain.win, updateRequest?.accounts, - isLocked + isLocked, + isLockable ), ], ...this.items, diff --git a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts new file mode 100644 index 0000000000..92f9391b13 --- /dev/null +++ b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts @@ -0,0 +1,91 @@ +import { mock, mockReset } from "jest-mock-extended"; + +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { + SymmetricCryptoKey, + UserKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; + +import { ElectronCryptoService } from "./electron-crypto.service"; +import { ElectronStateService } from "./electron-state.service.abstraction"; + +describe("electronCryptoService", () => { + let electronCryptoService: ElectronCryptoService; + + const cryptoFunctionService = mock(); + const encryptService = mock(); + const platformUtilService = mock(); + const logService = mock(); + const stateService = mock(); + + const mockUserId = "mock user id"; + + beforeEach(() => { + mockReset(cryptoFunctionService); + mockReset(encryptService); + mockReset(platformUtilService); + mockReset(logService); + mockReset(stateService); + + electronCryptoService = new ElectronCryptoService( + cryptoFunctionService, + encryptService, + platformUtilService, + logService, + stateService + ); + }); + + it("instantiates", () => { + expect(electronCryptoService).not.toBeFalsy(); + }); + + describe("setUserKey", () => { + let mockUserKey: UserKey; + + beforeEach(() => { + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + }); + + describe("Biometric Key refresh", () => { + it("sets an Biometric key if getBiometricUnlock is true and the platform supports secure storage", async () => { + stateService.getBiometricUnlock.mockResolvedValue(true); + platformUtilService.supportsSecureStorage.mockReturnValue(true); + stateService.getBiometricRequirePasswordOnStart.mockResolvedValue(false); + + await electronCryptoService.setUserKey(mockUserKey, mockUserId); + + expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith( + expect.objectContaining({ key: expect.any(String), clientEncKeyHalf: null }), + { + userId: mockUserId, + } + ); + }); + + it("clears the Biometric key if getBiometricUnlock is false or the platform does not support secure storage", async () => { + stateService.getBiometricUnlock.mockResolvedValue(true); + platformUtilService.supportsSecureStorage.mockReturnValue(false); + + await electronCryptoService.setUserKey(mockUserKey, mockUserId); + + expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith(null, { + userId: mockUserId, + }); + }); + + it("clears the old deprecated Biometric key whenever a User Key is set", async () => { + await electronCryptoService.setUserKey(mockUserKey, mockUserId); + + expect(stateService.setCryptoMasterKeyBiometric).toHaveBeenCalledWith(null, { + userId: mockUserId, + }); + }); + }); + }); +}); diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index 1fb90e52ca..e21d200197 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.ts @@ -4,7 +4,12 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { + MasterKey, + SymmetricCryptoKey, + UserKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { CsprngString } from "@bitwarden/common/types/csprng"; @@ -21,36 +26,80 @@ export class ElectronCryptoService extends CryptoService { super(cryptoFunctionService, encryptService, platformUtilsService, logService, stateService); } - protected override async storeKey(key: SymmetricCryptoKey, userId?: string) { - await super.storeKey(key, userId); + override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise { + if (keySuffix === KeySuffixOptions.Biometric) { + // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3474) + const oldKey = await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId }); + return oldKey || (await this.stateService.hasUserKeyBiometric({ userId: userId })); + } + return super.hasUserKeyStored(keySuffix, userId); + } + + override async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise { + if (keySuffix === KeySuffixOptions.Biometric) { + this.stateService.setUserKeyBiometric(null, { userId: userId }); + this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId); + return; + } + super.clearStoredUserKey(keySuffix, userId); + } + + protected override async storeAdditionalKeys(key: UserKey, userId?: string) { + await super.storeAdditionalKeys(key, userId); const storeBiometricKey = await this.shouldStoreKey(KeySuffixOptions.Biometric, userId); if (storeBiometricKey) { await this.storeBiometricKey(key, userId); } else { - await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId }); + await this.stateService.setUserKeyBiometric(null, { userId: userId }); } + await this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId); } - protected async storeBiometricKey(key: SymmetricCryptoKey, userId?: string): Promise { + protected override async getKeyFromStorage( + keySuffix: KeySuffixOptions, + userId?: string + ): Promise { + if (keySuffix === KeySuffixOptions.Biometric) { + await this.migrateBiometricKeyIfNeeded(userId); + const userKey = await this.stateService.getUserKeyBiometric({ userId: userId }); + return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey)) as UserKey; + } + return await super.getKeyFromStorage(keySuffix, userId); + } + + protected async storeBiometricKey(key: UserKey, userId?: string): Promise { let clientEncKeyHalf: CsprngString = null; if (await this.stateService.getBiometricRequirePasswordOnStart({ userId })) { clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userId); } - await this.stateService.setCryptoMasterKeyBiometric( + await this.stateService.setUserKeyBiometric( { key: key.keyB64, clientEncKeyHalf }, { userId: userId } ); } + protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: string): Promise { + if (keySuffix === KeySuffixOptions.Biometric) { + const biometricUnlock = await this.stateService.getBiometricUnlock({ userId: userId }); + return biometricUnlock && this.platformUtilService.supportsSecureStorage(); + } + return await super.shouldStoreKey(keySuffix, userId); + } + + protected override async clearAllStoredUserKeys(userId?: string): Promise { + await this.stateService.setUserKeyBiometric(null, { userId: userId }); + super.clearAllStoredUserKeys(userId); + } + private async getBiometricEncryptionClientKeyHalf(userId?: string): Promise { try { let biometricKey = await this.stateService .getBiometricEncryptionClientKeyHalf({ userId }) .then((result) => result?.decrypt(null /* user encrypted */)) .then((result) => result as CsprngString); - const userKey = await this.getKeyForUserEncryption(); + const userKey = await this.getUserKeyWithLegacySupport(); if (biometricKey == null && userKey != null) { const keyBytes = await this.cryptoFunctionService.randomBytes(32); biometricKey = Utils.fromBufferToUtf8(keyBytes) as CsprngString; @@ -63,4 +112,34 @@ export class ElectronCryptoService extends CryptoService { return null; } } + + // --LEGACY METHODS-- + // We previously used the master key for additional keys, but now we use the user key. + // These methods support migrating the old keys to the new ones. + // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3475) + + override async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: string) { + if (keySuffix === KeySuffixOptions.Biometric) { + await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId }); + } + + super.clearDeprecatedKeys(keySuffix, userId); + } + + private async migrateBiometricKeyIfNeeded(userId?: string) { + if (await this.stateService.hasCryptoMasterKeyBiometric({ userId })) { + const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId }); + // decrypt + const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldBiometricKey)) as MasterKey; + let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); + encUserKey = encUserKey ?? (await this.stateService.getMasterKeyEncryptedUserKey()); + if (!encUserKey) { + throw new Error("No user key found during biometric migration"); + } + const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey)); + // migrate + await this.storeBiometricKey(userKey, userId); + await this.stateService.setCryptoMasterKeyBiometric(null, { userId }); + } + } } diff --git a/apps/desktop/src/platform/services/electron-state.service.ts b/apps/desktop/src/platform/services/electron-state.service.ts index 334cab9669..0503aeb52a 100644 --- a/apps/desktop/src/platform/services/electron-state.service.ts +++ b/apps/desktop/src/platform/services/electron-state.service.ts @@ -98,6 +98,10 @@ export class ElectronStateService options ); + if (b64DeviceKey == null) { + return null; + } + return new SymmetricCryptoKey(Utils.fromB64ToArray(b64DeviceKey)) as DeviceKey; } diff --git a/apps/desktop/src/scss/pages.scss b/apps/desktop/src/scss/pages.scss index 5b3ad4a858..fda75e834f 100644 --- a/apps/desktop/src/scss/pages.scss +++ b/apps/desktop/src/scss/pages.scss @@ -5,7 +5,8 @@ #lock-page, #sso-page, #set-password-page, -#remove-password-page { +#remove-password-page, +#login-decryption-options-page { display: flex; justify-content: center; align-items: center; @@ -53,7 +54,8 @@ #hint-page, #two-factor-page, #lock-page, -#update-temp-password-page { +#update-temp-password-page, +#login-decryption-options-page { .content { width: 325px; transition: width 0.25s linear; @@ -189,7 +191,8 @@ } #login-page, -#login-with-device-page { +#login-with-device-page, +#login-decryption-options-page { flex-direction: column; justify-content: unset; padding-top: 20px; @@ -273,3 +276,14 @@ } } } + +#login-decryption-options-page { + .standard-bottom-margin { + margin-bottom: 20px; + } + + #rememberThisDeviceHintText { + font-size: $font-size-small; + color: $text-muted; + } +} diff --git a/apps/desktop/src/services/native-message-handler.service.ts b/apps/desktop/src/services/native-message-handler.service.ts index 644dfe8f90..9f5f1d460d 100644 --- a/apps/desktop/src/services/native-message-handler.service.ts +++ b/apps/desktop/src/services/native-message-handler.service.ts @@ -8,7 +8,7 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { StateService } from "@bitwarden/common/platform/services/state.service"; @@ -144,7 +144,9 @@ export class NativeMessageHandlerService { } private async handleEncryptedMessage(message: EncryptedMessage) { - message.encryptedCommand = EncString.fromJSON(message.encryptedCommand.toString()); + message.encryptedCommand = EncString.fromJSON( + message.encryptedCommand.toString() as EncryptedString + ); const decryptedCommandData = await this.decryptPayload(message); const { command } = decryptedCommandData; diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 7647410fd7..3928778f31 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -136,14 +136,23 @@ export class NativeMessagingService { }); } - const key = await this.cryptoService.getKeyFromStorage( + const userKey = await this.cryptoService.getUserKeyFromStorage( KeySuffixOptions.Biometric, message.userId ); + const masterKey = await this.cryptoService.getMasterKey(message.userId); - if (key != null) { + if (userKey != null) { + // we send the master key still for backwards compatibility + // with older browser extensions + // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) this.send( - { command: "biometricUnlock", response: "unlocked", keyB64: key.keyB64 }, + { + command: "biometricUnlock", + response: "unlocked", + keyB64: masterKey?.keyB64, + userKeyB64: userKey.keyB64, + }, appId ); } else { diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.ts index 31ced8aee1..ae754faabe 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.ts @@ -7,6 +7,7 @@ import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enum import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { BulkUserDetails } from "./bulk-status.component"; @@ -98,7 +99,7 @@ export class BulkConfirmComponent implements OnInit { ); } - protected getCryptoKey() { + protected getCryptoKey(): Promise { return this.cryptoService.getOrgKey(this.organizationId); } diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts index 5d2a5f54ba..7d0fae37e0 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts @@ -22,7 +22,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { + SymmetricCryptoKey, + UserKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; @@ -171,26 +174,32 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { orgSymKey ); - // Decrypt User's Reset Password Key to get EncKey + // Decrypt User's Reset Password Key to get UserKey const decValue = await this.cryptoService.rsaDecrypt(resetPasswordKey, decPrivateKey); - const userEncKey = new SymmetricCryptoKey(decValue); + const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey; - // Create new key and hash new password - const newKey = await this.cryptoService.makeKey( + // Create new master key and hash new password + const newMasterKey = await this.cryptoService.makeMasterKey( this.newPassword, this.email.trim().toLowerCase(), kdfType, new KdfConfig(kdfIterations, kdfMemory, kdfParallelism) ); - const newPasswordHash = await this.cryptoService.hashPassword(this.newPassword, newKey); + const newMasterKeyHash = await this.cryptoService.hashMasterKey( + this.newPassword, + newMasterKey + ); - // Create new encKey for the User - const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey); + // Create new encrypted user key for the User + const newUserKey = await this.cryptoService.encryptUserKeyWithMasterKey( + newMasterKey, + existingUserKey + ); // Create request const request = new OrganizationUserResetPasswordRequest(); - request.key = newEncKey[1].encryptedString; - request.newMasterPasswordHash = newPasswordHash; + request.key = newUserKey[1].encryptedString; + request.newMasterPasswordHash = newMasterKeyHash; // Change user's password return this.organizationUserService.putOrganizationUserResetPassword( diff --git a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts index dca1b18530..c4ca1dc241 100644 --- a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { AuthGuard } from "@bitwarden/angular/auth/guards/auth.guard"; +import { AuthGuard } from "@bitwarden/angular/auth/guards"; import { canAccessOrgAdmin, canAccessGroupsTab, diff --git a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts index a533967d8f..535d7d375a 100644 --- a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts +++ b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts @@ -58,8 +58,8 @@ export class EnrollMasterPasswordReset { const publicKey = Utils.fromB64ToArray(orgKeys.publicKey); // RSA Encrypt user's encKey.key with organization public key - const encKey = await this.cryptoService.getEncKey(); - const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey); + const userKey = await this.cryptoService.getUserKey(); + const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey); keyString = encryptedKey.encryptedString; toastStringRef = "enrollPasswordResetSuccess"; diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 8ce51df9e6..3066dbe093 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -11,7 +11,7 @@ import { EventUploadService } from "@bitwarden/common/abstractions/event/event-u import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; +import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; diff --git a/apps/web/src/app/auth/accept-organization.component.ts b/apps/web/src/app/auth/accept-organization.component.ts index bdc3511f76..ef2dda138d 100644 --- a/apps/web/src/app/auth/accept-organization.component.ts +++ b/apps/web/src/app/auth/accept-organization.component.ts @@ -18,6 +18,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrgKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { BaseAcceptComponent } from "../common/base.accept.component"; @@ -108,16 +109,14 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { const request = new OrganizationUserAcceptInitRequest(); request.token = qParams.token; - const [encryptedOrgShareKey, orgShareKey] = await this.cryptoService.makeShareKey(); - const [orgPublicKey, encryptedOrgPrivateKey] = await this.cryptoService.makeKeyPair( - orgShareKey - ); + const [encryptedOrgKey, orgKey] = await this.cryptoService.makeOrgKey(); + const [orgPublicKey, encryptedOrgPrivateKey] = await this.cryptoService.makeKeyPair(orgKey); const collection = await this.cryptoService.encrypt( this.i18nService.t("defaultCollection"), - orgShareKey + orgKey ); - request.key = encryptedOrgShareKey.encryptedString; + request.key = encryptedOrgKey.encryptedString; request.keys = new OrganizationKeysRequest( orgPublicKey, encryptedOrgPrivateKey.encryptedString @@ -141,8 +140,8 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { const publicKey = Utils.fromB64ToArray(response.publicKey); // RSA Encrypt user's encKey.key with organization public key - const encKey = await this.cryptoService.getEncKey(); - const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey); + const userKey = await this.cryptoService.getUserKey(); + const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey); // Add reset password key to accept request request.resetPasswordKey = encryptedKey.encryptedString; diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts index 185ce241d4..1cb97fbd55 100644 --- a/apps/web/src/app/auth/lock.component.ts +++ b/apps/web/src/app/auth/lock.component.ts @@ -3,11 +3,12 @@ import { Router } from "@angular/router"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.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 { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -38,12 +39,13 @@ export class LockComponent extends BaseLockComponent { stateService: StateService, apiService: ApiService, logService: LogService, - keyConnectorService: KeyConnectorService, ngZone: NgZone, policyApiService: PolicyApiServiceAbstraction, policyService: InternalPolicyService, passwordStrengthService: PasswordStrengthServiceAbstraction, - dialogService: DialogService + dialogService: DialogService, + deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + userVerificationService: UserVerificationService ) { super( router, @@ -57,12 +59,13 @@ export class LockComponent extends BaseLockComponent { stateService, apiService, logService, - keyConnectorService, ngZone, policyApiService, policyService, passwordStrengthService, - dialogService + dialogService, + deviceTrustCryptoService, + userVerificationService ); } diff --git a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html new file mode 100644 index 0000000000..ed59cc1238 --- /dev/null +++ b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html @@ -0,0 +1,105 @@ +
+
+
+ +
+ + +

+ + {{ "loading" | i18n }} +

+
+ +
+ +

{{ "loginInitiated" | i18n }}

+ +

+ {{ "deviceApprovalRequired" | i18n }} +

+ +
+ + + {{ "rememberThisDevice" | i18n }} + {{ "uncheckIfPublicDevice" | i18n }} + +
+ +
+ + + + + +
+
+ + +

{{ "loggedInExclamation" | i18n }}

+ +
+ + + {{ "rememberThisDevice" | i18n }} + {{ "uncheckIfPublicDevice" | i18n }} + +
+ + +
+ +
+ +
+

{{ "loggingInAs" | i18n }} {{ data.userEmail }}

+ {{ "notYou" | i18n }} +
+
+
+
diff --git a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts new file mode 100644 index 0000000000..2c97bd227f --- /dev/null +++ b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts @@ -0,0 +1,21 @@ +import { Component } from "@angular/core"; + +import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component"; +@Component({ + selector: "web-login-decryption-options", + templateUrl: "login-decryption-options.component.html", +}) +export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent { + override async createUser(): Promise { + try { + await super.createUser(); + await this.router.navigate(["/vault"]); + } catch (error) { + this.validationService.showError(error); + } + } + + createUserAction = async (): Promise => { + return this.createUser(); + }; +} diff --git a/apps/web/src/app/auth/login/login-with-device.component.html b/apps/web/src/app/auth/login/login-with-device.component.html index f190f8f5c6..80811ac8b6 100644 --- a/apps/web/src/app/auth/login/login-with-device.component.html +++ b/apps/web/src/app/auth/login/login-with-device.component.html @@ -5,42 +5,71 @@ >
-

- {{ "loginOrCreateNewAccount" | i18n }} -

-
-

{{ "logInInitiated" | i18n }}

+ +

+ {{ "loginOrCreateNewAccount" | i18n }} +

-
-

{{ "notificationSentDevice" | i18n }}

+
+

{{ "loginInitiated" | i18n }}

-

- {{ "fingerprintMatchInfo" | i18n }} -

+
+

{{ "notificationSentDevice" | i18n }}

+ +

+ {{ "fingerprintMatchInfo" | i18n }} +

+
+ +
+

{{ "fingerprintPhraseHeader" | i18n }}

+

+ {{ fingerprintPhrase }} +

+
+ + + +
+ +
+ {{ "loginWithDeviceEnabledNote" | i18n }} + {{ "viewAllLoginOptions" | i18n }} +
+ + +
+

{{ "adminApprovalRequested" | i18n }}

-
-

{{ "fingerprintPhraseHeader" | i18n }}

-

- {{ fingerprintPhrase }} -

+
+

{{ "adminApprovalRequestSentToAdmins" | i18n }}

+

{{ "youWillBeNotifiedOnceApproved" | i18n }}

+
+ +
+

{{ "fingerprintPhraseHeader" | i18n }}

+

+ {{ fingerprintPhrase }} +

+
+ +
+ +
+ {{ "troubleLoggingIn" | i18n }} + {{ "viewAllLoginOptions" | i18n }} +
- - - -
- -
- {{ "loginWithDeviceEnabledNote" | i18n }} - {{ "viewAllLoginOptions" | i18n }} -
-
+
diff --git a/apps/web/src/app/auth/login/login-with-device.component.ts b/apps/web/src/app/auth/login/login-with-device.component.ts index c5e73bf04b..ff66bdb886 100644 --- a/apps/web/src/app/auth/login/login-with-device.component.ts +++ b/apps/web/src/app/auth/login/login-with-device.component.ts @@ -4,7 +4,9 @@ import { Router } from "@angular/router"; import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-with-device.component"; import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -41,7 +43,9 @@ export class LoginWithDeviceComponent anonymousHubService: AnonymousHubService, validationService: ValidationService, stateService: StateService, - loginService: LoginService + loginService: LoginService, + deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + authReqCryptoService: AuthRequestCryptoServiceAbstraction ) { super( router, @@ -58,7 +62,9 @@ export class LoginWithDeviceComponent anonymousHubService, validationService, stateService, - loginService + loginService, + deviceTrustCryptoService, + authReqCryptoService ); } } diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index 316b353c49..3bc65542b7 100644 --- a/apps/web/src/app/auth/login/login.component.ts +++ b/apps/web/src/app/auth/login/login.component.ts @@ -6,7 +6,6 @@ import { first } from "rxjs/operators"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; -import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; @@ -14,6 +13,7 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.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 { ListResponse } from "@bitwarden/common/models/response/list.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; diff --git a/apps/web/src/app/auth/login/login.module.ts b/apps/web/src/app/auth/login/login.module.ts index b01fa01588..2a074ca2a6 100644 --- a/apps/web/src/app/auth/login/login.module.ts +++ b/apps/web/src/app/auth/login/login.module.ts @@ -4,12 +4,13 @@ import { CheckboxModule } from "@bitwarden/components"; import { SharedModule } from "../../../app/shared"; +import { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component"; import { LoginWithDeviceComponent } from "./login-with-device.component"; import { LoginComponent } from "./login.component"; @NgModule({ imports: [SharedModule, CheckboxModule], - declarations: [LoginComponent, LoginWithDeviceComponent], - exports: [LoginComponent, LoginWithDeviceComponent], + declarations: [LoginComponent, LoginWithDeviceComponent, LoginDecryptionOptionsComponent], + exports: [LoginComponent, LoginWithDeviceComponent, LoginDecryptionOptionsComponent], }) export class LoginModule {} diff --git a/apps/web/src/app/auth/recover-two-factor.component.ts b/apps/web/src/app/auth/recover-two-factor.component.ts index 06816d8546..2d6140780a 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.ts +++ b/apps/web/src/app/auth/recover-two-factor.component.ts @@ -35,7 +35,7 @@ export class RecoverTwoFactorComponent { request.recoveryCode = this.recoveryCode.replace(/\s/g, "").toLowerCase(); request.email = this.email.trim().toLowerCase(); const key = await this.authService.makePreloginKey(this.masterPassword, request.email); - request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key); + request.masterPasswordHash = await this.cryptoService.hashMasterKey(this.masterPassword, key); this.formPromise = this.apiService.postTwoFactorRecover(request); await this.formPromise; this.platformUtilsService.showToast( diff --git a/apps/web/src/app/settings/change-password.component.html b/apps/web/src/app/auth/settings/change-password.component.html similarity index 94% rename from apps/web/src/app/settings/change-password.component.html rename to apps/web/src/app/auth/settings/change-password.component.html index 3321b8f9df..7088cb5a68 100644 --- a/apps/web/src/app/settings/change-password.component.html +++ b/apps/web/src/app/auth/settings/change-password.component.html @@ -89,12 +89,12 @@ -
-
- -
- - + +
+ +
+ + +
+
+ + +
-
- - -
-
+
diff --git a/apps/web/src/app/settings/preferences.component.ts b/apps/web/src/app/settings/preferences.component.ts index 973aed0f5f..9412a3e8b6 100644 --- a/apps/web/src/app/settings/preferences.component.ts +++ b/apps/web/src/app/settings/preferences.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { concatMap, filter, map, Observable, Subject, takeUntil, tap } from "rxjs"; +import { concatMap, filter, firstValueFrom, map, Observable, Subject, takeUntil, tap } from "rxjs"; import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { ThemeType } from "@bitwarden/common/enums"; @@ -24,6 +24,8 @@ export class PreferencesComponent implements OnInit { // For use in template protected readonly VaultTimeoutAction = VaultTimeoutAction; + protected availableVaultTimeoutActions$: Observable; + vaultTimeoutPolicyCallout: Observable<{ timeout: { hours: number; minutes: number }; action: VaultTimeoutAction; @@ -89,6 +91,9 @@ export class PreferencesComponent implements OnInit { } async ngOnInit() { + this.availableVaultTimeoutActions$ = + this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(); + this.vaultTimeoutPolicyCallout = this.policyService.get$(PolicyType.MaximumVaultTimeout).pipe( filter((policy) => policy != null), map((policy) => { @@ -133,7 +138,9 @@ export class PreferencesComponent implements OnInit { .subscribe(); const initialFormValues = { vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(), - vaultTimeoutAction: await this.vaultTimeoutSettingsService.getVaultTimeoutAction(), + vaultTimeoutAction: await firstValueFrom( + this.vaultTimeoutSettingsService.vaultTimeoutAction$() + ), enableFavicons: !(await this.settingsService.getDisableFavicon()), enableFullWidth: await this.stateService.getEnableFullWidth(), theme: await this.stateService.getTheme(), diff --git a/apps/web/src/app/settings/security-keys.component.ts b/apps/web/src/app/settings/security-keys.component.ts index 887ba3d587..2a01f6d010 100644 --- a/apps/web/src/app/settings/security-keys.component.ts +++ b/apps/web/src/app/settings/security-keys.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ApiKeyComponent } from "./api-key.component"; @@ -20,14 +20,14 @@ export class SecurityKeysComponent implements OnInit { showChangeKdf = true; constructor( - private keyConnectorService: KeyConnectorService, + private userVerificationService: UserVerificationService, private stateService: StateService, private modalService: ModalService, private apiService: ApiService ) {} async ngOnInit() { - this.showChangeKdf = !(await this.keyConnectorService.getUsesKeyConnector()); + this.showChangeKdf = await this.userVerificationService.hasMasterPassword(); } async viewUserApiKey() { diff --git a/apps/web/src/app/settings/security-routing.module.ts b/apps/web/src/app/settings/security-routing.module.ts index 1d47a0d775..d08d4be16f 100644 --- a/apps/web/src/app/settings/security-routing.module.ts +++ b/apps/web/src/app/settings/security-routing.module.ts @@ -1,9 +1,9 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { ChangePasswordComponent } from "../auth/settings/change-password.component"; import { TwoFactorSetupComponent } from "../auth/settings/two-factor-setup.component"; -import { ChangePasswordComponent } from "./change-password.component"; import { SecurityKeysComponent } from "./security-keys.component"; import { SecurityComponent } from "./security.component"; diff --git a/apps/web/src/app/settings/security.component.ts b/apps/web/src/app/settings/security.component.ts index 1c70f85eda..3237a2e6a2 100644 --- a/apps/web/src/app/settings/security.component.ts +++ b/apps/web/src/app/settings/security.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; @Component({ selector: "app-security", @@ -9,9 +9,9 @@ import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-con export class SecurityComponent { showChangePassword = true; - constructor(private keyConnectorService: KeyConnectorService) {} + constructor(private userVerificationService: UserVerificationService) {} async ngOnInit() { - this.showChangePassword = !(await this.keyConnectorService.getUsesKeyConnector()); + this.showChangePassword = await this.userVerificationService.hasMasterPassword(); } } diff --git a/apps/web/src/app/settings/update-key.component.html b/apps/web/src/app/settings/update-key.component.html index b39a5eb7e1..7b94a6dca0 100644 --- a/apps/web/src/app/settings/update-key.component.html +++ b/apps/web/src/app/settings/update-key.component.html @@ -1,4 +1,4 @@ -