feat(new-tool): password strength analyzer (#502)

This commit is contained in:
Corentin THOMASSET 2023-06-25 10:26:29 +02:00 committed by GitHub
parent 6bda2caa04
commit a9c7b89193
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 223 additions and 1 deletions

1
components.d.ts vendored
View file

@ -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']

View file

@ -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',

View 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'),
});

View file

@ -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');
});
});

View file

@ -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);
});
});
});
});

View file

@ -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;
}

View file

@ -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>