aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--components.d.ts1
-rw-r--r--src/tools/index.ts3
-rw-r--r--src/tools/password-strength-analyser/index.ts12
-rw-r--r--src/tools/password-strength-analyser/password-strength-analyser.e2e.spec.ts19
-rw-r--r--src/tools/password-strength-analyser/password-strength-analyser.service.test.ts31
-rw-r--r--src/tools/password-strength-analyser/password-strength-analyser.service.ts96
-rw-r--r--src/tools/password-strength-analyser/password-strength-analyser.vue62
7 files changed, 223 insertions, 1 deletions
diff --git a/components.d.ts b/components.d.ts
index ae91be5..6dbe552 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 455bfb7..fb05045 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 0000000..7b86bdd
--- /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 0000000..a694c54
--- /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 0000000..cdcecbe
--- /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 0000000..7197b00
--- /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 0000000..7064ad0
--- /dev/null
+++ b/src/tools/password-strength-analyser/password-strength-analyser.vue
@@ -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>