mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[AC-1416] Expose Organization Fingerprint (#5557)
* refactor: change getFingerprint param to fingerprintMaterial, refs PM-1522 * feat: generate and show fingerprint for organization (WIP), refs AC-1416 * feat: update legacy params subscription to best practice (WIP), refs AC-1461 * refactor: update to use reactive forms, refs AC-1416 * refactor: remove boostrap specific classes and update to component library paradigms, refs AC-1416 * refactor: remove boostrap specific classes and update to component library paradigms, refs AC-1416 * refactor: create shared fingerprint component to redude boilerplate for settings fingerprint views, refs AC-1416 * refactor: use grid to emulate col-6 and remove unnecessary theme extensions, refs AC-1416 * refactor: remove negative margin and clean up extra divs, refs AC-1416 * [AC-1431] Add missing UserVerificationModule import (#5555) * [PM-2238] Add nord and solarize themes (#5491) * Fix simple configurable dialog stories (#5560) * chore(deps): update bitwarden/gh-actions digest to 72594be (#5523) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * refactor: remove extra div leftover from card-body class, refs AC-1416 * refactor: use bitTypography for headers, refs AC-1416 * fix: update crypto service abstraction path, refs AC-1416 * refactor: remove try/catch on handler, remove bootstrap class, update api chaining in observable, refs AC-1416 * fix: replace faulty combineLatest logic, refs AC-1416 * refactor: simplify observable logic again, refs AC-1416 --------- Co-authored-by: Shane Melton <smelton@bitwarden.com> Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,95 +1,66 @@
|
|||||||
<div class="page-header">
|
<h1 bitTypography="h1" class="tw-pb-2.5">{{ "organizationInfo" | i18n }}</h1>
|
||||||
<h1>{{ "organizationInfo" | i18n }}</h1>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="loading">
|
<div *ngIf="loading">
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
title="{{ 'loading' | i18n }}"
|
title="{{ 'loading' | i18n }}"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||||
</div>
|
</div>
|
||||||
<form
|
<form *ngIf="org && !loading" #form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||||
*ngIf="org && !loading"
|
<div class="tw-grid tw-grid-cols-2 tw-gap-5">
|
||||||
#form
|
<div>
|
||||||
(ngSubmit)="submit()"
|
<bit-form-field>
|
||||||
[appApiAction]="formPromise"
|
<bit-label>{{ "organizationName" | i18n }}</bit-label>
|
||||||
ngNativeValidate
|
<input bitInput id="orgName" type="text" formControlName="orgName" />
|
||||||
>
|
</bit-form-field>
|
||||||
<div class="row">
|
<bit-form-field>
|
||||||
<div class="col-6">
|
<bit-label>{{ "billingEmail" | i18n }}</bit-label>
|
||||||
<div class="form-group">
|
<input bitInput id="billingEmail" formControlName="billingEmail" type="email" />
|
||||||
<label for="name">{{ "organizationName" | i18n }}</label>
|
</bit-form-field>
|
||||||
<input
|
<bit-form-field>
|
||||||
id="name"
|
<bit-label>{{ "businessName" | i18n }}</bit-label>
|
||||||
class="form-control"
|
<input bitInput id="businessName" formControlName="businessName" type="text" />
|
||||||
type="text"
|
</bit-form-field>
|
||||||
name="Name"
|
|
||||||
[(ngModel)]="org.name"
|
|
||||||
[disabled]="selfHosted"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div>
|
||||||
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="billingEmail"
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="BillingEmail"
|
|
||||||
[(ngModel)]="org.billingEmail"
|
|
||||||
[disabled]="selfHosted || !canEditSubscription"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="businessName">{{ "businessName" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="businessName"
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="BusinessName"
|
|
||||||
[(ngModel)]="org.businessName"
|
|
||||||
[disabled]="selfHosted || !canEditSubscription"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<bit-avatar [text]="org.name" [id]="org.id" size="large"></bit-avatar>
|
<bit-avatar [text]="org.name" [id]="org.id" size="large"></bit-avatar>
|
||||||
|
<app-account-fingerprint
|
||||||
|
[fingerprintMaterial]="organizationId"
|
||||||
|
[publicKeyBuffer]="publicKeyBuffer"
|
||||||
|
fingerprintLabel="{{ 'yourOrganizationsFingerprint' | i18n }}"
|
||||||
|
>
|
||||||
|
</app-account-fingerprint>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
|
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||||
{{ "save" | i18n }}
|
{{ "save" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<ng-container *ngIf="canUseApi">
|
<ng-container *ngIf="canUseApi">
|
||||||
<div class="secondary-header border-0 mb-0">
|
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">{{ "apiKey" | i18n }}</h1>
|
||||||
<h1>{{ "apiKey" | i18n }}</h1>
|
|
||||||
</div>
|
|
||||||
<p>
|
<p>
|
||||||
{{ "apiKeyDesc" | i18n }}
|
{{ "apiKeyDesc" | i18n }}
|
||||||
<a href="https://docs.bitwarden.com" target="_blank" rel="noopener">
|
<a href="https://docs.bitwarden.com" target="_blank" rel="noopener">
|
||||||
{{ "learnMore" | i18n }}
|
{{ "learnMore" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="viewApiKey()">
|
<button type="button" bitButton buttonType="secondary" (click)="viewApiKey()">
|
||||||
{{ "viewApiKey" | i18n }}
|
{{ "viewApiKey" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="rotateApiKey()">
|
<button type="button" bitButton buttonType="secondary" (click)="rotateApiKey()">
|
||||||
{{ "rotateApiKey" | i18n }}
|
{{ "rotateApiKey" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="secondary-header text-danger border-0 mb-0">
|
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5 !tw-text-danger">{{ "dangerZone" | i18n }}</h1>
|
||||||
<h1>{{ "dangerZone" | i18n }}</h1>
|
<div class="tw-rounded tw-border tw-border-solid tw-border-danger-500 tw-bg-background tw-p-5">
|
||||||
</div>
|
|
||||||
<div class="card border-danger">
|
|
||||||
<div class="card-body">
|
|
||||||
<p>{{ "dangerZoneDesc" | i18n }}</p>
|
<p>{{ "dangerZoneDesc" | i18n }}</p>
|
||||||
<button type="button" class="btn btn-outline-danger" (click)="deleteOrganization()">
|
<button type="button" bitButton buttonType="danger" (click)="deleteOrganization()">
|
||||||
{{ "deleteOrganization" | i18n }}
|
{{ "deleteOrganization" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-danger" (click)="purgeVault()">
|
<button type="button" bitButton buttonType="danger" (click)="purgeVault()">
|
||||||
{{ "purgeVault" | i18n }}
|
{{ "purgeVault" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<ng-template #purgeOrganizationTemplate></ng-template>
|
<ng-template #purgeOrganizationTemplate></ng-template>
|
||||||
<ng-template #apiKeyTemplate></ng-template>
|
<ng-template #apiKeyTemplate></ng-template>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { lastValueFrom } from "rxjs";
|
import { combineLatest, lastValueFrom, Subject, switchMap, takeUntil, from } from "rxjs";
|
||||||
|
|
||||||
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@@ -13,6 +14,7 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
import { ApiKeyComponent } from "../../../settings/api-key.component";
|
import { ApiKeyComponent } from "../../../settings/api-key.component";
|
||||||
import { PurgeVaultComponent } from "../../../settings/purge-vault.component";
|
import { PurgeVaultComponent } from "../../../settings/purge-vault.component";
|
||||||
@@ -23,7 +25,6 @@ import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./
|
|||||||
selector: "app-org-account",
|
selector: "app-org-account",
|
||||||
templateUrl: "account.component.html",
|
templateUrl: "account.component.html",
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
|
||||||
export class AccountComponent {
|
export class AccountComponent {
|
||||||
@ViewChild("purgeOrganizationTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("purgeOrganizationTemplate", { read: ViewContainerRef, static: true })
|
||||||
purgeModalRef: ViewContainerRef;
|
purgeModalRef: ViewContainerRef;
|
||||||
@@ -40,7 +41,29 @@ export class AccountComponent {
|
|||||||
formPromise: Promise<OrganizationResponse>;
|
formPromise: Promise<OrganizationResponse>;
|
||||||
taxFormPromise: Promise<unknown>;
|
taxFormPromise: Promise<unknown>;
|
||||||
|
|
||||||
private organizationId: string;
|
// FormGroup validators taken from server Organization domain object
|
||||||
|
protected formGroup = this.formBuilder.group({
|
||||||
|
orgName: this.formBuilder.control(
|
||||||
|
{ value: "", disabled: true },
|
||||||
|
{
|
||||||
|
validators: [Validators.required, Validators.maxLength(50)],
|
||||||
|
updateOn: "change",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
billingEmail: this.formBuilder.control(
|
||||||
|
{ value: "", disabled: true },
|
||||||
|
{ validators: [Validators.required, Validators.email, Validators.maxLength(256)] }
|
||||||
|
),
|
||||||
|
businessName: this.formBuilder.control(
|
||||||
|
{ value: "", disabled: true },
|
||||||
|
{ validators: [Validators.maxLength(50)] }
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected organizationId: string;
|
||||||
|
protected publicKeyBuffer: ArrayBuffer;
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
@@ -52,34 +75,76 @@ export class AccountComponent {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
private dialogService: DialogServiceAbstraction
|
private dialogService: DialogServiceAbstraction,
|
||||||
|
private formBuilder: FormBuilder
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||||
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
this.route.parent.parent.params
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
.pipe(
|
||||||
this.organizationId = params.organizationId;
|
switchMap((params) => {
|
||||||
this.canEditSubscription = this.organizationService.get(
|
return combineLatest([
|
||||||
this.organizationId
|
// Organization domain
|
||||||
).canEditSubscription;
|
this.organizationService.get$(params.organizationId),
|
||||||
try {
|
// OrganizationResponse for form population
|
||||||
this.org = await this.organizationApiService.get(this.organizationId);
|
from(this.organizationApiService.get(params.organizationId)),
|
||||||
this.canUseApi = this.org.useApi;
|
// Organization Public Key
|
||||||
} catch (e) {
|
from(this.organizationApiService.getKeys(params.organizationId)),
|
||||||
this.logService.error(e);
|
]);
|
||||||
}
|
}),
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
)
|
||||||
|
.subscribe(([organization, orgResponse, orgKeys]) => {
|
||||||
|
// Set domain level organization variables
|
||||||
|
this.organizationId = organization.id;
|
||||||
|
this.canEditSubscription = organization.canEditSubscription;
|
||||||
|
this.canUseApi = organization.useApi;
|
||||||
|
|
||||||
|
// Org Response
|
||||||
|
this.org = orgResponse;
|
||||||
|
|
||||||
|
// Public Key Buffer for Org Fingerprint Generation
|
||||||
|
this.publicKeyBuffer = Utils.fromB64ToArray(orgKeys?.publicKey)?.buffer;
|
||||||
|
|
||||||
|
// Patch existing values
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
orgName: this.org.name,
|
||||||
|
billingEmail: this.org.billingEmail,
|
||||||
|
businessName: this.org.businessName,
|
||||||
});
|
});
|
||||||
this.loading = false;
|
|
||||||
|
// Update disabled states - reactive forms prefers not using disabled attribute
|
||||||
|
if (!this.selfHosted) {
|
||||||
|
this.formGroup.get("orgName").enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.selfHosted || this.canEditSubscription) {
|
||||||
|
this.formGroup.get("billingEmail").enable();
|
||||||
|
this.formGroup.get("businessName").enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
// You must first call .next() in order for the notifier to properly close subscriptions using takeUntil
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = async () => {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
if (this.formGroup.invalid) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
|
||||||
try {
|
|
||||||
const request = new OrganizationUpdateRequest();
|
const request = new OrganizationUpdateRequest();
|
||||||
request.name = this.org.name;
|
request.name = this.formGroup.value.orgName;
|
||||||
request.businessName = this.org.businessName;
|
request.businessName = this.formGroup.value.businessName;
|
||||||
request.billingEmail = this.org.billingEmail;
|
request.billingEmail = this.formGroup.value.billingEmail;
|
||||||
|
|
||||||
// Backfill pub/priv key if necessary
|
// Backfill pub/priv key if necessary
|
||||||
if (!this.org.hasPublicAndPrivateKeys) {
|
if (!this.org.hasPublicAndPrivateKeys) {
|
||||||
@@ -90,15 +155,8 @@ export class AccountComponent {
|
|||||||
|
|
||||||
this.formPromise = this.organizationApiService.save(this.organizationId, request);
|
this.formPromise = this.organizationApiService.save(this.organizationId, request);
|
||||||
await this.formPromise;
|
await this.formPromise;
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("organizationUpdated"));
|
||||||
"success",
|
};
|
||||||
null,
|
|
||||||
this.i18nService.t("organizationUpdated")
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteOrganization() {
|
async deleteOrganization() {
|
||||||
const dialog = openDeleteOrganizationDialog(this.dialogService, {
|
const dialog = openDeleteOrganizationDialog(this.dialogService, {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { LooseComponentsModule, SharedModule } from "../../../shared";
|
import { LooseComponentsModule, SharedModule } from "../../../shared";
|
||||||
|
import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component";
|
||||||
import { PoliciesModule } from "../../organizations/policies";
|
import { PoliciesModule } from "../../organizations/policies";
|
||||||
|
|
||||||
import { AccountComponent } from "./account.component";
|
import { AccountComponent } from "./account.component";
|
||||||
@@ -9,7 +10,13 @@ import { SettingsComponent } from "./settings.component";
|
|||||||
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
|
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule, LooseComponentsModule, PoliciesModule, OrganizationSettingsRoutingModule],
|
imports: [
|
||||||
|
SharedModule,
|
||||||
|
LooseComponentsModule,
|
||||||
|
PoliciesModule,
|
||||||
|
OrganizationSettingsRoutingModule,
|
||||||
|
AccountFingerprintComponent,
|
||||||
|
],
|
||||||
declarations: [SettingsComponent, AccountComponent, TwoFactorSetupComponent],
|
declarations: [SettingsComponent, AccountComponent, TwoFactorSetupComponent],
|
||||||
})
|
})
|
||||||
export class OrganizationSettingsModule {}
|
export class OrganizationSettingsModule {}
|
||||||
|
|||||||
@@ -46,19 +46,11 @@
|
|||||||
Customize
|
Customize
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<app-account-fingerprint
|
||||||
<p *ngIf="fingerprint">
|
[fingerprintMaterial]="fingerprintMaterial"
|
||||||
{{ "yourAccountsFingerprint" | i18n }}:
|
fingerprintLabel="{{ 'yourAccountsFingerprint' | i18n }}"
|
||||||
<a
|
|
||||||
href="https://bitwarden.com/help/fingerprint-phrase/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i></a
|
</app-account-fingerprint>
|
||||||
><br />
|
|
||||||
<code>{{ fingerprint }}</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import { Subject, takeUntil } from "rxjs";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
|
||||||
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
|
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
|
||||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@@ -21,7 +19,7 @@ import { ChangeAvatarComponent } from "./change-avatar.component";
|
|||||||
export class ProfileComponent implements OnInit, OnDestroy {
|
export class ProfileComponent implements OnInit, OnDestroy {
|
||||||
loading = true;
|
loading = true;
|
||||||
profile: ProfileResponse;
|
profile: ProfileResponse;
|
||||||
fingerprint: string;
|
fingerprintMaterial: string;
|
||||||
|
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
@ViewChild("avatarModalTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("avatarModalTemplate", { read: ViewContainerRef, static: true })
|
||||||
@@ -32,9 +30,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private keyConnectorService: KeyConnectorService,
|
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private modalService: ModalService
|
private modalService: ModalService
|
||||||
) {}
|
) {}
|
||||||
@@ -42,12 +38,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.profile = await this.apiService.getProfile();
|
this.profile = await this.apiService.getProfile();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
const fingerprint = await this.cryptoService.getFingerprint(
|
this.fingerprintMaterial = await this.stateService.getUserId();
|
||||||
await this.stateService.getUserId()
|
|
||||||
);
|
|
||||||
if (fingerprint != null) {
|
|
||||||
this.fingerprint = fingerprint.join("-");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnDestroy() {
|
async ngOnDestroy() {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<ng-container>
|
||||||
|
<hr />
|
||||||
|
<p *ngIf="fingerprint">
|
||||||
|
{{ fingerprintLabel }}:
|
||||||
|
<a
|
||||||
|
href="https://bitwarden.com/help/fingerprint-phrase/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle" aria-hidden="true"></i
|
||||||
|
></a>
|
||||||
|
<br />
|
||||||
|
<code class="tw-text-code">{{ fingerprint }}</code>
|
||||||
|
</p>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Component, Input, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
|
||||||
|
import { SharedModule } from "../../shared.module";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-account-fingerprint",
|
||||||
|
templateUrl: "account-fingerprint.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [SharedModule],
|
||||||
|
})
|
||||||
|
export class AccountFingerprintComponent implements OnInit {
|
||||||
|
@Input() fingerprintMaterial: string;
|
||||||
|
@Input() publicKeyBuffer: ArrayBuffer;
|
||||||
|
@Input() fingerprintLabel: string;
|
||||||
|
|
||||||
|
protected fingerprint: string;
|
||||||
|
|
||||||
|
constructor(private cryptoService: CryptoService) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
// TODO - In the future, remove this code and use the fingerprint pipe once merged
|
||||||
|
const generatedFingerprint = await this.cryptoService.getFingerprint(
|
||||||
|
this.fingerprintMaterial,
|
||||||
|
this.publicKeyBuffer
|
||||||
|
);
|
||||||
|
this.fingerprint = generatedFingerprint?.join("-") ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,6 +107,7 @@ import { AddEditComponent as OrgAddEditComponent } from "../vault/org-vault/add-
|
|||||||
import { AttachmentsComponent as OrgAttachmentsComponent } from "../vault/org-vault/attachments.component";
|
import { AttachmentsComponent as OrgAttachmentsComponent } from "../vault/org-vault/attachments.component";
|
||||||
import { CollectionsComponent as OrgCollectionsComponent } from "../vault/org-vault/collections.component";
|
import { CollectionsComponent as OrgCollectionsComponent } from "../vault/org-vault/collections.component";
|
||||||
|
|
||||||
|
import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component";
|
||||||
import { UserVerificationModule } from "./components/user-verification";
|
import { UserVerificationModule } from "./components/user-verification";
|
||||||
import { SharedModule } from "./shared.module";
|
import { SharedModule } from "./shared.module";
|
||||||
|
|
||||||
@@ -121,6 +122,7 @@ import { SharedModule } from "./shared.module";
|
|||||||
UserVerificationModule,
|
UserVerificationModule,
|
||||||
ChangeKdfModule,
|
ChangeKdfModule,
|
||||||
DynamicAvatarComponent,
|
DynamicAvatarComponent,
|
||||||
|
AccountFingerprintComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
PremiumBadgeComponent,
|
PremiumBadgeComponent,
|
||||||
|
|||||||
@@ -6845,6 +6845,10 @@
|
|||||||
"updatedTempPassword": {
|
"updatedTempPassword": {
|
||||||
"message": "User updated a password issued through account recovery."
|
"message": "User updated a password issued through account recovery."
|
||||||
},
|
},
|
||||||
|
"yourOrganizationsFingerprint": {
|
||||||
|
"message": "Your organization's fingerprint phrase",
|
||||||
|
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their organization's public key with another user, for the purposes of sharing."
|
||||||
|
},
|
||||||
"deviceApprovals": {
|
"deviceApprovals": {
|
||||||
"message": "Device approvals"
|
"message": "Device approvals"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export abstract class CryptoService {
|
|||||||
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
|
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
|
||||||
getPublicKey: () => Promise<ArrayBuffer>;
|
getPublicKey: () => Promise<ArrayBuffer>;
|
||||||
getPrivateKey: () => Promise<ArrayBuffer>;
|
getPrivateKey: () => Promise<ArrayBuffer>;
|
||||||
getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise<string[]>;
|
getFingerprint: (fingerprintMaterial: string, publicKey?: ArrayBuffer) => Promise<string[]>;
|
||||||
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
|
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
|
||||||
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
|
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
|
||||||
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;
|
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return privateKey;
|
return privateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise<string[]> {
|
async getFingerprint(fingerprintMaterial: string, publicKey?: ArrayBuffer): Promise<string[]> {
|
||||||
if (publicKey == null) {
|
if (publicKey == null) {
|
||||||
publicKey = await this.getPublicKey();
|
publicKey = await this.getPublicKey();
|
||||||
}
|
}
|
||||||
@@ -214,7 +214,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256");
|
const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256");
|
||||||
const userFingerprint = await this.cryptoFunctionService.hkdfExpand(
|
const userFingerprint = await this.cryptoFunctionService.hkdfExpand(
|
||||||
keyFingerprint,
|
keyFingerprint,
|
||||||
userId,
|
fingerprintMaterial,
|
||||||
32,
|
32,
|
||||||
"sha256"
|
"sha256"
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user