mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
@@ -5,4 +5,5 @@ export * from "./settings";
|
||||
export { EmailRandomizer } from "./email-randomizer";
|
||||
export { EmailCalculator } from "./email-calculator";
|
||||
export { PasswordRandomizer } from "./password-randomizer";
|
||||
export { SdkPasswordRandomizer } from "./sdk-password-randomizer";
|
||||
export { UsernameRandomizer } from "./username-randomizer";
|
||||
|
||||
103
libs/tools/generator/core/src/engine/sdk-password-randomizer.ts
Normal file
103
libs/tools/generator/core/src/engine/sdk-password-randomizer.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
BitwardenClient,
|
||||
PassphraseGeneratorRequest,
|
||||
PasswordGeneratorRequest,
|
||||
} from "@bitwarden/sdk-internal";
|
||||
|
||||
import { Type } from "../metadata";
|
||||
import {
|
||||
CredentialGenerator,
|
||||
GenerateRequest,
|
||||
GeneratedCredential,
|
||||
PassphraseGenerationOptions,
|
||||
PasswordGenerationOptions,
|
||||
} from "../types";
|
||||
|
||||
/** Generation algorithms that produce randomized secrets by calling on functionality from the SDK */
|
||||
export class SdkPasswordRandomizer
|
||||
implements
|
||||
CredentialGenerator<PassphraseGenerationOptions>,
|
||||
CredentialGenerator<PasswordGenerationOptions>
|
||||
{
|
||||
/** Instantiates the password randomizer
|
||||
* @param client access to SDK client to call upon password/passphrase generation
|
||||
* @param currentTime gets the current datetime in epoch time
|
||||
*/
|
||||
constructor(
|
||||
private client: BitwardenClient,
|
||||
private currentTime: () => number,
|
||||
) {}
|
||||
|
||||
generate(
|
||||
request: GenerateRequest,
|
||||
settings: PasswordGenerationOptions,
|
||||
): Promise<GeneratedCredential>;
|
||||
generate(
|
||||
request: GenerateRequest,
|
||||
settings: PassphraseGenerationOptions,
|
||||
): Promise<GeneratedCredential>;
|
||||
async generate(
|
||||
request: GenerateRequest,
|
||||
settings: PasswordGenerationOptions | PassphraseGenerationOptions,
|
||||
) {
|
||||
if (isPasswordGenerationOptions(settings)) {
|
||||
const password = await this.client.generator().password(convertPasswordRequest(settings));
|
||||
|
||||
return new GeneratedCredential(
|
||||
password,
|
||||
Type.password,
|
||||
this.currentTime(),
|
||||
request.source,
|
||||
request.website,
|
||||
);
|
||||
} else if (isPassphraseGenerationOptions(settings)) {
|
||||
const passphrase = await this.client
|
||||
.generator()
|
||||
.passphrase(convertPassphraseRequest(settings));
|
||||
|
||||
return new GeneratedCredential(
|
||||
passphrase,
|
||||
Type.password,
|
||||
this.currentTime(),
|
||||
request.source,
|
||||
request.website,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error("Invalid settings received by generator.");
|
||||
}
|
||||
}
|
||||
|
||||
function convertPasswordRequest(settings: PasswordGenerationOptions): PasswordGeneratorRequest {
|
||||
return {
|
||||
lowercase: settings.lowercase!,
|
||||
uppercase: settings.uppercase!,
|
||||
numbers: settings.number!,
|
||||
special: settings.special!,
|
||||
length: settings.length!,
|
||||
avoidAmbiguous: settings.ambiguous!,
|
||||
minLowercase: settings.minLowercase!,
|
||||
minUppercase: settings.minUppercase!,
|
||||
minNumber: settings.minNumber!,
|
||||
minSpecial: settings.minSpecial!,
|
||||
};
|
||||
}
|
||||
|
||||
function convertPassphraseRequest(
|
||||
settings: PassphraseGenerationOptions,
|
||||
): PassphraseGeneratorRequest {
|
||||
return {
|
||||
numWords: settings.numWords!,
|
||||
wordSeparator: settings.wordSeparator!,
|
||||
capitalize: settings.capitalize!,
|
||||
includeNumber: settings.includeNumber!,
|
||||
};
|
||||
}
|
||||
|
||||
function isPasswordGenerationOptions(settings: any): settings is PasswordGenerationOptions {
|
||||
return "length" in (settings ?? {});
|
||||
}
|
||||
|
||||
function isPassphraseGenerationOptions(settings: any): settings is PassphraseGenerationOptions {
|
||||
return "numWords" in (settings ?? {});
|
||||
}
|
||||
@@ -8,6 +8,12 @@ export const Algorithm = Object.freeze({
|
||||
/** A password composed of random words from the EFF word list */
|
||||
passphrase: "passphrase",
|
||||
|
||||
/** A password composed of random characters, retrieved from SDK */
|
||||
sdkPassword: "sdkPassword",
|
||||
|
||||
/** A password composed of random words from the EFF word list, retrieved from SDK */
|
||||
sdkPassphrase: "sdkPassphrase",
|
||||
|
||||
/** A username composed of words from the EFF word list */
|
||||
username: "username",
|
||||
|
||||
@@ -38,7 +44,12 @@ export const Profile = Object.freeze({
|
||||
/** Credential generation algorithms grouped by purpose. */
|
||||
export const AlgorithmsByType = deepFreeze({
|
||||
/** Algorithms that produce passwords */
|
||||
[Type.password]: [Algorithm.password, Algorithm.passphrase] as const,
|
||||
[Type.password]: [
|
||||
Algorithm.password,
|
||||
Algorithm.passphrase,
|
||||
Algorithm.sdkPassword,
|
||||
Algorithm.sdkPassphrase,
|
||||
] as const,
|
||||
|
||||
/** Algorithms that produce usernames */
|
||||
[Type.username]: [Algorithm.username] as const,
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
|
||||
import { SdkPasswordRandomizer } from "../../engine";
|
||||
import { PassphrasePolicyConstraints } from "../../policies";
|
||||
import { PassphraseGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
||||
import { Profile } from "../data";
|
||||
import { CoreProfileMetadata } from "../profile-metadata";
|
||||
import { isCoreProfile } from "../util";
|
||||
|
||||
import sdkEffPassphrase from "./sdk-eff-word-list";
|
||||
|
||||
const dependencyProvider = mock<GeneratorDependencyProvider>();
|
||||
|
||||
describe("password - eff words generator metadata", () => {
|
||||
describe("engine.create", () => {
|
||||
it("returns an email randomizer", () => {
|
||||
expect(sdkEffPassphrase.engine.create(dependencyProvider)).toBeInstanceOf(
|
||||
SdkPasswordRandomizer,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("profiles[account]", () => {
|
||||
let accountProfile: CoreProfileMetadata<PassphraseGenerationOptions> | null = null;
|
||||
beforeEach(() => {
|
||||
const profile = sdkEffPassphrase.profiles[Profile.account];
|
||||
if (isCoreProfile(profile!)) {
|
||||
accountProfile = profile;
|
||||
} else {
|
||||
accountProfile = null;
|
||||
}
|
||||
});
|
||||
|
||||
describe("storage.options.deserializer", () => {
|
||||
it("returns its input", () => {
|
||||
const value: PassphraseGenerationOptions = { ...accountProfile!.storage.initial };
|
||||
|
||||
const result = accountProfile!.storage.options.deserializer(value);
|
||||
|
||||
expect(result).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constraints.create", () => {
|
||||
// these tests check that the wiring is correct by exercising the behavior
|
||||
// of functionality encapsulated by `create`. These methods may fail if the
|
||||
// enclosed behaviors change.
|
||||
|
||||
it("creates a passphrase policy constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile!.constraints.default };
|
||||
|
||||
const constraints = accountProfile!.constraints.create([], context);
|
||||
|
||||
expect(constraints).toBeInstanceOf(PassphrasePolicyConstraints);
|
||||
});
|
||||
|
||||
it("forwards the policy to the constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile!.constraints.default };
|
||||
const policies = [
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
minNumberWords: 6,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
},
|
||||
},
|
||||
] as Policy[];
|
||||
|
||||
const constraints = accountProfile!.constraints.create(policies, context);
|
||||
|
||||
expect(constraints.constraints.numWords?.min).toEqual(6);
|
||||
});
|
||||
|
||||
it("combines multiple policies in the constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile!.constraints.default };
|
||||
const policies = [
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
minNumberWords: 6,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
minNumberWords: 3,
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
},
|
||||
},
|
||||
] as Policy[];
|
||||
|
||||
const constraints = accountProfile!.constraints.create(policies, context);
|
||||
|
||||
expect(constraints.constraints.numWords?.min).toEqual(6);
|
||||
expect(constraints.constraints.capitalize?.requiredValue).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
|
||||
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
|
||||
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
|
||||
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { SdkPasswordRandomizer } from "../../engine";
|
||||
import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies";
|
||||
import {
|
||||
CredentialGenerator,
|
||||
GeneratorDependencyProvider,
|
||||
PassphraseGenerationOptions,
|
||||
} from "../../types";
|
||||
import { Algorithm, Profile, Type } from "../data";
|
||||
import { GeneratorMetadata } from "../generator-metadata";
|
||||
|
||||
const sdkPassphrase: GeneratorMetadata<PassphraseGenerationOptions> = {
|
||||
id: Algorithm.sdkPassphrase,
|
||||
category: Type.password,
|
||||
weight: 130,
|
||||
i18nKeys: {
|
||||
name: "passphrase",
|
||||
credentialType: "passphrase",
|
||||
generateCredential: "generatePassphrase",
|
||||
credentialGenerated: "passphraseGenerated",
|
||||
copyCredential: "copyPassphrase",
|
||||
useCredential: "useThisPassphrase",
|
||||
},
|
||||
capabilities: {
|
||||
autogenerate: false,
|
||||
fields: [],
|
||||
},
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<PassphraseGenerationOptions> {
|
||||
return new SdkPasswordRandomizer(new BitwardenClient(), Date.now); // @TODO hook up a real SDK client
|
||||
},
|
||||
},
|
||||
profiles: {
|
||||
[Profile.account]: {
|
||||
type: "core",
|
||||
storage: {
|
||||
key: "passphraseGeneratorSettings",
|
||||
target: "object",
|
||||
format: "plain",
|
||||
classifier: new PublicClassifier<PassphraseGenerationOptions>([
|
||||
"numWords",
|
||||
"wordSeparator",
|
||||
"capitalize",
|
||||
"includeNumber",
|
||||
]),
|
||||
state: GENERATOR_DISK,
|
||||
initial: {
|
||||
numWords: 6,
|
||||
wordSeparator: "-",
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
},
|
||||
options: {
|
||||
deserializer(value) {
|
||||
return value;
|
||||
},
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
} satisfies ObjectKey<PassphraseGenerationOptions>,
|
||||
constraints: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
default: {
|
||||
wordSeparator: { maxLength: 1 },
|
||||
numWords: {
|
||||
min: 3,
|
||||
max: 20,
|
||||
recommendation: 6,
|
||||
},
|
||||
},
|
||||
create(policies, context) {
|
||||
const initial = {
|
||||
minNumberWords: 0,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
};
|
||||
const policy = policies.reduce(passphraseLeastPrivilege, initial);
|
||||
const constraints = new PassphrasePolicyConstraints(policy, context.defaultConstraints);
|
||||
return constraints;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default sdkPassphrase;
|
||||
@@ -0,0 +1,107 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
|
||||
import { SdkPasswordRandomizer } from "../../engine";
|
||||
import { DynamicPasswordPolicyConstraints } from "../../policies";
|
||||
import { PasswordGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
||||
import { Profile } from "../data";
|
||||
import { CoreProfileMetadata } from "../profile-metadata";
|
||||
import { isCoreProfile } from "../util";
|
||||
|
||||
import sdkPassword from "./sdk-random-password";
|
||||
|
||||
const dependencyProvider = mock<GeneratorDependencyProvider>();
|
||||
|
||||
describe("password - characters generator metadata", () => {
|
||||
describe("engine.create", () => {
|
||||
it("returns an email randomizer", () => {
|
||||
expect(sdkPassword.engine.create(dependencyProvider)).toBeInstanceOf(SdkPasswordRandomizer);
|
||||
});
|
||||
});
|
||||
|
||||
describe("profiles[account]", () => {
|
||||
let accountProfile: CoreProfileMetadata<PasswordGenerationOptions> = null!;
|
||||
beforeEach(() => {
|
||||
const profile = sdkPassword.profiles[Profile.account];
|
||||
if (isCoreProfile(profile!)) {
|
||||
accountProfile = profile;
|
||||
} else {
|
||||
throw new Error("this branch should never run");
|
||||
}
|
||||
});
|
||||
|
||||
describe("storage.options.deserializer", () => {
|
||||
it("returns its input", () => {
|
||||
const value: PasswordGenerationOptions = { ...accountProfile.storage.initial };
|
||||
|
||||
const result = accountProfile.storage.options.deserializer(value);
|
||||
|
||||
expect(result).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constraints.create", () => {
|
||||
// these tests check that the wiring is correct by exercising the behavior
|
||||
// of functionality encapsulated by `create`. These methods may fail if the
|
||||
// enclosed behaviors change.
|
||||
|
||||
it("creates a passphrase policy constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
||||
|
||||
const constraints = accountProfile.constraints.create([], context);
|
||||
|
||||
expect(constraints).toBeInstanceOf(DynamicPasswordPolicyConstraints);
|
||||
});
|
||||
|
||||
it("forwards the policy to the constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
||||
const policies = [
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
enabled: true,
|
||||
data: {
|
||||
minLength: 10,
|
||||
capitalize: false,
|
||||
useNumbers: false,
|
||||
},
|
||||
},
|
||||
] as Policy[];
|
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context);
|
||||
|
||||
expect(constraints.constraints.length?.min).toEqual(10);
|
||||
});
|
||||
|
||||
it("combines multiple policies in the constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
||||
const policies = [
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
enabled: true,
|
||||
data: {
|
||||
minLength: 14,
|
||||
useSpecial: false,
|
||||
useNumbers: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
enabled: true,
|
||||
data: {
|
||||
minLength: 10,
|
||||
useSpecial: true,
|
||||
includeNumber: false,
|
||||
},
|
||||
},
|
||||
] as Policy[];
|
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context);
|
||||
|
||||
expect(constraints.constraints.length?.min).toEqual(14);
|
||||
expect(constraints.constraints.special?.requiredValue).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
|
||||
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
|
||||
import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { SdkPasswordRandomizer } from "../../engine";
|
||||
import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies";
|
||||
import {
|
||||
CredentialGenerator,
|
||||
GeneratorDependencyProvider,
|
||||
PasswordGeneratorSettings,
|
||||
} from "../../types";
|
||||
import { Algorithm, Profile, Type } from "../data";
|
||||
import { GeneratorMetadata } from "../generator-metadata";
|
||||
|
||||
const sdkPassword: GeneratorMetadata<PasswordGeneratorSettings> = deepFreeze({
|
||||
id: Algorithm.sdkPassword,
|
||||
category: Type.password,
|
||||
weight: 120,
|
||||
i18nKeys: {
|
||||
name: "password",
|
||||
generateCredential: "generatePassword",
|
||||
credentialGenerated: "passwordGenerated",
|
||||
credentialType: "password",
|
||||
copyCredential: "copyPassword",
|
||||
useCredential: "useThisPassword",
|
||||
},
|
||||
capabilities: {
|
||||
autogenerate: true,
|
||||
fields: [],
|
||||
},
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<PasswordGeneratorSettings> {
|
||||
return new SdkPasswordRandomizer(new BitwardenClient(), Date.now); // @TODO hook up a real SDK client
|
||||
},
|
||||
},
|
||||
profiles: {
|
||||
[Profile.account]: {
|
||||
type: "core",
|
||||
storage: {
|
||||
key: "passwordGeneratorSettings",
|
||||
target: "object",
|
||||
format: "plain",
|
||||
classifier: new PublicClassifier<PasswordGeneratorSettings>([
|
||||
"length",
|
||||
"ambiguous",
|
||||
"uppercase",
|
||||
"minUppercase",
|
||||
"lowercase",
|
||||
"minLowercase",
|
||||
"number",
|
||||
"minNumber",
|
||||
"special",
|
||||
"minSpecial",
|
||||
]),
|
||||
state: GENERATOR_DISK,
|
||||
initial: {
|
||||
length: 14,
|
||||
ambiguous: true,
|
||||
uppercase: true,
|
||||
minUppercase: 1,
|
||||
lowercase: true,
|
||||
minLowercase: 1,
|
||||
number: true,
|
||||
minNumber: 1,
|
||||
special: false,
|
||||
minSpecial: 0,
|
||||
},
|
||||
options: {
|
||||
deserializer(value) {
|
||||
return value;
|
||||
},
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
},
|
||||
constraints: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
default: {
|
||||
length: {
|
||||
min: 5,
|
||||
max: 128,
|
||||
recommendation: 14,
|
||||
},
|
||||
minNumber: {
|
||||
min: 0,
|
||||
max: 9,
|
||||
},
|
||||
minSpecial: {
|
||||
min: 0,
|
||||
max: 9,
|
||||
},
|
||||
},
|
||||
create(policies, context) {
|
||||
const initial = {
|
||||
minLength: 0,
|
||||
useUppercase: false,
|
||||
useLowercase: false,
|
||||
useNumbers: false,
|
||||
numberCount: 0,
|
||||
useSpecial: false,
|
||||
specialCount: 0,
|
||||
};
|
||||
const policy = policies.reduce(passwordLeastPrivilege, initial);
|
||||
const constraints = new DynamicPasswordPolicyConstraints(
|
||||
policy,
|
||||
context.defaultConstraints,
|
||||
);
|
||||
return constraints;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default sdkPassword;
|
||||
Reference in New Issue
Block a user