├── .gitignore ├── pwgendemo.gif ├── .npmignore ├── .prettierrc ├── .github └── workflows │ └── nodejs.yml ├── lib └── src │ ├── password_generator.interface.ts │ ├── charsets.ts │ ├── password_generator.ts │ └── password_generator.spec.ts ├── package.json ├── bin └── pwgen ├── README.md └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/dest/ 3 | .npmrc 4 | -------------------------------------------------------------------------------- /pwgendemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dislick/ts-pwgen/HEAD/pwgendemo.gif -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /lib/src 3 | /lib/dest/*.spec.js 4 | /lib/dest/*.spec.d.ts 5 | /pwgendemo.gif 6 | /.prettierrc 7 | /tsconfig.json 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "jsxSingleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [10.x, 12.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm ci 25 | - run: npm run build 26 | - run: npm test 27 | -------------------------------------------------------------------------------- /lib/src/password_generator.interface.ts: -------------------------------------------------------------------------------- 1 | import { Answers } from 'inquirer'; 2 | 3 | export interface PasswordGeneratorOptions { 4 | lowercaseLetters: boolean; 5 | uppercaseLetters: boolean; 6 | numbers: boolean; 7 | specialCharacters: boolean; 8 | latin1Characters: boolean; 9 | parts: { 10 | amount: number; 11 | length: number; 12 | delimiter: string; 13 | }; 14 | } 15 | 16 | export interface GeneratedPassword { 17 | value: string; 18 | charsetLength: number; 19 | differentCharacters?: number; 20 | } 21 | 22 | export interface PasswordAnswer extends Answers { 23 | password: string; 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/charsets.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all of the character sets that password_generator.ts uses. 3 | */ 4 | export const lowercaseLettersList: string[] = JSON.parse( 5 | '["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"]', 6 | ); 7 | export const uppercaseLettersList: string[] = JSON.parse( 8 | '["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]', 9 | ); 10 | export const numbersList: string[] = JSON.parse( 11 | '["0","1","2","3","4","5","6","7","8","9"]', 12 | ); 13 | export const specialCharactersList: string[] = JSON.parse( 14 | '["!","\\"","#","%","&","(",")","*","+",",","-",".","/",":",";","<","=",">","?","@","[","]","^","_","`","{","|","}","~"]', 15 | ); 16 | export const latin1List: string[] = JSON.parse( 17 | '["¡","¢","£","¤","¥","¦","§","¨","©","ª","«","¬","®","¯","°","±","²","³","´","µ","¶","·","¸","¹","º","»","¼","½","¾","¿","À","Á","Â","Ã","Ä","Å","Æ","Ç","È","É","Ê","Ë","Ì","Í","Î","Ï","Ð","Ñ","Ò","Ó","Ô","Õ","Ö","×","Ø","Ù","Ú","Û","Ü","Ý","Þ","ß","à","á","â","ã","ä","å","æ","ç","è","é","ê","ë","ì","í","î","ï","ð","ñ","ò","ó","ô","õ","ö","÷","ø","ù","ú","û","ü","ý","þ","ÿ"]', 18 | ); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-pwgen", 3 | "version": "1.2.2", 4 | "description": "A command line password generator in TypeScript", 5 | "main": "lib/dest/main.js", 6 | "bin": { 7 | "pwgen": "./bin/pwgen" 8 | }, 9 | "scripts": { 10 | "test": "jest", 11 | "test:watch": "jest --watch", 12 | "build": "tsc", 13 | "format": "prettier --write \"lib/src/*.ts\"" 14 | }, 15 | "keywords": [ 16 | "password", 17 | "generator", 18 | "typescript", 19 | "passwordgenerator" 20 | ], 21 | "author": "Patrick Muff", 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/dislick/ts-pwgen" 26 | }, 27 | "devDependencies": { 28 | "@types/chai": "^4.1.7", 29 | "@types/colors": "^1.2.1", 30 | "@types/copy-paste": "^1.1.30", 31 | "@types/inquirer": "6.0.3", 32 | "@types/jest": "^25.2.1", 33 | "@types/node": "^12.0.10", 34 | "jest": "^25.5.4", 35 | "prettier": "^2.0.5", 36 | "ts-jest": "^25.5.1", 37 | "typescript": "^3.9.2" 38 | }, 39 | "dependencies": { 40 | "colors": "^1.3.3", 41 | "copy-paste": "^1.3.0", 42 | "inquirer": "^6.4.1", 43 | "yargs": "^13.2.4" 44 | }, 45 | "jest": { 46 | "preset": "ts-jest", 47 | "testEnvironment": "node", 48 | "rootDir": "lib/src" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /bin/pwgen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const PasswordGenerator = require('../lib/dest/password_generator.js') 4 | .PasswordGenerator; 5 | const argv = require('yargs') 6 | .usage( 7 | 'Usage: $0 [--length 10] [--parts 3] [--delimiter "-"] [--count 3] [-aAns]' 8 | ) 9 | .describe('p', 'Define how many parts there should be') 10 | .describe('l', 'Define the length of a part') 11 | .describe('d', 'Define the delimiter to use if there are multiple parts') 12 | .describe('c', 'Define how many passwords to generate') 13 | .describe('a', 'Use lowercase letters') 14 | .describe('A', 'Use uppercase letters') 15 | .describe('n', 'Use numbers') 16 | .describe('s', 'Use special characters') 17 | .describe('x', 'Use latin1 characters') 18 | .describe( 19 | 'v', 20 | 'Verbose output, see how long it would take a supercomputer to crack your generated password(s)' 21 | ) 22 | .describe('k', 'Do not copy the password to the clipboard') 23 | .describe('version', 'Display current version') 24 | .help('h') 25 | .boolean('a') 26 | .boolean('A') 27 | .boolean('n') 28 | .boolean('s') 29 | .boolean('x') 30 | .boolean('v') 31 | .default('v', false) 32 | .boolean('k') 33 | .default('k', false) 34 | .boolean('version') 35 | .default('c', 1) 36 | .alias('h', 'help') 37 | .alias('p', 'parts') 38 | .alias('l', 'length') 39 | .alias('d', 'delimiter') 40 | .alias('a', 'ascii') 41 | .alias('A', 'ASCII') 42 | .alias('n', 'numbers') 43 | .alias('s', 'special') 44 | .alias('x', 'latin1') 45 | .alias('c', 'count') 46 | .alias('v', 'verbose').argv; 47 | 48 | if (argv.version) { 49 | console.log(`ts-pwgen version ${require('../package.json').version}`); 50 | process.exit(0); 51 | } 52 | 53 | const pwgen = new PasswordGenerator(); 54 | 55 | if (argv.parts) { 56 | pwgen.options.parts.amount = argv.p || argv.parts; 57 | } 58 | 59 | if (argv.length) { 60 | pwgen.options.parts.length = argv.l || argv.length; 61 | } 62 | 63 | if (argv.delimiter) { 64 | pwgen.options.parts.delimiter = argv.d || argv.delimiter; 65 | } 66 | 67 | pwgen.options.lowercaseLetters = argv.ascii; 68 | pwgen.options.uppercaseLetters = argv.ASCII; 69 | pwgen.options.numbers = argv.numbers; 70 | pwgen.options.specialCharacters = argv.special; 71 | pwgen.options.latin1Characters = argv.latin1; 72 | 73 | if ( 74 | !argv.ascii && 75 | !argv.ASCII && 76 | !argv.numbers && 77 | !argv.special && 78 | !argv.latin1 79 | ) { 80 | pwgen.options.lowercaseLetters = true; 81 | pwgen.options.uppercaseLetters = true; 82 | pwgen.options.numbers = true; 83 | pwgen.options.specialCharacters = true; 84 | pwgen.options.latin1Characters = false; 85 | } 86 | 87 | pwgen.interactive(argv.count, argv.verbose, argv.k); 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts-pwgen 2 | 3 | Command-Line Password Generator in TypeScript 4 | 5 | [![npm](https://img.shields.io/npm/v/ts-pwgen.svg)](https://www.npmjs.com/package/ts-pwgen) [![npm](https://img.shields.io/npm/dt/ts-pwgen.svg)](https://www.npmjs.com/package/ts-pwgen) 6 | 7 | ## Installation 8 | 9 | ``` 10 | npm install -g ts-pwgen 11 | ``` 12 | 13 | ## Good to know 14 | 15 | - It has a default password length of 30 characters 16 | - It uses lowercase/uppercase letters, numbers and special characters when you don't pass any arguments saying otherwise 17 | - It uses [`crypto.randomBytes()`](https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback) instead of `Math.random()`. 18 | - You should try the `--verbose` flag to see how long it would take a supercomputer (10^12 pw/sec) to crack your generated password 19 | - Use `-k` if you don't like the copy-to-clipboard feature 20 | 21 | ## Demo 22 | 23 | ![demo video](pwgendemo.gif) 24 | 25 | ## Usage 26 | 27 | ``` 28 | pwgen --help 29 | ``` 30 | 31 | ``` 32 | Usage: pwgen [--length 10] [--parts 3] [--delimiter "-"] [--count 3] [-aAns] 33 | 34 | Options: 35 | -p, --parts Define how many parts there should be 36 | -l, --length Define the length of a part 37 | -d, --delimiter Define the delimiter to use if there are multiple parts 38 | -c, --count Define how many passwords to generate [Standard: 1] 39 | -a, --ascii Use lowercase letters [boolean] 40 | -A, --ASCII Use uppercase letters [boolean] 41 | -n, --numbers Use numbers [boolean] 42 | -s, --special Use special characters [boolean] 43 | -x, --latin1 Use latin1 characters [boolean] 44 | -v, --verbose Verbose output, see how long it would take a supercomputer to 45 | crack your generated password(s) [boolean] [Standard: false] 46 | -k Do not copy the password to the clipboard 47 | [boolean] [Standard: false] 48 | --version Display current version [boolean] 49 | -h, --help Show help [boolean] 50 | ``` 51 | 52 | ## Examples 53 | 54 | ### Custom length 55 | 56 | `pwgen -l 16` or `pwgen --length 16` or `pwgen --length=16` 57 | 58 | ``` 59 | -TI)!9~GmQm~a=jj 60 | ``` 61 | 62 | ### Parts with delimiter 63 | 64 | `pwgen --parts=3 --length=5 --delimiter="-"` 65 | 66 | ``` 67 | 1TX)C-rivp<-MWvZ5 68 | ``` 69 | 70 | #### ProTip: Use parts for WiFi-friendly passwords 71 | 72 | `pwgen -p 4 -l 5 -an` 73 | 74 | ``` 75 | jhnxp-geehp-rtz2n-3m4vt 76 | ``` 77 | 78 | The delimiter defaults to `-`. 79 | 80 | ### See how long it would take a supercomputer to crack your password 81 | 82 | `pwgen -l 16 -v` 83 | 84 | ``` 85 | Password length: 16 86 | Different characters: 91 87 | Possible combinations: 2.211374397284394e+31 88 | 89 | Required time to crack (10^12 passwords/s) 90 | Seconds: 11056871986421970000 91 | Years: 350566645098.98 92 | Age of the universe: 25.71 93 | 94 | a*aO)~}xv|4s+Z4* 95 | 96 | Password successfully copied to clipboard! 97 | ``` 98 | 99 | ### Create multiple passwords 100 | 101 | `pwgen -c 5` 102 | 103 | ``` 104 | @T:6Z8}5G"dIENbab^qvh;^}##LY{F 105 | 3Qy7@`&Ujh;aQL0dMz%@M(IfswdLfa 106 | Sg5O+@L<:Ni1E>k{,XvS|{Y|^W| 107 | ZKf/]e-unOP8YWPv4W@eYe*yL6{lk} 108 | N^)@rFD+1F-G!v%UZcxeV)FzIMfn]] 109 | ``` 110 | 111 | ### Use lowercase letters only 112 | 113 | `pwgen -a` 114 | 115 | ``` 116 | jpgthoyjmwumnoiroqynbhywoxhjnb 117 | ``` 118 | 119 | ### Use uppercase letters only 120 | 121 | `pwgen -A` 122 | 123 | ``` 124 | AUAYYZJXWMIKMPJGHVQSPKUGPUFTCU 125 | ``` 126 | 127 | ### Use numbers only 128 | 129 | `pwgen -n` 130 | 131 | ``` 132 | 647681199479680747570268980919 133 | ``` 134 | 135 | ### Use special characters only 136 | 137 | `pwgen -s` 138 | 139 | ``` 140 | =<}(<>}/,_.*@:|<*++"=)^;^:|)_+ 141 | ``` 142 | 143 | ### Use latin1 characters only 144 | 145 | `pwgen -x` 146 | 147 | ``` 148 | »°ÃÑîÍí¢ÆñÓò¾ÅÚïñ°ÕÔÏúñ¼Ï´¦îÓê 149 | ``` 150 | 151 | ### Use letters and numbers (combine charsets) 152 | 153 | `pwgen -aAn` 154 | 155 | ``` 156 | APlngye5IiXTu0z7NETffgS67bOX48 157 | ``` 158 | 159 | ## Development 160 | 161 | ### Unit tests 162 | 163 | ``` 164 | npm test 165 | ``` 166 | 167 | ### Build from TypeScript source 168 | 169 | ``` 170 | npm run build 171 | ``` 172 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true /* Generates corresponding '.d.ts' file. */, 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./lib/dest" /* Redirect output structure to the directory. */, 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/password_generator.ts: -------------------------------------------------------------------------------- 1 | import 'colors'; 2 | import * as inquirer from 'inquirer'; 3 | import * as copyPaste from 'copy-paste'; 4 | import * as crypto from 'crypto'; 5 | import { 6 | PasswordGeneratorOptions, 7 | GeneratedPassword, 8 | PasswordAnswer, 9 | } from './password_generator.interface'; 10 | import { 11 | latin1List, 12 | lowercaseLettersList, 13 | numbersList, 14 | specialCharactersList, 15 | uppercaseLettersList, 16 | } from './charsets'; 17 | 18 | const defaultOptions: PasswordGeneratorOptions = { 19 | lowercaseLetters: true, 20 | uppercaseLetters: true, 21 | numbers: true, 22 | specialCharacters: true, 23 | latin1Characters: false, 24 | parts: { 25 | amount: 1, 26 | length: 30, 27 | delimiter: '-', 28 | }, 29 | }; 30 | 31 | export class PasswordGenerator { 32 | constructor(public options: PasswordGeneratorOptions = defaultOptions) {} 33 | 34 | /** 35 | * Checks if a password string contains at least one character from a string 36 | * array. 37 | */ 38 | private containsFromCharset(password: string, charset: string[]): boolean { 39 | for (let char of charset) { 40 | if (password.indexOf(char) !== -1) { 41 | return true; 42 | } 43 | } 44 | return false; 45 | } 46 | 47 | /** 48 | * Count how many charsets are being used in the current configuration. 49 | */ 50 | countActiveCharsets(): number { 51 | return [ 52 | this.options.lowercaseLetters, 53 | this.options.uppercaseLetters, 54 | this.options.numbers, 55 | this.options.specialCharacters, 56 | this.options.latin1Characters, 57 | ].reduce((prev, curr) => (prev += +curr), 0); 58 | } 59 | 60 | /** 61 | * Returns the password length, taking all parts and potential delimiters into 62 | * account. 63 | */ 64 | get passwordLength() { 65 | let { amount, length, delimiter } = this.options.parts; 66 | return amount * length + (amount - 1) * delimiter.length; 67 | } 68 | 69 | /** 70 | * Generate a cryptographically more secure random number than Math.random(). 71 | */ 72 | random(): number { 73 | let hexRepresentation = '3ff' + crypto.randomBytes(6).toString('hex') + '0'; 74 | return Buffer.from(hexRepresentation, 'hex').readDoubleBE(0) - 1; 75 | } 76 | 77 | /** 78 | * Generates a password based on this.options. This method will recursively 79 | * call itself if the password does not contain at least one character from 80 | * each specified charset. 81 | */ 82 | generate(): GeneratedPassword { 83 | let list: string[] = []; // This will hold all the characters that are going to be used 84 | let password: string = ''; 85 | 86 | if (this.passwordLength < this.countActiveCharsets()) { 87 | throw new Error( 88 | 'Cannot generate a password with the current configuration', 89 | ); 90 | } 91 | 92 | if (this.options.lowercaseLetters) { 93 | list = list.concat(lowercaseLettersList); 94 | } 95 | if (this.options.uppercaseLetters) { 96 | list = list.concat(uppercaseLettersList); 97 | } 98 | if (this.options.numbers) { 99 | list = list.concat(numbersList); 100 | } 101 | if (this.options.specialCharacters) { 102 | list = list.concat(specialCharactersList); 103 | } 104 | if (this.options.latin1Characters) { 105 | list = list.concat(latin1List); 106 | } 107 | 108 | // If the parts have a length of 0 or below, abort. 109 | if (this.options.parts.length <= 0) { 110 | return { 111 | value: '', 112 | charsetLength: list.length, 113 | }; 114 | } 115 | 116 | let { amount, length, delimiter } = this.options.parts; 117 | for (let partIndex = 0; partIndex < amount; partIndex++) { 118 | let part = ''; 119 | 120 | while (part.length < length) { 121 | let randomIndex = Math.floor(this.random() * list.length); 122 | part += list[randomIndex]; 123 | } 124 | 125 | // If this is not the last part, add the delimiter. 126 | if (partIndex !== amount - 1) { 127 | part += delimiter; 128 | } 129 | 130 | password += part; 131 | } 132 | 133 | // Make sure that at least one character from each used charset is present, 134 | // otherwise call this method again. 135 | if ( 136 | (this.options.lowercaseLetters && !/[a-z]/.test(password)) || 137 | (this.options.uppercaseLetters && !/[A-Z]/.test(password)) || 138 | (this.options.numbers && !/[0-9]/.test(password)) || 139 | (this.options.specialCharacters && 140 | !this.containsFromCharset(password, specialCharactersList)) || 141 | (this.options.latin1Characters && 142 | !this.containsFromCharset(password, latin1List)) 143 | ) { 144 | return this.generate(); 145 | } 146 | 147 | return { 148 | value: password, 149 | charsetLength: list.length, 150 | }; 151 | } 152 | 153 | /** 154 | * Generates any positive amount of passwords using this.generate(). 155 | */ 156 | generateMultiple(amount: number): GeneratedPassword[] { 157 | let passwords: GeneratedPassword[] = []; 158 | for (let i = 0; i < amount; i++) { 159 | passwords.push(this.generate()); 160 | } 161 | return passwords; 162 | } 163 | 164 | /** 165 | * Interactively ask the user which password they would like if they ask for 166 | * more than 1. Also copies it to the clipboard. 167 | */ 168 | async interactive( 169 | amount: number, 170 | verbose: boolean = false, 171 | noClipboard: boolean = false, 172 | ) { 173 | let passwords: GeneratedPassword[]; 174 | let chosenPassword: string = ''; 175 | 176 | try { 177 | passwords = this.generateMultiple(amount); 178 | } catch (error) { 179 | return console.log(error.message); 180 | } 181 | 182 | if (amount <= 0) { 183 | return; 184 | } 185 | 186 | if (verbose) 187 | this.logInformation(passwords[0].value, passwords[0].charsetLength); 188 | 189 | if (amount === 1) { 190 | console.log(passwords[0].value); 191 | chosenPassword = passwords[0].value; 192 | } else if (noClipboard) { 193 | passwords.forEach((pw) => console.log(pw.value)); 194 | } else { 195 | let answer = await inquirer.prompt([ 196 | { 197 | type: 'list', 198 | name: 'password', 199 | message: 'Choose password:', 200 | choices: passwords.map((pw) => pw.value), 201 | }, 202 | ]); 203 | chosenPassword = answer.password; 204 | } 205 | 206 | if (!noClipboard) { 207 | copyPaste.copy(chosenPassword, () => { 208 | console.log('\nPassword successfully copied to clipboard!'.gray); 209 | process.exit(0); 210 | }); 211 | } else { 212 | process.exit(0); 213 | } 214 | } 215 | 216 | /** 217 | * Log information about the security of the current password options. 218 | */ 219 | private logInformation(password: string, charsetLength: number): void { 220 | const round = (input: number) => Math.round(input * 100) / 100; 221 | const ageOfUniverse = 4.3 * 10 ** 17; 222 | const secondsInYear = 31540000; 223 | const combinations = charsetLength ** password.length; 224 | const secs = round(combinations / (2 * 10 ** 12)); 225 | 226 | console.log(`Password length: `.gray + password.length); 227 | console.log(`Different characters: `.gray + charsetLength); 228 | console.log(`Possible combinations: `.gray + combinations); 229 | console.log(`\nRequired time to crack (10^12 passwords/s)`.gray.underline); 230 | console.log(` Seconds: `.gray + secs); 231 | console.log(` Years: `.gray + round(secs / secondsInYear)); 232 | console.log( 233 | ` Age of the universe: `.gray + round(secs / ageOfUniverse), 234 | '\n', 235 | ); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /lib/src/password_generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { PasswordGenerator } from './password_generator'; 2 | 3 | describe('PasswordGenerator', () => { 4 | describe('generate()', () => { 5 | it('should throw an error when asking for a password of length 0', () => { 6 | let pwgen = new PasswordGenerator(); 7 | pwgen.options.parts.length = 0; 8 | expect(() => { 9 | pwgen.generate(); 10 | }).toThrow(); 11 | }); 12 | 13 | it('should be able to generate a password of length 10', () => { 14 | let pwgen = new PasswordGenerator(); 15 | pwgen.options.parts.length = 10; 16 | expect(pwgen.generate().value).toHaveLength(10); 17 | }); 18 | 19 | it('should be able to generate a password of length 20', () => { 20 | let pwgen = new PasswordGenerator(); 21 | pwgen.options.parts.length = 20; 22 | expect(pwgen.generate().value).toHaveLength(20); 23 | }); 24 | 25 | it('should be able to generate a password of length 100', () => { 26 | let pwgen = new PasswordGenerator(); 27 | pwgen.options.parts.length = 100; 28 | expect(pwgen.generate().value).toHaveLength(100); 29 | }); 30 | 31 | it('should be able to generate 2 parts of 5 with a dash delimiter', () => { 32 | let pwgen = new PasswordGenerator(); 33 | pwgen.options.specialCharacters = false; 34 | pwgen.options.parts = { 35 | amount: 2, 36 | length: 5, 37 | delimiter: '-', 38 | }; 39 | let expectRegex = /[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}/; 40 | expect(expectRegex.test(pwgen.generate().value)).toBe(true); 41 | }); 42 | 43 | it('should be able to generate 3 parts of 6 with a dash delimiter', () => { 44 | let pwgen = new PasswordGenerator(); 45 | pwgen.options.specialCharacters = false; 46 | pwgen.options.parts = { 47 | amount: 3, 48 | length: 6, 49 | delimiter: '-', 50 | }; 51 | let expectRegex = /[a-zA-Z0-9]{6}-[a-zA-Z0-9]{6}-[a-zA-Z0-9]{6}/; 52 | expect(expectRegex.test(pwgen.generate().value)).toBe(true); 53 | }); 54 | 55 | it('should be able to generate 3 parts of 10 with a ! delimiter', () => { 56 | let pwgen = new PasswordGenerator(); 57 | pwgen.options.specialCharacters = false; 58 | pwgen.options.parts = { 59 | amount: 3, 60 | length: 10, 61 | delimiter: '!', 62 | }; 63 | let expectRegex = /[a-zA-Z0-9]{10}![a-zA-Z0-9]{10}![a-zA-Z0-9]{10}/; 64 | expect(expectRegex.test(pwgen.generate().value)).toBe(true); 65 | }); 66 | 67 | it('should include at least one letter from each specified charset', () => { 68 | let pwgen = new PasswordGenerator(); 69 | pwgen.options = { 70 | lowercaseLetters: true, 71 | uppercaseLetters: true, 72 | numbers: true, 73 | specialCharacters: false, 74 | latin1Characters: false, 75 | parts: { 76 | amount: 1, 77 | length: 3, 78 | delimiter: '-', 79 | }, 80 | }; 81 | 82 | for (let i = 0; i < 1000; i++) { 83 | let password = pwgen.generate().value; 84 | expect(/[a-z]/.test(password)).toBe(true); 85 | expect(/[A-Z]/.test(password)).toBe(true); 86 | expect(/[0-9]/.test(password)).toBe(true); 87 | } 88 | }); 89 | 90 | it('should throw an error if the length is smaller than the amount of charsets', () => { 91 | let pwgen = new PasswordGenerator({ 92 | lowercaseLetters: true, 93 | uppercaseLetters: true, 94 | numbers: true, 95 | specialCharacters: true, 96 | latin1Characters: true, 97 | parts: { amount: 1, length: 3, delimiter: '-' }, 98 | }); 99 | expect(() => { 100 | pwgen.generate(); 101 | }).toThrow(); 102 | }); 103 | 104 | it('should not throw an error if the length is smaller than the amount of charsets but there are enough parts', () => { 105 | // 2 parts with length 2 and a 1 char delimiter equals in a total length 106 | // of 5, so it should be fine. 107 | let pwgen = new PasswordGenerator({ 108 | lowercaseLetters: true, 109 | uppercaseLetters: true, 110 | numbers: true, 111 | specialCharacters: true, 112 | latin1Characters: true, 113 | parts: { amount: 2, length: 2, delimiter: '-' }, 114 | }); 115 | expect(() => { 116 | pwgen.generate(); 117 | }).not.toThrow(); 118 | }); 119 | }); 120 | 121 | describe('generateMultiple()', () => { 122 | it('should be able to generate 1 password', () => { 123 | let pwgen = new PasswordGenerator(); 124 | let output = pwgen.generateMultiple(1); 125 | expect(output).toHaveLength(1); 126 | }); 127 | 128 | it('should be able to generate 3 passwords', () => { 129 | let pwgen = new PasswordGenerator(); 130 | let output = pwgen.generateMultiple(3); 131 | expect(output).toHaveLength(3); 132 | }); 133 | 134 | it('should be able to generate 20 passwords', () => { 135 | let pwgen = new PasswordGenerator(); 136 | let output = pwgen.generateMultiple(20); 137 | expect(output).toHaveLength(20); 138 | }); 139 | }); 140 | 141 | describe('countActiveCharsets()', () => { 142 | it('should return 0 because no charsets are active', () => { 143 | let pwgen = new PasswordGenerator({ 144 | lowercaseLetters: false, 145 | uppercaseLetters: false, 146 | numbers: false, 147 | specialCharacters: false, 148 | latin1Characters: false, 149 | parts: { amount: 1, length: 30, delimiter: '-' }, 150 | }); 151 | expect(pwgen.countActiveCharsets()).toBe(0); 152 | }); 153 | 154 | it('should return 1 because 1 charset is active', () => { 155 | let pwgen = new PasswordGenerator({ 156 | lowercaseLetters: true, 157 | uppercaseLetters: false, 158 | numbers: false, 159 | specialCharacters: false, 160 | latin1Characters: false, 161 | parts: { amount: 1, length: 30, delimiter: '-' }, 162 | }); 163 | expect(pwgen.countActiveCharsets()).toBe(1); 164 | }); 165 | 166 | it('should return 5 because 5 charsets are active', () => { 167 | let pwgen = new PasswordGenerator({ 168 | lowercaseLetters: true, 169 | uppercaseLetters: true, 170 | numbers: true, 171 | specialCharacters: true, 172 | latin1Characters: true, 173 | parts: { amount: 1, length: 30, delimiter: '-' }, 174 | }); 175 | expect(pwgen.countActiveCharsets()).toBe(5); 176 | }); 177 | }); 178 | 179 | describe('#random()', () => { 180 | it('should average at about 0.5', () => { 181 | let pwgen = new PasswordGenerator(); 182 | 183 | let all = []; 184 | for (let i = 0; i < 10000; i++) { 185 | all.push(pwgen.random()); 186 | } 187 | let average = all.reduce((prev, curr) => (curr += prev), 0) / all.length; 188 | expect(average).toBeCloseTo(0.5, 0.01); 189 | }); 190 | }); 191 | 192 | describe('#passwordLength()', () => { 193 | it('should return the password length for 1 part passwords', () => { 194 | let pwgen = new PasswordGenerator({ 195 | lowercaseLetters: true, 196 | uppercaseLetters: true, 197 | numbers: true, 198 | specialCharacters: true, 199 | latin1Characters: true, 200 | parts: { amount: 1, length: 30, delimiter: '-' }, 201 | }); 202 | 203 | expect(pwgen.passwordLength).toBe(30); 204 | }); 205 | 206 | it('should return the password length for 2 part passwords', () => { 207 | let pwgen = new PasswordGenerator({ 208 | lowercaseLetters: true, 209 | uppercaseLetters: true, 210 | numbers: true, 211 | specialCharacters: true, 212 | latin1Characters: true, 213 | parts: { amount: 2, length: 10, delimiter: '-' }, 214 | }); 215 | 216 | expect(pwgen.passwordLength).toBe(21); // 10 chars + delimiter + 10 chars 217 | }); 218 | 219 | it('should return the password length for 3 part passwords', () => { 220 | let pwgen = new PasswordGenerator({ 221 | lowercaseLetters: true, 222 | uppercaseLetters: true, 223 | numbers: true, 224 | specialCharacters: true, 225 | latin1Characters: true, 226 | parts: { amount: 3, length: 5, delimiter: '-' }, 227 | }); 228 | 229 | expect(pwgen.passwordLength).toBe(17); 230 | }); 231 | 232 | it('should return the password length for delimiters > 1 char', () => { 233 | let pwgen = new PasswordGenerator({ 234 | lowercaseLetters: true, 235 | uppercaseLetters: true, 236 | numbers: true, 237 | specialCharacters: true, 238 | latin1Characters: true, 239 | parts: { amount: 2, length: 25, delimiter: '---' }, 240 | }); 241 | 242 | expect(pwgen.passwordLength).toBe(53); 243 | }); 244 | 245 | it('should return the password length for delimiters > 1 char', () => { 246 | let pwgen = new PasswordGenerator({ 247 | lowercaseLetters: true, 248 | uppercaseLetters: true, 249 | numbers: true, 250 | specialCharacters: true, 251 | latin1Characters: true, 252 | parts: { amount: 3, length: 10, delimiter: '---' }, 253 | }); 254 | 255 | expect(pwgen.passwordLength).toBe(36); 256 | }); 257 | }); 258 | }); 259 | --------------------------------------------------------------------------------