mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-24 00:36:14 -04:00
feat(new-tool): password strength analyzer (#502)
This commit is contained in:
parent
6bda2caa04
commit
a9c7b89193
7 changed files with 223 additions and 1 deletions
|
@ -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',
|
||||
|
|
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…
Add table
Add a link
Reference in a new issue