diff --git a/.github/renovate.json b/.github/renovate.json index 95fd2dc11e1..e202e026675 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -69,14 +69,7 @@ "reviewers": ["team:team-admin-console-dev"] }, { - "matchPackageNames": [ - "@types/duo_web_sdk", - "@types/node-ipc", - "duo_web_sdk", - "node-ipc", - "qrious", - "regedit" - ], + "matchPackageNames": ["@types/node-ipc", "node-ipc", "qrious", "regedit"], "description": "Auth owned dependencies", "commitMessagePrefix": "[deps] Auth:", "reviewers": ["team:team-auth-dev"] diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 7a0c059a5c8..64f039bb8b2 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, diff --git a/apps/browser/src/auth/popup/two-factor.component.html b/apps/browser/src/auth/popup/two-factor.component.html index 3047ddfd7e1..126b0ea5a99 100644 --- a/apps/browser/src/auth/popup/two-factor.component.html +++ b/apps/browser/src/auth/popup/two-factor.component.html @@ -111,7 +111,7 @@ -
+

{{ "duoRequiredForAccount" | i18n }}

@@ -127,17 +127,6 @@
- -
- -
- - -
-
@@ -158,7 +147,7 @@
- +
@@ -148,10 +140,7 @@
-
+
+ diff --git a/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.component.ts b/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.component.ts index 0caf39ea794..16e9f76d217 100644 --- a/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.component.ts @@ -1,83 +1,28 @@ -import { Component } from "@angular/core"; -import { UntypedFormBuilder } from "@angular/forms"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { EventType } from "@bitwarden/common/enums"; -import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; +import { ExportComponent } from "@bitwarden/vault-export-ui"; -import { ExportComponent } from "../../../../tools/vault-export/export.component"; +import { LooseComponentsModule, SharedModule } from "../../../../shared"; @Component({ - selector: "app-org-export", - templateUrl: "../../../../tools/vault-export/export.component.html", + templateUrl: "org-vault-export.component.html", + standalone: true, + imports: [SharedModule, ExportComponent, LooseComponentsModule], }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class OrganizationVaultExportComponent extends ExportComponent { - constructor( - i18nService: I18nService, - toastService: ToastService, - exportService: VaultExportServiceAbstraction, - eventCollectionService: EventCollectionService, - private route: ActivatedRoute, - policyService: PolicyService, - logService: LogService, - formBuilder: UntypedFormBuilder, - fileDownloadService: FileDownloadService, - dialogService: DialogService, - organizationService: OrganizationService, - ) { - super( - i18nService, - toastService, - exportService, - eventCollectionService, - policyService, - logService, - formBuilder, - fileDownloadService, - dialogService, - organizationService, - ); - } +export class OrganizationVaultExportComponent implements OnInit { + protected routeOrgId: string = null; + protected loading = false; + protected disabled = false; - protected get disabledByPolicy(): boolean { - return false; - } + constructor(private route: ActivatedRoute) {} async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent.parent.params.subscribe(async (params) => { - this.organizationId = params.organizationId; - }); - - await super.ngOnInit(); + this.routeOrgId = this.route.snapshot.paramMap.get("organizationId"); } - getExportData() { - return this.exportService.getOrganizationExport( - this.organizationId, - this.format, - this.filePassword, - ); - } - - getFileName() { - return super.getFileName("org"); - } - - async collectEvent(): Promise { - await this.eventCollectionService.collect( - EventType.Organization_ClientExportedVault, - null, - null, - this.organizationId, - ); - } + /** + * Callback that is called after a successful export. + */ + protected async onSuccessfulExport(organizationId: string): Promise {} } diff --git a/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.module.ts b/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.module.ts deleted file mode 100644 index ca8a75165b5..00000000000 --- a/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NgModule } from "@angular/core"; - -import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui"; - -import { LooseComponentsModule, SharedModule } from "../../../../shared"; - -import { OrganizationVaultExportRoutingModule } from "./org-vault-export-routing.module"; -import { OrganizationVaultExportComponent } from "./org-vault-export.component"; - -@NgModule({ - imports: [ - SharedModule, - LooseComponentsModule, - OrganizationVaultExportRoutingModule, - ExportScopeCalloutComponent, - ], - declarations: [OrganizationVaultExportComponent], -}) -export class OrganizationVaultExportModule {} diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts index e9b35dd33d5..2a615ef397a 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts @@ -5,17 +5,11 @@ import { Subject, takeUntil } from "rxjs"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { Argon2KdfConfig, + DEFAULT_KDF_CONFIG, KdfConfig, PBKDF2KdfConfig, } from "@bitwarden/common/auth/models/domain/kdf-config"; -import { - DEFAULT_KDF_CONFIG, - PBKDF2_ITERATIONS, - ARGON2_ITERATIONS, - ARGON2_MEMORY, - ARGON2_PARALLELISM, - KdfType, -} from "@bitwarden/common/platform/enums"; +import { KdfType } from "@bitwarden/common/platform/enums"; import { DialogService } from "@bitwarden/components"; import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.component"; @@ -36,30 +30,34 @@ export class ChangeKdfComponent implements OnInit { this.kdfConfig.iterations, [ Validators.required, - Validators.min(PBKDF2_ITERATIONS.min), - Validators.max(PBKDF2_ITERATIONS.max), + Validators.min(PBKDF2KdfConfig.ITERATIONS.min), + Validators.max(PBKDF2KdfConfig.ITERATIONS.max), ], ], memory: [ null as number, - [Validators.required, Validators.min(ARGON2_MEMORY.min), Validators.max(ARGON2_MEMORY.max)], + [ + Validators.required, + Validators.min(Argon2KdfConfig.MEMORY.min), + Validators.max(Argon2KdfConfig.MEMORY.max), + ], ], parallelism: [ null as number, [ Validators.required, - Validators.min(ARGON2_PARALLELISM.min), - Validators.max(ARGON2_PARALLELISM.max), + Validators.min(Argon2KdfConfig.PARALLELISM.min), + Validators.max(Argon2KdfConfig.PARALLELISM.max), ], ], }), }); // Default values for template - protected PBKDF2_ITERATIONS = PBKDF2_ITERATIONS; - protected ARGON2_ITERATIONS = ARGON2_ITERATIONS; - protected ARGON2_MEMORY = ARGON2_MEMORY; - protected ARGON2_PARALLELISM = ARGON2_PARALLELISM; + protected PBKDF2_ITERATIONS = PBKDF2KdfConfig.ITERATIONS; + protected ARGON2_ITERATIONS = Argon2KdfConfig.ITERATIONS; + protected ARGON2_MEMORY = Argon2KdfConfig.MEMORY; + protected ARGON2_PARALLELISM = Argon2KdfConfig.PARALLELISM; constructor( private dialogService: DialogService, @@ -97,26 +95,26 @@ export class ChangeKdfComponent implements OnInit { config = new PBKDF2KdfConfig(); validators.iterations = [ Validators.required, - Validators.min(PBKDF2_ITERATIONS.min), - Validators.max(PBKDF2_ITERATIONS.max), + Validators.min(PBKDF2KdfConfig.ITERATIONS.min), + Validators.max(PBKDF2KdfConfig.ITERATIONS.max), ]; break; case KdfType.Argon2id: config = new Argon2KdfConfig(); validators.iterations = [ Validators.required, - Validators.min(ARGON2_ITERATIONS.min), - Validators.max(ARGON2_ITERATIONS.max), + Validators.min(Argon2KdfConfig.ITERATIONS.min), + Validators.max(Argon2KdfConfig.ITERATIONS.max), ]; validators.memory = [ Validators.required, - Validators.min(ARGON2_MEMORY.min), - Validators.max(ARGON2_MEMORY.max), + Validators.min(Argon2KdfConfig.MEMORY.min), + Validators.max(Argon2KdfConfig.MEMORY.max), ]; validators.parallelism = [ Validators.required, - Validators.min(ARGON2_PARALLELISM.min), - Validators.max(ARGON2_PARALLELISM.max), + Validators.min(Argon2KdfConfig.PARALLELISM.min), + Validators.max(Argon2KdfConfig.PARALLELISM.max), ]; break; default: diff --git a/apps/web/src/app/auth/settings/two-factor-duo.component.ts b/apps/web/src/app/auth/settings/two-factor-duo.component.ts index 6dbfa668563..9a1ec6c4da6 100644 --- a/apps/web/src/app/auth/settings/two-factor-duo.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-duo.component.ts @@ -59,8 +59,8 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent { protected async enable() { const request = await this.buildRequestModel(UpdateTwoFactorDuoRequest); - request.integrationKey = this.clientId; - request.secretKey = this.clientSecret; + request.clientId = this.clientId; + request.clientSecret = this.clientSecret; request.host = this.host; return super.enable(async () => { @@ -78,8 +78,8 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent { } private processResponse(response: TwoFactorDuoResponse) { - this.clientId = response.integrationKey; - this.clientSecret = response.secretKey; + this.clientId = response.clientId; + this.clientSecret = response.clientSecret; this.host = response.host; this.enabled = response.enabled; } diff --git a/apps/web/src/app/auth/settings/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor-setup.component.ts index b1592dc72ac..10f113d4963 100644 --- a/apps/web/src/app/auth/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-setup.component.ts @@ -39,8 +39,6 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { @ViewChild("duoTemplate", { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef; @ViewChild("emailTemplate", { read: ViewContainerRef, static: true }) emailModalRef: ViewContainerRef; - @ViewChild("webAuthnTemplate", { read: ViewContainerRef, static: true }) - webAuthnModalRef: ViewContainerRef; organizationId: string; organization: Organization; @@ -192,12 +190,11 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { if (!result) { return; } - const webAuthnComp = await this.openModal( - this.webAuthnModalRef, - TwoFactorWebAuthnComponent, + const webAuthnComp: DialogRef = TwoFactorWebAuthnComponent.open( + this.dialogService, + { data: result }, ); - webAuthnComp.auth(result); - webAuthnComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { + webAuthnComp.componentInstance.onChangeStatus.subscribe((enabled: boolean) => { this.updateStatus(enabled, TwoFactorProviderType.WebAuthn); }); break; diff --git a/apps/web/src/app/auth/settings/two-factor-webauthn.component.html b/apps/web/src/app/auth/settings/two-factor-webauthn.component.html index f5a84397c80..9dc9bd40684 100644 --- a/apps/web/src/app/auth/settings/two-factor-webauthn.component.html +++ b/apps/web/src/app/auth/settings/two-factor-webauthn.component.html @@ -1,152 +1,118 @@ - + - + {{ "remove" | i18n }} + + + +
+

{{ "twoFactorWebAuthnAdd" | i18n }}:

+
    +
  1. {{ "twoFactorU2fGiveName" | i18n }}
  2. +
  3. {{ "twoFactorU2fPlugInReadKey" | i18n }}
  4. +
  5. {{ "twoFactorU2fTouchButton" | i18n }}
  6. +
  7. {{ "twoFactorU2fSaveForm" | i18n }}
  8. +
+
+ + {{ "name" | i18n }} + + +
+ + + + + + + + {{ "twoFactorU2fWaiting" | i18n }}... + + + + {{ "twoFactorU2fClickSave" | i18n }} + + + + {{ "twoFactorU2fProblemReadingTryAgain" | i18n }} + + + + + + + + + + diff --git a/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts b/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts index fc2de267210..5e8ea37e930 100644 --- a/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts @@ -1,4 +1,6 @@ -import { Component, NgZone } from "@angular/core"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, EventEmitter, Inject, NgZone, Output } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; @@ -31,6 +33,7 @@ interface Key { templateUrl: "two-factor-webauthn.component.html", }) export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { + @Output() onChangeStatus = new EventEmitter(); type = TwoFactorProviderType.WebAuthn; name: string; keys: Key[]; @@ -44,7 +47,13 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { override componentName = "app-two-factor-webauthn"; + protected formGroup = new FormGroup({ + name: new FormControl({ value: "", disabled: !this.keyIdAvailable }), + }); + constructor( + @Inject(DIALOG_DATA) protected data: AuthResponse, + private dialogRef: DialogRef, apiService: ApiService, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -61,6 +70,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { userVerificationService, dialogService, ); + this.auth(data); } auth(authResponse: AuthResponse) { @@ -68,7 +78,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { this.processResponse(authResponse.response); } - async submit() { + submit = async () => { if (this.webAuthnResponse == null || this.keyIdAvailable == null) { // Should never happen. return Promise.reject(); @@ -76,16 +86,28 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest); request.deviceResponse = this.webAuthnResponse; request.id = this.keyIdAvailable; - request.name = this.name; + request.name = this.formGroup.value.name; + return this.enableWebAuth(request); + }; + + private enableWebAuth(request: any) { return super.enable(async () => { this.formPromise = this.apiService.putTwoFactorWebAuthn(request); const response = await this.formPromise; - await this.processResponse(response); + this.processResponse(response); }); } - disable() { + disable = async () => { + await this.disableWebAuth(); + if (!this.enabled) { + this.onChangeStatus.emit(this.enabled); + this.dialogRef.close(); + } + }; + + private async disableWebAuth() { return super.disable(this.formPromise); } @@ -116,19 +138,15 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { } } - async readKey() { + readKey = async () => { if (this.keyIdAvailable == null) { return; } const request = await this.buildRequestModel(SecretVerificationRequest); - try { - this.challengePromise = this.apiService.getTwoFactorWebAuthnChallenge(request); - const challenge = await this.challengePromise; - this.readDevice(challenge); - } catch (e) { - this.logService.error(e); - } - } + this.challengePromise = this.apiService.getTwoFactorWebAuthnChallenge(request); + const challenge = await this.challengePromise; + this.readDevice(challenge); + }; private readDevice(webAuthnChallenge: ChallengeResponse) { // eslint-disable-next-line @@ -164,7 +182,8 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { this.resetWebAuthn(); this.keys = []; this.keyIdAvailable = null; - this.name = null; + this.formGroup.get("name").enable(); + this.formGroup.get("name").setValue(null); this.keysConfiguredCount = 0; for (let i = 1; i <= 5; i++) { if (response.keys != null) { @@ -187,5 +206,13 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { } } this.enabled = response.enabled; + this.onChangeStatus.emit(this.enabled); + } + + static open( + dialogService: DialogService, + config: DialogConfig>, + ) { + return dialogService.open(TwoFactorWebAuthnComponent, config); } } diff --git a/apps/web/src/app/auth/two-factor.component.html b/apps/web/src/app/auth/two-factor.component.html index 639e2e9de79..b09ab68c312 100644 --- a/apps/web/src/app/auth/two-factor.component.html +++ b/apps/web/src/app/auth/two-factor.component.html @@ -54,25 +54,14 @@ - -

- {{ "duoRequiredByOrgForAccount" | i18n }} -

-

{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}

-
- - -
- -
-
+

+ {{ "duoRequiredByOrgForAccount" | i18n }} +

+

{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}

{{ "rememberMe" | i18n }} @@ -107,7 +96,7 @@ buttonType="primary" bitButton bitFormButton - *ngIf="duoFrameless && isDuoProvider" + *ngIf="isDuoProvider" > {{ "launchDuo" | i18n }} diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index e7236348a6c..dee6228530b 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -448,8 +448,13 @@ const routes: Routes = [ }, { path: "export", - loadChildren: () => - import("./tools/vault-export/export.module").then((m) => m.ExportModule), + loadComponent: () => + import("./tools/vault-export/export-web.component").then( + (mod) => mod.ExportWebComponent, + ), + data: { + titleId: "exportVault", + } satisfies DataProperties, }, { path: "generator", diff --git a/apps/web/src/app/tools/vault-export/export-routing.module.ts b/apps/web/src/app/tools/vault-export/export-routing.module.ts deleted file mode 100644 index 3afda4a06f2..00000000000 --- a/apps/web/src/app/tools/vault-export/export-routing.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NgModule } from "@angular/core"; -import { RouterModule, Routes } from "@angular/router"; - -import { ExportComponent } from "./export.component"; - -const routes: Routes = [ - { - path: "", - component: ExportComponent, - data: { titleId: "exportVault" }, - }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], -}) -export class ExportRoutingModule {} diff --git a/apps/web/src/app/tools/vault-export/export-web.component.html b/apps/web/src/app/tools/vault-export/export-web.component.html new file mode 100644 index 00000000000..e3d0ca75d25 --- /dev/null +++ b/apps/web/src/app/tools/vault-export/export-web.component.html @@ -0,0 +1,20 @@ + + + + + + diff --git a/apps/web/src/app/tools/vault-export/export-web.component.ts b/apps/web/src/app/tools/vault-export/export-web.component.ts new file mode 100644 index 00000000000..f2612656cee --- /dev/null +++ b/apps/web/src/app/tools/vault-export/export-web.component.ts @@ -0,0 +1,24 @@ +import { Component } from "@angular/core"; +import { Router } from "@angular/router"; + +import { ExportComponent } from "@bitwarden/vault-export-ui"; + +import { HeaderModule } from "../../layouts/header/header.module"; +import { SharedModule } from "../../shared"; + +@Component({ + templateUrl: "export-web.component.html", + standalone: true, + imports: [SharedModule, ExportComponent, HeaderModule], +}) +export class ExportWebComponent { + protected loading = false; + protected disabled = false; + + constructor(private router: Router) {} + + /** + * Callback that is called after a successful export. + */ + protected async onSuccessfulExport(organizationId: string): Promise {} +} diff --git a/apps/web/src/app/tools/vault-export/export.component.html b/apps/web/src/app/tools/vault-export/export.component.html deleted file mode 100644 index 9f47adf8aa5..00000000000 --- a/apps/web/src/app/tools/vault-export/export.component.html +++ /dev/null @@ -1,115 +0,0 @@ - - - -
- - {{ "personalVaultExportPolicyInEffect" | i18n }} - - - - - - {{ "exportFrom" | i18n }} - - - - - - - - - {{ "fileFormat" | i18n }} - - - - - - - - {{ "exportTypeHeading" | i18n }} - - - {{ "accountRestricted" | i18n }} - {{ "accountRestrictedOptionDescription" | i18n }} - - - - {{ "passwordProtected" | i18n }} - {{ "passwordProtectedOptionDescription" | i18n }} - - - - -
- - {{ "filePassword" | i18n }} - - - {{ "exportPasswordDescription" | i18n }} - - - -
- - {{ "confirmFilePassword" | i18n }} - - - -
-
- - -
-
diff --git a/apps/web/src/app/tools/vault-export/export.component.ts b/apps/web/src/app/tools/vault-export/export.component.ts deleted file mode 100644 index 8b5f82167d6..00000000000 --- a/apps/web/src/app/tools/vault-export/export.component.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Component } from "@angular/core"; -import { UntypedFormBuilder } from "@angular/forms"; - -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; -import { ExportComponent as BaseExportComponent } from "@bitwarden/vault-export-ui"; - -@Component({ - selector: "app-export", - templateUrl: "export.component.html", -}) -export class ExportComponent extends BaseExportComponent { - constructor( - i18nService: I18nService, - toastService: ToastService, - exportService: VaultExportServiceAbstraction, - eventCollectionService: EventCollectionService, - policyService: PolicyService, - logService: LogService, - formBuilder: UntypedFormBuilder, - fileDownloadService: FileDownloadService, - dialogService: DialogService, - organizationService: OrganizationService, - ) { - super( - i18nService, - toastService, - exportService, - eventCollectionService, - policyService, - logService, - formBuilder, - fileDownloadService, - dialogService, - organizationService, - ); - } - - protected saved() { - super.saved(); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("exportSuccess"), - }); - } -} diff --git a/apps/web/src/app/tools/vault-export/export.module.ts b/apps/web/src/app/tools/vault-export/export.module.ts deleted file mode 100644 index ddf82b0a100..00000000000 --- a/apps/web/src/app/tools/vault-export/export.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NgModule } from "@angular/core"; - -import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui"; - -import { LooseComponentsModule, SharedModule } from "../../shared"; - -import { ExportRoutingModule } from "./export-routing.module"; -import { ExportComponent } from "./export.component"; - -@NgModule({ - imports: [SharedModule, LooseComponentsModule, ExportRoutingModule, ExportScopeCalloutComponent], - declarations: [ExportComponent], -}) -export class ExportModule {} diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts index 1cb8e13cb3e..172d81c48af 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts @@ -5,9 +5,10 @@ import { mergeMap, take } from "rxjs/operators"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; +import { KdfType } from "@bitwarden/common/platform/enums"; import { StateProvider, ActiveUserState, @@ -200,7 +201,7 @@ export class VaultBannersService { const kdfConfig = await this.kdfConfigService.getKdfConfig(); return ( kdfConfig.kdfType === KdfType.PBKDF2_SHA256 && - kdfConfig.iterations < PBKDF2_ITERATIONS.defaultValue + kdfConfig.iterations < PBKDF2KdfConfig.ITERATIONS.defaultValue ); } diff --git a/apps/web/src/connectors/duo.html b/apps/web/src/connectors/duo.html deleted file mode 100644 index 8d315219c20..00000000000 --- a/apps/web/src/connectors/duo.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - Bitwarden Duo Connector - - - - diff --git a/apps/web/src/connectors/duo.scss b/apps/web/src/connectors/duo.scss deleted file mode 100644 index 0fa97956ee6..00000000000 --- a/apps/web/src/connectors/duo.scss +++ /dev/null @@ -1,18 +0,0 @@ -html, -body { - margin: 0; - padding: 0; -} - -body { - background: #efeff4 url("../images/loading.svg") 0 0 no-repeat; -} - -iframe { - display: block; - width: 100%; - height: 400px; - border: none; - margin: 0; - padding: 0; -} diff --git a/apps/web/src/connectors/duo.ts b/apps/web/src/connectors/duo.ts deleted file mode 100644 index b041c0d6a20..00000000000 --- a/apps/web/src/connectors/duo.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as DuoWebSDK from "duo_web_sdk"; - -import { getQsParam } from "./common"; - -require("./duo.scss"); - -document.addEventListener("DOMContentLoaded", () => { - const frameElement = document.createElement("iframe"); - frameElement.setAttribute("id", "duo_iframe"); - setFrameHeight(); - document.body.appendChild(frameElement); - - const hostParam = getQsParam("host"); - const requestParam = getQsParam("request"); - - const hostUrl = new URL("https://" + hostParam); - if ( - !hostUrl.hostname.endsWith(".duosecurity.com") && - !hostUrl.hostname.endsWith(".duofederal.com") - ) { - return; - } - - DuoWebSDK.init({ - iframe: "duo_iframe", - host: hostUrl.hostname, - sig_request: requestParam, - submit_callback: (form: any) => { - invokeCSCode(form.elements.sig_response.value); - }, - }); - - window.onresize = setFrameHeight; - - function setFrameHeight() { - frameElement.style.height = window.innerHeight + "px"; - } -}); - -function invokeCSCode(data: string) { - try { - (window as any).invokeCSharpAction(data); - } catch (err) { - // eslint-disable-next-line - console.log(err); - } -} diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index f22d98f081d..884a0bc9978 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -91,11 +91,6 @@ const plugins = [ chunks: ["theme_head", "app/polyfills", "app/vendor", "app/main"], }), new HtmlWebpackInjector(), - new HtmlWebpackPlugin({ - template: "./src/connectors/duo.html", - filename: "duo-connector.html", - chunks: ["connectors/duo"], - }), new HtmlWebpackPlugin({ template: "./src/connectors/webauthn.html", filename: "webauthn-connector.html", @@ -324,7 +319,6 @@ const webpackConfig = { "app/main": "./src/main.ts", "connectors/webauthn": "./src/connectors/webauthn.ts", "connectors/webauthn-fallback": "./src/connectors/webauthn-fallback.ts", - "connectors/duo": "./src/connectors/duo.ts", "connectors/sso": "./src/connectors/sso.ts", "connectors/captcha": "./src/connectors/captcha.ts", "connectors/duo-redirect": "./src/connectors/duo-redirect.ts", diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/validators/domain-name.validator.spec.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/validators/domain-name.validator.spec.ts new file mode 100644 index 00000000000..44eaa17875b --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/validators/domain-name.validator.spec.ts @@ -0,0 +1,37 @@ +import { AbstractControl, ValidationErrors } from "@angular/forms"; + +import { domainNameValidator } from "./domain-name.validator"; + +describe("domainNameValidator", () => { + let validatorFn: (control: AbstractControl) => ValidationErrors | null; + const errorMessage = "Invalid domain name"; + + beforeEach(() => { + validatorFn = domainNameValidator(errorMessage); + }); + + const testCases = [ + { value: "e.com", expected: null }, + { value: "example.com", expected: null }, + { value: "sub.example.com", expected: null }, + { value: "sub.sub.example.com", expected: null }, + { value: "example.co.uk", expected: null }, + { value: "example", expected: { invalidDomainName: { message: errorMessage } } }, + { value: "-example.com", expected: { invalidDomainName: { message: errorMessage } } }, + { value: "example-.com", expected: { invalidDomainName: { message: errorMessage } } }, + { value: "example..com", expected: { invalidDomainName: { message: errorMessage } } }, + { value: "http://example.com", expected: { invalidDomainName: { message: errorMessage } } }, + { value: "www.example.com", expected: { invalidDomainName: { message: errorMessage } } }, + { value: "", expected: null }, + { value: "x".repeat(64) + ".com", expected: { invalidDomainName: { message: errorMessage } } }, + ]; + + describe("run test cases", () => { + testCases.forEach(({ value, expected }) => { + test(`should return ${JSON.stringify(expected)} for value "${value}"`, () => { + const control = { value } as AbstractControl; + expect(validatorFn(control)).toEqual(expected); + }); + }); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/validators/domain-name.validator.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/validators/domain-name.validator.ts index e49ca16e193..55682c891f9 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/validators/domain-name.validator.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/validators/domain-name.validator.ts @@ -13,24 +13,22 @@ export function domainNameValidator(errorMessage: string): ValidatorFn { // We do not want any prefixes per industry standards. // Must support top-level domains and any number of subdomains. - // / # start regex - // ^ # start of string - // (?!(http(s)?:\/\/|www\.)) # negative lookahead to check if input doesn't match "http://", "https://" or "www." - // [a-zA-Z0-9] # first character must be a letter or a number - // [a-zA-Z0-9-]{0,61} # domain name can have 0 to 61 characters that are letters, numbers, or hyphens - // [a-zA-Z0-9] # domain name must end with a letter or a number - // (?: # start of non-capturing group (subdomain sections are optional) - // \. # subdomain must have a period - // [a-zA-Z0-9] # first character of subdomain must be a letter or a number - // [a-zA-Z0-9-]{0,61} # subdomain can have 0 to 61 characters that are letters, numbers, or hyphens - // [a-zA-Z0-9] # subdomain must end with a letter or a number - // )* # end of non-capturing group (subdomain sections are optional) - // \. # domain name must have a period - // [a-zA-Z]{2,} # domain name must have at least two letters (the domain extension) - // $/ # end of string + // / # start regex + // ^ # start of string + // (?!(http(s)?:\/\/|www\.)) # negative lookahead to check if input doesn't match "http://", "https://" or "www." + // ( # start of capturing group for the entire domain + // [a-zA-Z0-9] # first character of domain must be a letter or a number + // ( # start of optional group for subdomain or domain section + // [a-zA-Z0-9-]{0,61} # subdomain/domain section can have 0 to 61 characters that are letters, numbers, or hyphens + // [a-zA-Z0-9] # subdomain/domain section must end with a letter or a number + // )? # end of optional group for subdomain or domain section + // \. # subdomain/domain section must have a period + // )+ # end of capturing group for the entire domain, repeatable for subdomains + // [a-zA-Z]{2,} # domain name must have at least two letters (the domain extension) + // $/ # end of string const validDomainNameRegex = - /^(?!(http(s)?:\/\/|www\.))[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])*\.[a-zA-Z]{2,}$/; + /^(?!(http(s)?:\/\/|www\.))([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/; const invalid = !validDomainNameRegex.test(control.value); diff --git a/libs/angular/src/auth/components/register.component.ts b/libs/angular/src/auth/components/register.component.ts index e3197355dc3..533920d13c8 100644 --- a/libs/angular/src/auth/components/register.component.ts +++ b/libs/angular/src/auth/components/register.component.ts @@ -5,6 +5,7 @@ import { Router } from "@angular/router"; import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config"; import { RegisterResponse } from "@bitwarden/common/auth/models/response/register.response"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request"; @@ -15,7 +16,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index 73c38c8ddb9..b08c7967532 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -17,6 +17,7 @@ import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -24,7 +25,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { HashPurpose, DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; +import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 687fd3fb6f5..4ef18985d8c 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -1,6 +1,5 @@ import { Directive, Inject, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, NavigationExtras, Router } from "@angular/router"; -import * as DuoWebSDK from "duo_web_sdk"; import { firstValueFrom } from "rxjs"; import { first } from "rxjs/operators"; @@ -53,7 +52,6 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI emailPromise: Promise; orgIdentifier: string = null; - duoFrameless = false; duoFramelessUrl: string = null; duoResultListenerInitialized = false; @@ -177,42 +175,14 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI break; case TwoFactorProviderType.Duo: case TwoFactorProviderType.OrganizationDuo: - // 2 Duo 2FA flows available - // 1. Duo Web SDK (iframe) - existing, to be deprecated - // 2. Duo Frameless (new tab) - new - - // AuthUrl only exists for new Duo Frameless flow - if (providerData.AuthUrl) { - this.duoFrameless = true; - // Setup listener for duo-redirect.ts connector to send back the code - - if (!this.duoResultListenerInitialized) { - // setup client specific duo result listener - this.setupDuoResultListener(); - this.duoResultListenerInitialized = true; - } - - // flow must be launched by user so they can choose to remember the device or not. - this.duoFramelessUrl = providerData.AuthUrl; - } else { - // Duo Web SDK (iframe) flow - // TODO: remove when we remove the "duo-redirect" feature flag - setTimeout(() => { - DuoWebSDK.init({ - iframe: undefined, - host: providerData.Host, - sig_request: providerData.Signature, - submit_callback: async (f: HTMLFormElement) => { - const sig = f.querySelector('input[name="sig_response"]') as HTMLInputElement; - if (sig != null) { - this.token = sig.value; - await this.submit(); - } - }, - }); - }, 0); + // Setup listener for duo-redirect.ts connector to send back the code + if (!this.duoResultListenerInitialized) { + // setup client specific duo result listener + this.setupDuoResultListener(); + this.duoResultListenerInitialized = true; } - + // flow must be launched by user so they can choose to remember the device or not. + this.duoFramelessUrl = providerData.AuthUrl; break; case TwoFactorProviderType.Email: this.twoFactorEmail = providerData.Email; diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index cc91e2a2552..49d02361d49 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -6,10 +6,12 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; -import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + DEFAULT_KDF_CONFIG, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { MasterKey } from "@bitwarden/common/types/key"; import { diff --git a/libs/auth/src/angular/input-password/input-password.mdx b/libs/auth/src/angular/input-password/input-password.mdx new file mode 100644 index 00000000000..d442ea02fc9 --- /dev/null +++ b/libs/auth/src/angular/input-password/input-password.mdx @@ -0,0 +1,75 @@ +import { Meta, Story } from "@storybook/addon-docs"; + +import * as stories from "./input-password.stories.ts"; + + + +# InputPassword Component + +The `InputPasswordComponent` allows a user to enter a master password and hint. On submission it +creates a master key, master key hash, and emits those values to the parent (along with the hint and +default kdfConfig). + +The component is intended for re-use in different scenarios throughout the application. Therefore it +is mostly presentational and simply emits values rather than acting on them itself. It is the job of +the parent component to act on those values as needed. + +
+ +## `@Input()`'s + +- `email` (**required**) - the parent component must provide an email so that the + `InputPasswordComponent` can create a master key. +- `buttonText` (optional) - an `i18n` translated string that can be used as button text (default + text is "Set master password"). +- `orgId` (optional) - used to retreive and enforce the master password policy requirements for an + org. + +
+ +## Form Input Fields + +The `InputPasswordComponent` allows a user to enter: + +1. Master password +2. Master password confirmation +3. Hint (optional) +4. Chooses whether to check for password breaches (checkbox) + +Validation ensures that the master password and confirmed master password are the same, and that the +master password and hint values are not the same. + +
+ +## On Submit + +When the form is submitted, the `InputPasswordComponent` does the following in order: + +1. If the user selected the checkbox to check for password breaches, they will recieve a popup + dialog if their entered password is found in a breach. The popup will give them the option to + continue with the password or to back out and choose a different password. +2. If there is a master password policy being enforced by an org, it will check to make sure the + entered master password meets the policy requirements. +3. The component will use the password, email, and default kdfConfig to create a master key and + master key hash. +4. The component will emit the following values (defined in the `PasswordInputResult` interface) to + be used by the parent component as needed: + +```typescript +export interface PasswordInputResult { + masterKey: MasterKey; + masterKeyHash: string; + kdfConfig: PBKDF2KdfConfig; + hint: string; +} +``` + +# Default Example + + + +
+ +# With Policy Requrements + + diff --git a/libs/auth/src/angular/input-password/input-password.stories.ts b/libs/auth/src/angular/input-password/input-password.stories.ts index 6144e39e64a..a0dee87b27f 100644 --- a/libs/auth/src/angular/input-password/input-password.stories.ts +++ b/libs/auth/src/angular/input-password/input-password.stories.ts @@ -29,7 +29,10 @@ const mockMasterPasswordPolicyOptions = { export default { title: "Auth/Input Password", component: InputPasswordComponent, - decorators: [ +} as Meta; + +const decorators = (options: { hasPolicy?: boolean }) => { + return [ applicationConfig({ providers: [ importProvidersFrom(PreloadedEnglishI18nModule), @@ -56,13 +59,15 @@ export default { { provide: PolicyApiServiceAbstraction, useValue: { - getMasterPasswordPolicyOptsForOrgUser: () => mockMasterPasswordPolicyOptions, + getMasterPasswordPolicyOptsForOrgUser: () => + options.hasPolicy ? mockMasterPasswordPolicyOptions : null, } as Partial, }, { provide: PolicyService, useValue: { - masterPasswordPolicyOptions$: () => of(mockMasterPasswordPolicyOptions), + masterPasswordPolicyOptions$: () => + options.hasPolicy ? of(mockMasterPasswordPolicyOptions) : null, evaluateMasterPassword: (score) => { if (score < 4) { return false; @@ -101,8 +106,8 @@ export default { }, ], }), - ], -} as Meta; + ]; +}; type Story = StoryObj; @@ -113,4 +118,19 @@ export const Default: Story = { `, }), + decorators: decorators({ + hasPolicy: false, + }), +}; + +export const WithPolicy: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + hasPolicy: true, + }), }; diff --git a/libs/auth/src/common/services/pin/pin.service.spec.ts b/libs/auth/src/common/services/pin/pin.service.spec.ts index 834e581dc6a..b40d37d4246 100644 --- a/libs/auth/src/common/services/pin/pin.service.spec.ts +++ b/libs/auth/src/common/services/pin/pin.service.spec.ts @@ -1,13 +1,13 @@ import { mock } from "jest-mock-extended"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; 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"; diff --git a/libs/common/src/auth/models/domain/kdf-config.ts b/libs/common/src/auth/models/domain/kdf-config.ts index ce01f097028..7378081550b 100644 --- a/libs/common/src/auth/models/domain/kdf-config.ts +++ b/libs/common/src/auth/models/domain/kdf-config.ts @@ -1,12 +1,7 @@ import { Jsonify } from "type-fest"; -import { - ARGON2_ITERATIONS, - ARGON2_MEMORY, - ARGON2_PARALLELISM, - KdfType, - PBKDF2_ITERATIONS, -} from "../../../platform/enums/kdf-type.enum"; +import { KdfType } from "../../../platform/enums/kdf-type.enum"; +import { RangeWithDefault } from "../../../platform/misc/range-with-default"; /** * Represents a type safe KDF configuration. @@ -17,11 +12,12 @@ export type KdfConfig = PBKDF2KdfConfig | Argon2KdfConfig; * Password-Based Key Derivation Function 2 (PBKDF2) KDF configuration. */ export class PBKDF2KdfConfig { + static ITERATIONS = new RangeWithDefault(600_000, 2_000_000, 600_000); kdfType: KdfType.PBKDF2_SHA256 = KdfType.PBKDF2_SHA256; iterations: number; constructor(iterations?: number) { - this.iterations = iterations ?? PBKDF2_ITERATIONS.defaultValue; + this.iterations = iterations ?? PBKDF2KdfConfig.ITERATIONS.defaultValue; } /** @@ -29,9 +25,9 @@ export class PBKDF2KdfConfig { * A Valid PBKDF2 KDF configuration has KDF iterations between the 600_000 and 2_000_000. */ validateKdfConfig(): void { - if (!PBKDF2_ITERATIONS.inRange(this.iterations)) { + if (!PBKDF2KdfConfig.ITERATIONS.inRange(this.iterations)) { throw new Error( - `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, + `PBKDF2 iterations must be between ${PBKDF2KdfConfig.ITERATIONS.min} and ${PBKDF2KdfConfig.ITERATIONS.max}`, ); } } @@ -45,15 +41,18 @@ export class PBKDF2KdfConfig { * Argon2 KDF configuration. */ export class Argon2KdfConfig { + static MEMORY = new RangeWithDefault(16, 1024, 64); + static PARALLELISM = new RangeWithDefault(1, 16, 4); + static ITERATIONS = new RangeWithDefault(2, 10, 3); kdfType: KdfType.Argon2id = KdfType.Argon2id; iterations: number; memory: number; parallelism: number; constructor(iterations?: number, memory?: number, parallelism?: number) { - this.iterations = iterations ?? ARGON2_ITERATIONS.defaultValue; - this.memory = memory ?? ARGON2_MEMORY.defaultValue; - this.parallelism = parallelism ?? ARGON2_PARALLELISM.defaultValue; + this.iterations = iterations ?? Argon2KdfConfig.ITERATIONS.defaultValue; + this.memory = memory ?? Argon2KdfConfig.MEMORY.defaultValue; + this.parallelism = parallelism ?? Argon2KdfConfig.PARALLELISM.defaultValue; } /** @@ -61,21 +60,21 @@ export class Argon2KdfConfig { * A Valid Argon2 KDF configuration has iterations between 2 and 10, memory between 16mb and 1024mb, and parallelism between 1 and 16. */ validateKdfConfig(): void { - if (!ARGON2_ITERATIONS.inRange(this.iterations)) { + if (!Argon2KdfConfig.ITERATIONS.inRange(this.iterations)) { throw new Error( - `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, + `Argon2 iterations must be between ${Argon2KdfConfig.ITERATIONS.min} and ${Argon2KdfConfig.ITERATIONS.max}`, ); } - if (!ARGON2_MEMORY.inRange(this.memory)) { + if (!Argon2KdfConfig.MEMORY.inRange(this.memory)) { throw new Error( - `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, + `Argon2 memory must be between ${Argon2KdfConfig.MEMORY.min}mb and ${Argon2KdfConfig.MEMORY.max}mb`, ); } - if (!ARGON2_PARALLELISM.inRange(this.parallelism)) { + if (!Argon2KdfConfig.PARALLELISM.inRange(this.parallelism)) { throw new Error( - `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}.`, + `Argon2 parallelism must be between ${Argon2KdfConfig.PARALLELISM.min} and ${Argon2KdfConfig.PARALLELISM.max}.`, ); } } @@ -84,3 +83,5 @@ export class Argon2KdfConfig { return new Argon2KdfConfig(json.iterations, json.memory, json.parallelism); } } + +export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2KdfConfig.ITERATIONS.defaultValue); diff --git a/libs/common/src/auth/models/request/update-two-factor-duo.request.ts b/libs/common/src/auth/models/request/update-two-factor-duo.request.ts index d113715b375..c67b1117412 100644 --- a/libs/common/src/auth/models/request/update-two-factor-duo.request.ts +++ b/libs/common/src/auth/models/request/update-two-factor-duo.request.ts @@ -1,7 +1,7 @@ import { SecretVerificationRequest } from "./secret-verification.request"; export class UpdateTwoFactorDuoRequest extends SecretVerificationRequest { - integrationKey: string; - secretKey: string; + clientId: string; + clientSecret: string; host: string; } diff --git a/libs/common/src/auth/models/response/two-factor-duo.response.ts b/libs/common/src/auth/models/response/two-factor-duo.response.ts index b91c9f0fb67..a195aa236dd 100644 --- a/libs/common/src/auth/models/response/two-factor-duo.response.ts +++ b/libs/common/src/auth/models/response/two-factor-duo.response.ts @@ -3,14 +3,14 @@ import { BaseResponse } from "../../../models/response/base.response"; export class TwoFactorDuoResponse extends BaseResponse { enabled: boolean; host: string; - secretKey: string; - integrationKey: string; + clientSecret: string; + clientId: string; constructor(response: any) { super(response); this.enabled = this.getResponseProperty("Enabled"); this.host = this.getResponseProperty("Host"); - this.secretKey = this.getResponseProperty("SecretKey"); - this.integrationKey = this.getResponseProperty("IntegrationKey"); + this.clientSecret = this.getResponseProperty("ClientSecret"); + this.clientId = this.getResponseProperty("ClientId"); } } diff --git a/libs/common/src/auth/services/kdf-config.service.spec.ts b/libs/common/src/auth/services/kdf-config.service.spec.ts index 67bcf721bc4..7f8357ffb55 100644 --- a/libs/common/src/auth/services/kdf-config.service.spec.ts +++ b/libs/common/src/auth/services/kdf-config.service.spec.ts @@ -1,10 +1,4 @@ import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; -import { - ARGON2_ITERATIONS, - ARGON2_MEMORY, - ARGON2_PARALLELISM, - PBKDF2_ITERATIONS, -} from "../../platform/enums/kdf-type.enum"; import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; import { Argon2KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config"; @@ -77,28 +71,28 @@ describe("KdfConfigService", () => { it("validateKdfConfig(): should throw an error for invalid PBKDF2 iterations", () => { const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(100); expect(() => kdfConfig.validateKdfConfig()).toThrow( - `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, + `PBKDF2 iterations must be between ${PBKDF2KdfConfig.ITERATIONS.min} and ${PBKDF2KdfConfig.ITERATIONS.max}`, ); }); it("validateKdfConfig(): should throw an error for invalid Argon2 iterations", () => { const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(11, 64, 4); expect(() => kdfConfig.validateKdfConfig()).toThrow( - `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, + `Argon2 iterations must be between ${Argon2KdfConfig.ITERATIONS.min} and ${Argon2KdfConfig.ITERATIONS.max}`, ); }); it("validateKdfConfig(): should throw an error for invalid Argon2 memory", () => { const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 1025, 4); expect(() => kdfConfig.validateKdfConfig()).toThrow( - `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, + `Argon2 memory must be between ${Argon2KdfConfig.MEMORY.min}mb and ${Argon2KdfConfig.MEMORY.max}mb`, ); }); it("validateKdfConfig(): should throw an error for invalid Argon2 parallelism", () => { const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 17); expect(() => kdfConfig.validateKdfConfig()).toThrow( - `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}`, + `Argon2 parallelism must be between ${Argon2KdfConfig.PARALLELISM.min} and ${Argon2KdfConfig.PARALLELISM.max}`, ); }); }); diff --git a/libs/common/src/platform/enums/kdf-type.enum.ts b/libs/common/src/platform/enums/kdf-type.enum.ts index fd29bf308c2..29fcd9f1f8e 100644 --- a/libs/common/src/platform/enums/kdf-type.enum.ts +++ b/libs/common/src/platform/enums/kdf-type.enum.ts @@ -1,15 +1,4 @@ -import { PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config"; -import { RangeWithDefault } from "../misc/range-with-default"; - export enum KdfType { PBKDF2_SHA256 = 0, Argon2id = 1, } - -export const ARGON2_MEMORY = new RangeWithDefault(16, 1024, 64); -export const ARGON2_PARALLELISM = new RangeWithDefault(1, 16, 4); -export const ARGON2_ITERATIONS = new RangeWithDefault(2, 10, 3); - -export const DEFAULT_KDF_TYPE = KdfType.PBKDF2_SHA256; -export const PBKDF2_ITERATIONS = new RangeWithDefault(600_000, 2_000_000, 600_000); -export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2_ITERATIONS.defaultValue); diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts index 13e1d8f282b..597f2d8f32e 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts @@ -19,6 +19,7 @@ import { } from "../../abstractions/fido2/fido2-client.service.abstraction"; import { Utils } from "../../misc/utils"; +import * as DomainUtils from "./domain-utils"; import { Fido2AuthenticatorService } from "./fido2-authenticator.service"; import { Fido2ClientService } from "./fido2-client.service"; import { Fido2Utils } from "./fido2-utils"; @@ -36,6 +37,7 @@ describe("FidoAuthenticatorService", () => { let domainSettingsService: MockProxy; let client!: Fido2ClientService; let tab!: chrome.tabs.Tab; + let isValidRpId!: jest.SpyInstance; beforeEach(async () => { authenticator = mock(); @@ -44,6 +46,8 @@ describe("FidoAuthenticatorService", () => { vaultSettingsService = mock(); domainSettingsService = mock(); + isValidRpId = jest.spyOn(DomainUtils, "isValidRpId"); + client = new Fido2ClientService( authenticator, configService, @@ -58,6 +62,10 @@ describe("FidoAuthenticatorService", () => { tab = { id: 123, windowId: 456 } as chrome.tabs.Tab; }); + afterEach(() => { + isValidRpId.mockRestore(); + }); + describe("createCredential", () => { describe("input parameters validation", () => { // Spec: If sameOriginWithAncestors is false, return a "NotAllowedError" DOMException. @@ -113,6 +121,7 @@ describe("FidoAuthenticatorService", () => { }); // Spec: If options.rp.id is not a registrable domain suffix of and is not equal to effectiveDomain, return a DOMException whose name is "SecurityError", and terminate this algorithm. + // This is actually checked by `isValidRpId` function, but we'll test it here as well it("should throw error if rp.id is not valid for this origin", async () => { const params = createParams({ origin: "https://passwordless.dev", @@ -126,6 +135,20 @@ describe("FidoAuthenticatorService", () => { await rejects.toBeInstanceOf(DOMException); }); + // Sanity check to make sure that we use `isValidRpId` to validate the rp.id + it("should throw if isValidRpId returns false", async () => { + const params = createParams(); + authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); + // `params` actually has a valid rp.id, but we're mocking the function to return false + isValidRpId.mockReturnValue(false); + + const result = async () => await client.createCredential(params, tab); + + const rejects = expect(result).rejects; + await rejects.toMatchObject({ name: "SecurityError" }); + await rejects.toBeInstanceOf(DOMException); + }); + it("should fallback if origin hostname is found in neverDomains", async () => { const params = createParams({ origin: "https://bitwarden.com", @@ -151,6 +174,16 @@ describe("FidoAuthenticatorService", () => { await rejects.toBeInstanceOf(DOMException); }); + it("should not throw error if localhost is http", async () => { + const params = createParams({ + origin: "http://localhost", + rp: { id: undefined, name: "localhost" }, + }); + authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); + + await client.createCredential(params, tab); + }); + // Spec: If credTypesAndPubKeyAlgs is empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm. it("should throw error if no support key algorithms were found", async () => { const params = createParams({ @@ -360,6 +393,7 @@ describe("FidoAuthenticatorService", () => { }); // Spec: If options.rp.id is not a registrable domain suffix of and is not equal to effectiveDomain, return a DOMException whose name is "SecurityError", and terminate this algorithm. + // This is actually checked by `isValidRpId` function, but we'll test it here as well it("should throw error if rp.id is not valid for this origin", async () => { const params = createParams({ origin: "https://passwordless.dev", @@ -373,6 +407,20 @@ describe("FidoAuthenticatorService", () => { await rejects.toBeInstanceOf(DOMException); }); + // Sanity check to make sure that we use `isValidRpId` to validate the rp.id + it("should throw if isValidRpId returns false", async () => { + const params = createParams(); + authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); + // `params` actually has a valid rp.id, but we're mocking the function to return false + isValidRpId.mockReturnValue(false); + + const result = async () => await client.assertCredential(params, tab); + + const rejects = expect(result).rejects; + await rejects.toMatchObject({ name: "SecurityError" }); + await rejects.toBeInstanceOf(DOMException); + }); + it("should fallback if origin hostname is found in neverDomains", async () => { const params = createParams({ origin: "https://bitwarden.com", @@ -506,6 +554,16 @@ describe("FidoAuthenticatorService", () => { expect.anything(), ); }); + + it("should not throw error if localhost is http", async () => { + const params = createParams({ + origin: "http://localhost", + }); + params.rpId = undefined; + authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); + + await client.assertCredential(params, tab); + }); }); describe("assert discoverable credential", () => { diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.ts b/libs/common/src/platform/services/fido2/fido2-client.service.ts index 5b6a13bc3f5..d22b91fda05 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.ts @@ -103,7 +103,10 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { } params.rp.id = params.rp.id ?? parsedOrigin.hostname; - if (parsedOrigin.hostname == undefined || !params.origin.startsWith("https://")) { + if ( + parsedOrigin.hostname == undefined || + (!params.origin.startsWith("https://") && parsedOrigin.hostname !== "localhost") + ) { this.logService?.warning(`[Fido2Client] Invalid https origin: ${params.origin}`); throw new DOMException("'origin' is not a valid https origin", "SecurityError"); } @@ -238,7 +241,10 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { params.rpId = params.rpId ?? parsedOrigin.hostname; - if (parsedOrigin.hostname == undefined || !params.origin.startsWith("https://")) { + if ( + parsedOrigin.hostname == undefined || + (!params.origin.startsWith("https://") && parsedOrigin.hostname !== "localhost") + ) { this.logService?.warning(`[Fido2Client] Invalid https origin: ${params.origin}`); throw new DOMException("'origin' is not a valid https origin", "SecurityError"); } diff --git a/libs/common/src/platform/services/key-generation.service.ts b/libs/common/src/platform/services/key-generation.service.ts index 2a25ffde2fd..b1c1ddfcf17 100644 --- a/libs/common/src/platform/services/key-generation.service.ts +++ b/libs/common/src/platform/services/key-generation.service.ts @@ -1,14 +1,8 @@ -import { KdfConfig } from "../../auth/models/domain/kdf-config"; +import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config"; import { CsprngArray } from "../../types/csprng"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "../abstractions/key-generation.service"; -import { - ARGON2_ITERATIONS, - ARGON2_MEMORY, - ARGON2_PARALLELISM, - KdfType, - PBKDF2_ITERATIONS, -} from "../enums"; +import { KdfType } from "../enums"; import { Utils } from "../misc/utils"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; @@ -51,21 +45,21 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction { let key: Uint8Array = null; if (kdfConfig.kdfType == null || kdfConfig.kdfType === KdfType.PBKDF2_SHA256) { if (kdfConfig.iterations == null) { - kdfConfig.iterations = PBKDF2_ITERATIONS.defaultValue; + kdfConfig.iterations = PBKDF2KdfConfig.ITERATIONS.defaultValue; } key = await this.cryptoFunctionService.pbkdf2(password, salt, "sha256", kdfConfig.iterations); } else if (kdfConfig.kdfType == KdfType.Argon2id) { if (kdfConfig.iterations == null) { - kdfConfig.iterations = ARGON2_ITERATIONS.defaultValue; + kdfConfig.iterations = Argon2KdfConfig.ITERATIONS.defaultValue; } if (kdfConfig.memory == null) { - kdfConfig.memory = ARGON2_MEMORY.defaultValue; + kdfConfig.memory = Argon2KdfConfig.MEMORY.defaultValue; } if (kdfConfig.parallelism == null) { - kdfConfig.parallelism = ARGON2_PARALLELISM.defaultValue; + kdfConfig.parallelism = Argon2KdfConfig.PARALLELISM.defaultValue; } const saltHash = await this.cryptoFunctionService.hash(salt, "sha256"); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index 9b5c4d8bf55..44df18116de 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -2,10 +2,14 @@ import { mock, MockProxy } from "jest-mock-extended"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { + DEFAULT_KDF_CONFIG, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { DEFAULT_KDF_CONFIG, KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; +import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -238,7 +242,7 @@ describe("VaultExportService", () => { }); it("specifies kdfIterations", () => { - expect(exportObject.kdfIterations).toEqual(PBKDF2_ITERATIONS.defaultValue); + expect(exportObject.kdfIterations).toEqual(PBKDF2KdfConfig.ITERATIONS.defaultValue); }); it("has kdfType", () => { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts index 9b5c4d8bf55..44df18116de 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts @@ -2,10 +2,14 @@ import { mock, MockProxy } from "jest-mock-extended"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { + DEFAULT_KDF_CONFIG, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { DEFAULT_KDF_CONFIG, KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; +import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -238,7 +242,7 @@ describe("VaultExportService", () => { }); it("specifies kdfIterations", () => { - expect(exportObject.kdfIterations).toEqual(PBKDF2_ITERATIONS.defaultValue); + expect(exportObject.kdfIterations).toEqual(PBKDF2KdfConfig.ITERATIONS.defaultValue); }); it("has kdfType", () => { diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 9f81f5e5502..baa463d913e 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -1,5 +1,13 @@ import { CommonModule } from "@angular/common"; -import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@angular/core"; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ViewChild, +} from "@angular/core"; import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/forms"; import { map, merge, Observable, startWith, Subject, takeUntil } from "rxjs"; @@ -53,6 +61,26 @@ import { ExportScopeCalloutComponent } from "./export-scope-callout.component"; ], }) export class ExportComponent implements OnInit, OnDestroy { + private _organizationId: string; + + get organizationId(): string { + return this._organizationId; + } + + /** + * Enables the hosting control to pass in an organizationId + * If a organizationId is provided, the organization selection is disabled. + */ + @Input() set organizationId(value: string) { + this._organizationId = value; + this.organizationService + .get$(this._organizationId) + .pipe(takeUntil(this.destroy$)) + .subscribe((organization) => { + this._organizationId = organization?.id; + }); + } + /** * The hosting control also needs a bitSubmitDirective (on the Submit button) which calls this components {@link submit}-method. * This components formState (loading/disabled) is emitted back up to the hosting component so for example the Submit button can be enabled/disabled and show loading state. @@ -82,7 +110,6 @@ export class ExportComponent implements OnInit, OnDestroy { @Output() onSuccessfulExport = new EventEmitter(); - @Output() onSaved = new EventEmitter(); @ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent; encryptedExportType = EncryptedExportType; @@ -91,7 +118,6 @@ export class ExportComponent implements OnInit, OnDestroy { filePasswordValue: string = null; private _disabledByPolicy = false; - protected organizationId: string = null; organizations$: Observable; protected get disabledByPolicy(): boolean { @@ -120,6 +146,7 @@ export class ExportComponent implements OnInit, OnDestroy { ]; private destroy$ = new Subject(); + private onlyManagedCollections = true; constructor( protected i18nService: I18nService, @@ -163,6 +190,8 @@ export class ExportComponent implements OnInit, OnDestroy { ); this.exportForm.controls.vaultSelector.patchValue(this.organizationId); this.exportForm.controls.vaultSelector.disable(); + + this.onlyManagedCollections = false; return; } @@ -211,7 +240,12 @@ export class ExportComponent implements OnInit, OnDestroy { try { const data = await this.getExportData(); this.downloadFile(data); - this.saved(); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("exportSuccess"), + }); + this.onSuccessfulExport.emit(this.organizationId); await this.collectEvent(); this.exportForm.get("secret").setValue(""); this.exportForm.clearValidators(); @@ -252,11 +286,6 @@ export class ExportComponent implements OnInit, OnDestroy { await this.doExport(); }; - protected saved() { - this.onSaved.emit(); - this.onSuccessfulExport.emit(this.organizationId); - } - private async verifyUser(): Promise { let confirmDescription = "exportWarningDesc"; if (this.isFileEncryptedExport) { @@ -298,7 +327,7 @@ export class ExportComponent implements OnInit, OnDestroy { this.organizationId, this.format, this.filePassword, - true, + this.onlyManagedCollections, ); } diff --git a/package-lock.json b/package-lock.json index c067a0b1229..991849fd011 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,6 @@ "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.36.1", - "duo_web_sdk": "github:duosecurity/duo_web_sdk", "form-data": "4.0.0", "https-proxy-agent": "7.0.2", "inquirer": "8.2.6", @@ -68,7 +67,7 @@ "rxjs": "7.8.1", "tabbable": "6.2.0", "tldts": "6.1.29", - "utf-8-validate": "6.0.3", + "utf-8-validate": "6.0.4", "zone.js": "0.13.3", "zxcvbn": "4.4.2" }, @@ -97,7 +96,6 @@ "@storybook/testing-library": "0.2.2", "@types/argon2-browser": "1.18.1", "@types/chrome": "0.0.262", - "@types/duo_web_sdk": "2.7.1", "@types/firefox-webext-browser": "111.0.5", "@types/inquirer": "8.2.10", "@types/jest": "29.5.12", @@ -110,7 +108,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "20.14.1", + "@types/node": "20.14.8", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -237,7 +235,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.6.5", + "version": "2024.6.6", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -11352,12 +11350,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/duo_web_sdk": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@types/duo_web_sdk/-/duo_web_sdk-2.7.1.tgz", - "integrity": "sha512-DePanZjFww36yGSxXwC8B3AsjrrDuPxEcufeh4gTqVsUMpCYByxjX4PERiYZdW0typzKSt9E4I14PPp+PrSIQA==", - "dev": true - }, "node_modules/@types/ejs": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", @@ -11750,9 +11742,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.1.tgz", - "integrity": "sha512-T2MzSGEu+ysB/FkWfqmhV3PLyQlowdptmmgD20C6QxsS8Fmv5SjpZ1ayXaEC0S21/h5UJ9iA6W/5vSNU5l00OA==", + "version": "20.14.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", + "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==", "dev": true, "license": "MIT", "dependencies": { @@ -18249,11 +18241,6 @@ "node": ">=12" } }, - "node_modules/duo_web_sdk": { - "version": "2.7.0", - "resolved": "git+ssh://git@github.com/duosecurity/duo_web_sdk.git#29cad7338eff2cd909a361ecdd525458862938be", - "license": "SEE LICENSE IN LICENSE" - }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -38757,10 +38744,11 @@ } }, "node_modules/utf-8-validate": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.3.tgz", - "integrity": "sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.4.tgz", + "integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "node-gyp-build": "^4.3.0" }, diff --git a/package.json b/package.json index e30a00f5804..c835300fcc8 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "@storybook/testing-library": "0.2.2", "@types/argon2-browser": "1.18.1", "@types/chrome": "0.0.262", - "@types/duo_web_sdk": "2.7.1", "@types/firefox-webext-browser": "111.0.5", "@types/inquirer": "8.2.10", "@types/jest": "29.5.12", @@ -71,7 +70,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "20.14.1", + "@types/node": "20.14.8", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -176,7 +175,6 @@ "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.36.1", - "duo_web_sdk": "github:duosecurity/duo_web_sdk", "form-data": "4.0.0", "https-proxy-agent": "7.0.2", "inquirer": "8.2.6", @@ -205,7 +203,7 @@ "rxjs": "7.8.1", "tabbable": "6.2.0", "tldts": "6.1.29", - "utf-8-validate": "6.0.3", + "utf-8-validate": "6.0.4", "zone.js": "0.13.3", "zxcvbn": "4.4.2" },