mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +00:00
[PM-8279] password generation component (#10805)
This commit is contained in:
@@ -77,7 +77,7 @@ export type SingleUserDependency = {
|
||||
export type OnDependency = {
|
||||
/** The stream that controls emissions
|
||||
*/
|
||||
on$: Observable<void>;
|
||||
on$: Observable<any>;
|
||||
};
|
||||
|
||||
/** 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
|
||||
* when an integration is invoked.
|
||||
*/
|
||||
export type IntegrationRequest = {
|
||||
/** @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;
|
||||
};
|
||||
export type IntegrationRequest = Partial<GenerationRequest>;
|
||||
|
||||
@@ -46,3 +46,20 @@ export type Constraints<T> = {
|
||||
|
||||
/** utility type for methods that evaluate constraints generically. */
|
||||
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 { 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 { 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 {
|
||||
CardComponent,
|
||||
CheckboxModule,
|
||||
ColorPasswordModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
InputModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
ToggleGroupModule,
|
||||
} 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 */
|
||||
@NgModule({
|
||||
imports: [SectionComponent, SectionHeaderComponent, CardComponent],
|
||||
imports: [CardComponent, SectionComponent, SectionHeaderComponent],
|
||||
exports: [
|
||||
CardComponent,
|
||||
CheckboxModule,
|
||||
CommonModule,
|
||||
ColorPasswordModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
InputModule,
|
||||
ItemModule,
|
||||
JslibModule,
|
||||
JslibServicesModule,
|
||||
FormFieldModule,
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
ColorPasswordModule,
|
||||
InputModule,
|
||||
CheckboxModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
CardComponent,
|
||||
ToggleGroupModule,
|
||||
],
|
||||
providers: [
|
||||
safeProvider({
|
||||
provide: RANDOMIZER,
|
||||
useFactory: createRandomizer,
|
||||
deps: [CryptoService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CredentialGeneratorService,
|
||||
useClass: CredentialGeneratorService,
|
||||
deps: [StateProvider, PolicyService],
|
||||
deps: [RANDOMIZER, StateProvider, PolicyService],
|
||||
}),
|
||||
],
|
||||
declarations: [],
|
||||
|
||||
@@ -2,3 +2,4 @@ export { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
||||
export { CredentialGeneratorHistoryComponent } from "./credential-generator-history.component";
|
||||
export { EmptyCredentialHistoryComponent } from "./empty-credential-history.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 {
|
||||
CredentialGenerator,
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGeneratorPolicy,
|
||||
PasswordGenerationOptions,
|
||||
@@ -14,6 +17,12 @@ import { DefaultPasswordGenerationOptions } from "./default-password-generation-
|
||||
import { Policies } from "./policies";
|
||||
|
||||
const PASSPHRASE = Object.freeze({
|
||||
category: "passphrase",
|
||||
engine: {
|
||||
create(randomizer: Randomizer): CredentialGenerator<PassphraseGenerationOptions> {
|
||||
return new PasswordRandomizer(randomizer);
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: DefaultPassphraseGenerationOptions,
|
||||
constraints: {
|
||||
@@ -32,6 +41,12 @@ const PASSPHRASE = Object.freeze({
|
||||
>);
|
||||
|
||||
const PASSWORD = Object.freeze({
|
||||
category: "password",
|
||||
engine: {
|
||||
create(randomizer: Randomizer): CredentialGenerator<PasswordGenerationOptions> {
|
||||
return new PasswordRandomizer(randomizer);
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: DefaultPasswordGenerationOptions,
|
||||
constraints: {
|
||||
|
||||
@@ -335,4 +335,40 @@ describe("PasswordRandomizer", () => {
|
||||
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 { GenerationRequest } from "@bitwarden/common/tools/types";
|
||||
|
||||
import {
|
||||
CredentialGenerator,
|
||||
GeneratedCredential,
|
||||
PassphraseGenerationOptions,
|
||||
PasswordGenerationOptions,
|
||||
} from "../types";
|
||||
import { optionsToEffWordListRequest, optionsToRandomAsciiRequest } from "../util";
|
||||
|
||||
import { Randomizer } from "./abstractions";
|
||||
import { Ascii } from "./data";
|
||||
import { CharacterSet, EffWordListRequest, RandomAsciiRequest } from "./types";
|
||||
|
||||
/** Generation algorithms that produce randomized secrets */
|
||||
export class PasswordRandomizer {
|
||||
export class PasswordRandomizer
|
||||
implements
|
||||
CredentialGenerator<PassphraseGenerationOptions>,
|
||||
CredentialGenerator<PasswordGenerationOptions>
|
||||
{
|
||||
/** Instantiates the password randomizer
|
||||
* @param random data source for random data
|
||||
* @param randomizer data source for random data
|
||||
*/
|
||||
constructor(private randomizer: Randomizer) {}
|
||||
|
||||
@@ -52,6 +65,41 @@ export class PasswordRandomizer {
|
||||
|
||||
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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { 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 { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { FakeStateProvider, FakeAccountService, awaitAsync } from "../../../../../common/spec";
|
||||
import { PolicyEvaluator } from "../abstractions";
|
||||
import { CredentialGeneratorConfiguration } from "../types";
|
||||
import {
|
||||
FakeStateProvider,
|
||||
FakeAccountService,
|
||||
awaitAsync,
|
||||
ObservableTracker,
|
||||
} from "../../../../../common/spec";
|
||||
import { PolicyEvaluator, Randomizer } from "../abstractions";
|
||||
import { CredentialGeneratorConfiguration, GeneratedCredential } from "../types";
|
||||
|
||||
import { CredentialGeneratorService } from "./credential-generator.service";
|
||||
|
||||
@@ -34,8 +39,23 @@ const somePolicy = new Policy({
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const SomeTime = new Date(1);
|
||||
const SomeCategory = "passphrase";
|
||||
|
||||
// fake the configuration
|
||||
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: {
|
||||
initial: { foo: "initial" },
|
||||
constraints: { foo: {} },
|
||||
@@ -87,6 +107,9 @@ const accountService = new FakeAccountService({
|
||||
// fake state
|
||||
const stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
// fake randomizer
|
||||
const randomizer = mock<Randomizer>();
|
||||
|
||||
describe("CredentialGeneratorService", () => {
|
||||
beforeEach(async () => {
|
||||
await accountService.switchAccount(SomeUser);
|
||||
@@ -94,10 +117,242 @@ describe("CredentialGeneratorService", () => {
|
||||
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$", () => {
|
||||
it("defaults to the configuration's initial settings if settings aren't found", async () => {
|
||||
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));
|
||||
|
||||
@@ -107,7 +362,7 @@ describe("CredentialGeneratorService", () => {
|
||||
it("reads from the active user's configuration-defined storage", async () => {
|
||||
const settings = { foo: "value" };
|
||||
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));
|
||||
|
||||
@@ -119,7 +374,7 @@ describe("CredentialGeneratorService", () => {
|
||||
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
||||
const policy$ = new BehaviorSubject([somePolicy]);
|
||||
policyService.getAll$.mockReturnValue(policy$);
|
||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
|
||||
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||
|
||||
@@ -131,7 +386,7 @@ describe("CredentialGeneratorService", () => {
|
||||
const anotherSettings = { foo: "another" };
|
||||
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
||||
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
|
||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
const results: any = [];
|
||||
const sub = generator.settings$(SomeConfiguration).subscribe((r) => results.push(r));
|
||||
|
||||
@@ -148,7 +403,7 @@ describe("CredentialGeneratorService", () => {
|
||||
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
||||
const anotherSettings = { foo: "another" };
|
||||
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 result = await firstValueFrom(generator.settings$(SomeConfiguration, { userId$ }));
|
||||
@@ -161,7 +416,7 @@ describe("CredentialGeneratorService", () => {
|
||||
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
||||
const anotherSettings = { foo: "another" };
|
||||
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$ = userId.asObservable();
|
||||
const results: any = [];
|
||||
@@ -180,7 +435,7 @@ describe("CredentialGeneratorService", () => {
|
||||
|
||||
it("errors when the arbitrary user's stream errors", async () => {
|
||||
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$ = userId.asObservable();
|
||||
let error = null;
|
||||
@@ -198,7 +453,7 @@ describe("CredentialGeneratorService", () => {
|
||||
|
||||
it("completes when the arbitrary user's stream completes", async () => {
|
||||
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$ = userId.asObservable();
|
||||
let completed = false;
|
||||
@@ -216,7 +471,7 @@ describe("CredentialGeneratorService", () => {
|
||||
|
||||
it("ignores repeated arbitrary user emissions", async () => {
|
||||
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$ = userId.asObservable();
|
||||
let count = 0;
|
||||
@@ -240,7 +495,7 @@ describe("CredentialGeneratorService", () => {
|
||||
describe("settings", () => {
|
||||
it("writes to the user's state", async () => {
|
||||
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$ });
|
||||
|
||||
subject.next({ foo: "next value" });
|
||||
@@ -253,7 +508,7 @@ describe("CredentialGeneratorService", () => {
|
||||
it("waits for the user to become available", async () => {
|
||||
const singleUserId = new BehaviorSubject(null);
|
||||
const singleUserId$ = singleUserId.asObservable();
|
||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
|
||||
let completed = false;
|
||||
const promise = generator.settings(SomeConfiguration, { singleUserId$ }).then((settings) => {
|
||||
@@ -271,7 +526,7 @@ describe("CredentialGeneratorService", () => {
|
||||
|
||||
describe("policy$", () => {
|
||||
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 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 () => {
|
||||
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||
const policy$ = new BehaviorSubject([somePolicy]);
|
||||
policyService.getAll$.mockReturnValue(policy$);
|
||||
@@ -293,7 +548,7 @@ describe("CredentialGeneratorService", () => {
|
||||
});
|
||||
|
||||
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$ = userId.asObservable();
|
||||
const somePolicySubject = new BehaviorSubject([somePolicy]);
|
||||
@@ -316,7 +571,7 @@ describe("CredentialGeneratorService", () => {
|
||||
});
|
||||
|
||||
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$ = userId.asObservable();
|
||||
const somePolicy$ = new BehaviorSubject([somePolicy]).asObservable();
|
||||
@@ -340,7 +595,7 @@ describe("CredentialGeneratorService", () => {
|
||||
});
|
||||
|
||||
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$ = userId.asObservable();
|
||||
const expectedError = { some: "error" };
|
||||
@@ -358,7 +613,7 @@ describe("CredentialGeneratorService", () => {
|
||||
});
|
||||
|
||||
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$ = userId.asObservable();
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
concatMap,
|
||||
distinctUntilChanged,
|
||||
endWith,
|
||||
filter,
|
||||
@@ -8,32 +10,84 @@ import {
|
||||
map,
|
||||
mergeMap,
|
||||
Observable,
|
||||
race,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
import { Simplify } from "type-fest";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
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 { Constraints } from "@bitwarden/common/tools/types";
|
||||
|
||||
import { PolicyEvaluator } from "../abstractions";
|
||||
import { PolicyEvaluator, Randomizer } from "../abstractions";
|
||||
import { mapPolicyToEvaluatorV2 } from "../rx";
|
||||
import { CredentialGeneratorConfiguration as Configuration } from "../types/credential-generator-configuration";
|
||||
|
||||
type Policy$Dependencies = 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
|
||||
// in `PolicyEvaluator<P, S>` and bake-in the constraints type.
|
||||
type Evaluator<Settings, Policy> = PolicyEvaluator<Policy, Settings> & Constraints<Settings>;
|
||||
|
||||
export class CredentialGeneratorService {
|
||||
constructor(
|
||||
private randomizer: Randomizer,
|
||||
private stateProvider: StateProvider,
|
||||
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
|
||||
* @param configuration determines which generator's settings are loaded
|
||||
* @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
|
||||
*/
|
||||
async settings<Settings, Policy>(
|
||||
configuration: Configuration<Settings, Policy>,
|
||||
configuration: Readonly<Configuration<Settings, Policy>>,
|
||||
dependencies: SingleUserDependency,
|
||||
) {
|
||||
const userId = await firstValueFrom(
|
||||
|
||||
@@ -17,7 +17,7 @@ import { PASSPHRASE_SETTINGS } from "./storage";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
|
||||
describe("Password generation strategy", () => {
|
||||
describe("Passphrase generation strategy", () => {
|
||||
describe("toEvaluator()", () => {
|
||||
it("should map to the policy evaluator", async () => {
|
||||
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 { GeneratorStrategy } from "../abstractions";
|
||||
import { DefaultPassphraseBoundaries, DefaultPassphraseGenerationOptions, Policies } from "../data";
|
||||
import { DefaultPassphraseGenerationOptions, Policies } from "../data";
|
||||
import { PasswordRandomizer } from "../engine";
|
||||
import { mapPolicyToEvaluator } from "../rx";
|
||||
import { PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
|
||||
import { observe$PerUserId, sharedStateByUserId } from "../util";
|
||||
import { observe$PerUserId, optionsToEffWordListRequest, sharedStateByUserId } from "../util";
|
||||
|
||||
import { PASSPHRASE_SETTINGS } from "./storage";
|
||||
|
||||
@@ -33,13 +33,7 @@ export class PassphraseGeneratorStrategy
|
||||
|
||||
// algorithm
|
||||
async generate(options: PassphraseGenerationOptions): Promise<string> {
|
||||
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,
|
||||
};
|
||||
const request = optionsToEffWordListRequest(options);
|
||||
|
||||
return this.randomizer.randomEffLongWords(request);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Policies, DefaultPasswordGenerationOptions } from "../data";
|
||||
import { PasswordRandomizer } from "../engine";
|
||||
import { mapPolicyToEvaluator } from "../rx";
|
||||
import { PasswordGenerationOptions, PasswordGeneratorPolicy } from "../types";
|
||||
import { observe$PerUserId, sharedStateByUserId, sum } from "../util";
|
||||
import { observe$PerUserId, optionsToRandomAsciiRequest, sharedStateByUserId } from "../util";
|
||||
|
||||
import { PASSWORD_SETTINGS } from "./storage";
|
||||
|
||||
@@ -32,62 +32,7 @@ export class PasswordGeneratorStrategy
|
||||
|
||||
// algorithm
|
||||
async generate(options: PasswordGenerationOptions): Promise<string> {
|
||||
// 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.
|
||||
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 request = optionsToRandomAsciiRequest(options);
|
||||
const result = await this.randomizer.randomAscii(request);
|
||||
|
||||
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 { Constraints } from "@bitwarden/common/tools/types";
|
||||
|
||||
import { Randomizer } from "../abstractions";
|
||||
import { PolicyConfiguration } from "../types";
|
||||
|
||||
import { CredentialCategory } from "./credential-category";
|
||||
import { CredentialGenerator } from "./credential-generator";
|
||||
|
||||
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: {
|
||||
/** value used when an account's settings haven't been initialized */
|
||||
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 "./catchall-generator-options";
|
||||
export * from "./credential-category";
|
||||
export * from "./credential-generator";
|
||||
export * from "./credential-generator-configuration";
|
||||
export * from "./eff-username-generator-options";
|
||||
export * from "./forwarder-options";
|
||||
export * from "./generated-credential";
|
||||
export * from "./generator-options";
|
||||
export * from "./generator-type";
|
||||
export * from "./no-policy";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sum } from "./util";
|
||||
import { DefaultPassphraseGenerationOptions } from "./data";
|
||||
import { optionsToEffWordListRequest, optionsToRandomAsciiRequest, sum } from "./util";
|
||||
|
||||
describe("sum", () => {
|
||||
it("returns 0 when the list is empty", () => {
|
||||
@@ -15,3 +16,411 @@ describe("sum", () => {
|
||||
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";
|
||||
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. */
|
||||
export function observe$PerUserId<Value>(
|
||||
create: () => Partial<Value>,
|
||||
@@ -50,3 +57,79 @@ export function sharedStateByUserId<Value>(key: UserKeyDefinition<Value>, provid
|
||||
/** returns the sum of items in the list. */
|
||||
export const sum = (...items: number[]) =>
|
||||
(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