mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-20 06:55:06 -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
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…
Add table
Add a link
Reference in a new issue