diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index dda27d202de..d61e3f962b4 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -27,6 +27,7 @@ export enum FeatureFlag { EnableRiskInsightsNotifications = "enable-risk-insights-notifications", DesktopSendUIRefresh = "desktop-send-ui-refresh", ExportAttachments = "export-attachments", + SDKGenerators = "sdk-generators", /* Vault */ PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", @@ -83,6 +84,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.EnableRiskInsightsNotifications]: FALSE, [FeatureFlag.DesktopSendUIRefresh]: FALSE, [FeatureFlag.ExportAttachments]: FALSE, + [FeatureFlag.SDKGenerators]: FALSE, /* Vault */ [FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE, diff --git a/libs/tools/generator/core/src/engine/index.ts b/libs/tools/generator/core/src/engine/index.ts index 2d272e7c11b..3caf199ee4b 100644 --- a/libs/tools/generator/core/src/engine/index.ts +++ b/libs/tools/generator/core/src/engine/index.ts @@ -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"; diff --git a/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts b/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts new file mode 100644 index 00000000000..81e2ab2e182 --- /dev/null +++ b/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts @@ -0,0 +1,87 @@ +import { + CredentialGenerator, + GenerateRequest, + GeneratedCredential, + PassphraseGenerationOptions, + PasswordGenerationOptions, +} from "../types"; +import { optionsToEffWordListRequest, optionsToRandomAsciiRequest } from "../util"; + +import { EffWordListRequest, RandomAsciiRequest } from "./types"; + +/** Generation algorithms that produce randomized secrets */ +export class SDKPasswordRandomizer + implements + CredentialGenerator, + CredentialGenerator +{ + /** Instantiates the password randomizer + */ + constructor() {} + + /** create a password from ASCII codepoints + * @param request refines the generated password + * @returns a promise that completes with the generated password + */ + async randomSDKAscii(request: RandomAsciiRequest) { + // TODO + return ""; + } + + /** create a passphrase from the EFF's "5 dice" word list + * @param request refines the generated passphrase + * @returns a promise that completes with the generated passphrase + */ + async randomSDKEffLongWords(request: EffWordListRequest) { + // TODO + + return ""; + } + + generate( + request: GenerateRequest, + settings: PasswordGenerationOptions, + ): Promise; + generate( + request: GenerateRequest, + settings: PassphraseGenerationOptions, + ): Promise; + async generate( + request: GenerateRequest, + settings: PasswordGenerationOptions | PassphraseGenerationOptions, + ) { + if (isPasswordGenerationOptions(settings)) { + const req = optionsToRandomAsciiRequest(settings); + const password = await this.randomSDKAscii(req); + + return new GeneratedCredential( + password, + "password", + Date.now(), + request.source, + request.website, + ); + } else if (isPassphraseGenerationOptions(settings)) { + const req = optionsToEffWordListRequest(settings); + const passphrase = await this.randomSDKEffLongWords(req); + + return new GeneratedCredential( + passphrase, + "passphrase", + Date.now(), + request.source, + request.website, + ); + } + + 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 ?? {}); +} diff --git a/libs/tools/generator/core/src/metadata/data.ts b/libs/tools/generator/core/src/metadata/data.ts index 2b9dad50557..8f38910a3db 100644 --- a/libs/tools/generator/core/src/metadata/data.ts +++ b/libs/tools/generator/core/src/metadata/data.ts @@ -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 generated from the SDK */ + sdkPassword: "sdkpassword", + + /** A password composed of random words from the EFF word list generated from the 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, diff --git a/libs/tools/generator/core/src/metadata/password/sdk-eff-word-list.ts b/libs/tools/generator/core/src/metadata/password/sdk-eff-word-list.ts new file mode 100644 index 00000000000..90b71980796 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/password/sdk-eff-word-list.ts @@ -0,0 +1,91 @@ +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 { 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 = { + 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 { + return new SDKPasswordRandomizer(); + }, + }, + profiles: { + [Profile.account]: { + type: "core", + storage: { + key: "passphraseGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "numWords", + "wordSeparator", + "capitalize", + "includeNumber", + ]), + state: GENERATOR_DISK, + initial: { + numWords: 6, + wordSeparator: "-", + capitalize: false, + includeNumber: false, + }, + options: { + deserializer(value) { + return value; + }, + clearOn: ["logout"], + }, + } satisfies ObjectKey, + 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; diff --git a/libs/tools/generator/core/src/metadata/password/sdk-random-password.ts b/libs/tools/generator/core/src/metadata/password/sdk-random-password.ts new file mode 100644 index 00000000000..7d72d00dc34 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/password/sdk-random-password.ts @@ -0,0 +1,117 @@ +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 { 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 = deepFreeze({ + id: Algorithm.sdkPassphrase, + 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 { + return new SDKPasswordRandomizer(); + }, + }, + profiles: { + [Profile.account]: { + type: "core", + storage: { + key: "passwordGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "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;