mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +00:00
[PM-8279] password generation component (#10805)
This commit is contained in:
@@ -1 +1 @@
|
|||||||
<bit-password-settings />
|
<bit-password-generator />
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
import {
|
import { PasswordGeneratorComponent } from "@bitwarden/generator-components";
|
||||||
PassphraseSettingsComponent,
|
|
||||||
PasswordSettingsComponent,
|
|
||||||
} from "@bitwarden/generator-components";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: "credential-generator",
|
selector: "credential-generator",
|
||||||
templateUrl: "credential-generator.component.html",
|
templateUrl: "credential-generator.component.html",
|
||||||
imports: [PassphraseSettingsComponent, PasswordSettingsComponent],
|
imports: [PasswordGeneratorComponent],
|
||||||
})
|
})
|
||||||
export class CredentialGeneratorComponent {}
|
export class CredentialGeneratorComponent {}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export type SingleUserDependency = {
|
|||||||
export type OnDependency = {
|
export type OnDependency = {
|
||||||
/** The stream that controls emissions
|
/** The stream that controls emissions
|
||||||
*/
|
*/
|
||||||
on$: Observable<void>;
|
on$: Observable<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A pattern for types that emit when a dependency is `true`.
|
/** A pattern for types that emit when a dependency is `true`.
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
|
import { GenerationRequest } from "../../types";
|
||||||
|
|
||||||
/** Options that provide contextual information about the application state
|
/** Options that provide contextual information about the application state
|
||||||
* when an integration is invoked.
|
* when an integration is invoked.
|
||||||
*/
|
*/
|
||||||
export type IntegrationRequest = {
|
export type IntegrationRequest = Partial<GenerationRequest>;
|
||||||
/** @param website The domain of the website the requested integration is used
|
|
||||||
* within. This should be set to `null` when the request is not specific
|
|
||||||
* to any website.
|
|
||||||
* @remarks this field contains sensitive data
|
|
||||||
*/
|
|
||||||
website: string | null;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -46,3 +46,20 @@ export type Constraints<T> = {
|
|||||||
|
|
||||||
/** utility type for methods that evaluate constraints generically. */
|
/** utility type for methods that evaluate constraints generically. */
|
||||||
export type AnyConstraint = PrimitiveConstraint & StringConstraints & NumberConstraints;
|
export type AnyConstraint = PrimitiveConstraint & StringConstraints & NumberConstraints;
|
||||||
|
|
||||||
|
/** Options that provide contextual information about the application state
|
||||||
|
* when a generator is invoked.
|
||||||
|
*/
|
||||||
|
export type VaultItemRequest = {
|
||||||
|
/** The domain of the website the requested credential is used
|
||||||
|
* within. This should be set to `null` when the request is not specific
|
||||||
|
* to any website.
|
||||||
|
* @remarks this field contains sensitive data
|
||||||
|
*/
|
||||||
|
website: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Options that provide contextual information about the application state
|
||||||
|
* when a generator is invoked.
|
||||||
|
*/
|
||||||
|
export type GenerationRequest = Partial<VaultItemRequest>;
|
||||||
|
|||||||
@@ -4,41 +4,60 @@ import { ReactiveFormsModule } from "@angular/forms";
|
|||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||||
|
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import {
|
import {
|
||||||
CardComponent,
|
CardComponent,
|
||||||
CheckboxModule,
|
CheckboxModule,
|
||||||
ColorPasswordModule,
|
ColorPasswordModule,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
|
IconButtonModule,
|
||||||
InputModule,
|
InputModule,
|
||||||
|
ItemModule,
|
||||||
SectionComponent,
|
SectionComponent,
|
||||||
SectionHeaderComponent,
|
SectionHeaderComponent,
|
||||||
|
ToggleGroupModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { CredentialGeneratorService } from "@bitwarden/generator-core";
|
import {
|
||||||
|
createRandomizer,
|
||||||
|
CredentialGeneratorService,
|
||||||
|
Randomizer,
|
||||||
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
|
const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
||||||
|
|
||||||
/** Shared module containing generator component dependencies */
|
/** Shared module containing generator component dependencies */
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SectionComponent, SectionHeaderComponent, CardComponent],
|
imports: [CardComponent, SectionComponent, SectionHeaderComponent],
|
||||||
exports: [
|
exports: [
|
||||||
|
CardComponent,
|
||||||
|
CheckboxModule,
|
||||||
|
CommonModule,
|
||||||
|
ColorPasswordModule,
|
||||||
|
FormFieldModule,
|
||||||
|
IconButtonModule,
|
||||||
|
InputModule,
|
||||||
|
ItemModule,
|
||||||
JslibModule,
|
JslibModule,
|
||||||
JslibServicesModule,
|
JslibServicesModule,
|
||||||
FormFieldModule,
|
|
||||||
CommonModule,
|
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
ColorPasswordModule,
|
|
||||||
InputModule,
|
|
||||||
CheckboxModule,
|
|
||||||
SectionComponent,
|
SectionComponent,
|
||||||
SectionHeaderComponent,
|
SectionHeaderComponent,
|
||||||
CardComponent,
|
ToggleGroupModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
safeProvider({
|
||||||
|
provide: RANDOMIZER,
|
||||||
|
useFactory: createRandomizer,
|
||||||
|
deps: [CryptoService],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: CredentialGeneratorService,
|
provide: CredentialGeneratorService,
|
||||||
useClass: CredentialGeneratorService,
|
useClass: CredentialGeneratorService,
|
||||||
deps: [StateProvider, PolicyService],
|
deps: [RANDOMIZER, StateProvider, PolicyService],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
declarations: [],
|
declarations: [],
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
|||||||
export { CredentialGeneratorHistoryComponent } from "./credential-generator-history.component";
|
export { CredentialGeneratorHistoryComponent } from "./credential-generator-history.component";
|
||||||
export { EmptyCredentialHistoryComponent } from "./empty-credential-history.component";
|
export { EmptyCredentialHistoryComponent } from "./empty-credential-history.component";
|
||||||
export { PasswordSettingsComponent } from "./password-settings.component";
|
export { PasswordSettingsComponent } from "./password-settings.component";
|
||||||
|
export { PasswordGeneratorComponent } from "./password-generator.component";
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<bit-toggle-group
|
||||||
|
fullWidth
|
||||||
|
class="tw-mb-4"
|
||||||
|
[selected]="credentialType$ | async"
|
||||||
|
(selectedChange)="onCredentialTypeChanged($event)"
|
||||||
|
attr.aria-label="{{ 'type' | i18n }}"
|
||||||
|
>
|
||||||
|
<bit-toggle value="password">
|
||||||
|
{{ "password" | i18n }}
|
||||||
|
</bit-toggle>
|
||||||
|
<bit-toggle value="passphrase">
|
||||||
|
{{ "passphrase" | i18n }}
|
||||||
|
</bit-toggle>
|
||||||
|
</bit-toggle-group>
|
||||||
|
<bit-card class="tw-flex tw-justify-between tw-mb-4">
|
||||||
|
<div class="tw-grow">
|
||||||
|
<bit-color-password class="tw-font-mono" [password]="value$ | async"></bit-color-password>
|
||||||
|
</div>
|
||||||
|
<div class="tw-space-x-1 tw-flex-none tw-w-4">
|
||||||
|
<button type="button" bitIconButton="bwi-generate" buttonType="main" (click)="generate$.next()">
|
||||||
|
{{ "generatePassword" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-clone"
|
||||||
|
buttonType="main"
|
||||||
|
[appCopyClick]="value$ | async"
|
||||||
|
>
|
||||||
|
{{ "copyPassword" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</bit-card>
|
||||||
|
<bit-password-settings
|
||||||
|
class="tw-mt-6"
|
||||||
|
*ngIf="(credentialType$ | async) === 'password'"
|
||||||
|
[userId]="this.userId$ | async"
|
||||||
|
(onUpdated)="generate$.next()"
|
||||||
|
/>
|
||||||
|
<bit-passphrase-settings
|
||||||
|
class="tw-mt-6"
|
||||||
|
*ngIf="(credentialType$ | async) === 'passphrase'"
|
||||||
|
[userId]="this.userId$ | async"
|
||||||
|
(onUpdated)="generate$.next()"
|
||||||
|
/>
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
|
import { BehaviorSubject, distinctUntilChanged, map, Subject, switchMap, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CredentialGeneratorService, Generators, GeneratorType } from "@bitwarden/generator-core";
|
||||||
|
import { GeneratedCredential } from "@bitwarden/generator-history";
|
||||||
|
|
||||||
|
import { DependenciesModule } from "./dependencies";
|
||||||
|
import { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
||||||
|
import { PasswordSettingsComponent } from "./password-settings.component";
|
||||||
|
|
||||||
|
/** Options group for passwords */
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "bit-password-generator",
|
||||||
|
templateUrl: "password-generator.component.html",
|
||||||
|
imports: [DependenciesModule, PasswordSettingsComponent, PassphraseSettingsComponent],
|
||||||
|
})
|
||||||
|
export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||||
|
constructor(
|
||||||
|
private generatorService: CredentialGeneratorService,
|
||||||
|
private accountService: AccountService,
|
||||||
|
private zone: NgZone,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Binds the passphrase component to a specific user's settings.
|
||||||
|
* When this input is not provided, the form binds to the active
|
||||||
|
* user
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
userId: UserId | null;
|
||||||
|
|
||||||
|
/** tracks the currently selected credential type */
|
||||||
|
protected credentialType$ = new BehaviorSubject<GeneratorType>("password");
|
||||||
|
|
||||||
|
/** Emits the last generated value. */
|
||||||
|
protected readonly value$ = new BehaviorSubject<string>("");
|
||||||
|
|
||||||
|
/** Emits when the userId changes */
|
||||||
|
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||||
|
|
||||||
|
/** Emits when a new credential is requested */
|
||||||
|
protected readonly generate$ = new Subject<void>();
|
||||||
|
|
||||||
|
/** Tracks changes to the selected credential type
|
||||||
|
* @param type the new credential type
|
||||||
|
*/
|
||||||
|
protected onCredentialTypeChanged(type: GeneratorType) {
|
||||||
|
if (this.credentialType$.value !== type) {
|
||||||
|
this.credentialType$.next(type);
|
||||||
|
this.generate$.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Emits credentials created from a generation request. */
|
||||||
|
@Output()
|
||||||
|
readonly onGenerated = new EventEmitter<GeneratedCredential>();
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
if (this.userId) {
|
||||||
|
this.userId$.next(this.userId);
|
||||||
|
} else {
|
||||||
|
this.accountService.activeAccount$
|
||||||
|
.pipe(
|
||||||
|
map((acct) => acct.id),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
takeUntil(this.destroyed),
|
||||||
|
)
|
||||||
|
.subscribe(this.userId$);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.credentialType$
|
||||||
|
.pipe(
|
||||||
|
switchMap((type) => this.typeToGenerator$(type)),
|
||||||
|
takeUntil(this.destroyed),
|
||||||
|
)
|
||||||
|
.subscribe((generated) => {
|
||||||
|
// update subjects within the angular zone so that the
|
||||||
|
// template bindings refresh immediately
|
||||||
|
this.zone.run(() => {
|
||||||
|
this.onGenerated.next(generated);
|
||||||
|
this.value$.next(generated.credential);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private typeToGenerator$(type: GeneratorType) {
|
||||||
|
const dependencies = {
|
||||||
|
on$: this.generate$,
|
||||||
|
userId$: this.userId$,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "password":
|
||||||
|
return this.generatorService.generate$(Generators.Password, dependencies);
|
||||||
|
|
||||||
|
case "passphrase":
|
||||||
|
return this.generatorService.generate$(Generators.Passphrase, dependencies);
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid generator type: "${type}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly destroyed = new Subject<void>();
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
// tear down subscriptions
|
||||||
|
this.destroyed.complete();
|
||||||
|
|
||||||
|
// finalize subjects
|
||||||
|
this.generate$.complete();
|
||||||
|
this.value$.complete();
|
||||||
|
|
||||||
|
// finalize component bindings
|
||||||
|
this.onGenerated.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { Randomizer } from "../abstractions";
|
||||||
|
import { PasswordRandomizer } from "../engine";
|
||||||
import { PASSPHRASE_SETTINGS, PASSWORD_SETTINGS } from "../strategies/storage";
|
import { PASSPHRASE_SETTINGS, PASSWORD_SETTINGS } from "../strategies/storage";
|
||||||
import {
|
import {
|
||||||
|
CredentialGenerator,
|
||||||
PassphraseGenerationOptions,
|
PassphraseGenerationOptions,
|
||||||
PassphraseGeneratorPolicy,
|
PassphraseGeneratorPolicy,
|
||||||
PasswordGenerationOptions,
|
PasswordGenerationOptions,
|
||||||
@@ -14,6 +17,12 @@ import { DefaultPasswordGenerationOptions } from "./default-password-generation-
|
|||||||
import { Policies } from "./policies";
|
import { Policies } from "./policies";
|
||||||
|
|
||||||
const PASSPHRASE = Object.freeze({
|
const PASSPHRASE = Object.freeze({
|
||||||
|
category: "passphrase",
|
||||||
|
engine: {
|
||||||
|
create(randomizer: Randomizer): CredentialGenerator<PassphraseGenerationOptions> {
|
||||||
|
return new PasswordRandomizer(randomizer);
|
||||||
|
},
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
initial: DefaultPassphraseGenerationOptions,
|
initial: DefaultPassphraseGenerationOptions,
|
||||||
constraints: {
|
constraints: {
|
||||||
@@ -32,6 +41,12 @@ const PASSPHRASE = Object.freeze({
|
|||||||
>);
|
>);
|
||||||
|
|
||||||
const PASSWORD = Object.freeze({
|
const PASSWORD = Object.freeze({
|
||||||
|
category: "password",
|
||||||
|
engine: {
|
||||||
|
create(randomizer: Randomizer): CredentialGenerator<PasswordGenerationOptions> {
|
||||||
|
return new PasswordRandomizer(randomizer);
|
||||||
|
},
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
initial: DefaultPasswordGenerationOptions,
|
initial: DefaultPasswordGenerationOptions,
|
||||||
constraints: {
|
constraints: {
|
||||||
|
|||||||
@@ -335,4 +335,40 @@ describe("PasswordRandomizer", () => {
|
|||||||
expect(result).toEqual("foo-foo");
|
expect(result).toEqual("foo-foo");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("generate", () => {
|
||||||
|
it("processes password generation options", async () => {
|
||||||
|
const password = new PasswordRandomizer(randomizer);
|
||||||
|
|
||||||
|
const result = await password.generate(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
length: 10,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.category).toEqual("password");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("processes passphrase generation options", async () => {
|
||||||
|
const password = new PasswordRandomizer(randomizer);
|
||||||
|
|
||||||
|
const result = await password.generate(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
numWords: 10,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.category).toEqual("passphrase");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when it cannot recognize the options type", async () => {
|
||||||
|
const password = new PasswordRandomizer(randomizer);
|
||||||
|
|
||||||
|
const result = password.generate({}, {});
|
||||||
|
|
||||||
|
await expect(result).rejects.toBeInstanceOf(Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||||
|
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CredentialGenerator,
|
||||||
|
GeneratedCredential,
|
||||||
|
PassphraseGenerationOptions,
|
||||||
|
PasswordGenerationOptions,
|
||||||
|
} from "../types";
|
||||||
|
import { optionsToEffWordListRequest, optionsToRandomAsciiRequest } from "../util";
|
||||||
|
|
||||||
import { Randomizer } from "./abstractions";
|
import { Randomizer } from "./abstractions";
|
||||||
import { Ascii } from "./data";
|
import { Ascii } from "./data";
|
||||||
import { CharacterSet, EffWordListRequest, RandomAsciiRequest } from "./types";
|
import { CharacterSet, EffWordListRequest, RandomAsciiRequest } from "./types";
|
||||||
|
|
||||||
/** Generation algorithms that produce randomized secrets */
|
/** Generation algorithms that produce randomized secrets */
|
||||||
export class PasswordRandomizer {
|
export class PasswordRandomizer
|
||||||
|
implements
|
||||||
|
CredentialGenerator<PassphraseGenerationOptions>,
|
||||||
|
CredentialGenerator<PasswordGenerationOptions>
|
||||||
|
{
|
||||||
/** Instantiates the password randomizer
|
/** Instantiates the password randomizer
|
||||||
* @param random data source for random data
|
* @param randomizer data source for random data
|
||||||
*/
|
*/
|
||||||
constructor(private randomizer: Randomizer) {}
|
constructor(private randomizer: Randomizer) {}
|
||||||
|
|
||||||
@@ -52,6 +65,41 @@ export class PasswordRandomizer {
|
|||||||
|
|
||||||
return wordList.join(request.separator);
|
return wordList.join(request.separator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generate(
|
||||||
|
request: GenerationRequest,
|
||||||
|
settings: PasswordGenerationOptions,
|
||||||
|
): Promise<GeneratedCredential>;
|
||||||
|
generate(
|
||||||
|
request: GenerationRequest,
|
||||||
|
settings: PassphraseGenerationOptions,
|
||||||
|
): Promise<GeneratedCredential>;
|
||||||
|
async generate(
|
||||||
|
_request: GenerationRequest,
|
||||||
|
settings: PasswordGenerationOptions | PassphraseGenerationOptions,
|
||||||
|
) {
|
||||||
|
if (isPasswordGenerationOptions(settings)) {
|
||||||
|
const request = optionsToRandomAsciiRequest(settings);
|
||||||
|
const password = await this.randomAscii(request);
|
||||||
|
|
||||||
|
return new GeneratedCredential(password, "password", Date.now());
|
||||||
|
} else if (isPassphraseGenerationOptions(settings)) {
|
||||||
|
const request = optionsToEffWordListRequest(settings);
|
||||||
|
const passphrase = await this.randomEffLongWords(request);
|
||||||
|
|
||||||
|
return new GeneratedCredential(passphrase, "passphrase", Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Invalid settings received by generator.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPasswordGenerationOptions(settings: any): settings is PasswordGenerationOptions {
|
||||||
|
return "length" in (settings ?? {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPassphraseGenerationOptions(settings: any): settings is PassphraseGenerationOptions {
|
||||||
|
return "numWords" in (settings ?? {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// given a generator request, convert each of its `number | undefined` properties
|
// given a generator request, convert each of its `number | undefined` properties
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
import { BehaviorSubject, filter, firstValueFrom, Subject } from "rxjs";
|
||||||
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
@@ -8,9 +8,14 @@ import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/st
|
|||||||
import { Constraints } from "@bitwarden/common/tools/types";
|
import { Constraints } from "@bitwarden/common/tools/types";
|
||||||
import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
|
import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { FakeStateProvider, FakeAccountService, awaitAsync } from "../../../../../common/spec";
|
import {
|
||||||
import { PolicyEvaluator } from "../abstractions";
|
FakeStateProvider,
|
||||||
import { CredentialGeneratorConfiguration } from "../types";
|
FakeAccountService,
|
||||||
|
awaitAsync,
|
||||||
|
ObservableTracker,
|
||||||
|
} from "../../../../../common/spec";
|
||||||
|
import { PolicyEvaluator, Randomizer } from "../abstractions";
|
||||||
|
import { CredentialGeneratorConfiguration, GeneratedCredential } from "../types";
|
||||||
|
|
||||||
import { CredentialGeneratorService } from "./credential-generator.service";
|
import { CredentialGeneratorService } from "./credential-generator.service";
|
||||||
|
|
||||||
@@ -34,8 +39,23 @@ const somePolicy = new Policy({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const SomeTime = new Date(1);
|
||||||
|
const SomeCategory = "passphrase";
|
||||||
|
|
||||||
// fake the configuration
|
// fake the configuration
|
||||||
const SomeConfiguration: CredentialGeneratorConfiguration<SomeSettings, SomePolicy> = {
|
const SomeConfiguration: CredentialGeneratorConfiguration<SomeSettings, SomePolicy> = {
|
||||||
|
category: SomeCategory,
|
||||||
|
engine: {
|
||||||
|
create: (randomizer) => {
|
||||||
|
return {
|
||||||
|
generate: (request, settings) => {
|
||||||
|
const credential = request.website ? `${request.website}|${settings.foo}` : settings.foo;
|
||||||
|
const result = new GeneratedCredential(credential, SomeCategory, SomeTime);
|
||||||
|
return Promise.resolve(result);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
initial: { foo: "initial" },
|
initial: { foo: "initial" },
|
||||||
constraints: { foo: {} },
|
constraints: { foo: {} },
|
||||||
@@ -87,6 +107,9 @@ const accountService = new FakeAccountService({
|
|||||||
// fake state
|
// fake state
|
||||||
const stateProvider = new FakeStateProvider(accountService);
|
const stateProvider = new FakeStateProvider(accountService);
|
||||||
|
|
||||||
|
// fake randomizer
|
||||||
|
const randomizer = mock<Randomizer>();
|
||||||
|
|
||||||
describe("CredentialGeneratorService", () => {
|
describe("CredentialGeneratorService", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await accountService.switchAccount(SomeUser);
|
await accountService.switchAccount(SomeUser);
|
||||||
@@ -94,10 +117,242 @@ describe("CredentialGeneratorService", () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("generate$", () => {
|
||||||
|
it("emits a generation for the active user when subscribed", async () => {
|
||||||
|
const settings = { foo: "value" };
|
||||||
|
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
||||||
|
|
||||||
|
const result = await generated.expectEmission();
|
||||||
|
|
||||||
|
expect(result).toEqual(new GeneratedCredential("value", SomeCategory, SomeTime));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("follows the active user", async () => {
|
||||||
|
const someSettings = { foo: "some value" };
|
||||||
|
const anotherSettings = { foo: "another value" };
|
||||||
|
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
||||||
|
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
||||||
|
|
||||||
|
await accountService.switchAccount(AnotherUser);
|
||||||
|
await generated.pauseUntilReceived(2);
|
||||||
|
generated.unsubscribe();
|
||||||
|
|
||||||
|
expect(generated.emissions).toEqual([
|
||||||
|
new GeneratedCredential("some value", SomeCategory, SomeTime),
|
||||||
|
new GeneratedCredential("another value", SomeCategory, SomeTime),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits a generation when the settings change", async () => {
|
||||||
|
const someSettings = { foo: "some value" };
|
||||||
|
const anotherSettings = { foo: "another value" };
|
||||||
|
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
||||||
|
|
||||||
|
await stateProvider.setUserState(SettingsKey, anotherSettings, SomeUser);
|
||||||
|
await generated.pauseUntilReceived(2);
|
||||||
|
generated.unsubscribe();
|
||||||
|
|
||||||
|
expect(generated.emissions).toEqual([
|
||||||
|
new GeneratedCredential("some value", SomeCategory, SomeTime),
|
||||||
|
new GeneratedCredential("another value", SomeCategory, SomeTime),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXME: test these when the fake state provider can create the required emissions
|
||||||
|
it.todo("errors when the settings error");
|
||||||
|
it.todo("completes when the settings complete");
|
||||||
|
|
||||||
|
it("includes `website$`'s last emitted value", async () => {
|
||||||
|
const settings = { foo: "value" };
|
||||||
|
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const website$ = new BehaviorSubject("some website");
|
||||||
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { website$ }));
|
||||||
|
|
||||||
|
const result = await generated.expectEmission();
|
||||||
|
|
||||||
|
expect(result).toEqual(new GeneratedCredential("some website|value", SomeCategory, SomeTime));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when `website$` errors", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const website$ = new BehaviorSubject("some website");
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
generator.generate$(SomeConfiguration, { website$ }).subscribe({
|
||||||
|
error: (e: unknown) => {
|
||||||
|
error = e;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
website$.error({ some: "error" });
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(error).toEqual({ some: "error" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when `website$` completes", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const website$ = new BehaviorSubject("some website");
|
||||||
|
let completed = false;
|
||||||
|
|
||||||
|
generator.generate$(SomeConfiguration, { website$ }).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
completed = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
website$.complete();
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(completed).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits a generation for a specific user when `user$` supplied", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
||||||
|
await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||||
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ }));
|
||||||
|
|
||||||
|
const result = await generated.expectEmission();
|
||||||
|
|
||||||
|
expect(result).toEqual(new GeneratedCredential("another", SomeCategory, SomeTime));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits a generation for a specific user when `user$` emits", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
||||||
|
await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.pipe(filter((u) => !!u));
|
||||||
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ }));
|
||||||
|
|
||||||
|
userId.next(AnotherUser);
|
||||||
|
const result = await generated.pauseUntilReceived(2);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
new GeneratedCredential("value", SomeCategory, SomeTime),
|
||||||
|
new GeneratedCredential("another", SomeCategory, SomeTime),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when `user$` errors", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const userId$ = new BehaviorSubject(SomeUser);
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
generator.generate$(SomeConfiguration, { userId$ }).subscribe({
|
||||||
|
error: (e: unknown) => {
|
||||||
|
error = e;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
userId$.error({ some: "error" });
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(error).toEqual({ some: "error" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when `user$` completes", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const userId$ = new BehaviorSubject(SomeUser);
|
||||||
|
let completed = false;
|
||||||
|
|
||||||
|
generator.generate$(SomeConfiguration, { userId$ }).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
completed = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
userId$.complete();
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(completed).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits a generation only when `on$` emits", async () => {
|
||||||
|
// This test breaks from arrange/act/assert because it is testing causality
|
||||||
|
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const on$ = new Subject<void>();
|
||||||
|
const results: any[] = [];
|
||||||
|
|
||||||
|
// confirm no emission during subscription
|
||||||
|
const sub = generator
|
||||||
|
.generate$(SomeConfiguration, { on$ })
|
||||||
|
.subscribe((result) => results.push(result));
|
||||||
|
await awaitAsync();
|
||||||
|
expect(results.length).toEqual(0);
|
||||||
|
|
||||||
|
// confirm forwarded emission
|
||||||
|
on$.next();
|
||||||
|
await awaitAsync();
|
||||||
|
expect(results).toEqual([new GeneratedCredential("value", SomeCategory, SomeTime)]);
|
||||||
|
|
||||||
|
// confirm updating settings does not cause an emission
|
||||||
|
await stateProvider.setUserState(SettingsKey, { foo: "next" }, SomeUser);
|
||||||
|
await awaitAsync();
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
|
||||||
|
// confirm forwarded emission takes latest value
|
||||||
|
on$.next();
|
||||||
|
await awaitAsync();
|
||||||
|
sub.unsubscribe();
|
||||||
|
|
||||||
|
expect(results).toEqual([
|
||||||
|
new GeneratedCredential("value", SomeCategory, SomeTime),
|
||||||
|
new GeneratedCredential("next", SomeCategory, SomeTime),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when `on$` errors", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const on$ = new Subject<void>();
|
||||||
|
let error: any = null;
|
||||||
|
|
||||||
|
// confirm no emission during subscription
|
||||||
|
generator.generate$(SomeConfiguration, { on$ }).subscribe({
|
||||||
|
error: (e: unknown) => {
|
||||||
|
error = e;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
on$.error({ some: "error" });
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(error).toEqual({ some: "error" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when `on$` completes", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const on$ = new Subject<void>();
|
||||||
|
let complete = false;
|
||||||
|
|
||||||
|
// confirm no emission during subscription
|
||||||
|
generator.generate$(SomeConfiguration, { on$ }).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
complete = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
on$.complete();
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(complete).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("settings$", () => {
|
describe("settings$", () => {
|
||||||
it("defaults to the configuration's initial settings if settings aren't found", async () => {
|
it("defaults to the configuration's initial settings if settings aren't found", async () => {
|
||||||
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||||
|
|
||||||
@@ -107,7 +362,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
it("reads from the active user's configuration-defined storage", async () => {
|
it("reads from the active user's configuration-defined storage", async () => {
|
||||||
const settings = { foo: "value" };
|
const settings = { foo: "value" };
|
||||||
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||||
|
|
||||||
@@ -119,7 +374,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
||||||
const policy$ = new BehaviorSubject([somePolicy]);
|
const policy$ = new BehaviorSubject([somePolicy]);
|
||||||
policyService.getAll$.mockReturnValue(policy$);
|
policyService.getAll$.mockReturnValue(policy$);
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||||
|
|
||||||
@@ -131,7 +386,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
const anotherSettings = { foo: "another" };
|
const anotherSettings = { foo: "another" };
|
||||||
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
||||||
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
|
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const results: any = [];
|
const results: any = [];
|
||||||
const sub = generator.settings$(SomeConfiguration).subscribe((r) => results.push(r));
|
const sub = generator.settings$(SomeConfiguration).subscribe((r) => results.push(r));
|
||||||
|
|
||||||
@@ -148,7 +403,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
||||||
const anotherSettings = { foo: "another" };
|
const anotherSettings = { foo: "another" };
|
||||||
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
|
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.settings$(SomeConfiguration, { userId$ }));
|
const result = await firstValueFrom(generator.settings$(SomeConfiguration, { userId$ }));
|
||||||
@@ -161,7 +416,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
||||||
const anotherSettings = { foo: "another" };
|
const anotherSettings = { foo: "another" };
|
||||||
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
|
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
const results: any = [];
|
const results: any = [];
|
||||||
@@ -180,7 +435,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
|
|
||||||
it("errors when the arbitrary user's stream errors", async () => {
|
it("errors when the arbitrary user's stream errors", async () => {
|
||||||
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
let error = null;
|
let error = null;
|
||||||
@@ -198,7 +453,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
|
|
||||||
it("completes when the arbitrary user's stream completes", async () => {
|
it("completes when the arbitrary user's stream completes", async () => {
|
||||||
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
let completed = false;
|
let completed = false;
|
||||||
@@ -216,7 +471,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
|
|
||||||
it("ignores repeated arbitrary user emissions", async () => {
|
it("ignores repeated arbitrary user emissions", async () => {
|
||||||
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -240,7 +495,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
describe("settings", () => {
|
describe("settings", () => {
|
||||||
it("writes to the user's state", async () => {
|
it("writes to the user's state", async () => {
|
||||||
const singleUserId$ = new BehaviorSubject(SomeUser).asObservable();
|
const singleUserId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const subject = await generator.settings(SomeConfiguration, { singleUserId$ });
|
const subject = await generator.settings(SomeConfiguration, { singleUserId$ });
|
||||||
|
|
||||||
subject.next({ foo: "next value" });
|
subject.next({ foo: "next value" });
|
||||||
@@ -253,7 +508,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
it("waits for the user to become available", async () => {
|
it("waits for the user to become available", async () => {
|
||||||
const singleUserId = new BehaviorSubject(null);
|
const singleUserId = new BehaviorSubject(null);
|
||||||
const singleUserId$ = singleUserId.asObservable();
|
const singleUserId$ = singleUserId.asObservable();
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
let completed = false;
|
let completed = false;
|
||||||
const promise = generator.settings(SomeConfiguration, { singleUserId$ }).then((settings) => {
|
const promise = generator.settings(SomeConfiguration, { singleUserId$ }).then((settings) => {
|
||||||
@@ -271,7 +526,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
|
|
||||||
describe("policy$", () => {
|
describe("policy$", () => {
|
||||||
it("creates a disabled policy evaluator when there is no policy", async () => {
|
it("creates a disabled policy evaluator when there is no policy", async () => {
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||||
|
|
||||||
const result = await firstValueFrom(generator.policy$(SomeConfiguration, { userId$ }));
|
const result = await firstValueFrom(generator.policy$(SomeConfiguration, { userId$ }));
|
||||||
@@ -281,7 +536,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("creates an active policy evaluator when there is a policy", async () => {
|
it("creates an active policy evaluator when there is a policy", async () => {
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||||
const policy$ = new BehaviorSubject([somePolicy]);
|
const policy$ = new BehaviorSubject([somePolicy]);
|
||||||
policyService.getAll$.mockReturnValue(policy$);
|
policyService.getAll$.mockReturnValue(policy$);
|
||||||
@@ -293,7 +548,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("follows policy emissions", async () => {
|
it("follows policy emissions", async () => {
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
const somePolicySubject = new BehaviorSubject([somePolicy]);
|
const somePolicySubject = new BehaviorSubject([somePolicy]);
|
||||||
@@ -316,7 +571,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("follows user emissions", async () => {
|
it("follows user emissions", async () => {
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
const somePolicy$ = new BehaviorSubject([somePolicy]).asObservable();
|
const somePolicy$ = new BehaviorSubject([somePolicy]).asObservable();
|
||||||
@@ -340,7 +595,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("errors when the user errors", async () => {
|
it("errors when the user errors", async () => {
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
const expectedError = { some: "error" };
|
const expectedError = { some: "error" };
|
||||||
@@ -358,7 +613,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("completes when the user completes", async () => {
|
it("completes when the user completes", async () => {
|
||||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
const userId$ = userId.asObservable();
|
const userId$ = userId.asObservable();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
|
concatMap,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
endWith,
|
endWith,
|
||||||
filter,
|
filter,
|
||||||
@@ -8,32 +10,84 @@ import {
|
|||||||
map,
|
map,
|
||||||
mergeMap,
|
mergeMap,
|
||||||
Observable,
|
Observable,
|
||||||
|
race,
|
||||||
switchMap,
|
switchMap,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
|
withLatestFrom,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
import { Simplify } from "type-fest";
|
||||||
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { SingleUserDependency, UserDependency } from "@bitwarden/common/tools/dependencies";
|
import {
|
||||||
|
OnDependency,
|
||||||
|
SingleUserDependency,
|
||||||
|
UserDependency,
|
||||||
|
} from "@bitwarden/common/tools/dependencies";
|
||||||
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||||
import { Constraints } from "@bitwarden/common/tools/types";
|
import { Constraints } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
import { PolicyEvaluator } from "../abstractions";
|
import { PolicyEvaluator, Randomizer } from "../abstractions";
|
||||||
import { mapPolicyToEvaluatorV2 } from "../rx";
|
import { mapPolicyToEvaluatorV2 } from "../rx";
|
||||||
import { CredentialGeneratorConfiguration as Configuration } from "../types/credential-generator-configuration";
|
import { CredentialGeneratorConfiguration as Configuration } from "../types/credential-generator-configuration";
|
||||||
|
|
||||||
type Policy$Dependencies = UserDependency;
|
type Policy$Dependencies = UserDependency;
|
||||||
type Settings$Dependencies = Partial<UserDependency>;
|
type Settings$Dependencies = Partial<UserDependency>;
|
||||||
|
type Generate$Dependencies = Simplify<Partial<OnDependency> & Partial<UserDependency>> & {
|
||||||
|
/** Emits the active website when subscribed.
|
||||||
|
*
|
||||||
|
* The generator does not respond to emissions of this interface;
|
||||||
|
* If it is provided, the generator blocks until a value becomes available.
|
||||||
|
* When `website$` is omitted, the generator uses the empty string instead.
|
||||||
|
* When `website$` completes, the generator completes.
|
||||||
|
* When `website$` errors, the generator forwards the error.
|
||||||
|
*/
|
||||||
|
website$?: Observable<string>;
|
||||||
|
};
|
||||||
// FIXME: once the modernization is complete, switch the type parameters
|
// FIXME: once the modernization is complete, switch the type parameters
|
||||||
// in `PolicyEvaluator<P, S>` and bake-in the constraints type.
|
// in `PolicyEvaluator<P, S>` and bake-in the constraints type.
|
||||||
type Evaluator<Settings, Policy> = PolicyEvaluator<Policy, Settings> & Constraints<Settings>;
|
type Evaluator<Settings, Policy> = PolicyEvaluator<Policy, Settings> & Constraints<Settings>;
|
||||||
|
|
||||||
export class CredentialGeneratorService {
|
export class CredentialGeneratorService {
|
||||||
constructor(
|
constructor(
|
||||||
|
private randomizer: Randomizer,
|
||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/** Generates a stream of credentials
|
||||||
|
* @param configuration determines which generator's settings are loaded
|
||||||
|
* @param dependencies.on$ when specified, a new credential is emitted when
|
||||||
|
* this emits. Otherwise, a new credential is emitted when the settings
|
||||||
|
* update.
|
||||||
|
*/
|
||||||
|
generate$<Settings, Policy>(
|
||||||
|
configuration: Readonly<Configuration<Settings, Policy>>,
|
||||||
|
dependencies?: Generate$Dependencies,
|
||||||
|
) {
|
||||||
|
// instantiate the engine
|
||||||
|
const engine = configuration.engine.create(this.randomizer);
|
||||||
|
|
||||||
|
// stream blocks until all of these values are received
|
||||||
|
const website$ = dependencies?.website$ ?? new BehaviorSubject<string>(null);
|
||||||
|
const request$ = website$.pipe(map((website) => ({ website })));
|
||||||
|
const settings$ = this.settings$(configuration, dependencies);
|
||||||
|
|
||||||
|
// monitor completion
|
||||||
|
const requestComplete$ = request$.pipe(ignoreElements(), endWith(true));
|
||||||
|
const settingsComplete$ = request$.pipe(ignoreElements(), endWith(true));
|
||||||
|
const complete$ = race(requestComplete$, settingsComplete$);
|
||||||
|
|
||||||
|
// generation proper
|
||||||
|
const generate$ = (dependencies?.on$ ?? settings$).pipe(
|
||||||
|
withLatestFrom(request$, settings$),
|
||||||
|
concatMap(([, request, settings]) => engine.generate(request, settings)),
|
||||||
|
takeUntil(complete$),
|
||||||
|
);
|
||||||
|
|
||||||
|
return generate$;
|
||||||
|
}
|
||||||
|
|
||||||
/** Get the settings for the provided configuration
|
/** Get the settings for the provided configuration
|
||||||
* @param configuration determines which generator's settings are loaded
|
* @param configuration determines which generator's settings are loaded
|
||||||
* @param dependencies.userId$ identifies the user to which the settings are bound.
|
* @param dependencies.userId$ identifies the user to which the settings are bound.
|
||||||
@@ -82,7 +136,7 @@ export class CredentialGeneratorService {
|
|||||||
* @remarks the subject enforces policy for the settings
|
* @remarks the subject enforces policy for the settings
|
||||||
*/
|
*/
|
||||||
async settings<Settings, Policy>(
|
async settings<Settings, Policy>(
|
||||||
configuration: Configuration<Settings, Policy>,
|
configuration: Readonly<Configuration<Settings, Policy>>,
|
||||||
dependencies: SingleUserDependency,
|
dependencies: SingleUserDependency,
|
||||||
) {
|
) {
|
||||||
const userId = await firstValueFrom(
|
const userId = await firstValueFrom(
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { PASSPHRASE_SETTINGS } from "./storage";
|
|||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Password generation strategy", () => {
|
describe("Passphrase generation strategy", () => {
|
||||||
describe("toEvaluator()", () => {
|
describe("toEvaluator()", () => {
|
||||||
it("should map to the policy evaluator", async () => {
|
it("should map to the policy evaluator", async () => {
|
||||||
const strategy = new PassphraseGeneratorStrategy(null, null);
|
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
|||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
import { DefaultPassphraseBoundaries, DefaultPassphraseGenerationOptions, Policies } from "../data";
|
import { DefaultPassphraseGenerationOptions, Policies } from "../data";
|
||||||
import { PasswordRandomizer } from "../engine";
|
import { PasswordRandomizer } from "../engine";
|
||||||
import { mapPolicyToEvaluator } from "../rx";
|
import { mapPolicyToEvaluator } from "../rx";
|
||||||
import { PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
|
import { PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
|
||||||
import { observe$PerUserId, sharedStateByUserId } from "../util";
|
import { observe$PerUserId, optionsToEffWordListRequest, sharedStateByUserId } from "../util";
|
||||||
|
|
||||||
import { PASSPHRASE_SETTINGS } from "./storage";
|
import { PASSPHRASE_SETTINGS } from "./storage";
|
||||||
|
|
||||||
@@ -33,13 +33,7 @@ export class PassphraseGeneratorStrategy
|
|||||||
|
|
||||||
// algorithm
|
// algorithm
|
||||||
async generate(options: PassphraseGenerationOptions): Promise<string> {
|
async generate(options: PassphraseGenerationOptions): Promise<string> {
|
||||||
const requestWords = options.numWords ?? DefaultPassphraseGenerationOptions.numWords;
|
const request = optionsToEffWordListRequest(options);
|
||||||
const request = {
|
|
||||||
numberOfWords: Math.max(requestWords, DefaultPassphraseBoundaries.numWords.min),
|
|
||||||
capitalize: options.capitalize ?? DefaultPassphraseGenerationOptions.capitalize,
|
|
||||||
number: options.includeNumber ?? DefaultPassphraseGenerationOptions.includeNumber,
|
|
||||||
separator: options.wordSeparator ?? DefaultPassphraseGenerationOptions.wordSeparator,
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.randomizer.randomEffLongWords(request);
|
return this.randomizer.randomEffLongWords(request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Policies, DefaultPasswordGenerationOptions } from "../data";
|
|||||||
import { PasswordRandomizer } from "../engine";
|
import { PasswordRandomizer } from "../engine";
|
||||||
import { mapPolicyToEvaluator } from "../rx";
|
import { mapPolicyToEvaluator } from "../rx";
|
||||||
import { PasswordGenerationOptions, PasswordGeneratorPolicy } from "../types";
|
import { PasswordGenerationOptions, PasswordGeneratorPolicy } from "../types";
|
||||||
import { observe$PerUserId, sharedStateByUserId, sum } from "../util";
|
import { observe$PerUserId, optionsToRandomAsciiRequest, sharedStateByUserId } from "../util";
|
||||||
|
|
||||||
import { PASSWORD_SETTINGS } from "./storage";
|
import { PASSWORD_SETTINGS } from "./storage";
|
||||||
|
|
||||||
@@ -32,62 +32,7 @@ export class PasswordGeneratorStrategy
|
|||||||
|
|
||||||
// algorithm
|
// algorithm
|
||||||
async generate(options: PasswordGenerationOptions): Promise<string> {
|
async generate(options: PasswordGenerationOptions): Promise<string> {
|
||||||
// converts password generation option sets, which are defined by
|
const request = optionsToRandomAsciiRequest(options);
|
||||||
// an "enabled" and "quantity" parameter, to the password engine's
|
|
||||||
// parameters, which represent disabled options as `undefined`
|
|
||||||
// properties.
|
|
||||||
function process(
|
|
||||||
// values read from the options
|
|
||||||
enabled: boolean,
|
|
||||||
quantity: number,
|
|
||||||
// value used if an option is missing
|
|
||||||
defaultEnabled: boolean,
|
|
||||||
defaultQuantity: number,
|
|
||||||
) {
|
|
||||||
const isEnabled = enabled ?? defaultEnabled;
|
|
||||||
const actualQuantity = quantity ?? defaultQuantity;
|
|
||||||
const result = isEnabled ? actualQuantity : undefined;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = {
|
|
||||||
uppercase: process(
|
|
||||||
options.uppercase,
|
|
||||||
options.minUppercase,
|
|
||||||
DefaultPasswordGenerationOptions.uppercase,
|
|
||||||
DefaultPasswordGenerationOptions.minUppercase,
|
|
||||||
),
|
|
||||||
lowercase: process(
|
|
||||||
options.lowercase,
|
|
||||||
options.minLowercase,
|
|
||||||
DefaultPasswordGenerationOptions.lowercase,
|
|
||||||
DefaultPasswordGenerationOptions.minLowercase,
|
|
||||||
),
|
|
||||||
digits: process(
|
|
||||||
options.number,
|
|
||||||
options.minNumber,
|
|
||||||
DefaultPasswordGenerationOptions.number,
|
|
||||||
DefaultPasswordGenerationOptions.minNumber,
|
|
||||||
),
|
|
||||||
special: process(
|
|
||||||
options.special,
|
|
||||||
options.minSpecial,
|
|
||||||
DefaultPasswordGenerationOptions.special,
|
|
||||||
DefaultPasswordGenerationOptions.minSpecial,
|
|
||||||
),
|
|
||||||
ambiguous: options.ambiguous ?? DefaultPasswordGenerationOptions.ambiguous,
|
|
||||||
all: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// engine represents character sets as "include only"; you assert how many all
|
|
||||||
// characters there can be rather than a total length. This conversion has
|
|
||||||
// the character classes win, so that the result is always consistent with policy
|
|
||||||
// minimums.
|
|
||||||
const required = sum(request.uppercase, request.lowercase, request.digits, request.special);
|
|
||||||
const remaining = (options.length ?? 0) - required;
|
|
||||||
request.all = Math.max(remaining, 0);
|
|
||||||
|
|
||||||
const result = await this.randomizer.randomAscii(request);
|
const result = await this.randomizer.randomAscii(request);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/** Kinds of credentials that can be stored by the history service
|
||||||
|
* password - a secret consisting of arbitrary characters used to authenticate a user
|
||||||
|
* passphrase - a secret consisting of words used to authenticate a user
|
||||||
|
*/
|
||||||
|
export type CredentialCategory = "password" | "passphrase";
|
||||||
@@ -1,9 +1,29 @@
|
|||||||
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
|
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||||
import { Constraints } from "@bitwarden/common/tools/types";
|
import { Constraints } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
|
import { Randomizer } from "../abstractions";
|
||||||
import { PolicyConfiguration } from "../types";
|
import { PolicyConfiguration } from "../types";
|
||||||
|
|
||||||
|
import { CredentialCategory } from "./credential-category";
|
||||||
|
import { CredentialGenerator } from "./credential-generator";
|
||||||
|
|
||||||
export type CredentialGeneratorConfiguration<Settings, Policy> = {
|
export type CredentialGeneratorConfiguration<Settings, Policy> = {
|
||||||
|
/** Category describing usage of the credential generated by this configuration
|
||||||
|
*/
|
||||||
|
category: CredentialCategory;
|
||||||
|
|
||||||
|
/** An algorithm that generates credentials when ran. */
|
||||||
|
engine: {
|
||||||
|
/** Factory for the generator
|
||||||
|
*/
|
||||||
|
// FIXME: note that this erases the engine's type so that credentials are
|
||||||
|
// generated uniformly. This property needs to be maintained for
|
||||||
|
// the credential generator, but engine configurations should return
|
||||||
|
// the underlying type. `create` may be able to do double-duty w/ an
|
||||||
|
// engine definition if `CredentialGenerator` can be made covariant.
|
||||||
|
create: (randomizer: Randomizer) => CredentialGenerator<Settings>;
|
||||||
|
};
|
||||||
|
/** Defines the stored parameters for credential generation */
|
||||||
settings: {
|
settings: {
|
||||||
/** value used when an account's settings haven't been initialized */
|
/** value used when an account's settings haven't been initialized */
|
||||||
initial: Readonly<Partial<Settings>>;
|
initial: Readonly<Partial<Settings>>;
|
||||||
|
|||||||
12
libs/tools/generator/core/src/types/credential-generator.ts
Normal file
12
libs/tools/generator/core/src/types/credential-generator.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
|
import { GeneratedCredential } from "./generated-credential";
|
||||||
|
|
||||||
|
/** An algorithm that generates credentials. */
|
||||||
|
export type CredentialGenerator<Settings> = {
|
||||||
|
/** Generates a credential
|
||||||
|
* @param request runtime parameters
|
||||||
|
* @param settings stored parameters
|
||||||
|
*/
|
||||||
|
generate: (request: GenerationRequest, settings: Settings) => Promise<GeneratedCredential>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { CredentialCategory, GeneratedCredential } from ".";
|
||||||
|
|
||||||
|
describe("GeneratedCredential", () => {
|
||||||
|
describe("constructor", () => {
|
||||||
|
it("assigns credential", () => {
|
||||||
|
const result = new GeneratedCredential("example", "passphrase", new Date(100));
|
||||||
|
|
||||||
|
expect(result.credential).toEqual("example");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("assigns category", () => {
|
||||||
|
const result = new GeneratedCredential("example", "passphrase", new Date(100));
|
||||||
|
|
||||||
|
expect(result.category).toEqual("passphrase");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through date parameters", () => {
|
||||||
|
const result = new GeneratedCredential("example", "password", new Date(100));
|
||||||
|
|
||||||
|
expect(result.generationDate).toEqual(new Date(100));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts numeric dates to Dates", () => {
|
||||||
|
const result = new GeneratedCredential("example", "password", 100);
|
||||||
|
|
||||||
|
expect(result.generationDate).toEqual(new Date(100));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toJSON converts from a credential into a JSON object", () => {
|
||||||
|
const credential = new GeneratedCredential("example", "password", new Date(100));
|
||||||
|
|
||||||
|
const result = credential.toJSON();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
credential: "example",
|
||||||
|
category: "password" as CredentialCategory,
|
||||||
|
generationDate: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fromJSON converts Json objects into credentials", () => {
|
||||||
|
const jsonValue = {
|
||||||
|
credential: "example",
|
||||||
|
category: "password" as CredentialCategory,
|
||||||
|
generationDate: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = GeneratedCredential.fromJSON(jsonValue);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(GeneratedCredential);
|
||||||
|
expect(result).toEqual({
|
||||||
|
credential: "example",
|
||||||
|
category: "password",
|
||||||
|
generationDate: new Date(100),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
47
libs/tools/generator/core/src/types/generated-credential.ts
Normal file
47
libs/tools/generator/core/src/types/generated-credential.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { CredentialCategory } from "./credential-category";
|
||||||
|
|
||||||
|
/** A credential generation result */
|
||||||
|
export class GeneratedCredential {
|
||||||
|
/**
|
||||||
|
* Instantiates a generated credential
|
||||||
|
* @param credential The value of the generated credential (e.g. a password)
|
||||||
|
* @param category The kind of credential
|
||||||
|
* @param generationDate The date that the credential was generated.
|
||||||
|
* Numeric values should are interpreted using {@link Date.valueOf}
|
||||||
|
* semantics.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
readonly credential: string,
|
||||||
|
readonly category: CredentialCategory,
|
||||||
|
generationDate: Date | number,
|
||||||
|
) {
|
||||||
|
if (typeof generationDate === "number") {
|
||||||
|
this.generationDate = new Date(generationDate);
|
||||||
|
} else {
|
||||||
|
this.generationDate = generationDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The date that the credential was generated */
|
||||||
|
generationDate: Date;
|
||||||
|
|
||||||
|
/** Constructs a credential from its `toJSON` representation */
|
||||||
|
static fromJSON(jsonValue: Jsonify<GeneratedCredential>) {
|
||||||
|
return new GeneratedCredential(
|
||||||
|
jsonValue.credential,
|
||||||
|
jsonValue.category,
|
||||||
|
jsonValue.generationDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Serializes a credential to a JSON-compatible object */
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
credential: this.credential,
|
||||||
|
category: this.category,
|
||||||
|
generationDate: this.generationDate.valueOf(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
export * from "./boundary";
|
export * from "./boundary";
|
||||||
export * from "./catchall-generator-options";
|
export * from "./catchall-generator-options";
|
||||||
|
export * from "./credential-category";
|
||||||
|
export * from "./credential-generator";
|
||||||
export * from "./credential-generator-configuration";
|
export * from "./credential-generator-configuration";
|
||||||
export * from "./eff-username-generator-options";
|
export * from "./eff-username-generator-options";
|
||||||
export * from "./forwarder-options";
|
export * from "./forwarder-options";
|
||||||
|
export * from "./generated-credential";
|
||||||
export * from "./generator-options";
|
export * from "./generator-options";
|
||||||
export * from "./generator-type";
|
export * from "./generator-type";
|
||||||
export * from "./no-policy";
|
export * from "./no-policy";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { sum } from "./util";
|
import { DefaultPassphraseGenerationOptions } from "./data";
|
||||||
|
import { optionsToEffWordListRequest, optionsToRandomAsciiRequest, sum } from "./util";
|
||||||
|
|
||||||
describe("sum", () => {
|
describe("sum", () => {
|
||||||
it("returns 0 when the list is empty", () => {
|
it("returns 0 when the list is empty", () => {
|
||||||
@@ -15,3 +16,411 @@ describe("sum", () => {
|
|||||||
expect(sum(1, 2, 3)).toBe(6);
|
expect(sum(1, 2, 3)).toBe(6);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("optionsToRandomAsciiRequest", () => {
|
||||||
|
it("should map options", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 20,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
number: true,
|
||||||
|
special: true,
|
||||||
|
minUppercase: 1,
|
||||||
|
minLowercase: 2,
|
||||||
|
minNumber: 3,
|
||||||
|
minSpecial: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 10,
|
||||||
|
uppercase: 1,
|
||||||
|
lowercase: 2,
|
||||||
|
digits: 3,
|
||||||
|
special: 4,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should disable uppercase", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 3,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: false,
|
||||||
|
lowercase: true,
|
||||||
|
number: true,
|
||||||
|
special: true,
|
||||||
|
minUppercase: 1,
|
||||||
|
minLowercase: 1,
|
||||||
|
minNumber: 1,
|
||||||
|
minSpecial: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 0,
|
||||||
|
uppercase: undefined,
|
||||||
|
lowercase: 1,
|
||||||
|
digits: 1,
|
||||||
|
special: 1,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should disable lowercase", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 3,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: false,
|
||||||
|
number: true,
|
||||||
|
special: true,
|
||||||
|
minUppercase: 1,
|
||||||
|
minLowercase: 1,
|
||||||
|
minNumber: 1,
|
||||||
|
minSpecial: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 0,
|
||||||
|
uppercase: 1,
|
||||||
|
lowercase: undefined,
|
||||||
|
digits: 1,
|
||||||
|
special: 1,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should disable digits", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 3,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
number: false,
|
||||||
|
special: true,
|
||||||
|
minUppercase: 1,
|
||||||
|
minLowercase: 1,
|
||||||
|
minNumber: 1,
|
||||||
|
minSpecial: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 0,
|
||||||
|
uppercase: 1,
|
||||||
|
lowercase: 1,
|
||||||
|
digits: undefined,
|
||||||
|
special: 1,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should disable special", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 3,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
number: true,
|
||||||
|
special: false,
|
||||||
|
minUppercase: 1,
|
||||||
|
minLowercase: 1,
|
||||||
|
minNumber: 1,
|
||||||
|
minSpecial: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 0,
|
||||||
|
uppercase: 1,
|
||||||
|
lowercase: 1,
|
||||||
|
digits: 1,
|
||||||
|
special: undefined,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should override length with minimums", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 20,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
number: true,
|
||||||
|
special: true,
|
||||||
|
minUppercase: 1,
|
||||||
|
minLowercase: 2,
|
||||||
|
minNumber: 3,
|
||||||
|
minSpecial: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 10,
|
||||||
|
uppercase: 1,
|
||||||
|
lowercase: 2,
|
||||||
|
digits: 3,
|
||||||
|
special: 4,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default uppercase", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 2,
|
||||||
|
ambiguous: true,
|
||||||
|
lowercase: true,
|
||||||
|
number: true,
|
||||||
|
special: true,
|
||||||
|
minUppercase: 2,
|
||||||
|
minLowercase: 0,
|
||||||
|
minNumber: 0,
|
||||||
|
minSpecial: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 0,
|
||||||
|
uppercase: 2,
|
||||||
|
lowercase: 0,
|
||||||
|
digits: 0,
|
||||||
|
special: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default lowercase", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: true,
|
||||||
|
number: true,
|
||||||
|
special: true,
|
||||||
|
minUppercase: 0,
|
||||||
|
minLowercase: 2,
|
||||||
|
minNumber: 0,
|
||||||
|
minSpecial: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 0,
|
||||||
|
uppercase: 0,
|
||||||
|
lowercase: 2,
|
||||||
|
digits: 0,
|
||||||
|
special: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default number", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
special: true,
|
||||||
|
minUppercase: 0,
|
||||||
|
minLowercase: 0,
|
||||||
|
minNumber: 2,
|
||||||
|
minSpecial: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 0,
|
||||||
|
uppercase: 0,
|
||||||
|
lowercase: 0,
|
||||||
|
digits: 2,
|
||||||
|
special: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default special", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
number: true,
|
||||||
|
minUppercase: 0,
|
||||||
|
minLowercase: 0,
|
||||||
|
minNumber: 0,
|
||||||
|
minSpecial: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 0,
|
||||||
|
uppercase: 0,
|
||||||
|
lowercase: 0,
|
||||||
|
digits: 0,
|
||||||
|
special: undefined,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default minUppercase", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
number: true,
|
||||||
|
special: true,
|
||||||
|
minLowercase: 0,
|
||||||
|
minNumber: 0,
|
||||||
|
minSpecial: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 0,
|
||||||
|
uppercase: 1,
|
||||||
|
lowercase: 0,
|
||||||
|
digits: 0,
|
||||||
|
special: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default minLowercase", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
number: true,
|
||||||
|
special: true,
|
||||||
|
minUppercase: 0,
|
||||||
|
minNumber: 0,
|
||||||
|
minSpecial: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 0,
|
||||||
|
uppercase: 0,
|
||||||
|
lowercase: 1,
|
||||||
|
digits: 0,
|
||||||
|
special: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default minNumber", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
number: true,
|
||||||
|
special: true,
|
||||||
|
minUppercase: 0,
|
||||||
|
minLowercase: 0,
|
||||||
|
minSpecial: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 0,
|
||||||
|
uppercase: 0,
|
||||||
|
lowercase: 0,
|
||||||
|
digits: 1,
|
||||||
|
special: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default minSpecial", async () => {
|
||||||
|
const result = optionsToRandomAsciiRequest({
|
||||||
|
length: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
number: true,
|
||||||
|
special: true,
|
||||||
|
minUppercase: 0,
|
||||||
|
minLowercase: 0,
|
||||||
|
minNumber: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
all: 0,
|
||||||
|
uppercase: 0,
|
||||||
|
lowercase: 0,
|
||||||
|
digits: 0,
|
||||||
|
special: 0,
|
||||||
|
ambiguous: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("optionsToEffWordListRequest", () => {
|
||||||
|
it("should map options", async () => {
|
||||||
|
const result = optionsToEffWordListRequest({
|
||||||
|
numWords: 4,
|
||||||
|
capitalize: true,
|
||||||
|
includeNumber: true,
|
||||||
|
wordSeparator: "!",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
numberOfWords: 4,
|
||||||
|
capitalize: true,
|
||||||
|
number: true,
|
||||||
|
separator: "!",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default numWords", async () => {
|
||||||
|
const result = optionsToEffWordListRequest({
|
||||||
|
capitalize: true,
|
||||||
|
includeNumber: true,
|
||||||
|
wordSeparator: "!",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
numberOfWords: DefaultPassphraseGenerationOptions.numWords,
|
||||||
|
capitalize: true,
|
||||||
|
number: true,
|
||||||
|
separator: "!",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default capitalize", async () => {
|
||||||
|
const result = optionsToEffWordListRequest({
|
||||||
|
numWords: 4,
|
||||||
|
includeNumber: true,
|
||||||
|
wordSeparator: "!",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
numberOfWords: 4,
|
||||||
|
capitalize: DefaultPassphraseGenerationOptions.capitalize,
|
||||||
|
number: true,
|
||||||
|
separator: "!",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default includeNumber", async () => {
|
||||||
|
const result = optionsToEffWordListRequest({
|
||||||
|
numWords: 4,
|
||||||
|
capitalize: true,
|
||||||
|
wordSeparator: "!",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
numberOfWords: 4,
|
||||||
|
capitalize: true,
|
||||||
|
number: DefaultPassphraseGenerationOptions.includeNumber,
|
||||||
|
separator: "!",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default wordSeparator", async () => {
|
||||||
|
const result = optionsToEffWordListRequest({
|
||||||
|
numWords: 4,
|
||||||
|
capitalize: true,
|
||||||
|
includeNumber: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
numberOfWords: 4,
|
||||||
|
capitalize: true,
|
||||||
|
number: true,
|
||||||
|
separator: DefaultPassphraseGenerationOptions.wordSeparator,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ import {
|
|||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DefaultPassphraseBoundaries,
|
||||||
|
DefaultPassphraseGenerationOptions,
|
||||||
|
DefaultPasswordGenerationOptions,
|
||||||
|
} from "./data";
|
||||||
|
import { PassphraseGenerationOptions, PasswordGenerationOptions } from "./types";
|
||||||
|
|
||||||
/** construct a method that outputs a copy of `defaultValue` as an observable. */
|
/** construct a method that outputs a copy of `defaultValue` as an observable. */
|
||||||
export function observe$PerUserId<Value>(
|
export function observe$PerUserId<Value>(
|
||||||
create: () => Partial<Value>,
|
create: () => Partial<Value>,
|
||||||
@@ -50,3 +57,79 @@ export function sharedStateByUserId<Value>(key: UserKeyDefinition<Value>, provid
|
|||||||
/** returns the sum of items in the list. */
|
/** returns the sum of items in the list. */
|
||||||
export const sum = (...items: number[]) =>
|
export const sum = (...items: number[]) =>
|
||||||
(items ?? []).reduce((sum: number, current: number) => sum + (current ?? 0), 0);
|
(items ?? []).reduce((sum: number, current: number) => sum + (current ?? 0), 0);
|
||||||
|
|
||||||
|
/* converts password generation option sets, which are defined by
|
||||||
|
* an "enabled" and "quantity" parameter, to the password engine's
|
||||||
|
* parameters, which represent disabled options as `undefined`
|
||||||
|
* properties.
|
||||||
|
*/
|
||||||
|
export function optionsToRandomAsciiRequest(options: PasswordGenerationOptions) {
|
||||||
|
// helper for processing common option sets
|
||||||
|
function process(
|
||||||
|
// values read from the options
|
||||||
|
enabled: boolean,
|
||||||
|
quantity: number,
|
||||||
|
// value used if an option is missing
|
||||||
|
defaultEnabled: boolean,
|
||||||
|
defaultQuantity: number,
|
||||||
|
) {
|
||||||
|
const isEnabled = enabled ?? defaultEnabled;
|
||||||
|
const actualQuantity = quantity ?? defaultQuantity;
|
||||||
|
const result = isEnabled ? actualQuantity : undefined;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
uppercase: process(
|
||||||
|
options.uppercase,
|
||||||
|
options.minUppercase,
|
||||||
|
DefaultPasswordGenerationOptions.uppercase,
|
||||||
|
DefaultPasswordGenerationOptions.minUppercase,
|
||||||
|
),
|
||||||
|
lowercase: process(
|
||||||
|
options.lowercase,
|
||||||
|
options.minLowercase,
|
||||||
|
DefaultPasswordGenerationOptions.lowercase,
|
||||||
|
DefaultPasswordGenerationOptions.minLowercase,
|
||||||
|
),
|
||||||
|
digits: process(
|
||||||
|
options.number,
|
||||||
|
options.minNumber,
|
||||||
|
DefaultPasswordGenerationOptions.number,
|
||||||
|
DefaultPasswordGenerationOptions.minNumber,
|
||||||
|
),
|
||||||
|
special: process(
|
||||||
|
options.special,
|
||||||
|
options.minSpecial,
|
||||||
|
DefaultPasswordGenerationOptions.special,
|
||||||
|
DefaultPasswordGenerationOptions.minSpecial,
|
||||||
|
),
|
||||||
|
ambiguous: options.ambiguous ?? DefaultPasswordGenerationOptions.ambiguous,
|
||||||
|
all: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// engine represents character sets as "include only"; you assert how many all
|
||||||
|
// characters there can be rather than a total length. This conversion has
|
||||||
|
// the character classes win, so that the result is always consistent with policy
|
||||||
|
// minimums.
|
||||||
|
const required = sum(request.uppercase, request.lowercase, request.digits, request.special);
|
||||||
|
const remaining = (options.length ?? 0) - required;
|
||||||
|
request.all = Math.max(remaining, 0);
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* converts passphrase generation option sets to the eff word list request
|
||||||
|
*/
|
||||||
|
export function optionsToEffWordListRequest(options: PassphraseGenerationOptions) {
|
||||||
|
const requestWords = options.numWords ?? DefaultPassphraseGenerationOptions.numWords;
|
||||||
|
const request = {
|
||||||
|
numberOfWords: Math.max(requestWords, DefaultPassphraseBoundaries.numWords.min),
|
||||||
|
capitalize: options.capitalize ?? DefaultPassphraseGenerationOptions.capitalize,
|
||||||
|
number: options.includeNumber ?? DefaultPassphraseGenerationOptions.includeNumber,
|
||||||
|
separator: options.wordSeparator ?? DefaultPassphraseGenerationOptions.wordSeparator,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user