feat(new-tool): password strength analyzer (#502)
This commit is contained in:
		
							parent
							
								
									6bda2caa04
								
							
						
					
					
						commit
						a9c7b89193
					
				
							
								
								
									
										1
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -140,6 +140,7 @@ declare module '@vue/runtime-core' { | |||||||
|     NUpload: typeof import('naive-ui')['NUpload'] |     NUpload: typeof import('naive-ui')['NUpload'] | ||||||
|     NUploadDragger: typeof import('naive-ui')['NUploadDragger'] |     NUploadDragger: typeof import('naive-ui')['NUploadDragger'] | ||||||
|     OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] |     OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] | ||||||
|  |     PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] | ||||||
|     PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default'] |     PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default'] | ||||||
|     PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default'] |     PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default'] | ||||||
|     QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default'] |     QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default'] | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| import { tool as base64FileConverter } from './base64-file-converter'; | import { tool as base64FileConverter } from './base64-file-converter'; | ||||||
| import { tool as base64StringConverter } from './base64-string-converter'; | import { tool as base64StringConverter } from './base64-string-converter'; | ||||||
| import { tool as basicAuthGenerator } from './basic-auth-generator'; | import { tool as basicAuthGenerator } from './basic-auth-generator'; | ||||||
|  | import { tool as passwordStrengthAnalyser } from './password-strength-analyser'; | ||||||
| import { tool as yamlToToml } from './yaml-to-toml'; | import { tool as yamlToToml } from './yaml-to-toml'; | ||||||
| import { tool as jsonToToml } from './json-to-toml'; | import { tool as jsonToToml } from './json-to-toml'; | ||||||
| import { tool as tomlToYaml } from './toml-to-yaml'; | import { tool as tomlToYaml } from './toml-to-yaml'; | ||||||
| @ -68,7 +69,7 @@ import { tool as xmlFormatter } from './xml-formatter'; | |||||||
| export const toolsByCategory: ToolCategory[] = [ | export const toolsByCategory: ToolCategory[] = [ | ||||||
|   { |   { | ||||||
|     name: 'Crypto', |     name: 'Crypto', | ||||||
|     components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator], |     components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Converter', |     name: 'Converter', | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								src/tools/password-strength-analyser/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/password-strength-analyser/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | import { defineTool } from '../tool'; | ||||||
|  | import PasswordIcon from '~icons/mdi/form-textbox-password'; | ||||||
|  | 
 | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'Password strength analyser', | ||||||
|  |   path: '/password-strength-analyser', | ||||||
|  |   description: 'Discover the strength of your password with this client side only password strength analyser and crack time estimation tool.', | ||||||
|  |   keywords: ['password', 'strength', 'analyser', 'and', 'crack', 'time', 'estimation', 'brute', 'force', 'attack', 'entropy', 'cracking', 'hash', 'hashing', 'algorithm', 'algorithms', 'md5', 'sha1', 'sha256', 'sha512', 'bcrypt', 'scrypt', 'argon2', 'argon2id', 'argon2i', 'argon2d'], | ||||||
|  |   component: () => import('./password-strength-analyser.vue'), | ||||||
|  |   icon: PasswordIcon, | ||||||
|  |   createdAt: new Date('2023-06-24'), | ||||||
|  | }); | ||||||
| @ -0,0 +1,19 @@ | |||||||
|  | import { expect, test } from '@playwright/test'; | ||||||
|  | 
 | ||||||
|  | test.describe('Tool - Password strength analyser', () => { | ||||||
|  |   test.beforeEach(async ({ page }) => { | ||||||
|  |     await page.goto('/password-strength-analyser'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   test('Has correct title', async ({ page }) => { | ||||||
|  |     await expect(page).toHaveTitle('Password strength analyser - IT Tools'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   test('Computes the brute force attack time of a password', async ({ page }) => { | ||||||
|  |     await page.getByTestId('password-input').fill('ABCabc123!@#'); | ||||||
|  | 
 | ||||||
|  |     const crackDuration = await page.getByTestId('crack-duration').textContent(); | ||||||
|  | 
 | ||||||
|  |     expect(crackDuration).toEqual('15,091 milleniums, 3 centurys'); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -0,0 +1,31 @@ | |||||||
|  | import { describe, expect, it } from 'vitest'; | ||||||
|  | import { getCharsetLength } from './password-strength-analyser.service'; | ||||||
|  | 
 | ||||||
|  | describe('password-strength-analyser-and-crack-time-estimation', () => { | ||||||
|  |   describe('getCharsetLength', () => { | ||||||
|  |     describe('computes the charset length of a given password', () => { | ||||||
|  |       it('the charset length is 26 when the password is only lowercase characters', () => { | ||||||
|  |         expect(getCharsetLength({ password: 'abcdefghijklmnopqrstuvwxyz' })).toBe(26); | ||||||
|  |       }); | ||||||
|  |       it('the charset length is 26 when the password is only uppercase characters', () => { | ||||||
|  |         expect(getCharsetLength({ password: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' })).toBe(26); | ||||||
|  |       }); | ||||||
|  |       it('the charset length is 10 when the password is only digits', () => { | ||||||
|  |         expect(getCharsetLength({ password: '0123456789' })).toBe(10); | ||||||
|  |       }); | ||||||
|  |       it('the charset length is 32 when the password is only special characters', () => { | ||||||
|  |         expect(getCharsetLength({ password: '-_(' })).toBe(32); | ||||||
|  |       }); | ||||||
|  |       it('the charset length is 0 when the password is empty', () => { | ||||||
|  |         expect(getCharsetLength({ password: '' })).toBe(0); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('the charset length is 36 when the password is lowercase characters and digits', () => { | ||||||
|  |         expect(getCharsetLength({ password: 'abcdefghijklmnopqrstuvwxyz0123456789' })).toBe(36); | ||||||
|  |       }); | ||||||
|  |       it('the charset length is 95 when the password is lowercase characters, uppercase characters, digits and special characters', () => { | ||||||
|  |         expect(getCharsetLength({ password: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_(' })).toBe(94); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -0,0 +1,96 @@ | |||||||
|  | import _ from 'lodash'; | ||||||
|  | 
 | ||||||
|  | export { getPasswordCrackTimeEstimation, getCharsetLength }; | ||||||
|  | 
 | ||||||
|  | function prettifyExponentialNotation(exponentialNotation: number) { | ||||||
|  |   const [base, exponent] = exponentialNotation.toString().split('e'); | ||||||
|  |   const baseAsNumber = parseFloat(base); | ||||||
|  |   const prettyBase = baseAsNumber % 1 === 0 ? baseAsNumber.toLocaleString() : baseAsNumber.toFixed(2); | ||||||
|  |   return exponent ? `${prettyBase}e${exponent}` : prettyBase; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getHumanFriendlyDuration({ seconds }: { seconds: number }) { | ||||||
|  |   if (seconds <= 0.001) { | ||||||
|  |     return 'Instantly'; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (seconds <= 1) { | ||||||
|  |     return 'Less than a second'; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const timeUnits = [ | ||||||
|  |     { unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation }, | ||||||
|  |     { unit: 'century', secondsInUnit: 3153600000 }, | ||||||
|  |     { unit: 'decade', secondsInUnit: 315360000 }, | ||||||
|  |     { unit: 'year', secondsInUnit: 31536000 }, | ||||||
|  |     { unit: 'month', secondsInUnit: 2592000 }, | ||||||
|  |     { unit: 'week', secondsInUnit: 604800 }, | ||||||
|  |     { unit: 'day', secondsInUnit: 86400 }, | ||||||
|  |     { unit: 'hour', secondsInUnit: 3600 }, | ||||||
|  |     { unit: 'minute', secondsInUnit: 60 }, | ||||||
|  |     { unit: 'second', secondsInUnit: 1 }, | ||||||
|  |   ]; | ||||||
|  | 
 | ||||||
|  |   return _.chain(timeUnits) | ||||||
|  |     .map(({ unit, secondsInUnit, format = _.identity }) => { | ||||||
|  |       const quantity = Math.floor(seconds / secondsInUnit); | ||||||
|  |       seconds %= secondsInUnit; | ||||||
|  | 
 | ||||||
|  |       if (quantity <= 0) { | ||||||
|  |         return undefined; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const formattedQuantity = format(quantity); | ||||||
|  |       return `${formattedQuantity} ${unit}${quantity > 1 ? 's' : ''}`; | ||||||
|  |     }) | ||||||
|  |     .compact() | ||||||
|  |     .take(2) | ||||||
|  |     .join(', ') | ||||||
|  |     .value(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getPasswordCrackTimeEstimation({ password, guessesPerSecond = 1e9 }: { password: string; guessesPerSecond?: number }) { | ||||||
|  |   const charsetLength = getCharsetLength({ password }); | ||||||
|  |   const passwordLength = password.length; | ||||||
|  | 
 | ||||||
|  |   const entropy = password === '' ? 0 : Math.log2(charsetLength) * passwordLength; | ||||||
|  | 
 | ||||||
|  |   const secondsToCrack = 2 ** entropy / guessesPerSecond; | ||||||
|  | 
 | ||||||
|  |   const crackDurationFormatted = getHumanFriendlyDuration({ seconds: secondsToCrack }); | ||||||
|  | 
 | ||||||
|  |   const score = Math.min(entropy / 128, 1); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     entropy, | ||||||
|  |     charsetLength, | ||||||
|  |     passwordLength, | ||||||
|  |     crackDurationFormatted, | ||||||
|  |     secondsToCrack, | ||||||
|  |     score, | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getCharsetLength({ password }: { password: string }) { | ||||||
|  |   const hasLowercase = /[a-z]/.test(password); | ||||||
|  |   const hasUppercase = /[A-Z]/.test(password); | ||||||
|  |   const hasDigits = /\d/.test(password); | ||||||
|  |   const hasSpecialChars = /\W|_/.test(password); | ||||||
|  | 
 | ||||||
|  |   let charsetLength = 0; | ||||||
|  | 
 | ||||||
|  |   if (hasLowercase) { | ||||||
|  |     charsetLength += 26; | ||||||
|  |   } | ||||||
|  |   if (hasUppercase) { | ||||||
|  |     charsetLength += 26; | ||||||
|  |   } | ||||||
|  |   if (hasDigits) { | ||||||
|  |     charsetLength += 10; | ||||||
|  |   } | ||||||
|  |   if (hasSpecialChars) { | ||||||
|  |     charsetLength += 32; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return charsetLength; | ||||||
|  | } | ||||||
| @ -0,0 +1,62 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { getPasswordCrackTimeEstimation } from './password-strength-analyser.service'; | ||||||
|  | 
 | ||||||
|  | const password = ref(''); | ||||||
|  | const crackTimeEstimation = computed(() => getPasswordCrackTimeEstimation({ password: password.value })); | ||||||
|  | 
 | ||||||
|  | const details = computed(() => [ | ||||||
|  |   { | ||||||
|  |     label: 'Password length:', | ||||||
|  |     value: crackTimeEstimation.value.passwordLength, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Entropy:', | ||||||
|  |     value: Math.round(crackTimeEstimation.value.entropy * 100) / 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Character set size:', | ||||||
|  |     value: crackTimeEstimation.value.charsetLength, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Score:', | ||||||
|  |     value: `${Math.round(crackTimeEstimation.value.score * 100)} / 100`, | ||||||
|  |   }, | ||||||
|  | ]); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div flex flex-col gap-3> | ||||||
|  |     <c-input-text | ||||||
|  |       v-model:value="password" | ||||||
|  |       type="password" | ||||||
|  |       placeholder="Enter a password..." | ||||||
|  |       clearable | ||||||
|  |       autofocus | ||||||
|  |       raw-text | ||||||
|  |       test-id="password-input" | ||||||
|  |     /> | ||||||
|  | 
 | ||||||
|  |     <c-card text-center> | ||||||
|  |       <div op-60> | ||||||
|  |         Duration to crack this password with brute force | ||||||
|  |       </div> | ||||||
|  |       <div text-2xl data-test-id="crack-duration"> | ||||||
|  |         {{ crackTimeEstimation.crackDurationFormatted }} | ||||||
|  |       </div> | ||||||
|  |     </c-card> | ||||||
|  |     <c-card> | ||||||
|  |       <div v-for="({ label, value }) of details" :key="label" flex gap-3> | ||||||
|  |         <div flex-1 text-right op-60> | ||||||
|  |           {{ label }} | ||||||
|  |         </div> | ||||||
|  |         <div flex-1 text-left> | ||||||
|  |           {{ value }} | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </c-card> | ||||||
|  |     <div op-70> | ||||||
|  |       <span font-bold>Note: </span> | ||||||
|  |       The computed strength is based on the time it would take to crack the password using a brute force approach, it does not take into account the possibility of a dictionary attack. | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user