Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jun 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
3 changes: 2 additions & 1 deletion src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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',
Expand Down
12 changes: 12 additions & 0 deletions src/tools/password-strength-analyser/index.ts
Original file line number Diff line number Diff line change
@@ -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'),
});
Original file line number Diff line number Diff line change
@@ -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');
});
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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>