From 3cbb61160b468a7285170d3d8ccc4f13bdcdc2e8 Mon Sep 17 00:00:00 2001 From: Gers2017 Date: Thu, 8 Sep 2022 22:48:54 -0600 Subject: [PATCH] Entropy meter - rust modules and PasswordInfo - Update generation methods and include entropy in UI - Add entropy meter and update css --- package.json | 3 +- pnpm-lock.yaml | 6 -- src-tauri/src/entropy/mod.rs | 24 +++++ src-tauri/src/gen_methods/mod.rs | 64 +++++++++++ src-tauri/src/lib.rs | 6 ++ src-tauri/src/main.rs | 93 ++++------------ src-tauri/src/password_info/mod.rs | 12 +++ src-tauri/tauri.conf.json | 2 +- src/App.vue | 165 +++++++++++++++-------------- src/components/CopyPassword.vue | 49 +++++++-- src/components/EntropyMeter.vue | 60 +++++++++++ src/style.css | 35 +++--- src/tauri.ts | 28 +++-- 13 files changed, 351 insertions(+), 196 deletions(-) create mode 100644 src-tauri/src/entropy/mod.rs create mode 100644 src-tauri/src/gen_methods/mod.rs create mode 100644 src-tauri/src/lib.rs create mode 100644 src-tauri/src/password_info/mod.rs create mode 100644 src/components/EntropyMeter.vue diff --git a/package.json b/package.json index 336b1da..fd8bbb5 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,7 @@ }, "dependencies": { "@tauri-apps/api": "^1.0.2", - "vue": "^3.2.37", - "vue-toasted": "^1.1.28" + "vue": "^3.2.37" }, "devDependencies": { "@tauri-apps/cli": "^1.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72dd22d..2da9239 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,13 +8,11 @@ specifiers: typescript: ^4.6.4 vite: ^3.0.2 vue: ^3.2.37 - vue-toasted: ^1.1.28 vue-tsc: ^0.40.0 dependencies: '@tauri-apps/api': 1.0.2 vue: 3.2.38 - vue-toasted: 1.1.28 devDependencies: '@tauri-apps/cli': 1.0.5 @@ -636,10 +634,6 @@ packages: fsevents: 2.3.2 dev: true - /vue-toasted/1.1.28: - resolution: {integrity: sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw==} - dev: false - /vue-tsc/0.40.9_typescript@4.8.2: resolution: {integrity: sha512-GnfwngCgbUvFgs+vaPesrJB76yoX1W/DSQZqoQ+pArjZ9+EFCFkqMpihE1D8W5p/tgTCAAPr/3Sfz/jtTiYGaA==} hasBin: true diff --git a/src-tauri/src/entropy/mod.rs b/src-tauri/src/entropy/mod.rs new file mode 100644 index 0000000..60b4344 --- /dev/null +++ b/src-tauri/src/entropy/mod.rs @@ -0,0 +1,24 @@ +use std::collections::HashSet; + +// https://en.wikipedia.org/wiki/Password_strength#Entropy_as_a_measure_of_password_strength +// R = pool of unique characters +// log2(R^Length) +pub fn bits_entropy(unique_symbols: usize, pool_length: usize) -> u32 { + let pool_length = pool_length as f64; + (pool_length.powf(unique_symbols as f64)).log2() as u32 +} + +pub fn get_password_strength(text: &String, pool_length: usize) -> u32 { + let unique_symbols = get_unique_chars_count(text); + bits_entropy(unique_symbols, pool_length) +} + +fn get_unique_chars_count(text: &String) -> usize { + let mut set: HashSet = HashSet::new(); + let chars: Vec = text.chars().into_iter().collect(); + for item in chars { + set.insert(item); + } + + set.len() +} diff --git a/src-tauri/src/gen_methods/mod.rs b/src-tauri/src/gen_methods/mod.rs new file mode 100644 index 0000000..a37c3ac --- /dev/null +++ b/src-tauri/src/gen_methods/mod.rs @@ -0,0 +1,64 @@ +use crate::{entropy::get_password_strength, password_info::PasswordInfo, POOL, SPECIAL}; +use hex::encode; +use rand::prelude::*; + +pub fn gen_schemed( + hash_length: usize, + is_inverted: bool, + words: &mut Vec, + use_special: bool, +) -> PasswordInfo { + let rng = &mut thread_rng(); + for _ in 0..10 { + words.shuffle(rng); + } + + let word = words.choose(rng).unwrap().to_owned(); + let nonsense = gen_nonsense(hash_length, use_special); + let hash = nonsense.text; + + let pool_length = POOL.len() + (use_special as usize) * SPECIAL.len(); + let text = if is_inverted { + format!("{}{}", hash, word) + } else { + format!("{}{}", word, hash) + }; + + let entropy = get_password_strength(&text, pool_length); + + PasswordInfo::new(text, entropy) +} + +pub fn gen_nonsense(length: usize, use_special: bool) -> PasswordInfo { + let rng = &mut thread_rng(); + let mut bytes: Vec = POOL.as_bytes().iter().cloned().collect(); + + if use_special { + bytes.extend(SPECIAL.as_bytes()); + } + + for _ in 0..10 { + bytes.shuffle(rng); + } + + let bucket: Vec = (0..length) + .map(|_| bytes.choose(rng).unwrap().to_owned()) + .collect(); + + let pool_length = POOL.len() + (use_special as usize) * SPECIAL.len(); + let text = String::from_utf8(bucket).unwrap_or(String::default()); + let entropy = get_password_strength(&text, pool_length); + + PasswordInfo::new(text, entropy) +} + +pub fn gen_random_bytes_hex(size: usize) -> PasswordInfo { + let text = encode(get_random_bytes(size)); + let entropy = get_password_strength(&text, 16); + PasswordInfo::new(text, entropy) +} + +fn get_random_bytes(size: usize) -> Vec { + let bytes: Vec = (0..size).map(|_| random::()).collect(); + bytes +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..a4a5568 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,6 @@ +pub mod entropy; +pub mod gen_methods; +pub mod password_info; + +pub const POOL: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; // 62 +pub const SPECIAL: &str = "!#$%&'()*+,-./:;<=>?{|}~[]^_@|\""; // 31 diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index dc7fcd6..2e336b0 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,94 +3,42 @@ windows_subsystem = "windows" )] -use rand::prelude::*; +use passwordo::gen_methods::*; +use passwordo::password_info::PasswordInfo; -const POOL: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; -const SPECIAL: &str = "*+-=?!<>#$%&?@^~_|"; - -fn get_words() -> Vec { - let words = vec!["lapras", "pikachu", "mewtwo", "eevee", "magmar", "bidoof"]; - words.iter().map(|it| it.to_string()).collect::>() -} - -fn gen_nonsense(length: usize, use_special: bool) -> String { - let rng = &mut thread_rng(); - let mut bytes: Vec = POOL.as_bytes().iter().cloned().collect(); - if use_special { - bytes.extend(SPECIAL.as_bytes()); - } - - bytes.shuffle(rng); - - let bucket: Vec = (0..length) - .map(|_| { - // bytes.choose(rng).unwrap().to_owned() - let i = rng.gen_range(0..bytes.len()); - bytes[i] - }) - .collect(); - - String::from_utf8(bucket).unwrap_or(String::default()) -} - -fn gen_schemed( - hash_length: usize, - is_inverted: bool, - words: &mut Vec, - use_special: bool, -) -> String { - let rng = &mut thread_rng(); - words.shuffle(rng); - - let word = words.choose(rng).unwrap().to_owned(); - let hash = gen_nonsense(hash_length, use_special); - - if is_inverted { - return format!("{}{}", hash, word); - } - - format!("{}{}", word, hash) -} - -fn get_random_bytes(size: usize) -> Vec { - let bytes: Vec = (0..size).map(|_| random::()).collect(); - bytes +#[tauri::command] +fn generate_random_bytes_passwords(amount: usize, length: usize) -> Vec { + let res: Vec = (0..amount).map(|_| gen_random_bytes_hex(length)).collect(); + res } #[tauri::command] fn generate_nonsense_passwords( - length: usize, amount: usize, - use_random_bytes: bool, + length: usize, use_special: bool, -) -> Vec { - let mut res: Vec = Vec::with_capacity(amount); - for _ in 0..amount { - let value = match use_random_bytes { - true => hex::encode(get_random_bytes(length)), - false => gen_nonsense(length, use_special), - }; - res.push(value); - } - +) -> Vec { + let res: Vec = (0..amount) + .map(|_| gen_nonsense(length, use_special)) + .collect(); res } #[tauri::command] fn generate_schemed_passwords( - hash_length: usize, amount: usize, + hash_length: usize, is_inverted: bool, list_of_words: Vec, use_special: bool, -) -> Vec { - let mut words: Vec = if list_of_words.is_empty() { - get_words() - } else { - list_of_words - }; +) -> Vec { + if list_of_words.is_empty() { + return vec![]; + } + + let mut words = list_of_words; - let res: Vec = (0..amount) + let res: Vec = (0..amount) .map(|_| gen_schemed(hash_length, is_inverted, &mut words, use_special)) .collect(); res @@ -100,7 +48,8 @@ fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ generate_nonsense_passwords, - generate_schemed_passwords + generate_schemed_passwords, + generate_random_bytes_passwords ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/password_info/mod.rs b/src-tauri/src/password_info/mod.rs new file mode 100644 index 0000000..9d99275 --- /dev/null +++ b/src-tauri/src/password_info/mod.rs @@ -0,0 +1,12 @@ +use serde::Serialize; +#[derive(Serialize)] +pub struct PasswordInfo { + pub text: String, + pub entropy: u32, +} + +impl PasswordInfo { + pub fn new(text: String, entropy: u32) -> Self { + PasswordInfo { text, entropy } + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0b05e3b..12e99cb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "passwordo", - "version": "0.0.0" + "version": "0.0.1" }, "tauri": { "allowlist": { diff --git a/src/App.vue b/src/App.vue index 84940e6..efbef0c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,79 +2,88 @@ import { reactive, ref, computed } from "vue"; import CopyPassword from "./components/CopyPassword.vue"; import Field from "./components/Field.vue"; -import { writeClipboard, GenerationMethod, gen_schemed_passwords, gen_nonsense_passwords } from "./tauri" +import { writeClipboard, GenerationMethod, gen_schemed_passwords, gen_nonsense_passwords, gen_rand_bytes_passwords, PasswordInfo } from "./tauri" import { sendNotification } from "@tauri-apps/api/notification"; -const methods: GenerationMethod[] = ["schema", "nonsense"] -const passwords = reactive([]) +const methods: GenerationMethod[] = ["schema", "nonsense", "random-bytes"] +const listOfWords = reactive([]); +const passwords = reactive([]); const method = ref(methods[0]); const useSpecial = ref(true); const passwordsToGenerate = ref(1); // schema method -const hashLength = ref(6); +const hashLength = ref(8); const invert = ref(false); -const listOfWords = reactive([]); +const wordsToUse = ref(""); // nonsense method const passwordLength = ref(12); -const useRandomBytes = ref(false); -const isSchemaMethod = computed(() => { - return method.value === "schema"; -}) +const isSchemaMethod = computed(() => method.value === "schema") +const isNonsenseMethod = computed(() => method.value === "nonsense") +const isRandBytesMethod = computed(() => method.value === "random-bytes") + +function clamp(amount: number, min = 1) { + return Math.max(amount, min); +} async function onSubmit(_e: Event) { - let generated: string[] = []; - const amount = Math.max(passwordsToGenerate.value, 1) - - console.log({ - method: method.value, - len: passwordLength.value, - amount, - randBytes: useRandomBytes.value, - invert: invert.value, - hashLength: hashLength.value, - useSpecial: useSpecial.value, - }); - - console.log(listOfWords); - - if (isSchemaMethod.value) { - const hLength = Math.max(hashLength.value, 1); - generated = await gen_schemed_passwords( - hLength, - amount, - invert.value, - listOfWords, - useSpecial.value - ); - } else { - const length = Math.max(passwordLength.value, 1); - generated = await gen_nonsense_passwords(length, amount, useRandomBytes.value, useSpecial.value); + let generated: PasswordInfo[] = []; + const amount = clamp(passwordsToGenerate.value); + + try { + switch (method.value) { + case "schema": + // empty listOfWord + listOfWords.splice(0, listOfWords.length) + // fill with new words + listOfWords.push(...extractWords(wordsToUse.value)); + + generated = await gen_schemed_passwords( + amount, + clamp(hashLength.value), + invert.value, + listOfWords, + useSpecial.value + ); + break; + case "nonsense": + + generated = await gen_nonsense_passwords(amount, clamp(passwordLength.value), useSpecial.value); + + break; + case "random-bytes": + + generated = await gen_rand_bytes_passwords(amount, clamp(passwordLength.value)); + + break; + default: + break; + } + + passwords.push(...generated); + sendNotification("Generating passwords"); + + } catch (e) { + console.error(e); } - - passwords.push(...generated); - sendNotification("Generating passwords"); } async function copyAll() { await writeClipboard(passwords.join("\n")); - sendNotification("All passwords copied to clipboard"); + sendNotification("All passwords were copied to the clipboard"); } function clearOutput() { passwords.splice(0, passwords.length); } -function handleWords(e: Event) { - let target = e.target as HTMLTextAreaElement; - const _words: string[] = - target.value.trim().split("\n").map(it => it.split(",").map(it => it.trim())).flat().filter(it => it.length > 0) - - listOfWords.splice(0, listOfWords.length) - listOfWords.push(..._words); +function extractWords(text: string): string[] { + const words: string[] = + text.trim().split("\n").map(it => it.split(",").map(it => it.trim())).flat().filter(it => it.length > 0) + return words; } @@ -95,54 +104,56 @@ function handleWords(e: Event) { - + - - - + - - - +
+ + + - - - + + + + + + +
- - - + - - - +
+ + + +
+
- - + +
-
  • - -
  • +
      + +
    diff --git a/src/components/CopyPassword.vue b/src/components/CopyPassword.vue index 0f71814..71dbd30 100644 --- a/src/components/CopyPassword.vue +++ b/src/components/CopyPassword.vue @@ -1,30 +1,59 @@ \ No newline at end of file diff --git a/src/components/EntropyMeter.vue b/src/components/EntropyMeter.vue new file mode 100644 index 0000000..f2eca94 --- /dev/null +++ b/src/components/EntropyMeter.vue @@ -0,0 +1,60 @@ + + + + + \ No newline at end of file diff --git a/src/style.css b/src/style.css index d414e64..1ebf266 100644 --- a/src/style.css +++ b/src/style.css @@ -37,19 +37,31 @@ body { display: grid; justify-content: center; grid-template-columns: 1fr 1fr; - /* grid-template-rows: max-content; */ + gap: 0.5rem; } +/* On Desktop */ +@media only screen and (min-width: 1200px) { + .content { + grid-template-columns: 1fr 2fr; + } +} + +/* On Mobile */ +@media only screen and (max-width: 780px) { + .content { + grid-template-columns: 1fr; + place-content: center; + } +} + .app-view { padding: 2rem 1rem; } .password-form { justify-self: start; - /* grid-column: 1 / 2; */ - /* place-self: center; */ - /* align-self: center; */ } a { @@ -133,18 +145,3 @@ input[type="checkbox"] { width: 20px; height: 20px; } - - -/* @media (prefers-color-scheme: dark) { - :root { - color: #f6f6f6; - background-color: #596475; - } - - input, - button { - color: #ffffff; - background-color: #0f0f0f98; - } -} - */ \ No newline at end of file diff --git a/src/tauri.ts b/src/tauri.ts index 512bec8..ec8f96a 100644 --- a/src/tauri.ts +++ b/src/tauri.ts @@ -1,38 +1,48 @@ import { invoke } from "@tauri-apps/api/tauri"; import { writeText } from "@tauri-apps/api/clipboard"; -export type GenerationMethod = "schema" | "nonsense"; +export type GenerationMethod = "schema" | "nonsense" | "random-bytes"; + +export interface PasswordInfo { + text: string; + entropy: number; +} export async function gen_nonsense_passwords( - length: number, amount: number, - useRandomBytes: boolean, + length: number, useSpecial: boolean ) { - return await invoke("generate_nonsense_passwords", { - length, + return await invoke("generate_nonsense_passwords", { amount, - useRandomBytes, + length, useSpecial, }); } export async function gen_schemed_passwords( - hashLength: number, amount: number, + hashLength: number, isInverted: boolean, listOfWords: string[], useSpecial: boolean ) { - return await invoke("generate_schemed_passwords", { - hashLength, + return await invoke("generate_schemed_passwords", { amount, + hashLength, isInverted, listOfWords, useSpecial, }); } +export async function gen_rand_bytes_passwords(amount: number, length: number) { + return await invoke("generate_random_bytes_passwords", { + amount, + length, + }); +} + export async function writeClipboard(text: string) { try { await writeText(text);