mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
Enforce Password Generator Policy (#75)
* Enforce Password Generator Policy * Move policy enforcement to service layer * Fixed typo (vscode didn't warn..) and adjust import spacing * Made requested changes
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory';
|
import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory';
|
||||||
|
import { PasswordGeneratorPolicyOptions } from '../models/domain/passwordGeneratorPolicyOptions';
|
||||||
|
|
||||||
export abstract class PasswordGenerationService {
|
export abstract class PasswordGenerationService {
|
||||||
generatePassword: (options: any) => Promise<string>;
|
generatePassword: (options: any) => Promise<string>;
|
||||||
generatePassphrase: (options: any) => Promise<string>;
|
generatePassphrase: (options: any) => Promise<string>;
|
||||||
getOptions: () => any;
|
getOptions: () => Promise<[any, PasswordGeneratorPolicyOptions]>;
|
||||||
|
getPasswordGeneratorPolicyOptions: () => Promise<PasswordGeneratorPolicyOptions>;
|
||||||
saveOptions: (options: any) => Promise<any>;
|
saveOptions: (options: any) => Promise<any>;
|
||||||
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
||||||
addHistory: (password: string) => Promise<any>;
|
addHistory: (password: string) => Promise<any>;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { I18nService } from '../../abstractions/i18n.service';
|
|||||||
import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service';
|
import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service';
|
||||||
import { PlatformUtilsService } from '../../abstractions/platformUtils.service';
|
import { PlatformUtilsService } from '../../abstractions/platformUtils.service';
|
||||||
|
|
||||||
|
import { PasswordGeneratorPolicyOptions } from '../../models/domain/passwordGeneratorPolicyOptions';
|
||||||
|
|
||||||
export class PasswordGeneratorComponent implements OnInit {
|
export class PasswordGeneratorComponent implements OnInit {
|
||||||
@Input() showSelect: boolean = false;
|
@Input() showSelect: boolean = false;
|
||||||
@Output() onSelected = new EventEmitter<string>();
|
@Output() onSelected = new EventEmitter<string>();
|
||||||
@@ -17,13 +19,16 @@ export class PasswordGeneratorComponent implements OnInit {
|
|||||||
password: string = '-';
|
password: string = '-';
|
||||||
showOptions = false;
|
showOptions = false;
|
||||||
avoidAmbiguous = false;
|
avoidAmbiguous = false;
|
||||||
|
enforcedPolicyOptions: PasswordGeneratorPolicyOptions;
|
||||||
|
|
||||||
constructor(protected passwordGenerationService: PasswordGenerationService,
|
constructor(protected passwordGenerationService: PasswordGenerationService,
|
||||||
protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService,
|
protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService,
|
||||||
private win: Window) { }
|
private win: Window) { }
|
||||||
|
|
||||||
async ngOnInit() {
|
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.avoidAmbiguous = !this.options.ambiguous;
|
||||||
this.options.type = this.options.type === 'passphrase' ? 'passphrase' : 'password';
|
this.options.type = this.options.type === 'passphrase' ? 'passphrase' : 'password';
|
||||||
this.password = await this.passwordGenerationService.generatePassword(this.options);
|
this.password = await this.passwordGenerationService.generatePassword(this.options);
|
||||||
@@ -95,6 +100,10 @@ export class PasswordGeneratorComponent implements OnInit {
|
|||||||
this.options.length = 128;
|
this.options.length = 128;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.length < this.enforcedPolicyOptions.minLength) {
|
||||||
|
this.options.length = this.enforcedPolicyOptions.minLength;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.options.minNumber) {
|
if (!this.options.minNumber) {
|
||||||
this.options.minNumber = 0;
|
this.options.minNumber = 0;
|
||||||
} else if (this.options.minNumber > this.options.length) {
|
} else if (this.options.minNumber > this.options.length) {
|
||||||
@@ -103,6 +112,10 @@ export class PasswordGeneratorComponent implements OnInit {
|
|||||||
this.options.minNumber = 9;
|
this.options.minNumber = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.minNumber < this.enforcedPolicyOptions.numberCount) {
|
||||||
|
this.options.minNumber = this.enforcedPolicyOptions.numberCount;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.options.minSpecial) {
|
if (!this.options.minSpecial) {
|
||||||
this.options.minSpecial = 0;
|
this.options.minSpecial = 0;
|
||||||
} else if (this.options.minSpecial > this.options.length) {
|
} else if (this.options.minSpecial > this.options.length) {
|
||||||
@@ -111,6 +124,10 @@ export class PasswordGeneratorComponent implements OnInit {
|
|||||||
this.options.minSpecial = 9;
|
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) {
|
if (this.options.minSpecial + this.options.minNumber > this.options.length) {
|
||||||
this.options.minSpecial = this.options.length - this.options.minNumber;
|
this.options.minSpecial = this.options.length - this.options.minNumber;
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/models/domain/passwordGeneratorPolicyOptions.ts
Normal file
11
src/models/domain/passwordGeneratorPolicyOptions.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -2,15 +2,20 @@ import * as zxcvbn from 'zxcvbn';
|
|||||||
|
|
||||||
import { CipherString } from '../models/domain/cipherString';
|
import { CipherString } from '../models/domain/cipherString';
|
||||||
import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory';
|
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 { CryptoService } from '../abstractions/crypto.service';
|
||||||
import {
|
import {
|
||||||
PasswordGenerationService as PasswordGenerationServiceAbstraction,
|
PasswordGenerationService as PasswordGenerationServiceAbstraction,
|
||||||
} from '../abstractions/passwordGeneration.service';
|
} from '../abstractions/passwordGeneration.service';
|
||||||
|
import { PolicyService } from '../abstractions/policy.service';
|
||||||
import { StorageService } from '../abstractions/storage.service';
|
import { StorageService } from '../abstractions/storage.service';
|
||||||
|
|
||||||
import { EEFLongWordList } from '../misc/wordlist';
|
import { EEFLongWordList } from '../misc/wordlist';
|
||||||
|
|
||||||
|
import { PolicyType } from '../enums/policyType';
|
||||||
|
|
||||||
const DefaultOptions = {
|
const DefaultOptions = {
|
||||||
length: 14,
|
length: 14,
|
||||||
ambiguous: false,
|
ambiguous: false,
|
||||||
@@ -40,7 +45,8 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
|||||||
private optionsCache: any;
|
private optionsCache: any;
|
||||||
private history: GeneratedPasswordHistory[];
|
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<string> {
|
async generatePassword(options: any): Promise<string> {
|
||||||
// overload defaults with given options
|
// overload defaults with given options
|
||||||
@@ -207,7 +213,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
|||||||
return wordList.join(o.wordSeparator);
|
return wordList.join(o.wordSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOptions() {
|
async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> {
|
||||||
if (this.optionsCache == null) {
|
if (this.optionsCache == null) {
|
||||||
const options = await this.storageService.get(Keys.options);
|
const options = await this.storageService.get(Keys.options);
|
||||||
if (options == null) {
|
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<PasswordGeneratorPolicyOptions> {
|
||||||
|
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) {
|
async saveOptions(options: any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user