1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

Create sdk generator engine (#14374)

* Add SDK generator engine
This commit is contained in:
adudek-bw
2025-05-20 10:25:40 -04:00
committed by GitHub
parent 13c8e26003
commit b2076e002e
7 changed files with 539 additions and 1 deletions

View File

@@ -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";

View 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 ?? {});
}

View File

@@ -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,

View File

@@ -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);
});
});
});
});

View File

@@ -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;

View File

@@ -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);
});
});
});
});

View File

@@ -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;