mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +00:00
[PM-23627] Require publicKey for keyService getFingerprint (#15933)
* require public key on keyService getFingerprint * Update consumers and add error handling & logging
This commit is contained in:
@@ -24,7 +24,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
|||||||
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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
@@ -79,7 +78,6 @@ describe("AccountSecurityComponent", () => {
|
|||||||
{ provide: PlatformUtilsService, useValue: platformUtilsService },
|
{ provide: PlatformUtilsService, useValue: platformUtilsService },
|
||||||
{ provide: PolicyService, useValue: policyService },
|
{ provide: PolicyService, useValue: policyService },
|
||||||
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
||||||
{ provide: StateService, useValue: mock<StateService>() },
|
|
||||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||||
{ provide: UserVerificationService, useValue: mock<UserVerificationService>() },
|
{ provide: UserVerificationService, useValue: mock<UserVerificationService>() },
|
||||||
{ provide: VaultTimeoutService, useValue: mock<VaultTimeoutService>() },
|
{ provide: VaultTimeoutService, useValue: mock<VaultTimeoutService>() },
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ import {
|
|||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import {
|
import {
|
||||||
DialogRef,
|
DialogRef,
|
||||||
@@ -149,7 +149,6 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
public messagingService: MessagingService,
|
public messagingService: MessagingService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private keyService: KeyService,
|
private keyService: KeyService,
|
||||||
private stateService: StateService,
|
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
@@ -159,6 +158,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
private vaultNudgesService: NudgesService,
|
private vaultNudgesService: NudgesService,
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private logService: LogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -683,10 +683,16 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async openAcctFingerprintDialog() {
|
async openAcctFingerprintDialog() {
|
||||||
const activeUserId = await firstValueFrom(
|
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
|
||||||
);
|
|
||||||
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId));
|
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId));
|
||||||
|
if (publicKey == null) {
|
||||||
|
this.logService.error(
|
||||||
|
"[AccountSecurityComponent] No public key available for the user: " +
|
||||||
|
activeUserId +
|
||||||
|
" fingerprint can't be displayed.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
|
const fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
|
||||||
|
|
||||||
const dialogRef = FingerprintDialogComponent.open(this.dialogService, {
|
const dialogRef = FingerprintDialogComponent.open(this.dialogService, {
|
||||||
|
|||||||
@@ -606,6 +606,9 @@ export class GetCommand extends DownloadCommand {
|
|||||||
if (id === "me") {
|
if (id === "me") {
|
||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId));
|
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId));
|
||||||
|
if (publicKey == null) {
|
||||||
|
return Response.error("No public key available for the active user.");
|
||||||
|
}
|
||||||
fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
|
fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
|
||||||
} else if (Utils.isGuid(id)) {
|
} else if (Utils.isGuid(id)) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -299,12 +299,22 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
break;
|
break;
|
||||||
case "showFingerprintPhrase": {
|
case "showFingerprintPhrase": {
|
||||||
const activeUserId = await firstValueFrom(
|
const activeUserId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
getUserId(this.accountService.activeAccount$),
|
||||||
);
|
);
|
||||||
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId));
|
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId));
|
||||||
const fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
|
if (publicKey == null) {
|
||||||
const dialogRef = FingerprintDialogComponent.open(this.dialogService, { fingerprint });
|
this.logService.error(
|
||||||
await firstValueFrom(dialogRef.closed);
|
"[AppComponent] No public key available for the user: " +
|
||||||
|
activeUserId +
|
||||||
|
" fingerprint can't be displayed.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
|
||||||
|
const dialogRef = FingerprintDialogComponent.open(this.dialogService, {
|
||||||
|
fingerprint,
|
||||||
|
});
|
||||||
|
await firstValueFrom(dialogRef.closed);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "deleteAccount":
|
case "deleteAccount":
|
||||||
|
|||||||
@@ -46,11 +46,14 @@
|
|||||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<app-account-fingerprint
|
@if (fingerprintMaterial && userPublicKey) {
|
||||||
[fingerprintMaterial]="fingerprintMaterial"
|
<app-account-fingerprint
|
||||||
fingerprintLabel="{{ 'yourAccountsFingerprint' | i18n }}"
|
[fingerprintMaterial]="fingerprintMaterial"
|
||||||
>
|
[publicKeyBuffer]="userPublicKey"
|
||||||
</app-account-fingerprint>
|
fingerprintLabel="{{ 'yourAccountsFingerprint' | i18n }}"
|
||||||
|
>
|
||||||
|
</app-account-fingerprint>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button bitButton bitFormButton type="submit" buttonType="primary">{{ "save" | i18n }}</button>
|
<button bitButton bitFormButton type="submit" buttonType="primary">{{ "save" | i18n }}</button>
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/upda
|
|||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||||
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 { UserPublicKey } from "@bitwarden/common/types/key";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { DynamicAvatarComponent } from "../../../components/dynamic-avatar.component";
|
import { DynamicAvatarComponent } from "../../../components/dynamic-avatar.component";
|
||||||
import { SharedModule } from "../../../shared";
|
import { SharedModule } from "../../../shared";
|
||||||
@@ -29,6 +32,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
loading = true;
|
loading = true;
|
||||||
profile: ProfileResponse;
|
profile: ProfileResponse;
|
||||||
fingerprintMaterial: string;
|
fingerprintMaterial: string;
|
||||||
|
userPublicKey: UserPublicKey;
|
||||||
managingOrganization$: Observable<Organization>;
|
managingOrganization$: Observable<Organization>;
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@@ -44,16 +48,24 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
|
private keyService: KeyService,
|
||||||
|
private logService: LogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.profile = await this.apiService.getProfile();
|
this.profile = await this.apiService.getProfile();
|
||||||
this.loading = false;
|
|
||||||
this.fingerprintMaterial = await firstValueFrom(
|
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||||
|
this.fingerprintMaterial = userId;
|
||||||
|
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(userId));
|
||||||
|
if (publicKey == null) {
|
||||||
|
this.logService.error(
|
||||||
|
"[ProfileComponent] No public key available for the user: " +
|
||||||
|
userId +
|
||||||
|
" fingerprint can't be displayed.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.userPublicKey = publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
this.managingOrganization$ = this.organizationService
|
this.managingOrganization$ = this.organizationService
|
||||||
.organizations$(userId)
|
.organizations$(userId)
|
||||||
@@ -70,6 +82,8 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
.subscribe((name) => {
|
.subscribe((name) => {
|
||||||
this.profile.name = name;
|
this.profile.name = name;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
openChangeAvatar = async () => {
|
openChangeAvatar = async () => {
|
||||||
|
|||||||
@@ -318,14 +318,14 @@ export abstract class KeyService {
|
|||||||
): Observable<{ privateKey: UserPrivateKey; publicKey: UserPublicKey } | null>;
|
): Observable<{ privateKey: UserPrivateKey; publicKey: UserPublicKey } | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a fingerprint phrase for the user based on their public key
|
* Generates a fingerprint phrase for the public key provided.
|
||||||
*
|
*
|
||||||
* @throws Error when publicKey is null and there is no active user, or the active user does not have a public key
|
* @throws Error when publicKey is null or undefined.
|
||||||
* @param fingerprintMaterial Fingerprint material
|
* @param fingerprintMaterial Fingerprint material
|
||||||
* @param publicKey The user's public key
|
* @param publicKey The public key to generate the fingerprint phrase for.
|
||||||
* @returns The user's fingerprint phrase
|
* @returns The fingerprint phrase
|
||||||
*/
|
*/
|
||||||
abstract getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]>;
|
abstract getFingerprint(fingerprintMaterial: string, publicKey: Uint8Array): Promise<string[]>;
|
||||||
/**
|
/**
|
||||||
* Generates a new keypair
|
* Generates a new keypair
|
||||||
* @param key A key to encrypt the private key with. If not provided,
|
* @param key A key to encrypt the private key with. If not provided,
|
||||||
|
|||||||
@@ -1227,6 +1227,63 @@ describe("keyService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getFingerprint", () => {
|
||||||
|
const mockFingerprintMaterial = "test@example.com";
|
||||||
|
const mockPublicKey = new Uint8Array(256);
|
||||||
|
const mockKeyFingerprint = Utils.fromB64ToArray("nfG2jTrJilBEsSrg7ffe9exE9PlClem4P2bxlQ6rNbs=");
|
||||||
|
const mockUserFingerprint = Utils.fromB64ToArray(
|
||||||
|
"V5AQSk83YXd6kZqCncC6d9J72R7UZ60Xl1eIoDoWgTc=",
|
||||||
|
);
|
||||||
|
const expectedFingerprint = ["predefine", "hunting", "pastime", "enrich", "unhearing"];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cryptoFunctionService.hash.mockResolvedValue(mockKeyFingerprint);
|
||||||
|
cryptoFunctionService.hkdfExpand.mockResolvedValue(mockUserFingerprint);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([null as unknown as Uint8Array, undefined as unknown as Uint8Array])(
|
||||||
|
"throws when publicKey is %s",
|
||||||
|
async (publicKey) => {
|
||||||
|
await expect(keyService.getFingerprint(mockFingerprintMaterial, publicKey)).rejects.toThrow(
|
||||||
|
"Public key is required to generate a fingerprint.",
|
||||||
|
);
|
||||||
|
expect(cryptoFunctionService.hash).not.toHaveBeenCalled();
|
||||||
|
expect(cryptoFunctionService.hkdfExpand).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("generates fingerprint successfully", async () => {
|
||||||
|
const result = await keyService.getFingerprint(mockFingerprintMaterial, mockPublicKey);
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedFingerprint);
|
||||||
|
expect(cryptoFunctionService.hash).toHaveBeenCalledWith(mockPublicKey, "sha256");
|
||||||
|
expect(cryptoFunctionService.hkdfExpand).toHaveBeenCalledWith(
|
||||||
|
mockKeyFingerprint,
|
||||||
|
mockFingerprintMaterial,
|
||||||
|
32,
|
||||||
|
"sha256",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when entropy of hash function is too small", async () => {
|
||||||
|
const keyFingerprint = new Uint8Array(3);
|
||||||
|
cryptoFunctionService.hash.mockResolvedValue(keyFingerprint);
|
||||||
|
cryptoFunctionService.hkdfExpand.mockResolvedValue(new Uint8Array(3));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
keyService.getFingerprint(mockFingerprintMaterial, mockPublicKey),
|
||||||
|
).rejects.toThrow("Output entropy of hash function is too small");
|
||||||
|
|
||||||
|
expect(cryptoFunctionService.hash).toHaveBeenCalledWith(mockPublicKey, "sha256");
|
||||||
|
expect(cryptoFunctionService.hkdfExpand).toHaveBeenCalledWith(
|
||||||
|
keyFingerprint,
|
||||||
|
mockFingerprintMaterial,
|
||||||
|
32,
|
||||||
|
"sha256",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("makeKeyPair", () => {
|
describe("makeKeyPair", () => {
|
||||||
test.each([null as unknown as SymmetricCryptoKey, undefined as unknown as SymmetricCryptoKey])(
|
test.each([null as unknown as SymmetricCryptoKey, undefined as unknown as SymmetricCryptoKey])(
|
||||||
"throws when the provided key is %s",
|
"throws when the provided key is %s",
|
||||||
|
|||||||
@@ -473,19 +473,11 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
|||||||
.update(() => encPrivateKey);
|
.update(() => encPrivateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make public key required
|
async getFingerprint(fingerprintMaterial: string, publicKey: Uint8Array): Promise<string[]> {
|
||||||
async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]> {
|
|
||||||
if (publicKey == null) {
|
if (publicKey == null) {
|
||||||
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
|
throw new Error("Public key is required to generate a fingerprint.");
|
||||||
if (activeUserId == null) {
|
|
||||||
throw new Error("No active user found.");
|
|
||||||
}
|
|
||||||
publicKey = (await firstValueFrom(this.userPublicKey$(activeUserId))) as Uint8Array;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publicKey === null) {
|
|
||||||
throw new Error("No public key available.");
|
|
||||||
}
|
|
||||||
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user