import * as zxcvbn from "zxcvbn" ; import { EncString } from "../models/domain/encString" ; 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 { StateService } from "../abstractions/state.service" ; import { EEFLongWordList } from "../misc/wordlist" ; import { PolicyType } from "../enums/policyType" ; const DefaultOptions = { length : 14 , ambiguous : false , number : true , minNumber : 1 , uppercase : true , minUppercase : 0 , lowercase : true , minLowercase : 0 , special : false , minSpecial : 1 , type : "password" , numWords : 3 , wordSeparator : "-" , capitalize : false , includeNumber : false , } ; const MaxPasswordsInHistory = 100 ; export class PasswordGenerationService implements PasswordGenerationServiceAbstraction { constructor ( private cryptoService , private policyService , private stateService ) { } async generatePassword ( options ) { const o = Object . assign ( { } , DefaultOptions , options ) ; if ( o . type === "passphrase" ) { return this . generatePassphrase ( options ) ; } this . sanitizePasswordLength ( o , true ) ; const minLength = o . minUppercase + o . minLowercase + o . minNumber + o . minSpecial ; if ( o . length < minLength ) { o . length = minLength ; } const positions = [ ] ; if ( o . lowercase && o . minLowercase > 0 ) { for ( let i = 0 ; i < o . minLowercase ; i ++ ) { positions . push ( "l" ) ; } } if ( o . uppercase && o . minUppercase > 0 ) { for ( let i = 0 ; i < o . minUppercase ; i ++ ) { positions . push ( "u" ) ; } } if ( o . number && o . minNumber > 0 ) { for ( let i = 0 ; i < o . minNumber ; i ++ ) { positions . push ( "n" ) ; } } if ( o . special && o . minSpecial > 0 ) { for ( let i = 0 ; i < o . minSpecial ; i ++ ) { positions . push ( "s" ) ; } } while ( positions . length < o . length ) { positions . push ( "a" ) ; } await this . shuffleArray ( positions ) ; let allCharSet = "" ; let lowercaseCharSet = "abcdefghijkmnopqrstuvwxyz" ; if ( o . ambiguous ) { lowercaseCharSet += "l" ; } if ( o . lowercase ) { allCharSet += lowercaseCharSet ; } let uppercaseCharSet = "ABCDEFGHJKLMNPQRSTUVWXYZ" ; if ( o . ambiguous ) { uppercaseCharSet += "IO" ; } if ( o . uppercase ) { allCharSet += uppercaseCharSet ; } let numberCharSet = "23456789" ; if ( o . ambiguous ) { numberCharSet += "01" ; } if ( o . number ) { allCharSet += numberCharSet ; } const specialCharSet = "!@#$%^&*" ; if ( o . special ) { allCharSet += specialCharSet ; } let password = "" ; for ( let i = 0 ; i < o . length ; i ++ ) { let positionChars ; switch ( positions [ i ] ) { case "l" : positionChars = lowercaseCharSet ; break ; case "u" : positionChars = uppercaseCharSet ; break ; case "n" : positionChars = numberCharSet ; break ; case "s" : positionChars = specialCharSet ; break ; case "a" : positionChars = allCharSet ; break ; default : break ; } const randomCharIndex = await this . cryptoService . randomNumber ( 0 , positionChars . length - 1 ) ; password += positionChars . charAt ( randomCharIndex ) ; } return password ; } async generatePassphrase ( options ) { const o = Object . assign ( { } , DefaultOptions , options ) ; if ( o . numWords == null || o . numWords <= 2 ) { o . numWords = DefaultOptions . numWords ; } if ( o . wordSeparator == null || o . wordSeparator . length === 0 || o . wordSeparator . length > 1 ) { o . wordSeparator = " " ; } if ( o . capitalize == null ) { o . capitalize = false ; } if ( o . includeNumber == null ) { o . includeNumber = false ; } const listLength = EEFLongWordList . length - 1 ; const wordList = new Array ( o . numWords ) ; for ( let i = 0 ; i < o . numWords ; i ++ ) { const wordIndex = await this . cryptoService . randomNumber ( 0 , listLength ) ; if ( o . capitalize ) { wordList [ i ] = this . capitalize ( EEFLongWordList [ wordIndex ] ) ; } else { wordList [ i ] = EEFLongWordList [ wordIndex ] ; } } if ( o . includeNumber ) { await this . appendRandomNumberToRandomWord ( wordList ) ; } return wordList . join ( o . wordSeparator ) ; } async getOptions ( ) { let options = await this . stateService . getPasswordGenerationOptions ( ) ; if ( options == null ) { options = DefaultOptions ; } else { options = Object . assign ( { } , DefaultOptions , options ) ; } await this . stateService . setPasswordGenerationOptions ( options ) ; const enforcedOptions = await this . enforcePasswordGeneratorPoliciesOnOptions ( options ) ; options = enforcedOptions [ 0 ] ; return [ options , enforcedOptions [ 1 ] ] ; } async enforcePasswordGeneratorPoliciesOnOptions ( options ) { let enforcedPolicyOptions = await this . getPasswordGeneratorPolicyOptions ( ) ; if ( enforcedPolicyOptions != null ) { if ( options . length < enforcedPolicyOptions . minLength ) { options . length = enforcedPolicyOptions . minLength ; } if ( enforcedPolicyOptions . useUppercase ) { options . uppercase = true ; } if ( enforcedPolicyOptions . useLowercase ) { options . lowercase = true ; } if ( enforcedPolicyOptions . useNumbers ) { options . number = true ; } if ( options . minNumber < enforcedPolicyOptions . numberCount ) { options . minNumber = enforcedPolicyOptions . numberCount ; } if ( enforcedPolicyOptions . useSpecial ) { options . special = true ; } if ( options . minSpecial < enforcedPolicyOptions . specialCount ) { options . minSpecial = enforcedPolicyOptions . specialCount ; } if ( options . minSpecial + options . minNumber > options . length ) { options . minSpecial = options . length - options . minNumber ; } if ( options . numWords < enforcedPolicyOptions . minNumberWords ) { options . numWords = enforcedPolicyOptions . minNumberWords ; } if ( enforcedPolicyOptions . capitalize ) { options . capitalize = true ; } if ( enforcedPolicyOptions . includeNumber ) { options . includeNumber = true ; } if ( enforcedPolicyOptions . defaultType === "password" || enforcedPolicyOptions . defaultType === "passphrase" ) { options . type = enforcedPolicyOptions . defaultType ; } } else { enforcedPolicyOptions = new PasswordGeneratorPolicyOptions ( ) ; } return [ options , enforcedPolicyOptions ] ; } async getPasswordGeneratorPolicyOptions ( ) { const policies = this . policyService == null ? null : await this . policyService . getAll ( PolicyType . PasswordGenerator ) ; let enforcedOptions = 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 . defaultType != null && enforcedOptions . defaultType !== "password" ) { enforcedOptions . defaultType = currentPolicy . data . defaultType ; } 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 ; } if ( currentPolicy . data . minNumberWords != null && currentPolicy . data . minNumberWords > enforcedOptions . minNumberWords ) { enforcedOptions . minNumberWords = currentPolicy . data . minNumberWords ; } if ( currentPolicy . data . capitalize ) { enforcedOptions . capitalize = true ; } if ( currentPolicy . data . includeNumber ) { enforcedOptions . includeNumber = true ; } } ) ; return enforcedOptions ; } async saveOptions ( options ) { await this . stateService . setPasswordGenerationOptions ( options ) ; } async getHistory ( ) { const hasKey = await this . cryptoService . hasKey ( ) ; if ( ! hasKey ) { return new Array < GeneratedPasswordHistory > ( ) ; } if ( ( await this . stateService . getDecryptedPasswordGenerationHistory ( ) ) != null ) { const encrypted = await this . stateService . getEncryptedPasswordGenerationHistory ( ) ; const decrypted = await this . decryptHistory ( encrypted ) ; await this . stateService . setDecryptedPasswordGenerationHistory ( decrypted ) ; } const passwordGenerationHistory = await this . stateService . getDecryptedPasswordGenerationHistory ( ) ; return passwordGenerationHistory != null ? passwordGenerationHistory : new Array < GeneratedPasswordHistory > ( ) ; } async addHistory ( password ) { const hasKey = await this . cryptoService . hasKey ( ) ; if ( ! hasKey ) { return ; } const currentHistory = await this . getHistory ( ) ; if ( this . matchesPrevious ( password , currentHistory ) ) { return ; } currentHistory . unshift ( new GeneratedPasswordHistory ( password , Date . now ( ) ) ) ; if ( currentHistory . length > MaxPasswordsInHistory ) { currentHistory . pop ( ) ; } const newHistory = await this . encryptHistory ( currentHistory ) ; return await this . stateService . setEncryptedPasswordGenerationHistory ( newHistory ) ; } async clear ( userId ? ) { await this . stateService . setEncryptedPasswordGenerationHistory ( null , { userId : userId } ) ; await this . stateService . setDecryptedPasswordGenerationHistory ( null , { userId : userId } ) ; } passwordStrength ( password , userInputs = null ) { if ( password == null || password . length === 0 ) { return null ; } let globalUserInputs = [ "bitwarden" , "bit" , "warden" ] ; if ( userInputs != null && userInputs . length > 0 ) { globalUserInputs = globalUserInputs . concat ( userInputs ) ; } const finalUserInputs = Array . from ( new Set ( globalUserInputs ) ) ; const result = zxcvbn ( password , finalUserInputs ) ; return result ; } normalizeOptions ( options , enforcedPolicyOptions ) { options . minLowercase = 0 ; options . minUppercase = 0 ; if ( ! options . length || options . length < 5 ) { options . length = 5 ; } else if ( options . length > 128 ) { options . length = 128 ; } if ( options . length < enforcedPolicyOptions . minLength ) { options . length = enforcedPolicyOptions . minLength ; } if ( ! options . minNumber ) { options . minNumber = 0 ; } else if ( options . minNumber > options . length ) { options . minNumber = options . length ; } else if ( options . minNumber > 9 ) { options . minNumber = 9 ; } if ( options . minNumber < enforcedPolicyOptions . numberCount ) { options . minNumber = enforcedPolicyOptions . numberCount ; } if ( ! options . minSpecial ) { options . minSpecial = 0 ; } else if ( options . minSpecial > options . length ) { options . minSpecial = options . length ; } else if ( options . minSpecial > 9 ) { options . minSpecial = 9 ; } if ( options . minSpecial < enforcedPolicyOptions . specialCount ) { options . minSpecial = enforcedPolicyOptions . specialCount ; } if ( options . minSpecial + options . minNumber > options . length ) { options . minSpecial = options . length - options . minNumber ; } if ( options . numWords == null || options . length < 3 ) { options . numWords = 3 ; } else if ( options . numWords > 20 ) { options . numWords = 20 ; } if ( options . numWords < enforcedPolicyOptions . minNumberWords ) { options . numWords = enforcedPolicyOptions . minNumberWords ; } if ( options . wordSeparator != null && options . wordSeparator . length > 1 ) { options . wordSeparator = options . wordSeparator [ 0 ] ; } this . sanitizePasswordLength ( options , false ) ; } private capitalize ( str ) { return str . charAt ( 0 ) . toUpperCase ( ) + str . slice ( 1 ) ; } private async appendRandomNumberToRandomWord ( wordList ) { if ( wordList == null || wordList . length <= 0 ) { return ; } const index = await this . cryptoService . randomNumber ( 0 , wordList . length - 1 ) ; const num = await this . cryptoService . randomNumber ( 0 , 9 ) ; wordList [ index ] = wordList [ index ] + num ; } private async encryptHistory ( history ) { if ( history == null || history . length === 0 ) { return Promise . resolve ( [ ] ) ; } const promises = history . map ( async ( item ) => { const encrypted = await this . cryptoService . encrypt ( item . password ) ; return new GeneratedPasswordHistory ( encrypted . encryptedString , item . date ) ; } ) ; return await Promise . all ( promises ) ; } private async decryptHistory ( history ) { if ( history == null || history . length === 0 ) { return Promise . resolve ( [ ] ) ; } const promises = history . map ( async ( item ) => { const decrypted = await this . cryptoService . decryptToUtf8 ( new EncString ( item . password ) ) ; return new GeneratedPasswordHistory ( decrypted , item . date ) ; } ) ; return await Promise . all ( promises ) ; } private matchesPrevious ( password , history ) { if ( history == null || history . length === 0 ) { return false ; } return history [ history . length - 1 ] . password === password ; } private async shuffleArray ( array ) { for ( let i = array . length - 1 ; i > 0 ; i -- ) { const j = await this . cryptoService . randomNumber ( 0 , i ) ; [ array [ i ] , array [ j ] ] = [ array [ j ] , array [ i ] ] ; } } private sanitizePasswordLength ( options , forGeneration ) { let minUppercaseCalc = 0 ; let minLowercaseCalc = 0 ; let minNumberCalc = options . minNumber ; let minSpecialCalc = options . minSpecial ; if ( options . uppercase && options . minUppercase <= 0 ) { minUppercaseCalc = 1 ; } else if ( ! options . uppercase ) { minUppercaseCalc = 0 ; } if ( options . lowercase && options . minLowercase <= 0 ) { minLowercaseCalc = 1 ; } else if ( ! options . lowercase ) { minLowercaseCalc = 0 ; } if ( options . number && options . minNumber <= 0 ) { minNumberCalc = 1 ; } else if ( ! options . number ) { minNumberCalc = 0 ; } if ( options . special && options . minSpecial <= 0 ) { minSpecialCalc = 1 ; } else if ( ! options . special ) { minSpecialCalc = 0 ; } if ( ! options . length || options . length < 1 ) { options . length = 10 ; } const minLength = minUppercaseCalc + minLowercaseCalc + minNumberCalc + minSpecialCalc ; if ( options . length < minLength ) { options . length = minLength ; } if ( forGeneration ) { options . minUppercase = minUppercaseCalc ; options . minLowercase = minLowercaseCalc ; options . minNumber = minNumberCalc ; options . minSpecial = minSpecialCalc ; } } }