From a9c7b891937ff39b9c5c70a1f4e95d28bc449db5 Mon Sep 17 00:00:00 2001 From: Corentin THOMASSET Date: Sun, 25 Jun 2023 10:26:29 +0200 Subject: [PATCH] feat(new-tool): password strength analyzer (#502) --- components.d.ts | 1 + src/tools/index.ts | 3 +- src/tools/password-strength-analyser/index.ts | 12 +++ .../password-strength-analyser.e2e.spec.ts | 19 ++++ ...password-strength-analyser.service.test.ts | 31 ++++++ .../password-strength-analyser.service.ts | 96 +++++++++++++++++++ .../password-strength-analyser.vue | 62 ++++++++++++ 7 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 src/tools/password-strength-analyser/index.ts create mode 100644 src/tools/password-strength-analyser/password-strength-analyser.e2e.spec.ts create mode 100644 src/tools/password-strength-analyser/password-strength-analyser.service.test.ts create mode 100644 src/tools/password-strength-analyser/password-strength-analyser.service.ts create mode 100644 src/tools/password-strength-analyser/password-strength-analyser.vue diff --git a/components.d.ts b/components.d.ts index ae91be51..6dbe5521 100644 --- a/components.d.ts +++ b/components.d.ts @@ -140,6 +140,7 @@ declare module '@vue/runtime-core' { NUpload: typeof import('naive-ui')['NUpload'] NUploadDragger: typeof import('naive-ui')['NUploadDragger'] 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'] 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'] diff --git a/src/tools/index.ts b/src/tools/index.ts index 455bfb7d..fb05045f 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,6 +1,7 @@ import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64StringConverter } from './base64-string-converter'; 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 jsonToToml } from './json-to-toml'; import { tool as tomlToYaml } from './toml-to-yaml'; @@ -68,7 +69,7 @@ import { tool as xmlFormatter } from './xml-formatter'; export const toolsByCategory: ToolCategory[] = [ { name: 'Crypto', - components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator], + components: [tokenGenerator, hashText, bcrypt, uuidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser], }, { name: 'Converter', diff --git a/src/tools/password-strength-analyser/index.ts b/src/tools/password-strength-analyser/index.ts new file mode 100644 index 00000000..7b86bdd5 --- /dev/null +++ b/src/tools/password-strength-analyser/index.ts @@ -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'), +}); diff --git a/src/tools/password-strength-analyser/password-strength-analyser.e2e.spec.ts b/src/tools/password-strength-analyser/password-strength-analyser.e2e.spec.ts new file mode 100644 index 00000000..a694c547 --- /dev/null +++ b/src/tools/password-strength-analyser/password-strength-analyser.e2e.spec.ts @@ -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'); + }); +}); diff --git a/src/tools/password-strength-analyser/password-strength-analyser.service.test.ts b/src/tools/password-strength-analyser/password-strength-analyser.service.test.ts new file mode 100644 index 00000000..cdcecbe0 --- /dev/null +++ b/src/tools/password-strength-analyser/password-strength-analyser.service.test.ts @@ -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); + }); + }); + }); +}); diff --git a/src/tools/password-strength-analyser/password-strength-analyser.service.ts b/src/tools/password-strength-analyser/password-strength-analyser.service.ts new file mode 100644 index 00000000..7197b001 --- /dev/null +++ b/src/tools/password-strength-analyser/password-strength-analyser.service.ts @@ -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; +} diff --git a/src/tools/password-strength-analyser/password-strength-analyser.vue b/src/tools/password-strength-analyser/password-strength-analyser.vue new file mode 100644 index 00000000..7064ad0c --- /dev/null +++ b/src/tools/password-strength-analyser/password-strength-analyser.vue @@ -0,0 +1,62 @@ + + +