diff --git a/apps/web/package.json b/apps/web/package.json index 82c447c9b4..4ae2970707 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,11 +3,8 @@ "version": "2024.6.2", "scripts": { "build:oss": "webpack", - "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", "build:oss:watch": "webpack serve", - "build:bit:watch": "webpack serve -c ../../bitwarden_license/bit-web/webpack.config.js", "build:bit:dev": "cross-env ENV=development npm run build:bit", - "build:bit:dev:analyze": "cross-env LOGGING=false webpack -c ../../bitwarden_license/bit-web/webpack.config.js --profile --json > stats.json && npx webpack-bundle-analyzer stats.json build/", "build:bit:dev:watch": "cross-env ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:bit:watch", "build:bit:qa": "cross-env NODE_ENV=production ENV=qa npm run build:bit", "build:bit:euprd": "cross-env NODE_ENV=production ENV=euprd npm run build:bit", diff --git a/apps/web/src/app/admin-console/organizations/create/organization-information.component.html b/apps/web/src/app/admin-console/organizations/create/organization-information.component.html index e0a8006081..789efd9264 100644 --- a/apps/web/src/app/admin-console/organizations/create/organization-information.component.html +++ b/apps/web/src/app/admin-console/organizations/create/organization-information.component.html @@ -12,7 +12,7 @@ - {{ "billingEmail" | i18n }} + {{ "email" | i18n }} diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 4383656bee..eb2f6a154e 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -147,6 +147,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { } canShowBillingTab(organization: Organization): boolean { + return false; // disable billing tab in Vaultwarden return canAccessBillingTab(organization); } diff --git a/apps/web/src/app/admin-console/organizations/members/people.component.ts b/apps/web/src/app/admin-console/organizations/members/people.component.ts index a47e0acd0c..06f21a5361 100644 --- a/apps/web/src/app/admin-console/organizations/members/people.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/people.component.ts @@ -182,11 +182,7 @@ export class PeopleComponent extends NewBasePeopleComponent p.organizationId === this.organization.id); this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled; - const billingMetadata = await this.billingApiService.getOrganizationBillingMetadata( - this.organization.id, - ); - - this.orgIsOnSecretsManagerStandalone = billingMetadata.isOnSecretsManagerStandalone; + this.orgIsOnSecretsManagerStandalone = false; // don't get billing metadata await this.load(); 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 7abee6b0d0..2e3b789b23 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 @@ -68,13 +68,6 @@ const routes: Routes = [ (m) => m.OrganizationReportingModule, ), }, - { - path: "billing", - loadChildren: () => - import("../../billing/organizations/organization-billing.module").then( - (m) => m.OrganizationBillingModule, - ), - }, ], }, ]; diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.html b/apps/web/src/app/admin-console/organizations/settings/account.component.html index ae27df1ce9..8a644c63e3 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.html +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.html @@ -17,7 +17,7 @@ - {{ "billingEmail" | i18n }} + {{ "email" | i18n }} diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 41cf9b9e8f..4d7f6426fd 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -87,7 +87,7 @@ export class AccountComponent { ) {} async ngOnInit() { - this.selfHosted = this.platformUtilsService.isSelfHost(); + this.selfHosted = false; // set to false so we can rename organizations this.route.params .pipe( @@ -169,6 +169,7 @@ export class AccountComponent { }; submitCollectionManagement = async () => { + return; // flexible collections are not supported by Vaultwarden // Early exit if self-hosted if (this.selfHosted) { return; 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 b228a4d135..e262fa51ff 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 @@ -2,6 +2,8 @@ import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -26,6 +28,7 @@ export class EnrollMasterPasswordReset { i18nService: I18nService, syncService: SyncService, logService: LogService, + userVerificationService: UserVerificationService, ) { const result = await UserVerificationDialogComponent.open(dialogService, { title: "enrollAccountRecovery", @@ -33,36 +36,42 @@ export class EnrollMasterPasswordReset { text: "resetPasswordEnrollmentWarning", type: "warning", }, + verificationType: { + type: "custom", + verificationFn: async (secret: VerificationWithSecret) => { + const request = + await userVerificationService.buildRequest( + secret, + ); + request.resetPasswordKey = await resetPasswordService.buildRecoveryKey( + data.organization.id, + ); + + // Process the enrollment request, which is an endpoint that is + // gated by a server-side check of the master password hash + await organizationUserService.putOrganizationUserResetPasswordEnrollment( + data.organization.id, + data.organization.userId, + request, + ); + return true; + }, + }, }); - // Handle the result of the dialog based on user action and verification success + // User canceled enrollment if (result.userAction === "cancel") { return; } - // User confirmed the dialog so check verification success + // Enrollment failed if (!result.verificationSuccess) { - // verification failed return; } - // Verification succeeded + // Enrollment succeeded try { - // This object is missing most of the properties in the - // `OrganizationUserResetPasswordEnrollmentRequest()`, but those - // properties don't carry over to the server model anyway and are - // never used by this flow. - const request = new OrganizationUserResetPasswordEnrollmentRequest(); - request.resetPasswordKey = await resetPasswordService.buildRecoveryKey(data.organization.id); - - await organizationUserService.putOrganizationUserResetPasswordEnrollment( - data.organization.id, - data.organization.userId, - request, - ); - platformUtilsService.showToast("success", null, i18nService.t("enrollPasswordResetSuccess")); - await syncService.fullSync(true); } catch (e) { logService.error(e); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 254f23eeb2..133eb4c059 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -219,6 +219,10 @@ export class AppComponent implements OnDestroy, OnInit { break; } case "showToast": + if (typeof message.text === "string" && typeof crypto.subtle === "undefined") { + message.title = "This browser requires HTTPS to use the web vault"; + message.text = "Check the Vaultwarden wiki for details on how to enable it"; + } this.toastService._showToast(message); break; case "convertAccountToKeyConnector": diff --git a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html index 4690a4e63a..9d297671d2 100644 --- a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html +++ b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html @@ -1,6 +1,6 @@
- +

- +

{{ "loginOrCreateNewAccount" | i18n }}

@@ -51,7 +51,7 @@
-
+

{{ "or" | i18n }}

- - diff --git a/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts b/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts index 88b695eb72..f9aafc450a 100644 --- a/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts @@ -112,11 +112,11 @@ export class TwoFactorAuthenticatorComponent new window.QRious({ element: document.getElementById("qr"), value: - "otpauth://totp/Bitwarden:" + + "otpauth://totp/Vaultwarden:" + Utils.encodeRFC3986URIComponent(email) + "?secret=" + encodeURIComponent(this.key) + - "&issuer=Bitwarden", + "&issuer=Vaultwarden", size: 160, }); }, 100); diff --git a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts index 78872aa6a9..eed953b91a 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts @@ -44,7 +44,7 @@ export class OrgBillingHistoryViewComponent implements OnInit, OnDestroy { return; } this.loading = true; - this.billing = await this.organizationApiService.getBilling(this.organizationId); + this.billing = null; this.loading = false; } } diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html index 1bd6b99dd1..ed391eb94c 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -6,7 +6,7 @@ >
{{ "loading" | i18n }} - +

{{ "uploadLicenseFileOrg" | i18n }}

@@ -33,12 +33,7 @@
-
+ !!plan.PasswordManager); @@ -192,6 +193,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.plan = providerDefaultPlan.type; this.product = providerDefaultPlan.product; } + end of asking /api/plans in Vaultwarden */ if (!this.createOrganization) { this.upgradeFlowPrefillForm(); @@ -263,6 +265,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } get selectableProducts() { + return null; // there are no products to select in Vaultwarden if (this.acceptingSponsorship) { const familyPlan = this.passwordManagerPlans.find( (plan) => plan.type === PlanType.FamiliesAnnually, @@ -294,6 +297,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } get selectablePlans() { + return null; // no plans to select in Vaultwarden const selectedProductType = this.formGroup.controls.product.value; const result = this.passwordManagerPlans?.filter( @@ -435,6 +439,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } get planOffersSecretsManager() { + return false; // no support for secrets manager in Vaultwarden return this.selectedSecretsManagerPlan != null; } @@ -443,6 +448,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } changedProduct() { + return; // no choice of products in Vaultwarden const selectedPlan = this.selectablePlans[0]; this.setPlanType(selectedPlan.type); @@ -562,11 +568,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { const collectionCt = collection.encryptedString; const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]); - if (this.selfHosted) { - orgId = await this.createSelfHosted(key, collectionCt, orgKeys); - } else { - orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]); - } + // always use createCloudHosted() to disable license file upload + orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]); this.platformUtilsService.showToast( "success", @@ -659,7 +662,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { request.billingEmail = this.formGroup.controls.billingEmail.value; request.initiationPath = "New organization creation in-product"; request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); + request.planType = PlanType.Free; // always select the free plan in Vaultwarden + /* there is no plan to select in Vaultwarden if (this.selectedPlan.type === PlanType.Free) { request.planType = PlanType.Free; } else { @@ -686,6 +691,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { // Secrets Manager this.buildSecretsManagerRequest(request); + end plan selection and no support for secret manager in Vaultwarden */ if (this.hasProvider) { const providerRequest = new ProviderOrganizationCreateRequest( @@ -765,6 +771,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } private upgradeFlowPrefillForm() { + return; // Vaultwarden only supports free plan if (this.acceptingSponsorship) { this.formGroup.controls.product.setValue(ProductType.Families); this.changedProduct(); diff --git a/apps/web/src/app/core/router.service.ts b/apps/web/src/app/core/router.service.ts index c0c1ec2640..fe4422a3b0 100644 --- a/apps/web/src/app/core/router.service.ts +++ b/apps/web/src/app/core/router.service.ts @@ -45,7 +45,7 @@ export class RouterService { .subscribe((event: NavigationEnd) => { this.currentUrl = event.url; - let title = i18nService.t("bitWebVault"); + let title = "Vaultwarden Web"; if (this.currentUrl.includes("/sm/")) { title = i18nService.t("bitSecretsManager"); diff --git a/apps/web/src/app/core/web-platform-utils.service.ts b/apps/web/src/app/core/web-platform-utils.service.ts index 02c7c29e34..9fd100024a 100644 --- a/apps/web/src/app/core/web-platform-utils.service.ts +++ b/apps/web/src/app/core/web-platform-utils.service.ts @@ -133,14 +133,17 @@ export class WebPlatformUtilsService implements PlatformUtilsService { } isDev(): boolean { + return false; // treat Vaultwarden as production ready return process.env.NODE_ENV === "development"; } isSelfHost(): boolean { + return true; // treat Vaultwarden as self hosted return WebPlatformUtilsService.isSelfHost(); } static isSelfHost(): boolean { + return true; // treat Vaultwarden as self hosted return process.env.ENV.toString() === "selfhosted"; } diff --git a/apps/web/src/app/layouts/frontend-layout.component.html b/apps/web/src/app/layouts/frontend-layout.component.html index 72f0f1f1da..cea0867131 100644 --- a/apps/web/src/app/layouts/frontend-layout.component.html +++ b/apps/web/src/app/layouts/frontend-layout.component.html @@ -1,6 +1,11 @@
- - © {{ year }} Bitwarden Inc.
+ Vaultwarden Web
{{ "versionNumber" | i18n: version }} +

+
+ A modified version of the Bitwarden® Web Vault for Vaultwarden (an unofficial rewrite of the + Bitwarden® server).
+ Vaultwarden is not associated with the Bitwarden® project nor Bitwarden Inc. +
diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index e2b3e7910a..fda32e9257 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -89,7 +89,12 @@ {{ "accountSettings" | i18n }}
- + {{ "getHelp" | i18n }} diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html index 17698367bf..e1ea9cbab4 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html @@ -11,7 +11,7 @@
{{ "moreFromBitwarden" | i18n }} diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html index 62d8b6a075..ec8a14c115 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html @@ -30,7 +30,7 @@
{{ "moreFromBitwarden" | i18n }} diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index 1ce8d4d227..ca8268adcc 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { Observable, combineLatest, concatMap } from "rxjs"; +import { Observable, of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -58,26 +58,7 @@ export class UserLayoutComponent implements OnInit { await this.syncService.fullSync(false); - this.hasFamilySponsorshipAvailable$ = this.organizationService.canManageSponsorships$; - - // We want to hide the subscription menu for organizations that provide premium. - // Except if the user has premium personally or has a billing history. - this.showSubscription$ = combineLatest([ - this.billingAccountProfileStateService.hasPremiumPersonally$, - this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$, - ]).pipe( - concatMap(async ([hasPremiumPersonally, hasPremiumFromOrg]) => { - const isCloud = !this.platformUtilsService.isSelfHost(); - - let billing = null; - if (isCloud) { - // TODO: We should remove the need to call this! - billing = await this.apiService.getUserBillingHistory(); - } - - const cloudAndBillingHistory = isCloud && !billing?.hasNoHistory; - return hasPremiumPersonally || !hasPremiumFromOrg || cloudAndBillingHistory; - }), - ); + this.hasFamilySponsorshipAvailable$ = of(false); // disable family Sponsorships in Vaultwarden + this.showSubscription$ = of(false); // always hide subscriptions in Vaultwarden } } diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index e543a6f083..343933b043 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -236,13 +236,6 @@ const routes: Routes = [ component: DomainRulesComponent, data: { titleId: "domainRules" }, }, - { - path: "subscription", - loadChildren: () => - import("./billing/individual/individual-billing.module").then( - (m) => m.IndividualBillingModule, - ), - }, { path: "emergency-access", children: [ diff --git a/apps/web/src/app/platform/web-environment.service.ts b/apps/web/src/app/platform/web-environment.service.ts index c2eb37eea5..54fc97943c 100644 --- a/apps/web/src/app/platform/web-environment.service.ts +++ b/apps/web/src/app/platform/web-environment.service.ts @@ -27,8 +27,17 @@ export class WebEnvironmentService extends DefaultEnvironmentService { super(stateProvider, accountService); // The web vault always uses the current location as the base url - const urls = process.env.URLS as Urls; - urls.base ??= this.win.location.origin; + // If the base URL is `https://vaultwarden.example.com/base/path/`, + // `window.location.href` should have one of the following forms: + // + // - `https://vaultwarden.example.com/base/path/` + // - `https://vaultwarden.example.com/base/path/#/some/route[?queryParam=...]` + // - `https://vaultwarden.example.com/base/path/?queryParam=...` + // + // We want to get to just `https://vaultwarden.example.com/base/path`. + let baseUrl = this.win.location.href; + baseUrl = baseUrl.replace(/(\/+|\/*#.*|\/*\?.*)$/, ""); // Strip off trailing `/`, `#`, `?` and everything after. + const urls = { base: baseUrl }; // Find the region const domain = Utils.getDomain(this.win.location.href); diff --git a/apps/web/src/app/tools/send/access.component.html b/apps/web/src/app/tools/send/access.component.html index 6fef7d361d..1deb1164ff 100644 --- a/apps/web/src/app/tools/send/access.component.html +++ b/apps/web/src/app/tools/send/access.component.html @@ -2,7 +2,7 @@
- +

View Send

@@ -66,19 +66,6 @@

{{ "sendAccessTaglineProductDesc" | i18n }} - {{ "sendAccessTaglineLearnMore" | i18n }} - Bitwarden Send - {{ "sendAccessTaglineOr" | i18n }} - {{ - "sendAccessTaglineSignUp" | i18n - }} - {{ "sendAccessTaglineTryToday" | i18n }}

diff --git a/apps/web/src/app/tools/send/add-edit.component.html b/apps/web/src/app/tools/send/add-edit.component.html index 3225b61350..2a192514bf 100644 --- a/apps/web/src/app/tools/send/add-edit.component.html +++ b/apps/web/src/app/tools/send/add-edit.component.html @@ -227,7 +227,12 @@ {{ "password" | i18n }} {{ "newPassword" | i18n }} - + {{ "sendPasswordDesc" | i18n }} diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index 8dd63e62dd..1a1e45cc35 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -10,6 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -48,6 +49,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private dialogService: DialogService, private resetPasswordService: OrganizationUserResetPasswordService, + private userVerificationService: UserVerificationService, ) {} async ngOnInit() { @@ -155,6 +157,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { this.i18nService, this.syncService, this.logService, + this.userVerificationService, ); } else { // Remove reset password diff --git a/apps/web/src/index.html b/apps/web/src/index.html index c3a2c03ed9..1a326771a6 100644 --- a/apps/web/src/index.html +++ b/apps/web/src/index.html @@ -5,7 +5,7 @@ - Bitwarden Web Vault + Vaultwarden Web @@ -17,7 +17,7 @@
- +

form:nth-child(1) > div:nth-child(3) { + @extend %vw-hide; +} + +/* Hide the `This account is owned by a business` checkbox and label */ +#ownedBusiness, +label[for^="ownedBusiness"] { + @extend %vw-hide; +} + +/* Hide the radio button and label for the `Custom` org user type */ +#userTypeCustom, +label[for^="userTypeCustom"] { + @extend %vw-hide; +} + +/* Hide Business Name */ +app-org-account form div bit-form-field.tw-block:nth-child(3) { + @extend %vw-hide; +} + +/* Hide organization plans */ +app-organization-plans > form > bit-section:nth-child(2) { + @extend %vw-hide; +} + +/* Hide Device Verification form at the Two Step Login screen */ +app-security > app-two-factor-setup > form { + @extend %vw-hide; +} + +/* Replace the Bitwarden Shield at the top left with a Vaultwarden icon */ +.bwi-shield:before { + content: "" !important; + width: 32px !important; + height: 40px !important; + display: block !important; + background-image: url(../images/icon-white.png) !important; + background-repeat: no-repeat; + background-position-y: bottom; +} +/**** END Vaultwarden CHANGES ****/ diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index 08673c3f9a..db1dd55694 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -6,7 +6,6 @@ config.content = [ "../../libs/components/src/**/*.{html,ts}", "../../libs/auth/src/**/*.{html,ts}", "../../libs/angular/src/**/*.{html,ts}", - "../../bitwarden_license/bit-web/src/**/*.{html,ts}", ]; module.exports = config; diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index f22d98f081..a9904a2ecf 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -138,8 +138,6 @@ const plugins = [ { from: "./src/favicon.ico" }, { from: "./src/browserconfig.xml" }, { from: "./src/app-id.json" }, - { from: "./src/404.html" }, - { from: "./src/404", to: "404" }, { from: "./src/images", to: "images" }, { from: "./src/locales", to: "locales" }, { from: "../../node_modules/qrious/dist/qrious.min.js", to: "scripts" }, diff --git a/clients.code-workspace b/clients.code-workspace index a424f91eeb..72f7c59185 100644 --- a/clients.code-workspace +++ b/clients.code-workspace @@ -8,18 +8,10 @@ "name": "web vault", "path": "apps/web", }, - { - "name": "web vault (bit)", - "path": "bitwarden_license/bit-web", - }, { "name": "cli", "path": "apps/cli", }, - { - "name": "cli (bit)", - "path": "bitwarden_license/bit-cli", - }, { "name": "desktop", "path": "apps/desktop", @@ -32,10 +24,6 @@ "name": "libs", "path": "libs", }, - { - "name": "common (bit)", - "path": "bitwarden_license/bit-common", - }, ], "settings": { "eslint.options": { diff --git a/jest.config.js b/jest.config.js index f4e97262a3..12e11b35a3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,9 +20,6 @@ module.exports = { "/apps/cli/jest.config.js", "/apps/desktop/jest.config.js", "/apps/web/jest.config.js", - "/bitwarden_license/bit-web/jest.config.js", - "/bitwarden_license/bit-cli/jest.config.js", - "/bitwarden_license/bit-common/jest.config.js", "/libs/admin-console/jest.config.js", "/libs/angular/jest.config.js", diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 64cd664f1f..88b042c5b8 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -17,9 +17,12 @@ import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; -import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; +import { + MasterPasswordVerification, + MasterPasswordVerificationResponse, +} from "@bitwarden/common/auth/types/verification"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -29,7 +32,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 { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums"; +import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; @@ -45,7 +48,7 @@ export class LockComponent implements OnInit, OnDestroy { pinEnabled = false; masterPasswordEnabled = false; webVaultHostname = ""; - formPromise: Promise; + formPromise: Promise; supportsBiometric: boolean; biometricLock: boolean; @@ -218,51 +221,30 @@ export class LockComponent implements OnInit, OnDestroy { } private async doUnlockWithMasterPassword() { - const kdfConfig = await this.kdfConfigService.getKdfConfig(); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const masterKey = await this.cryptoService.makeMasterKey( - this.masterPassword, - this.email, - kdfConfig, - ); - const storedMasterKeyHash = await firstValueFrom( - this.masterPasswordService.masterKeyHash$(userId), - ); + const verification = { + type: VerificationType.MasterPassword, + secret: this.masterPassword, + } as MasterPasswordVerification; let passwordValid = false; - - if (storedMasterKeyHash != null) { - // Offline unlock possible - passwordValid = await this.cryptoService.compareAndUpdateKeyHash( - this.masterPassword, - masterKey, + let response: MasterPasswordVerificationResponse; + try { + this.formPromise = this.userVerificationService.verifyUserByMasterPassword( + verification, + userId, + this.email, ); - } else { - // Online only - const request = new SecretVerificationRequest(); - const serverKeyHash = await this.cryptoService.hashMasterKey( - this.masterPassword, - masterKey, - HashPurpose.ServerAuthorization, + response = await this.formPromise; + this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse( + response.policyOptions, ); - request.masterPasswordHash = serverKeyHash; - try { - this.formPromise = this.apiService.postAccountVerifyPassword(request); - const response = await this.formPromise; - this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse(response); - passwordValid = true; - const localKeyHash = await this.cryptoService.hashMasterKey( - this.masterPassword, - masterKey, - HashPurpose.LocalAuthorization, - ); - await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); - } catch (e) { - this.logService.error(e); - } finally { - this.formPromise = null; - } + passwordValid = true; + } catch (e) { + this.logService.error(e); + } finally { + this.formPromise = null; } if (!passwordValid) { @@ -274,8 +256,9 @@ export class LockComponent implements OnInit, OnDestroy { return; } - const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); - await this.masterPasswordService.setMasterKey(masterKey, userId); + const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( + response.masterKey, + ); await this.setUserKeyAndContinue(userKey, true); } diff --git a/libs/angular/src/auth/components/register.component.ts b/libs/angular/src/auth/components/register.component.ts index e3197355dc..e3004ebdf7 100644 --- a/libs/angular/src/auth/components/register.component.ts +++ b/libs/angular/src/auth/components/register.component.ts @@ -110,6 +110,14 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn } async submit(showToast = true) { + if (typeof crypto.subtle === "undefined") { + this.platformUtilsService.showToast( + "error", + "This browser requires HTTPS to use the web vault", + "Check the Vaultwarden wiki for details on how to enable it", + ); + return; + } let email = this.formGroup.value.email; email = email.trim().toLowerCase(); let name = this.formGroup.value.name; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 048c182900..cd0bac042f 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -876,7 +876,6 @@ const safeProviders: SafeProvider[] = [ provide: UserVerificationServiceAbstraction, useClass: UserVerificationService, deps: [ - StateServiceAbstraction, CryptoServiceAbstraction, AccountServiceAbstraction, InternalMasterPasswordServiceAbstraction, diff --git a/libs/auth/src/angular/user-verification/user-verification-dialog.component.html b/libs/auth/src/angular/user-verification/user-verification-dialog.component.html index aa4d26ae61..fb3b7cf4cb 100644 --- a/libs/auth/src/angular/user-verification/user-verification-dialog.component.html +++ b/libs/auth/src/angular/user-verification/user-verification-dialog.component.html @@ -9,8 +9,8 @@ @@ -29,7 +29,7 @@ @@ -41,7 +41,7 @@ @@ -50,8 +50,8 @@ @@ -85,10 +85,12 @@ - + diff --git a/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts b/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts index 7b2c869e3a..f8746b5b24 100644 --- a/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts +++ b/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts @@ -142,6 +142,31 @@ export class UserVerificationDialogComponent { * return; * } * + * ---------------------------------------------------------- + * + * @example + * // Example 4: Custom user verification validation + * + * const result = await UserVerificationDialogComponent.open(dialogService, { + * verificationType: { + * type: "custom", + * // Pass in a function that will be used to validate the input of the + * // verification dialog, returning true when finished. + * verificationFn: async (secret: VerificationWithSecret) => { + * const request = await userVerificationService.buildRequest(secret); + * + * // ... Do something with the custom request type + * + * await someServicer.sendMyRequestThatVerfiesUserIdentity( + * // ... Some other data + * request, + * ); + * return true; + * }, + * }, + * }); + * + * // ... Evaluate the result as usual */ static async open( dialogService: DialogService, @@ -202,6 +227,18 @@ export class UserVerificationDialogComponent { } try { + if ( + typeof this.dialogOptions.verificationType === "object" && + this.dialogOptions.verificationType.type === "custom" + ) { + const success = await this.dialogOptions.verificationType.verificationFn(this.secret.value); + this.close({ + userAction: "confirm", + verificationSuccess: success, + }); + return; + } + // TODO: once we migrate all user verification scenarios to use this new implementation, // we should consider refactoring the user verification service handling of the // OTP and MP flows to not throw errors on verification failure. diff --git a/libs/auth/src/angular/user-verification/user-verification-dialog.types.ts b/libs/auth/src/angular/user-verification/user-verification-dialog.types.ts index f4637c770a..cb03f4e18f 100644 --- a/libs/auth/src/angular/user-verification/user-verification-dialog.types.ts +++ b/libs/auth/src/angular/user-verification/user-verification-dialog.types.ts @@ -1,3 +1,4 @@ +import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification"; import { ButtonType } from "@bitwarden/components"; /** @@ -60,12 +61,27 @@ export type UserVerificationDialogOptions = { */ confirmButtonOptions?: UserVerificationConfirmButtonOptions; - /** - * Indicates whether the verification is only performed client-side. Includes local MP verification, PIN, and Biometrics. - * Optional. - * **Important:** Only for use on desktop and browser platforms as when there are no client verification methods, the user is instructed to set a pin (which is not supported on web) + /** The validation method used to verify the secret. + * + * Possible values: + * + * - "default": Perform the default validation operation for the determined + * secret type. This would, for example, validate master passwords + * locally but OTPs on the server. + * - "client": Only do a client-side verification with no possible server + * request. Includes local MP verification, PIN, and Biometrics. + * **Important:** This option is only for use on desktop and browser + * platforms. When there are no client verification methods the user is + * instructed to set a pin, and this is not supported on web. + * - "custom": Custom validation is done to verify the secret. This is + * passed in from callers when opening the dialog. The custom type is + * meant to provide a mechanism where users can call a secured endpoint + * that performs user verification server side. */ - clientSideOnlyVerification?: boolean; + verificationType?: + | "default" + | "client" + | { type: "custom"; verificationFn: (secret: VerificationWithSecret) => Promise }; }; /** diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 73e4f74e63..dd1e603483 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -61,7 +61,6 @@ import { IdentityCaptchaResponse } from "../auth/models/response/identity-captch import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response"; -import { MasterPasswordPolicyResponse } from "../auth/models/response/master-password-policy.response"; import { PreloginResponse } from "../auth/models/response/prelogin.response"; import { RegisterResponse } from "../auth/models/response/register.response"; import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response"; @@ -175,9 +174,6 @@ export abstract class ApiService { postAccountKeys: (request: KeysRequest) => Promise; postAccountVerifyEmail: () => Promise; postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise; - postAccountVerifyPassword: ( - request: SecretVerificationRequest, - ) => Promise; postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; postAccountKdf: (request: KdfRequest) => Promise; diff --git a/libs/common/src/auth/abstractions/user-verification/user-verification-api.service.abstraction.ts b/libs/common/src/auth/abstractions/user-verification/user-verification-api.service.abstraction.ts index b861ce4471..ae17abe823 100644 --- a/libs/common/src/auth/abstractions/user-verification/user-verification-api.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/user-verification/user-verification-api.service.abstraction.ts @@ -1,6 +1,11 @@ +import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; import { VerifyOTPRequest } from "../../models/request/verify-otp.request"; +import { MasterPasswordPolicyResponse } from "../../models/response/master-password-policy.response"; export abstract class UserVerificationApiServiceAbstraction { postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise; postAccountRequestOTP: () => Promise; + postAccountVerifyPassword: ( + request: SecretVerificationRequest, + ) => Promise; } diff --git a/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts b/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts index 11fe537919..fd04b2e2c5 100644 --- a/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts @@ -1,19 +1,53 @@ +import { UserId } from "../../../types/guid"; import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; import { UserVerificationOptions } from "../../types/user-verification-options"; -import { Verification } from "../../types/verification"; +import { + MasterPasswordVerification, + MasterPasswordVerificationResponse, + Verification, +} from "../../types/verification"; export abstract class UserVerificationService { + /** + * Returns the available verification options for the user, can be + * restricted to a specific type of verification. + * @param verificationType Type of verification to restrict the options to + * @returns Available verification options for the user + */ + getAvailableVerificationOptions: ( + verificationType: keyof UserVerificationOptions, + ) => Promise; + /** + * Create a new request model to be used for server-side verification + * @param verification User-supplied verification data (Master Password or OTP) + * @param requestClass The request model to create + * @param alreadyHashed Whether the master password is already hashed + * @throws Error if the verification data is invalid + */ buildRequest: ( verification: Verification, requestClass?: new () => T, alreadyHashed?: boolean, ) => Promise; + /** + * Verifies the user using the provided verification data. + * PIN or biometrics are verified client-side. + * OTP is sent to the server for verification (with no other data) + * Master Password verifies client-side first if there is a MP hash, or server-side if not. + * @param verification User-supplied verification data (OTP, MP, PIN, or biometrics) + * @throws Error if the verification data is invalid or the verification fails + */ verifyUser: (verification: Verification) => Promise; + /** + * Request a one-time password (OTP) to be sent to the user's email + */ requestOTP: () => Promise; /** - * Check if user has master password or only uses passwordless technologies to log in + * Check if user has master password or can only use passwordless technologies to log in + * Note: This only checks the server, not the local state * @param userId The user id to check. If not provided, the current user is used * @returns True if the user has a master password + * @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead */ hasMasterPassword: (userId?: string) => Promise; /** @@ -22,8 +56,19 @@ export abstract class UserVerificationService { * @returns True if the user has a master password and has used it in the current session */ hasMasterPasswordAndMasterKeyHash: (userId?: string) => Promise; - - getAvailableVerificationOptions: ( - verificationType: keyof UserVerificationOptions, - ) => Promise; + /** + * Verifies the user using the provided master password. + * Attempts to verify client-side first, then server-side if necessary. + * IMPORTANT: Will throw an error if the master password is invalid. + * @param verification Master Password verification data + * @param userId The user to verify + * @param email The user's email + * @throws Error if the master password is invalid + * @returns An object containing the master key, and master password policy options if verified on server. + */ + verifyUserByMasterPassword: ( + verification: MasterPasswordVerification, + userId: UserId, + email: string, + ) => Promise; } diff --git a/libs/common/src/auth/services/user-verification/user-verification-api.service.ts b/libs/common/src/auth/services/user-verification/user-verification-api.service.ts index 0f0eb16e92..854aaed119 100644 --- a/libs/common/src/auth/services/user-verification/user-verification-api.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification-api.service.ts @@ -1,6 +1,8 @@ import { ApiService } from "../../../abstractions/api.service"; import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction"; +import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; import { VerifyOTPRequest } from "../../models/request/verify-otp.request"; +import { MasterPasswordPolicyResponse } from "../../models/response/master-password-policy.response"; export class UserVerificationApiService implements UserVerificationApiServiceAbstraction { constructor(private apiService: ApiService) {} @@ -11,4 +13,9 @@ export class UserVerificationApiService implements UserVerificationApiServiceAbs async postAccountRequestOTP(): Promise { return this.apiService.send("POST", "/accounts/request-otp", null, true, false); } + postAccountVerifyPassword( + request: SecretVerificationRequest, + ): Promise { + return this.apiService.send("POST", "/accounts/verify-password", request, true, true); + } } diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts new file mode 100644 index 0000000000..653c7a13b3 --- /dev/null +++ b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts @@ -0,0 +1,418 @@ +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { + PinLockType, + PinServiceAbstraction, + UserDecryptionOptions, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; + +import { FakeAccountService, mockAccountServiceWith } from "../../../../spec"; +import { VaultTimeoutSettingsService } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; +import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { I18nService } from "../../../platform/abstractions/i18n.service"; +import { LogService } from "../../../platform/abstractions/log.service"; +import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; +import { HashPurpose } from "../../../platform/enums"; +import { Utils } from "../../../platform/misc/utils"; +import { UserId } from "../../../types/guid"; +import { MasterKey } from "../../../types/key"; +import { KdfConfigService } from "../../abstractions/kdf-config.service"; +import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; +import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction"; +import { VerificationType } from "../../enums/verification-type"; +import { KdfConfig } from "../../models/domain/kdf-config"; +import { MasterPasswordPolicyResponse } from "../../models/response/master-password-policy.response"; +import { MasterPasswordVerification } from "../../types/verification"; + +import { UserVerificationService } from "./user-verification.service"; + +describe("UserVerificationService", () => { + let sut: UserVerificationService; + + const cryptoService = mock(); + const masterPasswordService = mock(); + const i18nService = mock(); + const userVerificationApiService = mock(); + const userDecryptionOptionsService = mock(); + const pinService = mock(); + const logService = mock(); + const vaultTimeoutSettingsService = mock(); + const platformUtilsService = mock(); + const kdfConfigService = mock(); + + const mockUserId = Utils.newGuid() as UserId; + let accountService: FakeAccountService; + + beforeEach(() => { + jest.clearAllMocks(); + accountService = mockAccountServiceWith(mockUserId); + + sut = new UserVerificationService( + cryptoService, + accountService, + masterPasswordService, + i18nService, + userVerificationApiService, + userDecryptionOptionsService, + pinService, + logService, + vaultTimeoutSettingsService, + platformUtilsService, + kdfConfigService, + ); + }); + + describe("getAvailableVerificationOptions", () => { + describe("client verification type", () => { + it("correctly returns master password availability", async () => { + setMasterPasswordAvailability(true); + setPinAvailability("DISABLED"); + disableBiometricsAvailability(); + + const result = await sut.getAvailableVerificationOptions("client"); + + expect(result).toEqual({ + client: { + masterPassword: true, + pin: false, + biometrics: false, + }, + server: { + masterPassword: false, + otp: false, + }, + }); + }); + + test.each([ + [true, "PERSISTENT"], + [true, "EPHEMERAL"], + [false, "DISABLED"], + ])( + "returns %s for PIN availability when pin lock type is %s", + async (expectedPin: boolean, pinLockType: PinLockType) => { + setMasterPasswordAvailability(false); + setPinAvailability(pinLockType); + disableBiometricsAvailability(); + + const result = await sut.getAvailableVerificationOptions("client"); + + expect(result).toEqual({ + client: { + masterPassword: false, + pin: expectedPin, + biometrics: false, + }, + server: { + masterPassword: false, + otp: false, + }, + }); + }, + ); + + test.each([ + [true, true, true, true], + [true, true, true, false], + [true, true, false, false], + [false, true, false, true], + [false, false, false, false], + [false, false, true, false], + [false, false, false, true], + ])( + "returns %s for biometrics availability when isBiometricLockSet is %s, hasUserKeyStored is %s, and supportsSecureStorage is %s", + async ( + expectedReturn: boolean, + isBiometricsLockSet: boolean, + isBiometricsUserKeyStored: boolean, + platformSupportSecureStorage: boolean, + ) => { + setMasterPasswordAvailability(false); + setPinAvailability("DISABLED"); + vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(isBiometricsLockSet); + cryptoService.hasUserKeyStored.mockResolvedValue(isBiometricsUserKeyStored); + platformUtilsService.supportsSecureStorage.mockReturnValue(platformSupportSecureStorage); + + const result = await sut.getAvailableVerificationOptions("client"); + + expect(result).toEqual({ + client: { + masterPassword: false, + pin: false, + biometrics: expectedReturn, + }, + server: { + masterPassword: false, + otp: false, + }, + }); + }, + ); + }); + + describe("server verification type", () => { + it("correctly returns master password availability", async () => { + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + of({ + hasMasterPassword: true, + } as UserDecryptionOptions), + ); + + const result = await sut.getAvailableVerificationOptions("server"); + + expect(result).toEqual({ + client: { + masterPassword: false, + pin: false, + biometrics: false, + }, + server: { + masterPassword: true, + otp: false, + }, + }); + }); + + it("correctly returns OTP availability", async () => { + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + of({ + hasMasterPassword: false, + } as UserDecryptionOptions), + ); + + const result = await sut.getAvailableVerificationOptions("server"); + + expect(result).toEqual({ + client: { + masterPassword: false, + pin: false, + biometrics: false, + }, + server: { + masterPassword: false, + otp: true, + }, + }); + }); + }); + }); + + describe("verifyUserByMasterPassword", () => { + beforeAll(() => { + i18nService.t.calledWith("invalidMasterPassword").mockReturnValue("Invalid master password"); + + kdfConfigService.getKdfConfig.mockResolvedValue("kdfConfig" as unknown as KdfConfig); + masterPasswordService.masterKey$.mockReturnValue(of("masterKey" as unknown as MasterKey)); + cryptoService.hashMasterKey + .calledWith("password", "masterKey" as unknown as MasterKey, HashPurpose.LocalAuthorization) + .mockResolvedValue("localHash"); + }); + + describe("client-side verification", () => { + beforeEach(() => { + setMasterPasswordAvailability(true); + }); + + it("returns if verification is successful", async () => { + cryptoService.compareAndUpdateKeyHash.mockResolvedValueOnce(true); + + const result = await sut.verifyUserByMasterPassword( + { + type: VerificationType.MasterPassword, + secret: "password", + } as MasterPasswordVerification, + mockUserId, + "email", + ); + + expect(cryptoService.compareAndUpdateKeyHash).toHaveBeenCalled(); + expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith( + "localHash", + mockUserId, + ); + expect(masterPasswordService.setMasterKey).toHaveBeenCalledWith("masterKey", mockUserId); + expect(result).toEqual({ + policyOptions: null, + masterKey: "masterKey", + }); + }); + + it("throws if verification fails", async () => { + cryptoService.compareAndUpdateKeyHash.mockResolvedValueOnce(false); + + await expect( + sut.verifyUserByMasterPassword( + { + type: VerificationType.MasterPassword, + secret: "password", + } as MasterPasswordVerification, + mockUserId, + "email", + ), + ).rejects.toThrow("Invalid master password"); + + expect(cryptoService.compareAndUpdateKeyHash).toHaveBeenCalled(); + expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith(); + expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith(); + }); + }); + + describe("server-side verification", () => { + beforeEach(() => { + setMasterPasswordAvailability(false); + }); + + it("returns if verification is successful", async () => { + cryptoService.hashMasterKey + .calledWith( + "password", + "masterKey" as unknown as MasterKey, + HashPurpose.ServerAuthorization, + ) + .mockResolvedValueOnce("serverHash"); + userVerificationApiService.postAccountVerifyPassword.mockResolvedValueOnce( + "MasterPasswordPolicyOptions" as unknown as MasterPasswordPolicyResponse, + ); + + const result = await sut.verifyUserByMasterPassword( + { + type: VerificationType.MasterPassword, + secret: "password", + } as MasterPasswordVerification, + mockUserId, + "email", + ); + + expect(cryptoService.compareAndUpdateKeyHash).not.toHaveBeenCalled(); + expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith( + "localHash", + mockUserId, + ); + expect(masterPasswordService.setMasterKey).toHaveBeenCalledWith("masterKey", mockUserId); + expect(result).toEqual({ + policyOptions: "MasterPasswordPolicyOptions", + masterKey: "masterKey", + }); + }); + + it("throws if verification fails", async () => { + cryptoService.hashMasterKey + .calledWith( + "password", + "masterKey" as unknown as MasterKey, + HashPurpose.ServerAuthorization, + ) + .mockResolvedValueOnce("serverHash"); + userVerificationApiService.postAccountVerifyPassword.mockRejectedValueOnce(new Error()); + + await expect( + sut.verifyUserByMasterPassword( + { + type: VerificationType.MasterPassword, + secret: "password", + } as MasterPasswordVerification, + mockUserId, + "email", + ), + ).rejects.toThrow("Invalid master password"); + + expect(cryptoService.compareAndUpdateKeyHash).not.toHaveBeenCalled(); + expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith(); + expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith(); + }); + }); + + describe("error handling", () => { + it("throws if any of the parameters are nullish", async () => { + await expect( + sut.verifyUserByMasterPassword( + { + type: VerificationType.MasterPassword, + secret: null, + } as MasterPasswordVerification, + mockUserId, + "email", + ), + ).rejects.toThrow( + "Master Password is required. Cannot verify user without a master password.", + ); + + await expect( + sut.verifyUserByMasterPassword( + { + type: VerificationType.MasterPassword, + secret: "password", + } as MasterPasswordVerification, + null, + "email", + ), + ).rejects.toThrow("User ID is required. Cannot verify user by master password."); + + await expect( + sut.verifyUserByMasterPassword( + { + type: VerificationType.MasterPassword, + secret: "password", + } as MasterPasswordVerification, + mockUserId, + null, + ), + ).rejects.toThrow("Email is required. Cannot verify user by master password."); + }); + + it("throws if kdf config is not available", async () => { + kdfConfigService.getKdfConfig.mockResolvedValueOnce(null); + + await expect( + sut.verifyUserByMasterPassword( + { + type: VerificationType.MasterPassword, + secret: "password", + } as MasterPasswordVerification, + mockUserId, + "email", + ), + ).rejects.toThrow("KDF config is required. Cannot verify user by master password."); + }); + + it("throws if master key cannot be created", async () => { + kdfConfigService.getKdfConfig.mockResolvedValueOnce("kdfConfig" as unknown as KdfConfig); + masterPasswordService.masterKey$.mockReturnValueOnce(of(null)); + cryptoService.makeMasterKey.mockResolvedValueOnce(null); + + await expect( + sut.verifyUserByMasterPassword( + { + type: VerificationType.MasterPassword, + secret: "password", + } as MasterPasswordVerification, + mockUserId, + "email", + ), + ).rejects.toThrow("Master key could not be created to verify the master password."); + }); + }); + }); + + // Helpers + function setMasterPasswordAvailability(hasMasterPassword: boolean) { + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + of({ + hasMasterPassword: hasMasterPassword, + } as UserDecryptionOptions), + ); + masterPasswordService.masterKeyHash$.mockReturnValue( + of(hasMasterPassword ? "masterKeyHash" : null), + ); + } + + function setPinAvailability(type: PinLockType) { + pinService.getPinLockType.mockResolvedValue(type); + } + + function disableBiometricsAvailability() { + vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(false); + } +}); diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 85640519ec..50fe7b3add 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -8,7 +8,7 @@ import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { LogService } from "../../../platform/abstractions/log.service"; import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; -import { StateService } from "../../../platform/abstractions/state.service"; +import { HashPurpose } from "../../../platform/enums"; import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enum"; import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; @@ -20,9 +20,11 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from ". import { VerificationType } from "../../enums/verification-type"; import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; import { VerifyOTPRequest } from "../../models/request/verify-otp.request"; +import { MasterPasswordPolicyResponse } from "../../models/response/master-password-policy.response"; import { UserVerificationOptions } from "../../types/user-verification-options"; import { MasterPasswordVerification, + MasterPasswordVerificationResponse, OtpVerification, PinVerification, ServerSideVerification, @@ -37,7 +39,6 @@ import { */ export class UserVerificationService implements UserVerificationServiceAbstraction { constructor( - private stateService: StateService, private cryptoService: CryptoService, private accountService: AccountService, private masterPasswordService: InternalMasterPasswordServiceAbstraction, @@ -54,14 +55,14 @@ export class UserVerificationService implements UserVerificationServiceAbstracti async getAvailableVerificationOptions( verificationType: keyof UserVerificationOptions, ): Promise { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; if (verificationType === "client") { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const [userHasMasterPassword, pinLockType, biometricsLockSet, biometricsUserKeyStored] = await Promise.all([ - this.hasMasterPasswordAndMasterKeyHash(), + this.hasMasterPasswordAndMasterKeyHash(userId), this.pinService.getPinLockType(userId), - this.vaultTimeoutSettingsService.isBiometricLockSet(), - this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric), + this.vaultTimeoutSettingsService.isBiometricLockSet(userId), + this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric, userId), ]); // note: we do not need to check this.platformUtilsService.supportsBiometric() because @@ -83,7 +84,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } else { // server // Don't check if have MP hash locally, because we are going to send the secret to the server to be verified. - const userHasMasterPassword = await this.hasMasterPassword(); + const userHasMasterPassword = await this.hasMasterPassword(userId); return { client: { @@ -96,12 +97,6 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } } - /** - * Create a new request model to be used for server-side verification - * @param verification User-supplied verification data (Master Password or OTP) - * @param requestClass The request model to create - * @param alreadyHashed Whether the master password is already hashed - */ async buildRequest( verification: ServerSideVerification, requestClass?: new () => T, @@ -134,11 +129,6 @@ export class UserVerificationService implements UserVerificationServiceAbstracti return request; } - /** - * Used to verify Master Password, PIN, or biometrics client-side, or send the OTP to the server for verification (with no other data) - * Generally used for client-side verification only. - * @param verification User-supplied verification data (OTP, MP, PIN, or biometrics) - */ async verifyUser(verification: Verification): Promise { if (verification == null) { throw new Error("Verification is required."); @@ -156,7 +146,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti case VerificationType.OTP: return this.verifyUserByOTP(verification); case VerificationType.MasterPassword: - return this.verifyUserByMasterPassword(verification, userId, email); + await this.verifyUserByMasterPassword(verification, userId, email); + return true; case VerificationType.PIN: return this.verifyUserByPIN(verification, userId); case VerificationType.Biometrics: @@ -179,33 +170,70 @@ export class UserVerificationService implements UserVerificationServiceAbstracti return true; } - private async verifyUserByMasterPassword( + async verifyUserByMasterPassword( verification: MasterPasswordVerification, userId: UserId, email: string, - ): Promise { + ): Promise { + if (!verification.secret) { + throw new Error("Master Password is required. Cannot verify user without a master password."); + } if (!userId) { throw new Error("User ID is required. Cannot verify user by master password."); } + if (!email) { + throw new Error("Email is required. Cannot verify user by master password."); + } + + const kdfConfig = await this.kdfConfigService.getKdfConfig(); + if (!kdfConfig) { + throw new Error("KDF config is required. Cannot verify user by master password."); + } let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (!masterKey) { - masterKey = await this.cryptoService.makeMasterKey( + masterKey = await this.cryptoService.makeMasterKey(verification.secret, email, kdfConfig); + } + + if (!masterKey) { + throw new Error("Master key could not be created to verify the master password."); + } + + let policyOptions: MasterPasswordPolicyResponse | null; + // Client-side verification + if (await this.hasMasterPasswordAndMasterKeyHash(userId)) { + const passwordValid = await this.cryptoService.compareAndUpdateKeyHash( verification.secret, - email, - await this.kdfConfigService.getKdfConfig(), + masterKey, ); + if (!passwordValid) { + throw new Error(this.i18nService.t("invalidMasterPassword")); + } + policyOptions = null; + } else { + // Server-side verification + const request = new SecretVerificationRequest(); + const serverKeyHash = await this.cryptoService.hashMasterKey( + verification.secret, + masterKey, + HashPurpose.ServerAuthorization, + ); + request.masterPasswordHash = serverKeyHash; + try { + policyOptions = await this.userVerificationApiService.postAccountVerifyPassword(request); + } catch (e) { + throw new Error(this.i18nService.t("invalidMasterPassword")); + } } - const passwordValid = await this.cryptoService.compareAndUpdateKeyHash( + + const localKeyHash = await this.cryptoService.hashMasterKey( verification.secret, masterKey, + HashPurpose.LocalAuthorization, ); - if (!passwordValid) { - throw new Error(this.i18nService.t("invalidMasterPassword")); - } - // TODO: we should re-evaluate later on if user verification should have the side effect of modifying state. Probably not. + await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); await this.masterPasswordService.setMasterKey(masterKey, userId); - return true; + return { policyOptions, masterKey }; } private async verifyUserByPIN(verification: PinVerification, userId: UserId): Promise { @@ -236,13 +264,6 @@ export class UserVerificationService implements UserVerificationServiceAbstracti await this.userVerificationApiService.postAccountRequestOTP(); } - /** - * Check if user has master password or can only use passwordless technologies to log in - * Note: This only checks the server, not the local state - * @param userId The user id to check. If not provided, the current user is used - * @returns True if the user has a master password - * @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead - */ async hasMasterPassword(userId?: string): Promise { if (userId) { const decryptionOptions = await firstValueFrom( diff --git a/libs/common/src/auth/types/verification.ts b/libs/common/src/auth/types/verification.ts index 8bb0813be7..2dddd5fb91 100644 --- a/libs/common/src/auth/types/verification.ts +++ b/libs/common/src/auth/types/verification.ts @@ -1,4 +1,6 @@ +import { MasterKey } from "../../types/key"; import { VerificationType } from "../enums/verification-type"; +import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response"; export type OtpVerification = { type: VerificationType.OTP; secret: string }; export type MasterPasswordVerification = { type: VerificationType.MasterPassword; secret: string }; @@ -17,3 +19,8 @@ export function verificationHasSecret( } export type ServerSideVerification = OtpVerification | MasterPasswordVerification; + +export type MasterPasswordVerificationResponse = { + masterKey: MasterKey; + policyOptions: MasterPasswordPolicyResponse; +}; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index bae9a34c10..a2b37f0a1d 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -70,7 +70,6 @@ import { IdentityCaptchaResponse } from "../auth/models/response/identity-captch import { IdentityTokenResponse } from "../auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response"; import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response"; -import { MasterPasswordPolicyResponse } from "../auth/models/response/master-password-policy.response"; import { PreloginResponse } from "../auth/models/response/prelogin.response"; import { RegisterResponse } from "../auth/models/response/register.response"; import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response"; @@ -424,12 +423,6 @@ export class ApiService implements ApiServiceAbstraction { return this.send("POST", "/accounts/verify-email-token", request, false, false); } - postAccountVerifyPassword( - request: SecretVerificationRequest, - ): Promise { - return this.send("POST", "/accounts/verify-password", request, true, true); - } - postAccountRecoverDelete(request: DeleteRecoverRequest): Promise { return this.send("POST", "/accounts/delete-recover", request, false, false); } diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 00ab2ff717..0950b9d787 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -14,16 +14,16 @@ --color-background: 255 255 255; --color-background-alt: 251 251 251; --color-background-alt2: 23 92 219; - --color-background-alt3: 18 82 163; - --color-background-alt4: 13 60 119; + --color-background-alt3: 33 37 41; /* bg of menu panel */ + --color-background-alt4: 16 18 21; /* bg of active menu item */ /* Can only be used behind the extension refresh flag */ --color-primary-100: 200 217 249; - --color-primary-300: 103 149 232; + --color-primary-300: 108 117 125; /* hover of menu items */ /* Can only be used behind the extension refresh flag */ --color-primary-500: 23 93 220; - --color-primary-600: 23 93 220; - --color-primary-700: 18 82 163; + --color-primary-600: 18 82 163; /* color of links and buttons */ + --color-primary-700: 13 60 119; /* hover of links and buttons */ --color-secondary-100: 240 240 240; --color-secondary-300: 206 212 220; diff --git a/libs/components/tailwind.config.js b/libs/components/tailwind.config.js index 7a53c82ec5..9d0a337bd2 100644 --- a/libs/components/tailwind.config.js +++ b/libs/components/tailwind.config.js @@ -6,7 +6,6 @@ config.content = [ "libs/auth/src/**/*.{html,ts,mdx}", "apps/web/src/**/*.{html,ts,mdx}", "apps/browser/src/**/*.{html,ts,mdx}", - "bitwarden_license/bit-web/src/**/*.{html,ts,mdx}", ".storybook/preview.tsx", ]; config.safelist = [ diff --git a/tailwind.config.js b/tailwind.config.js index 50d82bf7d8..9b543ed950 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -9,7 +9,6 @@ config.content = [ "./libs/platform/src/**/*.{html,ts,mdx}", "./libs/vault/src/**/*.{html,ts,mdx}", "./apps/web/src/**/*.{html,ts,mdx}", - "./bitwarden_license/bit-web/src/**/*.{html,ts,mdx}", "./.storybook/preview.js", ]; config.safelist = [ diff --git a/tsconfig.json b/tsconfig.json index 8085b9f832..81a954ec84 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,8 +33,7 @@ "@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"], "@bitwarden/node/*": ["./libs/node/src/*"], "@bitwarden/web-vault/*": ["./apps/web/src/*"], - "@bitwarden/vault": ["./libs/vault/src"], - "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"] + "@bitwarden/vault": ["./libs/vault/src"] }, "plugins": [ { @@ -43,13 +42,7 @@ ], "useDefineForClassFields": false }, - "include": [ - "apps/web/src/**/*", - "apps/browser/src/**/*", - "libs/*/src/**/*", - "bitwarden_license/bit-web/src/**/*", - "bitwarden_license/bit-common/src/**/*" - ], + "include": ["apps/web/src/**/*", "apps/browser/src/**/*", "libs/*/src/**/*"], "exclude": [ "apps/web/src/**/*.spec.ts", "apps/browser/src/**/*.spec.ts",