diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts index a94283d73a3..e4ed293d44e 100644 --- a/src/abstractions/passwordGeneration.service.ts +++ b/src/abstractions/passwordGeneration.service.ts @@ -1,9 +1,11 @@ import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory'; +import { PasswordGeneratorPolicyOptions } from '../models/domain/passwordGeneratorPolicyOptions'; export abstract class PasswordGenerationService { generatePassword: (options: any) => Promise; generatePassphrase: (options: any) => Promise; - getOptions: () => any; + getOptions: () => Promise<[any, PasswordGeneratorPolicyOptions]>; + getPasswordGeneratorPolicyOptions: () => Promise; saveOptions: (options: any) => Promise; getHistory: () => Promise; addHistory: (password: string) => Promise; diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index 97f4bc96952..f35892d7b29 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -9,6 +9,8 @@ import { I18nService } from '../../abstractions/i18n.service'; import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { PasswordGeneratorPolicyOptions } from '../../models/domain/passwordGeneratorPolicyOptions'; + export class PasswordGeneratorComponent implements OnInit { @Input() showSelect: boolean = false; @Output() onSelected = new EventEmitter(); @@ -17,13 +19,16 @@ export class PasswordGeneratorComponent implements OnInit { password: string = '-'; showOptions = false; avoidAmbiguous = false; + enforcedPolicyOptions: PasswordGeneratorPolicyOptions; constructor(protected passwordGenerationService: PasswordGenerationService, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, private win: Window) { } async ngOnInit() { - this.options = await this.passwordGenerationService.getOptions(); + const optionsResponse = await this.passwordGenerationService.getOptions(); + this.options = optionsResponse[0]; + this.enforcedPolicyOptions = optionsResponse[1]; this.avoidAmbiguous = !this.options.ambiguous; this.options.type = this.options.type === 'passphrase' ? 'passphrase' : 'password'; this.password = await this.passwordGenerationService.generatePassword(this.options); @@ -95,6 +100,10 @@ export class PasswordGeneratorComponent implements OnInit { this.options.length = 128; } + if (this.options.length < this.enforcedPolicyOptions.minLength) { + this.options.length = this.enforcedPolicyOptions.minLength; + } + if (!this.options.minNumber) { this.options.minNumber = 0; } else if (this.options.minNumber > this.options.length) { @@ -103,6 +112,10 @@ export class PasswordGeneratorComponent implements OnInit { this.options.minNumber = 9; } + if (this.options.minNumber < this.enforcedPolicyOptions.numberCount) { + this.options.minNumber = this.enforcedPolicyOptions.numberCount; + } + if (!this.options.minSpecial) { this.options.minSpecial = 0; } else if (this.options.minSpecial > this.options.length) { @@ -111,6 +124,10 @@ export class PasswordGeneratorComponent implements OnInit { this.options.minSpecial = 9; } + if (this.options.minSpecial < this.enforcedPolicyOptions.specialCount) { + this.options.minSpecial = this.enforcedPolicyOptions.specialCount; + } + if (this.options.minSpecial + this.options.minNumber > this.options.length) { this.options.minSpecial = this.options.length - this.options.minNumber; } diff --git a/src/models/domain/passwordGeneratorPolicyOptions.ts b/src/models/domain/passwordGeneratorPolicyOptions.ts new file mode 100644 index 00000000000..fb68016b952 --- /dev/null +++ b/src/models/domain/passwordGeneratorPolicyOptions.ts @@ -0,0 +1,11 @@ +import Domain from './domainBase'; + +export class PasswordGeneratorPolicyOptions extends Domain { + minLength: number = 0; + useUppercase: boolean = false; + useLowercase: boolean = false; + useNumbers: boolean = false; + numberCount: number = 0; + useSpecial: boolean = false; + specialCount: number = 0; +} diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 7270fdaf690..7e24b9f6652 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -2,15 +2,20 @@ import * as zxcvbn from 'zxcvbn'; import { CipherString } from '../models/domain/cipherString'; import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory'; +import { PasswordGeneratorPolicyOptions } from '../models/domain/passwordGeneratorPolicyOptions'; +import { Policy } from '../models/domain/policy'; import { CryptoService } from '../abstractions/crypto.service'; import { PasswordGenerationService as PasswordGenerationServiceAbstraction, } from '../abstractions/passwordGeneration.service'; +import { PolicyService } from '../abstractions/policy.service'; import { StorageService } from '../abstractions/storage.service'; import { EEFLongWordList } from '../misc/wordlist'; +import { PolicyType } from '../enums/policyType'; + const DefaultOptions = { length: 14, ambiguous: false, @@ -40,7 +45,8 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr private optionsCache: any; private history: GeneratedPasswordHistory[]; - constructor(private cryptoService: CryptoService, private storageService: StorageService) { } + constructor(private cryptoService: CryptoService, private storageService: StorageService, + private policyService: PolicyService) { } async generatePassword(options: any): Promise { // overload defaults with given options @@ -207,7 +213,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return wordList.join(o.wordSeparator); } - async getOptions() { + async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> { if (this.optionsCache == null) { const options = await this.storageService.get(Keys.options); if (options == null) { @@ -217,7 +223,98 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } } - return this.optionsCache; + let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions(); + + if (enforcedPolicyOptions != null) { + if (this.optionsCache.length < enforcedPolicyOptions.minLength) { + this.optionsCache.length = enforcedPolicyOptions.minLength; + } + + if (enforcedPolicyOptions.useUppercase) { + this.optionsCache.uppercase = true; + } + + if (enforcedPolicyOptions.useLowercase) { + this.optionsCache.lowercase = true; + } + + if (enforcedPolicyOptions.useNumbers) { + this.optionsCache.number = true; + } + + if (this.optionsCache.minNumber < enforcedPolicyOptions.numberCount) { + this.optionsCache.minNumber = enforcedPolicyOptions.numberCount; + } + + if (enforcedPolicyOptions.useSpecial) { + this.optionsCache.special = true; + } + + if (this.optionsCache.minSpecial < enforcedPolicyOptions.specialCount) { + this.optionsCache.minSpecial = enforcedPolicyOptions.specialCount; + } + + // Must normalize these fields because the receiving call expects all options to pass the current rules + if (this.optionsCache.minSpecial + this.optionsCache.minNumber > this.optionsCache.length) { + this.optionsCache.minSpecial = this.optionsCache.length - this.optionsCache.minNumber; + } + } else { // UI layer expects an instantiated object to prevent more explicit null checks + enforcedPolicyOptions = new PasswordGeneratorPolicyOptions(); + } + + return [this.optionsCache, enforcedPolicyOptions]; + } + + async getPasswordGeneratorPolicyOptions(): Promise { + const policies: Policy[] = await this.policyService.getAll(PolicyType.PasswordGenerator); + let enforcedOptions: PasswordGeneratorPolicyOptions = null; + + if (policies == null || policies.length === 0) { + return enforcedOptions; + } + + policies.forEach((currentPolicy) => { + if (!currentPolicy.enabled || currentPolicy.data == null) { + return; + } + + if (enforcedOptions == null) { + enforcedOptions = new PasswordGeneratorPolicyOptions(); + } + + if (currentPolicy.data.minLength != null + && currentPolicy.data.minLength > enforcedOptions.minLength) { + enforcedOptions.minLength = currentPolicy.data.minLength; + } + + if (currentPolicy.data.useUpper) { + enforcedOptions.useUppercase = true; + } + + if (currentPolicy.data.useLower) { + enforcedOptions.useLowercase = true; + } + + if (currentPolicy.data.useNumbers) { + enforcedOptions.useNumbers = true; + } + + if (currentPolicy.data.minNumbers != null + && currentPolicy.data.minNumbers > enforcedOptions.numberCount) { + enforcedOptions.numberCount = currentPolicy.data.minNumbers; + } + + if (currentPolicy.data.useSpecial) { + enforcedOptions.useSpecial = true; + } + + if (currentPolicy.data.minSpecial != null + && currentPolicy.data.minSpecial > enforcedOptions.specialCount) { + enforcedOptions.specialCount = currentPolicy.data.minSpecial; + } + }); + + return enforcedOptions; } async saveOptions(options: any) {