mirror of
https://github.com/bitwarden/browser
synced 2025-12-23 11:43:46 +00:00
[PM-7289] implement generator libraries (#9549)
This is a copy of the files. The source in `@bitwarden/common` will be deleted once all of the applications have been ported to the library.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { engine, services, strategies } from "@bitwarden/generator-core";
|
||||
|
||||
import { LocalGeneratorHistoryService } from "../history";
|
||||
import { DefaultGeneratorNavigationService } from "../navigation";
|
||||
|
||||
import { LegacyPasswordGenerationService } from "./legacy-password-generation.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
||||
|
||||
const PassphraseGeneratorStrategy = strategies.PassphraseGeneratorStrategy;
|
||||
const PasswordGeneratorStrategy = strategies.PasswordGeneratorStrategy;
|
||||
const CryptoServiceRandomizer = engine.CryptoServiceRandomizer;
|
||||
const DefaultGeneratorService = services.DefaultGeneratorService;
|
||||
|
||||
export function legacyPasswordGenerationServiceFactory(
|
||||
encryptService: EncryptService,
|
||||
cryptoService: CryptoService,
|
||||
policyService: PolicyService,
|
||||
accountService: AccountService,
|
||||
stateProvider: StateProvider,
|
||||
): PasswordGenerationServiceAbstraction {
|
||||
const randomizer = new CryptoServiceRandomizer(cryptoService);
|
||||
|
||||
const passwords = new DefaultGeneratorService(
|
||||
new PasswordGeneratorStrategy(randomizer, stateProvider),
|
||||
policyService,
|
||||
);
|
||||
|
||||
const passphrases = new DefaultGeneratorService(
|
||||
new PassphraseGeneratorStrategy(randomizer, stateProvider),
|
||||
policyService,
|
||||
);
|
||||
|
||||
const navigation = new DefaultGeneratorNavigationService(stateProvider, policyService);
|
||||
|
||||
const history = new LocalGeneratorHistoryService(encryptService, cryptoService, stateProvider);
|
||||
|
||||
return new LegacyPasswordGenerationService(
|
||||
accountService,
|
||||
navigation,
|
||||
passwords,
|
||||
passphrases,
|
||||
history,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./password-generation.service.abstraction";
|
||||
export * from "./factory";
|
||||
export * from "./password-generator-options";
|
||||
@@ -0,0 +1,562 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
GeneratorService,
|
||||
DefaultPassphraseGenerationOptions,
|
||||
DefaultPasswordGenerationOptions,
|
||||
DisabledPassphraseGeneratorPolicy,
|
||||
DisabledPasswordGeneratorPolicy,
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGeneratorPolicy,
|
||||
PasswordGenerationOptions,
|
||||
PasswordGeneratorPolicy,
|
||||
policies,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { mockAccountServiceWith } from "../../../../../common/spec";
|
||||
import { GeneratedCredential, GeneratorHistoryService, GeneratedPasswordHistory } from "../history";
|
||||
import {
|
||||
GeneratorNavigationService,
|
||||
DefaultGeneratorNavigation,
|
||||
GeneratorNavigation,
|
||||
GeneratorNavigationEvaluator,
|
||||
GeneratorNavigationPolicy,
|
||||
} from "../navigation";
|
||||
|
||||
import { LegacyPasswordGenerationService } from "./legacy-password-generation.service";
|
||||
import { PasswordGeneratorOptions } from "./password-generator-options";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
const PassphraseGeneratorOptionsEvaluator = policies.PassphraseGeneratorOptionsEvaluator;
|
||||
const PasswordGeneratorOptionsEvaluator = policies.PasswordGeneratorOptionsEvaluator;
|
||||
|
||||
function createPassphraseGenerator(
|
||||
options: PassphraseGenerationOptions = {},
|
||||
policy: PassphraseGeneratorPolicy = DisabledPassphraseGeneratorPolicy,
|
||||
) {
|
||||
let savedOptions = options;
|
||||
const generator = mock<GeneratorService<PassphraseGenerationOptions, PassphraseGeneratorPolicy>>({
|
||||
evaluator$(id: UserId) {
|
||||
const evaluator = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
return of(evaluator);
|
||||
},
|
||||
options$(id: UserId) {
|
||||
return of(savedOptions);
|
||||
},
|
||||
defaults$(id: UserId) {
|
||||
return of(DefaultPassphraseGenerationOptions);
|
||||
},
|
||||
saveOptions(userId, options) {
|
||||
savedOptions = options;
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
return generator;
|
||||
}
|
||||
|
||||
function createPasswordGenerator(
|
||||
options: PasswordGenerationOptions = {},
|
||||
policy: PasswordGeneratorPolicy = DisabledPasswordGeneratorPolicy,
|
||||
) {
|
||||
let savedOptions = options;
|
||||
const generator = mock<GeneratorService<PasswordGenerationOptions, PasswordGeneratorPolicy>>({
|
||||
evaluator$(id: UserId) {
|
||||
const evaluator = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
return of(evaluator);
|
||||
},
|
||||
options$(id: UserId) {
|
||||
return of(savedOptions);
|
||||
},
|
||||
defaults$(id: UserId) {
|
||||
return of(DefaultPasswordGenerationOptions);
|
||||
},
|
||||
saveOptions(userId, options) {
|
||||
savedOptions = options;
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
return generator;
|
||||
}
|
||||
|
||||
function createNavigationGenerator(
|
||||
options: GeneratorNavigation = {},
|
||||
policy: GeneratorNavigationPolicy = {},
|
||||
) {
|
||||
let savedOptions = options;
|
||||
const generator = mock<GeneratorNavigationService>({
|
||||
evaluator$(id: UserId) {
|
||||
const evaluator = new GeneratorNavigationEvaluator(policy);
|
||||
return of(evaluator);
|
||||
},
|
||||
options$(id: UserId) {
|
||||
return of(savedOptions);
|
||||
},
|
||||
defaults$(id: UserId) {
|
||||
return of(DefaultGeneratorNavigation);
|
||||
},
|
||||
saveOptions: jest.fn((userId, options) => {
|
||||
savedOptions = options;
|
||||
return Promise.resolve();
|
||||
}),
|
||||
});
|
||||
|
||||
return generator;
|
||||
}
|
||||
|
||||
describe("LegacyPasswordGenerationService", () => {
|
||||
// NOTE: in all tests, `null` constructor arguments are not used by the test.
|
||||
// They're set to `null` to avoid setting up unnecessary mocks.
|
||||
|
||||
describe("generatePassword", () => {
|
||||
it("invokes the inner password generator to generate passwords", async () => {
|
||||
const innerPassword = createPasswordGenerator();
|
||||
const generator = new LegacyPasswordGenerationService(null, null, innerPassword, null, null);
|
||||
const options = { type: "password" } as PasswordGeneratorOptions;
|
||||
|
||||
await generator.generatePassword(options);
|
||||
|
||||
expect(innerPassword.generate).toHaveBeenCalledWith(options);
|
||||
});
|
||||
|
||||
it("invokes the inner passphrase generator to generate passphrases", async () => {
|
||||
const innerPassphrase = createPassphraseGenerator();
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
innerPassphrase,
|
||||
null,
|
||||
);
|
||||
const options = { type: "passphrase" } as PasswordGeneratorOptions;
|
||||
|
||||
await generator.generatePassword(options);
|
||||
|
||||
expect(innerPassphrase.generate).toHaveBeenCalledWith(options);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generatePassphrase", () => {
|
||||
it("invokes the inner passphrase generator", async () => {
|
||||
const innerPassphrase = createPassphraseGenerator();
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
innerPassphrase,
|
||||
null,
|
||||
);
|
||||
const options = {} as PasswordGeneratorOptions;
|
||||
|
||||
await generator.generatePassphrase(options);
|
||||
|
||||
expect(innerPassphrase.generate).toHaveBeenCalledWith(options);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOptions", () => {
|
||||
it("combines options from its inner services", async () => {
|
||||
const innerPassword = createPasswordGenerator({
|
||||
length: 29,
|
||||
minLength: 20,
|
||||
ambiguous: false,
|
||||
uppercase: true,
|
||||
minUppercase: 1,
|
||||
lowercase: false,
|
||||
minLowercase: 2,
|
||||
number: true,
|
||||
minNumber: 3,
|
||||
special: false,
|
||||
minSpecial: 0,
|
||||
});
|
||||
const innerPassphrase = createPassphraseGenerator({
|
||||
numWords: 10,
|
||||
wordSeparator: "-",
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
});
|
||||
const navigation = createNavigationGenerator({
|
||||
type: "passphrase",
|
||||
username: "word",
|
||||
forwarder: "simplelogin",
|
||||
});
|
||||
const accountService = mockAccountServiceWith(SomeUser);
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
accountService,
|
||||
navigation,
|
||||
innerPassword,
|
||||
innerPassphrase,
|
||||
null,
|
||||
);
|
||||
|
||||
const [result] = await generator.getOptions();
|
||||
|
||||
expect(result).toEqual({
|
||||
type: "passphrase",
|
||||
length: 29,
|
||||
minLength: 5,
|
||||
ambiguous: false,
|
||||
uppercase: true,
|
||||
minUppercase: 1,
|
||||
lowercase: false,
|
||||
minLowercase: 0,
|
||||
number: true,
|
||||
minNumber: 3,
|
||||
special: false,
|
||||
minSpecial: 0,
|
||||
numWords: 10,
|
||||
wordSeparator: "-",
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
policyUpdated: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("sets default options when an inner service lacks a value", async () => {
|
||||
const innerPassword = createPasswordGenerator(null);
|
||||
const innerPassphrase = createPassphraseGenerator(null);
|
||||
const navigation = createNavigationGenerator(null);
|
||||
const accountService = mockAccountServiceWith(SomeUser);
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
accountService,
|
||||
navigation,
|
||||
innerPassword,
|
||||
innerPassphrase,
|
||||
null,
|
||||
);
|
||||
|
||||
const [result] = await generator.getOptions();
|
||||
|
||||
expect(result).toEqual({
|
||||
type: DefaultGeneratorNavigation.type,
|
||||
...DefaultPassphraseGenerationOptions,
|
||||
...DefaultPasswordGenerationOptions,
|
||||
minLowercase: 1,
|
||||
minUppercase: 1,
|
||||
policyUpdated: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("combines policies from its inner services", async () => {
|
||||
const innerPassword = createPasswordGenerator(
|
||||
{},
|
||||
{
|
||||
minLength: 20,
|
||||
numberCount: 10,
|
||||
specialCount: 11,
|
||||
useUppercase: true,
|
||||
useLowercase: false,
|
||||
useNumbers: true,
|
||||
useSpecial: false,
|
||||
},
|
||||
);
|
||||
const innerPassphrase = createPassphraseGenerator(
|
||||
{},
|
||||
{
|
||||
minNumberWords: 5,
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
},
|
||||
);
|
||||
const accountService = mockAccountServiceWith(SomeUser);
|
||||
const navigation = createNavigationGenerator(
|
||||
{},
|
||||
{
|
||||
defaultType: "password",
|
||||
},
|
||||
);
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
accountService,
|
||||
navigation,
|
||||
innerPassword,
|
||||
innerPassphrase,
|
||||
null,
|
||||
);
|
||||
|
||||
const [, policy] = await generator.getOptions();
|
||||
|
||||
expect(policy).toEqual({
|
||||
defaultType: "password",
|
||||
minLength: 20,
|
||||
numberCount: 10,
|
||||
specialCount: 11,
|
||||
useUppercase: true,
|
||||
useLowercase: false,
|
||||
useNumbers: true,
|
||||
useSpecial: false,
|
||||
minNumberWords: 5,
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("enforcePasswordGeneratorPoliciesOnOptions", () => {
|
||||
it("returns its options parameter with password policy applied", async () => {
|
||||
const innerPassword = createPasswordGenerator(
|
||||
{},
|
||||
{
|
||||
minLength: 15,
|
||||
numberCount: 5,
|
||||
specialCount: 5,
|
||||
useUppercase: true,
|
||||
useLowercase: true,
|
||||
useNumbers: true,
|
||||
useSpecial: true,
|
||||
},
|
||||
);
|
||||
const innerPassphrase = createPassphraseGenerator();
|
||||
const accountService = mockAccountServiceWith(SomeUser);
|
||||
const navigation = createNavigationGenerator();
|
||||
const options = {
|
||||
type: "password" as const,
|
||||
};
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
accountService,
|
||||
navigation,
|
||||
innerPassword,
|
||||
innerPassphrase,
|
||||
null,
|
||||
);
|
||||
|
||||
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
|
||||
|
||||
expect(result).toBe(options);
|
||||
expect(result).toMatchObject({
|
||||
length: 15,
|
||||
minLength: 15,
|
||||
minLowercase: 1,
|
||||
minNumber: 5,
|
||||
minUppercase: 1,
|
||||
minSpecial: 5,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
number: true,
|
||||
special: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns its options parameter with passphrase policy applied", async () => {
|
||||
const innerPassword = createPasswordGenerator();
|
||||
const innerPassphrase = createPassphraseGenerator(
|
||||
{},
|
||||
{
|
||||
minNumberWords: 5,
|
||||
capitalize: true,
|
||||
includeNumber: true,
|
||||
},
|
||||
);
|
||||
const accountService = mockAccountServiceWith(SomeUser);
|
||||
const navigation = createNavigationGenerator();
|
||||
const options = {
|
||||
type: "passphrase" as const,
|
||||
};
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
accountService,
|
||||
navigation,
|
||||
innerPassword,
|
||||
innerPassphrase,
|
||||
null,
|
||||
);
|
||||
|
||||
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
|
||||
|
||||
expect(result).toBe(options);
|
||||
expect(result).toMatchObject({
|
||||
numWords: 5,
|
||||
capitalize: true,
|
||||
includeNumber: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the applied policy", async () => {
|
||||
const innerPassword = createPasswordGenerator(
|
||||
{},
|
||||
{
|
||||
minLength: 20,
|
||||
numberCount: 10,
|
||||
specialCount: 11,
|
||||
useUppercase: true,
|
||||
useLowercase: false,
|
||||
useNumbers: true,
|
||||
useSpecial: false,
|
||||
},
|
||||
);
|
||||
const innerPassphrase = createPassphraseGenerator(
|
||||
{},
|
||||
{
|
||||
minNumberWords: 5,
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
},
|
||||
);
|
||||
const accountService = mockAccountServiceWith(SomeUser);
|
||||
const navigation = createNavigationGenerator(
|
||||
{},
|
||||
{
|
||||
defaultType: "password",
|
||||
},
|
||||
);
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
accountService,
|
||||
navigation,
|
||||
innerPassword,
|
||||
innerPassphrase,
|
||||
null,
|
||||
);
|
||||
|
||||
const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({});
|
||||
|
||||
expect(policy).toEqual({
|
||||
defaultType: "password",
|
||||
minLength: 20,
|
||||
numberCount: 10,
|
||||
specialCount: 11,
|
||||
useUppercase: true,
|
||||
useLowercase: false,
|
||||
useNumbers: true,
|
||||
useSpecial: false,
|
||||
minNumberWords: 5,
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveOptions", () => {
|
||||
it("loads saved password options", async () => {
|
||||
const innerPassword = createPasswordGenerator();
|
||||
const innerPassphrase = createPassphraseGenerator();
|
||||
const navigation = createNavigationGenerator();
|
||||
const accountService = mockAccountServiceWith(SomeUser);
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
accountService,
|
||||
navigation,
|
||||
innerPassword,
|
||||
innerPassphrase,
|
||||
null,
|
||||
);
|
||||
const options = {
|
||||
type: "password" as const,
|
||||
length: 29,
|
||||
minLength: 5,
|
||||
ambiguous: false,
|
||||
uppercase: true,
|
||||
minUppercase: 1,
|
||||
lowercase: false,
|
||||
minLowercase: 0,
|
||||
number: true,
|
||||
minNumber: 3,
|
||||
special: false,
|
||||
minSpecial: 0,
|
||||
};
|
||||
await generator.saveOptions(options);
|
||||
|
||||
const [result] = await generator.getOptions();
|
||||
|
||||
expect(result).toMatchObject(options);
|
||||
});
|
||||
|
||||
it("loads saved passphrase options", async () => {
|
||||
const innerPassword = createPasswordGenerator();
|
||||
const innerPassphrase = createPassphraseGenerator();
|
||||
const navigation = createNavigationGenerator();
|
||||
const accountService = mockAccountServiceWith(SomeUser);
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
accountService,
|
||||
navigation,
|
||||
innerPassword,
|
||||
innerPassphrase,
|
||||
null,
|
||||
);
|
||||
const options = {
|
||||
type: "passphrase" as const,
|
||||
numWords: 10,
|
||||
wordSeparator: "-",
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
};
|
||||
await generator.saveOptions(options);
|
||||
|
||||
const [result] = await generator.getOptions();
|
||||
|
||||
expect(result).toMatchObject(options);
|
||||
});
|
||||
|
||||
it("preserves saved navigation options", async () => {
|
||||
const innerPassword = createPasswordGenerator();
|
||||
const innerPassphrase = createPassphraseGenerator();
|
||||
const navigation = createNavigationGenerator({
|
||||
type: "password",
|
||||
username: "forwarded",
|
||||
forwarder: "firefoxrelay",
|
||||
});
|
||||
const accountService = mockAccountServiceWith(SomeUser);
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
accountService,
|
||||
navigation,
|
||||
innerPassword,
|
||||
innerPassphrase,
|
||||
null,
|
||||
);
|
||||
const options = {
|
||||
type: "passphrase" as const,
|
||||
numWords: 10,
|
||||
wordSeparator: "-",
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
};
|
||||
|
||||
await generator.saveOptions(options);
|
||||
|
||||
expect(navigation.saveOptions).toHaveBeenCalledWith(SomeUser, {
|
||||
type: "passphrase",
|
||||
username: "forwarded",
|
||||
forwarder: "firefoxrelay",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getHistory", () => {
|
||||
it("gets the active user's history from the history service", async () => {
|
||||
const history = mock<GeneratorHistoryService>();
|
||||
history.credentials$.mockReturnValue(
|
||||
of([new GeneratedCredential("foo", "password", new Date(100))]),
|
||||
);
|
||||
const accountService = mockAccountServiceWith(SomeUser);
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
accountService,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
history,
|
||||
);
|
||||
|
||||
const result = await generator.getHistory();
|
||||
|
||||
expect(history.credentials$).toHaveBeenCalledWith(SomeUser);
|
||||
expect(result).toEqual([new GeneratedPasswordHistory("foo", 100)]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addHistory", () => {
|
||||
it("adds a history item as a password credential", async () => {
|
||||
const history = mock<GeneratorHistoryService>();
|
||||
const accountService = mockAccountServiceWith(SomeUser);
|
||||
const generator = new LegacyPasswordGenerationService(
|
||||
accountService,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
history,
|
||||
);
|
||||
|
||||
await generator.addHistory("foo");
|
||||
|
||||
expect(history.track).toHaveBeenCalledWith(SomeUser, "foo", "password");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,381 @@
|
||||
import {
|
||||
concatMap,
|
||||
zip,
|
||||
map,
|
||||
firstValueFrom,
|
||||
combineLatest,
|
||||
pairwise,
|
||||
of,
|
||||
concat,
|
||||
Observable,
|
||||
filter,
|
||||
timeout,
|
||||
} from "rxjs";
|
||||
|
||||
import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
GeneratorService,
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGeneratorPolicy,
|
||||
PasswordGenerationOptions,
|
||||
PasswordGeneratorPolicy,
|
||||
PolicyEvaluator,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { GeneratedCredential, GeneratorHistoryService, GeneratedPasswordHistory } from "../history";
|
||||
import {
|
||||
GeneratorNavigationService,
|
||||
GeneratorNavigation,
|
||||
GeneratorNavigationPolicy,
|
||||
} from "../navigation";
|
||||
|
||||
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
||||
import { PasswordGeneratorOptions } from "./password-generator-options";
|
||||
|
||||
type MappedOptions = {
|
||||
generator: GeneratorNavigation;
|
||||
password: PasswordGenerationOptions;
|
||||
passphrase: PassphraseGenerationOptions;
|
||||
policyUpdated: boolean;
|
||||
};
|
||||
|
||||
/** Adapts the generator 2.0 design to 1.0 angular services. */
|
||||
export class LegacyPasswordGenerationService implements PasswordGenerationServiceAbstraction {
|
||||
constructor(
|
||||
private readonly accountService: AccountService,
|
||||
private readonly navigation: GeneratorNavigationService,
|
||||
private readonly passwords: GeneratorService<
|
||||
PasswordGenerationOptions,
|
||||
PasswordGeneratorPolicy
|
||||
>,
|
||||
private readonly passphrases: GeneratorService<
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGeneratorPolicy
|
||||
>,
|
||||
private readonly history: GeneratorHistoryService,
|
||||
) {}
|
||||
|
||||
generatePassword(options: PasswordGeneratorOptions) {
|
||||
if (options.type === "password") {
|
||||
return this.passwords.generate(options);
|
||||
} else {
|
||||
return this.passphrases.generate(options);
|
||||
}
|
||||
}
|
||||
|
||||
generatePassphrase(options: PasswordGeneratorOptions) {
|
||||
return this.passphrases.generate(options);
|
||||
}
|
||||
|
||||
private getRawOptions$() {
|
||||
// give the typechecker a nudge to avoid "implicit any" errors
|
||||
type RawOptionsIntermediateType = [
|
||||
PasswordGenerationOptions,
|
||||
PasswordGenerationOptions,
|
||||
[PolicyEvaluator<PasswordGeneratorPolicy, PasswordGenerationOptions>, number],
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGenerationOptions,
|
||||
[PolicyEvaluator<PassphraseGeneratorPolicy, PassphraseGenerationOptions>, number],
|
||||
GeneratorNavigation,
|
||||
GeneratorNavigation,
|
||||
[PolicyEvaluator<GeneratorNavigationPolicy, GeneratorNavigation>, number],
|
||||
];
|
||||
|
||||
function withSequenceNumber<T>(observable$: Observable<T>) {
|
||||
return observable$.pipe(map((evaluator, i) => [evaluator, i] as const));
|
||||
}
|
||||
|
||||
// initial array ensures that destructuring never fails; sequence numbers
|
||||
// set to `-1` so that the first update reflects that the policy changed from
|
||||
// "unknown" to "whatever was provided by the service". This needs to be called
|
||||
// each time the active user changes or the `concat` will block.
|
||||
function initial$() {
|
||||
const initial: RawOptionsIntermediateType = [
|
||||
null,
|
||||
null,
|
||||
[null, -1],
|
||||
null,
|
||||
null,
|
||||
[null, -1],
|
||||
null,
|
||||
null,
|
||||
[null, -1],
|
||||
];
|
||||
|
||||
return of(initial);
|
||||
}
|
||||
|
||||
function intermediatePairsToRawOptions([previous, current]: [
|
||||
RawOptionsIntermediateType,
|
||||
RawOptionsIntermediateType,
|
||||
]) {
|
||||
const [, , [, passwordPrevious], , , [, passphrasePrevious], , , [, generatorPrevious]] =
|
||||
previous;
|
||||
const [
|
||||
passwordOptions,
|
||||
passwordDefaults,
|
||||
[passwordEvaluator, passwordCurrent],
|
||||
passphraseOptions,
|
||||
passphraseDefaults,
|
||||
[passphraseEvaluator, passphraseCurrent],
|
||||
generatorOptions,
|
||||
generatorDefaults,
|
||||
[generatorEvaluator, generatorCurrent],
|
||||
] = current;
|
||||
|
||||
// when any of the sequence numbers change, the emission occurs as the result of
|
||||
// a policy update
|
||||
const policyEmitted =
|
||||
passwordPrevious < passwordCurrent ||
|
||||
passphrasePrevious < passphraseCurrent ||
|
||||
generatorPrevious < generatorCurrent;
|
||||
|
||||
const result = [
|
||||
passwordOptions,
|
||||
passwordDefaults,
|
||||
passwordEvaluator,
|
||||
passphraseOptions,
|
||||
passphraseDefaults,
|
||||
passphraseEvaluator,
|
||||
generatorOptions,
|
||||
generatorDefaults,
|
||||
generatorEvaluator,
|
||||
policyEmitted,
|
||||
] as const;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// look upon my works, ye mighty, and despair!
|
||||
const rawOptions$ = this.accountService.activeAccount$.pipe(
|
||||
concatMap((activeUser) =>
|
||||
concat(
|
||||
initial$(),
|
||||
combineLatest([
|
||||
this.passwords.options$(activeUser.id),
|
||||
this.passwords.defaults$(activeUser.id),
|
||||
withSequenceNumber(this.passwords.evaluator$(activeUser.id)),
|
||||
this.passphrases.options$(activeUser.id),
|
||||
this.passphrases.defaults$(activeUser.id),
|
||||
withSequenceNumber(this.passphrases.evaluator$(activeUser.id)),
|
||||
this.navigation.options$(activeUser.id),
|
||||
this.navigation.defaults$(activeUser.id),
|
||||
withSequenceNumber(this.navigation.evaluator$(activeUser.id)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
pairwise(),
|
||||
map(intermediatePairsToRawOptions),
|
||||
);
|
||||
|
||||
return rawOptions$;
|
||||
}
|
||||
|
||||
getOptions$() {
|
||||
const options$ = this.getRawOptions$().pipe(
|
||||
map(
|
||||
([
|
||||
passwordOptions,
|
||||
passwordDefaults,
|
||||
passwordEvaluator,
|
||||
passphraseOptions,
|
||||
passphraseDefaults,
|
||||
passphraseEvaluator,
|
||||
generatorOptions,
|
||||
generatorDefaults,
|
||||
generatorEvaluator,
|
||||
policyUpdated,
|
||||
]) => {
|
||||
const passwordOptionsWithPolicy = passwordEvaluator.applyPolicy(
|
||||
passwordOptions ?? passwordDefaults,
|
||||
);
|
||||
const passphraseOptionsWithPolicy = passphraseEvaluator.applyPolicy(
|
||||
passphraseOptions ?? passphraseDefaults,
|
||||
);
|
||||
const generatorOptionsWithPolicy = generatorEvaluator.applyPolicy(
|
||||
generatorOptions ?? generatorDefaults,
|
||||
);
|
||||
|
||||
const options = this.toPasswordGeneratorOptions({
|
||||
password: passwordEvaluator.sanitize(passwordOptionsWithPolicy),
|
||||
passphrase: passphraseEvaluator.sanitize(passphraseOptionsWithPolicy),
|
||||
generator: generatorEvaluator.sanitize(generatorOptionsWithPolicy),
|
||||
policyUpdated,
|
||||
});
|
||||
|
||||
const policy = Object.assign(
|
||||
new PasswordGeneratorPolicyOptions(),
|
||||
passwordEvaluator.policy,
|
||||
passphraseEvaluator.policy,
|
||||
generatorEvaluator.policy,
|
||||
);
|
||||
|
||||
return [options, policy] as [PasswordGeneratorOptions, PasswordGeneratorPolicyOptions];
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return options$;
|
||||
}
|
||||
|
||||
async getOptions() {
|
||||
return await firstValueFrom(this.getOptions$());
|
||||
}
|
||||
|
||||
async enforcePasswordGeneratorPoliciesOnOptions(options: PasswordGeneratorOptions) {
|
||||
const options$ = this.accountService.activeAccount$.pipe(
|
||||
concatMap((activeUser) =>
|
||||
zip(
|
||||
this.passwords.evaluator$(activeUser.id),
|
||||
this.passphrases.evaluator$(activeUser.id),
|
||||
this.navigation.evaluator$(activeUser.id),
|
||||
),
|
||||
),
|
||||
map(([passwordEvaluator, passphraseEvaluator, navigationEvaluator]) => {
|
||||
const policy = Object.assign(
|
||||
new PasswordGeneratorPolicyOptions(),
|
||||
passwordEvaluator.policy,
|
||||
passphraseEvaluator.policy,
|
||||
navigationEvaluator.policy,
|
||||
);
|
||||
|
||||
const navigationApplied = navigationEvaluator.applyPolicy(options);
|
||||
const navigationSanitized = {
|
||||
...options,
|
||||
...navigationEvaluator.sanitize(navigationApplied),
|
||||
};
|
||||
if (options.type === "password") {
|
||||
const applied = passwordEvaluator.applyPolicy(navigationSanitized);
|
||||
const sanitized = passwordEvaluator.sanitize(applied);
|
||||
return [sanitized, policy];
|
||||
} else {
|
||||
const applied = passphraseEvaluator.applyPolicy(navigationSanitized);
|
||||
const sanitized = passphraseEvaluator.sanitize(applied);
|
||||
return [sanitized, policy];
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const [sanitized, policy] = await firstValueFrom(options$);
|
||||
return [
|
||||
// callers assume this function updates the options parameter
|
||||
Object.assign(options, sanitized),
|
||||
policy,
|
||||
] as [PasswordGeneratorOptions, PasswordGeneratorPolicyOptions];
|
||||
}
|
||||
|
||||
async saveOptions(options: PasswordGeneratorOptions) {
|
||||
const stored = this.toStoredOptions(options);
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
|
||||
// generator settings needs to preserve whether password or passphrase is selected,
|
||||
// so `navigationOptions` is mutated.
|
||||
const navigationOptions$ = zip(
|
||||
this.navigation.options$(activeAccount.id),
|
||||
this.navigation.defaults$(activeAccount.id),
|
||||
).pipe(map(([options, defaults]) => options ?? defaults));
|
||||
let navigationOptions = await firstValueFrom(navigationOptions$);
|
||||
navigationOptions = Object.assign(navigationOptions, stored.generator);
|
||||
await this.navigation.saveOptions(activeAccount.id, navigationOptions);
|
||||
|
||||
// overwrite all other settings with latest values
|
||||
await this.passwords.saveOptions(activeAccount.id, stored.password);
|
||||
await this.passphrases.saveOptions(activeAccount.id, stored.passphrase);
|
||||
}
|
||||
|
||||
private toStoredOptions(options: PasswordGeneratorOptions): MappedOptions {
|
||||
return {
|
||||
generator: {
|
||||
type: options.type,
|
||||
},
|
||||
password: {
|
||||
length: options.length,
|
||||
minLength: options.minLength,
|
||||
ambiguous: options.ambiguous,
|
||||
uppercase: options.uppercase,
|
||||
minUppercase: options.minUppercase,
|
||||
lowercase: options.lowercase,
|
||||
minLowercase: options.minLowercase,
|
||||
number: options.number,
|
||||
minNumber: options.minNumber,
|
||||
special: options.special,
|
||||
minSpecial: options.minSpecial,
|
||||
},
|
||||
passphrase: {
|
||||
numWords: options.numWords,
|
||||
wordSeparator: options.wordSeparator,
|
||||
capitalize: options.capitalize,
|
||||
includeNumber: options.includeNumber,
|
||||
},
|
||||
policyUpdated: false,
|
||||
};
|
||||
}
|
||||
|
||||
private toPasswordGeneratorOptions(options: MappedOptions): PasswordGeneratorOptions {
|
||||
return {
|
||||
type: options.generator.type,
|
||||
length: options.password.length,
|
||||
minLength: options.password.minLength,
|
||||
ambiguous: options.password.ambiguous,
|
||||
uppercase: options.password.uppercase,
|
||||
minUppercase: options.password.minUppercase,
|
||||
lowercase: options.password.lowercase,
|
||||
minLowercase: options.password.minLowercase,
|
||||
number: options.password.number,
|
||||
minNumber: options.password.minNumber,
|
||||
special: options.password.special,
|
||||
minSpecial: options.password.minSpecial,
|
||||
numWords: options.passphrase.numWords,
|
||||
wordSeparator: options.passphrase.wordSeparator,
|
||||
capitalize: options.passphrase.capitalize,
|
||||
includeNumber: options.passphrase.includeNumber,
|
||||
policyUpdated: options.policyUpdated,
|
||||
};
|
||||
}
|
||||
|
||||
getHistory() {
|
||||
const history = this.accountService.activeAccount$.pipe(
|
||||
concatMap((account) => this.history.credentials$(account.id)),
|
||||
timeout({
|
||||
// timeout after 1 second
|
||||
each: 1000,
|
||||
with() {
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
map((history) => history.map(toGeneratedPasswordHistory)),
|
||||
);
|
||||
|
||||
return firstValueFrom(history);
|
||||
}
|
||||
|
||||
async addHistory(password: string) {
|
||||
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (account?.id) {
|
||||
// legacy service doesn't distinguish credential types
|
||||
await this.history.track(account.id, password, "password");
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
const history$ = this.accountService.activeAccount$.pipe(
|
||||
filter((account) => !!account?.id),
|
||||
concatMap((account) => this.history.clear(account.id)),
|
||||
timeout({
|
||||
// timeout after 1 second
|
||||
each: 1000,
|
||||
with() {
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
map((history) => history.map(toGeneratedPasswordHistory)),
|
||||
);
|
||||
|
||||
return firstValueFrom(history$);
|
||||
}
|
||||
}
|
||||
|
||||
function toGeneratedPasswordHistory(value: GeneratedCredential) {
|
||||
return new GeneratedPasswordHistory(value.credential, value.generationDate.valueOf());
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
|
||||
|
||||
import { GeneratedPasswordHistory } from "../history";
|
||||
|
||||
import { PasswordGeneratorOptions } from "./password-generator-options";
|
||||
|
||||
/** @deprecated Use {@link GeneratorService} with a password or passphrase {@link GeneratorStrategy} instead. */
|
||||
export abstract class PasswordGenerationServiceAbstraction {
|
||||
generatePassword: (options: PasswordGeneratorOptions) => Promise<string>;
|
||||
generatePassphrase: (options: PasswordGeneratorOptions) => Promise<string>;
|
||||
getOptions: () => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
||||
getOptions$: () => Observable<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
||||
enforcePasswordGeneratorPoliciesOnOptions: (
|
||||
options: PasswordGeneratorOptions,
|
||||
) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
||||
saveOptions: (options: PasswordGeneratorOptions) => Promise<void>;
|
||||
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
||||
addHistory: (password: string) => Promise<void>;
|
||||
clear: (userId?: string) => Promise<GeneratedPasswordHistory[]>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PassphraseGenerationOptions, PasswordGenerationOptions } from "@bitwarden/generator-core";
|
||||
|
||||
import { GeneratorNavigation } from "../navigation";
|
||||
|
||||
/** Request format for credential generation.
|
||||
* This type includes all properties suitable for reactive data binding.
|
||||
*/
|
||||
export type PasswordGeneratorOptions = PasswordGenerationOptions &
|
||||
PassphraseGenerationOptions &
|
||||
GeneratorNavigation & { policyUpdated?: boolean };
|
||||
Reference in New Issue
Block a user