1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-12 00:41:29 +01:00

[PM-8524] Add support for generating usernames/passwords and auditing passwords

This commit is contained in:
Shane Melton 2024-07-11 12:19:41 -07:00
parent 2d7cbeb96c
commit 39ac6061ca
No known key found for this signature in database
3 changed files with 106 additions and 7 deletions

View File

@ -9,6 +9,7 @@ import {
} from "@storybook/angular"; } from "@storybook/angular";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@ -17,7 +18,10 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { AsyncActionsModule, ButtonModule, ToastService } from "@bitwarden/components"; import { AsyncActionsModule, ButtonModule, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import {
PasswordGenerationServiceAbstraction,
UsernameGenerationServiceAbstraction,
} from "@bitwarden/generator-legacy";
import { CipherFormConfig, PasswordRepromptService } from "@bitwarden/vault"; import { CipherFormConfig, PasswordRepromptService } from "@bitwarden/vault";
import { PreloadedEnglishI18nModule } from "@bitwarden/web-vault/src/app/core/tests"; import { PreloadedEnglishI18nModule } from "@bitwarden/web-vault/src/app/core/tests";
@ -134,12 +138,25 @@ export default {
generatePassword: () => Promise.resolve("random-password"), generatePassword: () => Promise.resolve("random-password"),
}, },
}, },
{
provide: UsernameGenerationServiceAbstraction,
useValue: {
getOptions: () => Promise.resolve({}),
generateUsername: () => Promise.resolve("random-username"),
},
},
{ {
provide: TotpCaptureService, provide: TotpCaptureService,
useValue: { useValue: {
captureTotpFromTab: () => Promise.resolve("some-value"), captureTotpFromTab: () => Promise.resolve("some-value"),
}, },
}, },
{
provide: AuditService,
useValue: {
passwordLeaked: () => Promise.resolve(0),
},
},
], ],
}), }),
componentWrapperDecorator( componentWrapperDecorator(

View File

@ -9,11 +9,32 @@
<bit-form-field> <bit-form-field>
<bit-label>{{ "username" | i18n }}</bit-label> <bit-label>{{ "username" | i18n }}</bit-label>
<input bitInput formControlName="username" /> <input bitInput formControlName="username" />
<button
type="button"
bitIconButton="bwi-generate"
bitSuffix
*ngIf="loginDetailsForm.controls.username.enabled"
data-testid="generate-username-button"
[appA11yTitle]="'generateUsername' | i18n"
[bitAction]="generateUsername"
></button>
</bit-form-field> </bit-form-field>
<bit-form-field> <bit-form-field>
<bit-label>{{ "password" | i18n }}</bit-label> <bit-label>{{ "password" | i18n }}</bit-label>
<input bitInput formControlName="password" type="password" #passwordInput /> <input bitInput formControlName="password" type="password" #passwordInput />
<button
type="button"
bitIconButton="bwi-check-circle"
bitSuffix
*ngIf="
loginDetailsForm.controls.password.enabled &&
loginDetailsForm.controls.password.value.length > 0
"
data-testid="check-password-button"
[appA11yTitle]="'checkPassword' | i18n"
[bitAction]="checkPassword"
></button>
<button <button
type="button" type="button"
bitIconButton bitIconButton
@ -23,6 +44,15 @@
bitPasswordInputToggle bitPasswordInputToggle
[passwordInput]="passwordInput" [passwordInput]="passwordInput"
></button> ></button>
<button
type="button"
bitIconButton="bwi-generate"
bitSuffix
*ngIf="loginDetailsForm.controls.password.enabled"
data-testid="generate-password-button"
[appA11yTitle]="'generatePassword' | i18n"
[bitAction]="generatePassword"
></button>
</bit-form-field> </bit-form-field>
<bit-form-field *ngIf="hasPasskey"> <bit-form-field *ngIf="hasPasskey">
@ -68,7 +98,7 @@
bitIconButton="bwi-camera" bitIconButton="bwi-camera"
bitSuffix bitSuffix
*ngIf="canCaptureTotp" *ngIf="canCaptureTotp"
[bitAction]="captureTotpFromTab" [bitAction]="captureTotp"
[appA11yTitle]="'totpCapture' | i18n" [appA11yTitle]="'totpCapture' | i18n"
></button> ></button>
</bit-form-field> </bit-form-field>

View File

@ -5,6 +5,7 @@ import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
import { map } from "rxjs"; import { map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { import {
@ -18,7 +19,10 @@ import {
ToastService, ToastService,
TypographyModule, TypographyModule,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import {
PasswordGenerationServiceAbstraction,
UsernameGenerationServiceAbstraction,
} from "@bitwarden/generator-legacy";
import { TotpCaptureService } from "../../abstractions/totp-capture.service"; import { TotpCaptureService } from "../../abstractions/totp-capture.service";
import { CipherFormContainer } from "../../cipher-form-container"; import { CipherFormContainer } from "../../cipher-form-container";
@ -84,7 +88,9 @@ export class LoginDetailsSectionComponent implements OnInit {
private cipherFormContainer: CipherFormContainer, private cipherFormContainer: CipherFormContainer,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private i18nService: I18nService, private i18nService: I18nService,
private generatorService: PasswordGenerationServiceAbstraction, private passwordGenerationService: PasswordGenerationServiceAbstraction,
private usernameGenerationService: UsernameGenerationServiceAbstraction,
private auditService: AuditService,
private toastService: ToastService, private toastService: ToastService,
@Optional() private totpCaptureService?: TotpCaptureService, @Optional() private totpCaptureService?: TotpCaptureService,
) { ) {
@ -142,7 +148,7 @@ export class LoginDetailsSectionComponent implements OnInit {
this.loginDetailsForm.controls.password.patchValue(await this.generateNewPassword()); this.loginDetailsForm.controls.password.patchValue(await this.generateNewPassword());
} }
captureTotpFromTab = async () => { captureTotp = async () => {
if (!this.canCaptureTotp) { if (!this.canCaptureTotp) {
return; return;
} }
@ -173,8 +179,54 @@ export class LoginDetailsSectionComponent implements OnInit {
}); });
}; };
/**
* Generate a new password and update the form.
* TODO: Browser extension needs a means to cache the current form so values are not lost upon navigating to the generator.
*/
generatePassword = async () => {
const newPassword = await this.generateNewPassword();
this.loginDetailsForm.controls.password.patchValue(newPassword);
};
/**
* Generate a new username and update the form.
* TODO: Browser extension needs a means to cache the current form so values are not lost upon navigating to the generator.
*/
generateUsername = async () => {
const options = await this.usernameGenerationService.getOptions();
const newUsername = await this.usernameGenerationService.generateUsername(options);
this.loginDetailsForm.controls.username.patchValue(newUsername);
};
/**
* Checks if the password has been exposed in a data breach using the AuditService.
*/
checkPassword = async () => {
const password = this.loginDetailsForm.controls.password.value;
if (password == null || password === "") {
return;
}
const matches = await this.auditService.passwordLeaked(password);
if (matches > 0) {
this.toastService.showToast({
variant: "warning",
title: null,
message: this.i18nService.t("passwordExposed", matches.toString()),
});
} else {
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("passwordSafe"),
});
}
};
private async generateNewPassword() { private async generateNewPassword() {
const [options] = await this.generatorService.getOptions(); const [options] = await this.passwordGenerationService.getOptions();
return await this.generatorService.generatePassword(options); return await this.passwordGenerationService.generatePassword(options);
} }
} }