├── src ├── vite-env.d.ts ├── main.tsx ├── utils.ts ├── App.css ├── natives.ts ├── assets │ └── react.svg ├── hooks.ts └── App.tsx ├── src-tauri ├── build.rs ├── icons │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 32x32.png │ ├── icon.icns │ ├── StoreLogo.png │ ├── 128x128@2x.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ └── Square89x89Logo.png ├── .gitignore ├── src │ ├── main.rs │ └── lib.rs ├── capabilities │ └── default.json ├── tauri.conf.json └── Cargo.toml ├── app-icon.png ├── screenshot.png ├── passwords.afphoto ├── .vscode └── extensions.json ├── tsconfig.node.json ├── .gitignore ├── index.html ├── README.md ├── tsconfig.json ├── package.json ├── vite.config.ts ├── LICENSE └── public ├── vite.svg └── tauri.svg /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/app-icon.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/screenshot.png -------------------------------------------------------------------------------- /passwords.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/passwords.afphoto -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiql/passwords-app/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | passwords_app_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function sleep(millis: number) { 2 | return new Promise((resolve) => setTimeout(resolve, millis)); 3 | } 4 | 5 | export function isDigit(char: string): boolean { 6 | return /^\d$/.test(char); 7 | } 8 | 9 | export function isLetter(char: string): boolean { 10 | return /^[A-Za-z]$/.test(char); 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Passwords 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | image 2 | 3 | # Passwords 4 | 5 | A random password generator 6 | 7 | ![alt text](screenshot.png) 8 | 9 | ## Features 10 | 11 | - Generator 12 | - Random 13 | - Memoriable 14 | - PIN 15 | - Hasher 16 | - MD5 17 | - Bcrypt 18 | - SHA1/SHA224/SHA256/SHA384/SHA512 19 | - BASE64 20 | - Analyzer 21 | - Password strength test 22 | - Common password check 23 | - Estimate the time to crack 24 | 25 | ## License 26 | 27 | MIT 28 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | overflow: hidden; 5 | 6 | cursor: default; 7 | -webkit-user-select: none; /* Chrome, Safari, Opera */ 8 | -moz-user-select: none; /* Firefox */ 9 | -ms-user-select: none; /* Internet Explorer/Edge */ 10 | user-select: none; /* Standard syntax */ 11 | } 12 | 13 | :root { 14 | font-synthesis: none; 15 | text-rendering: optimizeLegibility; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | -webkit-text-size-adjust: 100%; 19 | } 20 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": ["main"], 6 | "permissions": [ 7 | "core:default", 8 | "opener:default", 9 | "core:window:allow-start-dragging", 10 | "core:window:allow-set-size", 11 | "core:window:allow-theme", 12 | "core:event:allow-listen", 13 | "clipboard-manager:default", 14 | "clipboard-manager:allow-write-text", 15 | "clipboard-manager:allow-read-text" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passwords-app", 3 | "private": true, 4 | "version": "0.1.4", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-icons": "^1.3.2", 14 | "@radix-ui/themes": "^3.2.0", 15 | "@tauri-apps/api": "^2", 16 | "@tauri-apps/plugin-clipboard-manager": "^2", 17 | "@tauri-apps/plugin-opener": "^2", 18 | "@tauri-apps/plugin-shell": "^2", 19 | "react": "^18.3.1", 20 | "react-dom": "^18.3.1", 21 | "use-resize-observer": "^9.1.0" 22 | }, 23 | "devDependencies": { 24 | "@tauri-apps/cli": "^2", 25 | "@types/react": "^18.3.1", 26 | "@types/react-dom": "^18.3.1", 27 | "@vitejs/plugin-react": "^4.3.4", 28 | "typescript": "~5.6.2", 29 | "vite": "^6.0.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // @ts-expect-error process is a nodejs global 5 | const host = process.env.TAURI_DEV_HOST; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig(async () => ({ 9 | plugins: [react()], 10 | 11 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 12 | // 13 | // 1. prevent vite from obscuring rust errors 14 | clearScreen: false, 15 | // 2. tauri expects a fixed port, fail if that port is not available 16 | server: { 17 | port: 1420, 18 | strictPort: true, 19 | host: host || false, 20 | hmr: host 21 | ? { 22 | protocol: "ws", 23 | host, 24 | port: 1421, 25 | } 26 | : undefined, 27 | watch: { 28 | // 3. tell vite to ignore watching `src-tauri` 29 | ignored: ["**/src-tauri/**"], 30 | }, 31 | }, 32 | })); 33 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "Passwords", 4 | "version": "0.1.4", 5 | "identifier": "com.github.hiql.passwords-app", 6 | "build": { 7 | "beforeDevCommand": "npm run dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "npm run build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "Passwords", 16 | "width": 480, 17 | "height": 600, 18 | "maximized": false, 19 | "resizable": false, 20 | "titleBarStyle": "Overlay" 21 | } 22 | ], 23 | "security": { 24 | "csp": null 25 | } 26 | }, 27 | "bundle": { 28 | "active": true, 29 | "targets": "all", 30 | "icon": [ 31 | "icons/32x32.png", 32 | "icons/128x128.png", 33 | "icons/128x128@2x.png", 34 | "icons/icon.icns", 35 | "icons/icon.ico" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 hiql 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passwords-app" 3 | version = "0.1.4" 4 | description = "A random password generator" 5 | authors = ["hiql"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "passwords_app_lib" 15 | crate-type = ["rlib", "cdylib", "staticlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2", features = [] } 19 | 20 | [dependencies] 21 | tauri = { version = "2", features = [] } 22 | tauri-plugin-opener = "2" 23 | tauri-plugin-clipboard-manager = "2" 24 | serde = { version = "1", features = ["derive"] } 25 | serde_json = "1" 26 | passwords = { version = "3.1.16", features = ["common-password", "crypto"] } 27 | rand = "0.9.0" 28 | md5 = "0.7.0" 29 | base64 = "0.22.1" 30 | bcrypt = "0.17.0" 31 | sha2 = "0.10.8" 32 | hex = "0.4.3" 33 | zxcvbn = "3.1.0" 34 | sha1 = "0.10.6" 35 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/natives.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from "@tauri-apps/api/core"; 2 | 3 | export interface AnalyzedResult { 4 | password: string; 5 | length: number; 6 | spaces_count: number; 7 | numbers_count: number; 8 | lowercase_letters_count: number; 9 | uppercase_letters_count: number; 10 | symbols_count: number; 11 | other_characters_count: number; 12 | consecutive_count: number; 13 | non_consecutive_count: number; 14 | progressive_count: number; 15 | is_common: boolean; 16 | crack_times?: string; 17 | score: number; 18 | } 19 | 20 | export interface Complexity { 21 | length: number; 22 | symbols: boolean; 23 | numbers: boolean; 24 | uppercase: boolean; 25 | lowercase: boolean; 26 | spaces: boolean; 27 | excludeSimilarCharacters: boolean; 28 | strict: boolean; 29 | } 30 | 31 | const generatePassword = async (options: Complexity): Promise => { 32 | return await invoke("gen_password", { ...options }); 33 | }; 34 | 35 | const generateWords = async ( 36 | length: number, 37 | fullWords: boolean 38 | ): Promise => { 39 | return await invoke("gen_words", { 40 | length, 41 | fullWords, 42 | }); 43 | }; 44 | 45 | const generatePin = async (length: number): Promise => { 46 | return await invoke("gen_pin", { 47 | length, 48 | }); 49 | }; 50 | 51 | const analyze = async (password: string): Promise => { 52 | return await invoke("analyze", { 53 | password, 54 | }); 55 | }; 56 | 57 | const score = async (password: string): Promise => { 58 | return await invoke("score", { 59 | password, 60 | }); 61 | }; 62 | 63 | const crackTimes = async (password: string): Promise => { 64 | return await invoke("crack_times", { 65 | password, 66 | }); 67 | }; 68 | 69 | const md5 = async (password: string): Promise => { 70 | return await invoke("md5", { 71 | password, 72 | }); 73 | }; 74 | 75 | const base64 = async (password: string): Promise => { 76 | return await invoke("base64", { 77 | password, 78 | }); 79 | }; 80 | 81 | const bcrypt = async (password: string, rounds: number): Promise => { 82 | return await invoke("bcrypt", { 83 | password, 84 | rounds, 85 | }); 86 | }; 87 | 88 | const sha1 = async (password: string): Promise => { 89 | return await invoke("sha1", { 90 | password, 91 | }); 92 | }; 93 | 94 | const sha224 = async (password: string): Promise => { 95 | return await invoke("sha224", { 96 | password, 97 | }); 98 | }; 99 | 100 | const sha256 = async (password: string): Promise => { 101 | return await invoke("sha256", { 102 | password, 103 | }); 104 | }; 105 | 106 | const sha384 = async (password: string): Promise => { 107 | return await invoke("sha384", { 108 | password, 109 | }); 110 | }; 111 | 112 | const sha512 = async (password: string): Promise => { 113 | return await invoke("sha512", { 114 | password, 115 | }); 116 | }; 117 | 118 | export default { 119 | generatePassword, 120 | generateWords, 121 | generatePin, 122 | score, 123 | crackTimes, 124 | analyze, 125 | md5, 126 | bcrypt, 127 | base64, 128 | sha1, 129 | sha224, 130 | sha256, 131 | sha384, 132 | sha512, 133 | }; 134 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef, useCallback } from "react"; 2 | import { getCurrentWindow } from "@tauri-apps/api/window"; 3 | import { UnlistenFn } from "@tauri-apps/api/event"; 4 | import { writeText } from "@tauri-apps/plugin-clipboard-manager"; 5 | 6 | export const useTheme = () => { 7 | const [theme, setTheme] = useState<"light" | "dark" | "inherit">("inherit"); 8 | 9 | useEffect(() => { 10 | let unlisten: UnlistenFn | undefined; 11 | 12 | (async () => { 13 | setTheme((await getCurrentWindow().theme()) || "inherit"); 14 | 15 | unlisten = await getCurrentWindow().onThemeChanged( 16 | ({ payload: theme }) => { 17 | console.log(`theme changed to ${theme}`); 18 | setTheme(theme); 19 | } 20 | ); 21 | })(); 22 | 23 | return () => { 24 | if (unlisten != null) { 25 | unlisten(); 26 | } 27 | }; 28 | }, []); 29 | 30 | return theme; 31 | }; 32 | 33 | export function useCopy() { 34 | const [isCopied, setIsCopied] = useState(false); 35 | 36 | const copyToClipboard = async (text: string) => { 37 | await writeText(text); 38 | setIsCopied(true); 39 | }; 40 | 41 | const resetCopyStatus = () => { 42 | setIsCopied(false); 43 | }; 44 | 45 | useEffect(() => { 46 | if (isCopied) { 47 | const timer = setTimeout(resetCopyStatus, 3000); // Reset copy status after 3 seconds 48 | return () => clearTimeout(timer); 49 | } 50 | }, [isCopied]); 51 | 52 | return { isCopied, copyToClipboard, resetCopyStatus }; 53 | } 54 | 55 | export function useDebounce(value: T, delay: number): T { 56 | // State and setters for debounced value 57 | const [debouncedValue, setDebouncedValue] = useState(value); 58 | useEffect( 59 | () => { 60 | // Update debounced value after delay 61 | const handler = setTimeout(() => { 62 | setDebouncedValue(value); 63 | }, delay); 64 | // Cancel the timeout if value changes (also on delay change or unmount) 65 | // This is how we prevent debounced value from updating if value is changed ... 66 | // .. within the delay period. Timeout gets cleared and restarted. 67 | return () => { 68 | clearTimeout(handler); 69 | }; 70 | }, 71 | [value, delay] // Only re-call effect if value or delay changes 72 | ); 73 | return debouncedValue; 74 | } 75 | 76 | export function useHover() { 77 | const [hovered, setHovered] = useState(false); 78 | const ref = useRef(null); 79 | const onMouseEnter = useCallback(() => setHovered(true), []); 80 | const onMouseLeave = useCallback(() => setHovered(false), []); 81 | 82 | useEffect(() => { 83 | if (ref.current) { 84 | ref.current.addEventListener("mouseenter", onMouseEnter); 85 | ref.current.addEventListener("mouseleave", onMouseLeave); 86 | 87 | return () => { 88 | ref.current?.removeEventListener("mouseenter", onMouseEnter); 89 | ref.current?.removeEventListener("mouseleave", onMouseLeave); 90 | }; 91 | } 92 | 93 | return undefined; 94 | }, []); 95 | 96 | return { ref, hovered }; 97 | } 98 | 99 | export function useLocalStorage(key: string, initialValue: T) { 100 | // State to store our value 101 | // Pass initial state function to useState so logic is only executed once 102 | const [storedValue, setStoredValue] = useState(() => { 103 | try { 104 | // Get from local storage by key 105 | const item = window.localStorage.getItem(key); 106 | // Parse stored json or if none return initialValue 107 | return item ? JSON.parse(item) : initialValue; 108 | } catch (error) { 109 | // If error also return initialValue 110 | console.log(error); 111 | return initialValue; 112 | } 113 | }); 114 | // Return a wrapped version of useState's setter function that ... 115 | // ... persists the new value to localStorage. 116 | const setValue = (value: T | ((val: T) => T)) => { 117 | try { 118 | // Allow value to be a function so we have same API as useState 119 | const valueToStore = 120 | value instanceof Function ? value(storedValue) : value; 121 | // Save state 122 | setStoredValue(valueToStore); 123 | // Save to local storage 124 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 125 | } catch (error) { 126 | // A more advanced implementation would handle the error case 127 | console.log(error); 128 | } 129 | }; 130 | return [storedValue, setValue] as const; 131 | } 132 | -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | use base64::{prelude::BASE64_STANDARD, Engine}; 2 | use passwords::{analyzer, scorer, PasswordGenerator}; 3 | use sha1::Sha1; 4 | use sha2::{Digest, Sha224, Sha256, Sha384, Sha512}; 5 | use tauri::menu::{AboutMetadata, MenuBuilder, MenuItemBuilder, SubmenuBuilder}; 6 | use tauri_plugin_opener::OpenerExt; 7 | use zxcvbn::zxcvbn; 8 | 9 | mod syllables; 10 | mod words; 11 | 12 | #[tauri::command] 13 | fn gen_password( 14 | length: usize, 15 | numbers: bool, 16 | symbols: bool, 17 | uppercase: bool, 18 | lowercase: bool, 19 | spaces: bool, 20 | exclude_similar_characters: bool, 21 | strict: bool, 22 | ) -> String { 23 | let pg = PasswordGenerator { 24 | length, 25 | numbers, 26 | lowercase_letters: lowercase, 27 | uppercase_letters: uppercase, 28 | symbols, 29 | spaces, 30 | exclude_similar_characters, 31 | strict, 32 | }; 33 | pg.generate_one().unwrap() 34 | } 35 | 36 | #[tauri::command] 37 | fn gen_pin(length: usize) -> String { 38 | let pg = PasswordGenerator { 39 | length, 40 | numbers: true, 41 | lowercase_letters: false, 42 | uppercase_letters: false, 43 | symbols: false, 44 | spaces: false, 45 | exclude_similar_characters: false, 46 | strict: true, 47 | }; 48 | pg.generate_one().unwrap() 49 | } 50 | 51 | #[tauri::command] 52 | fn gen_words(length: usize, full_words: bool) -> Vec<&'static str> { 53 | let max = if length > 256 { 256 } else { length }; 54 | let mut words: Vec<&str> = vec![]; 55 | let mut i = 0; 56 | 57 | while i < max { 58 | let word = if full_words { 59 | words::rand() 60 | } else { 61 | syllables::rand() 62 | }; 63 | 64 | let found = words.iter().find(|&&x| *x == *word); 65 | if found.is_some() { 66 | continue; 67 | } 68 | 69 | words.push(word); 70 | i += 1; 71 | } 72 | 73 | words 74 | } 75 | 76 | #[derive(serde::Serialize)] 77 | struct AnalyzedResult { 78 | password: String, 79 | length: usize, 80 | spaces_count: usize, 81 | numbers_count: usize, 82 | lowercase_letters_count: usize, 83 | uppercase_letters_count: usize, 84 | symbols_count: usize, 85 | other_characters_count: usize, 86 | consecutive_count: usize, 87 | non_consecutive_count: usize, 88 | progressive_count: usize, 89 | is_common: bool, 90 | } 91 | 92 | #[tauri::command] 93 | fn analyze(password: &str) -> AnalyzedResult { 94 | let result = analyzer::analyze(password); 95 | AnalyzedResult { 96 | password: result.password().to_string(), 97 | length: result.length(), 98 | spaces_count: result.spaces_count(), 99 | numbers_count: result.numbers_count(), 100 | lowercase_letters_count: result.lowercase_letters_count(), 101 | uppercase_letters_count: result.uppercase_letters_count(), 102 | symbols_count: result.symbols_count(), 103 | other_characters_count: result.other_characters_count(), 104 | consecutive_count: result.consecutive_count(), 105 | non_consecutive_count: result.non_consecutive_count(), 106 | progressive_count: result.progressive_count(), 107 | is_common: result.is_common(), 108 | } 109 | } 110 | 111 | #[tauri::command] 112 | fn score(password: &str) -> f64 { 113 | scorer::score(&analyzer::analyze(password)) 114 | } 115 | 116 | #[tauri::command] 117 | fn is_common_password(password: &str) -> bool { 118 | analyzer::is_common_password(password) 119 | } 120 | 121 | #[tauri::command] 122 | fn md5(password: &str) -> String { 123 | let digest = md5::compute(password.as_bytes()); 124 | format!("{:x}", digest) 125 | } 126 | 127 | #[tauri::command] 128 | fn bcrypt(password: &str, rounds: u32) -> String { 129 | bcrypt::hash(&password, rounds).unwrap() 130 | } 131 | 132 | #[tauri::command] 133 | fn base64(password: &str) -> String { 134 | BASE64_STANDARD.encode(password.as_bytes()) 135 | } 136 | 137 | #[tauri::command] 138 | fn sha1(password: &str) -> String { 139 | let mut hasher = Sha1::new(); 140 | hasher.update(password.as_bytes()); 141 | let result = hasher.finalize(); 142 | hex::encode(result) 143 | } 144 | 145 | #[tauri::command] 146 | fn sha224(password: &str) -> String { 147 | let mut hasher = Sha224::new(); 148 | hasher.update(password.as_bytes()); 149 | let result = hasher.finalize(); 150 | hex::encode(result) 151 | } 152 | 153 | #[tauri::command] 154 | fn sha256(password: &str) -> String { 155 | let mut hasher = Sha256::new(); 156 | hasher.update(password.as_bytes()); 157 | let result = hasher.finalize(); 158 | hex::encode(result) 159 | } 160 | 161 | #[tauri::command] 162 | fn sha384(password: &str) -> String { 163 | let mut hasher = Sha384::new(); 164 | hasher.update(password.as_bytes()); 165 | let result = hasher.finalize(); 166 | hex::encode(result) 167 | } 168 | #[tauri::command] 169 | fn sha512(password: &str) -> String { 170 | let mut hasher = Sha512::new(); 171 | hasher.update(password.as_bytes()); 172 | let result = hasher.finalize(); 173 | hex::encode(result) 174 | } 175 | 176 | #[tauri::command] 177 | fn crack_times(password: &str) -> String { 178 | let entropy = zxcvbn(password, &[]); 179 | entropy 180 | .crack_times() 181 | .offline_slow_hashing_1e4_per_second() 182 | .to_string() 183 | } 184 | 185 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 186 | pub fn run() { 187 | tauri::Builder::default() 188 | .setup(|app| { 189 | let github = MenuItemBuilder::new("Github").id("github").build(app)?; 190 | let app_submenu = SubmenuBuilder::new(app, "App") 191 | .about(Some(AboutMetadata { 192 | ..Default::default() 193 | })) 194 | .separator() 195 | .item(&github) 196 | .separator() 197 | .services() 198 | .separator() 199 | .hide() 200 | .hide_others() 201 | .quit() 202 | .build()?; 203 | let menu = MenuBuilder::new(app).items(&[&app_submenu]).build()?; 204 | 205 | app.set_menu(menu)?; 206 | app.on_menu_event(move |app, event| { 207 | if event.id() == github.id() { 208 | app.opener() 209 | .open_url("https://github.com/hiql/passwords-app", None::<&str>) 210 | .unwrap(); 211 | } 212 | }); 213 | Ok(()) 214 | }) 215 | .plugin(tauri_plugin_clipboard_manager::init()) 216 | .plugin(tauri_plugin_opener::init()) 217 | .invoke_handler(tauri::generate_handler![ 218 | gen_password, 219 | gen_pin, 220 | gen_words, 221 | score, 222 | analyze, 223 | is_common_password, 224 | md5, 225 | base64, 226 | bcrypt, 227 | sha1, 228 | sha224, 229 | sha256, 230 | sha384, 231 | sha512, 232 | crack_times 233 | ]) 234 | .run(tauri::generate_context!()) 235 | .expect("error while running tauri application"); 236 | } 237 | 238 | #[cfg(test)] 239 | mod test { 240 | use super::*; 241 | 242 | #[test] 243 | fn test_gen_words() { 244 | let words = gen_words(15, true); 245 | println!("{:?}", words); 246 | let words = gen_words(8, false); 247 | println!("{:?}", words); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useState } from "react"; 2 | import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window"; 3 | import { readText } from "@tauri-apps/plugin-clipboard-manager"; 4 | import useResizeObserver from "use-resize-observer"; 5 | import { 6 | Flex, 7 | Theme, 8 | Text, 9 | Button, 10 | Slider, 11 | TextField, 12 | Box, 13 | Badge, 14 | BadgeProps, 15 | Tabs, 16 | RadioCards, 17 | TextArea, 18 | IconButton, 19 | Checkbox, 20 | RadioGroup, 21 | Spinner, 22 | Table, 23 | ButtonProps, 24 | ThemeProps, 25 | Tooltip, 26 | Strong, 27 | } from "@radix-ui/themes"; 28 | import { 29 | ButtonIcon, 30 | CheckIcon, 31 | ClipboardIcon, 32 | CodeIcon, 33 | CopyIcon, 34 | EraserIcon, 35 | LetterCaseCapitalizeIcon, 36 | LightningBoltIcon, 37 | MagicWandIcon, 38 | QuestionMarkCircledIcon, 39 | ReaderIcon, 40 | ShuffleIcon, 41 | UpdateIcon, 42 | } from "@radix-ui/react-icons"; 43 | import "@radix-ui/themes/styles.css"; 44 | import "./App.css"; 45 | import { isDigit, isLetter } from "./utils"; 46 | import { 47 | useCopy, 48 | useDebounce, 49 | useHover, 50 | useLocalStorage, 51 | useTheme, 52 | } from "./hooks"; 53 | import natives, { AnalyzedResult } from "./natives"; 54 | 55 | const ACCENT_COLOR_KEY = "passwords-app-accent-color"; 56 | const DEFAULT_ACCENT_COLOR = "indigo"; 57 | const COLORS: ButtonProps["color"][] = [ 58 | "gray", 59 | "gold", 60 | "bronze", 61 | "brown", 62 | "yellow", 63 | "amber", 64 | "orange", 65 | "tomato", 66 | "red", 67 | "ruby", 68 | "crimson", 69 | "pink", 70 | "plum", 71 | "purple", 72 | "violet", 73 | "iris", 74 | "indigo", 75 | "blue", 76 | "cyan", 77 | "teal", 78 | "jade", 79 | "green", 80 | "grass", 81 | "lime", 82 | "mint", 83 | "sky", 84 | ]; 85 | 86 | const getStrengthString = (score: number): string => { 87 | if (score >= 0 && score < 20) { 88 | return "VERY DANGEROUS"; 89 | } else if (score >= 20 && score < 40) { 90 | return "DANGEROUS"; 91 | } else if (score >= 40 && score < 60) { 92 | return "VERY WEAK"; 93 | } else if (score >= 60 && score < 80) { 94 | return "WEAK"; 95 | } else if (score >= 80 && score < 90) { 96 | return "GOOD"; 97 | } else if (score >= 90 && score < 95) { 98 | return "STRONG"; 99 | } else if (score >= 95 && score < 99) { 100 | return "VERY STRONG"; 101 | } else if (score >= 99 && score <= 100) { 102 | return "INVULNERABLE"; 103 | } else return ""; 104 | }; 105 | 106 | const getStrengthColor = (score: number): BadgeProps["color"] | undefined => { 107 | if (score >= 0 && score < 40) { 108 | return "red"; 109 | } else if (score >= 40 && score < 60) { 110 | return "orange"; 111 | } else if (score >= 60 && score < 80) { 112 | return "yellow"; 113 | } else if (score >= 80 && score <= 100) { 114 | return "green"; 115 | } else { 116 | return undefined; 117 | } 118 | }; 119 | 120 | function App() { 121 | const [panelType, setPanelType] = useState("generator"); 122 | const [passwordType, setPasswordType] = useState("random"); 123 | const [randomLength, setRandomLength] = useState(20); 124 | const [randomSymbols, setRandomSymbols] = useState(false); 125 | const [randomNumbers, setRandomNumbers] = useState(true); 126 | const [randomUppercase, setRandomUppercase] = useState(true); 127 | const [randomExcludeSimilarChars, setRandomExcludeSimilarChars] = 128 | useState(false); 129 | const [randomStrict, setRandomStrict] = useState(true); 130 | const [memorableLength, setMemorableLength] = useState(4); 131 | const [memorableUseFullWords, setMemorableUseFullWords] = useState(true); 132 | const [memorableCapitalize, setMemorableCapitalize] = useState(false); 133 | const [memorableUppercase, setMemorableUppercase] = useState(false); 134 | const [memorableSeparator, setMemorableSeparator] = useState("-"); 135 | const [pinLength, setPinLength] = useState(6); 136 | const [strength, setStrength] = useState(""); 137 | const [strengthColor, setStrengthColor] = 138 | useState(undefined); 139 | const [crackTime, setCrackTime] = useState(""); 140 | const [isGenerating, setIsGenerating] = useState(false); 141 | const [password, setPassword] = useState(""); 142 | const [hashPassword, setHashPassword] = useState(""); 143 | const [md5String, setMd5String] = useState(""); 144 | const [md5Uppercase, setMd5Uppercase] = useState(false); 145 | const [bcryptString, setBcryptString] = useState(""); 146 | const [bcryptRounds, setBcryptRounds] = useState(10); 147 | const [base64String, setBase64String] = useState(""); 148 | const [shaType, setShaType] = useState("256"); 149 | const [sha1String, setSha1String] = useState(""); 150 | const [sha224String, setSha224String] = useState(""); 151 | const [sha256String, setSha256String] = useState(""); 152 | const [sha384String, setSha384String] = useState(""); 153 | const [sha512String, setSha512String] = useState(""); 154 | const [isCalculating, setIsCalculating] = useState(false); 155 | const [analysisPassword, setAnalysisPassword] = useState(""); 156 | const [analysisResult, setAnalysisResult] = useState( 157 | null 158 | ); 159 | const [isAnalyzing, setIsAnalyzing] = useState(false); 160 | 161 | const randomLengthDebounce = useDebounce(randomLength, 200); 162 | const memorableLengthDebounce = useDebounce(memorableLength, 200); 163 | const pinLengthDebounce = useDebounce(pinLength, 200); 164 | const hashDebounce = useDebounce(hashPassword, 400); 165 | const analyzeDebounce = useDebounce(analysisPassword, 400); 166 | const bcryptRoundsDebounce = useDebounce(bcryptRounds, 200); 167 | 168 | const theme = useTheme(); 169 | const { isCopied, copyToClipboard, resetCopyStatus } = useCopy(); 170 | const [storedThemeColorValue, setThemeColorValue] = useLocalStorage( 171 | ACCENT_COLOR_KEY, 172 | DEFAULT_ACCENT_COLOR 173 | ); 174 | 175 | async function generateRandomPassword() { 176 | const pass: string = await natives.generatePassword({ 177 | length: randomLength, 178 | symbols: randomSymbols, 179 | numbers: randomNumbers, 180 | uppercase: randomUppercase, 181 | lowercase: true, 182 | spaces: false, 183 | excludeSimilarCharacters: randomExcludeSimilarChars, 184 | strict: randomStrict, 185 | }); 186 | const score: number = await natives.score(pass); 187 | const time: string = await natives.crackTimes(pass); 188 | setPassword(pass); 189 | setStrength(getStrengthString(score)); 190 | setStrengthColor(getStrengthColor(score)); 191 | setCrackTime(time); 192 | setIsGenerating(false); 193 | resetCopyStatus(); 194 | } 195 | 196 | async function generateWords() { 197 | const words: string[] = await natives.generateWords( 198 | memorableLength, 199 | memorableUseFullWords 200 | ); 201 | let pass = words 202 | .map((w) => 203 | memorableUppercase 204 | ? w.toUpperCase() 205 | : memorableCapitalize 206 | ? w.charAt(0).toUpperCase() + w.slice(1) 207 | : w 208 | ) 209 | .join(memorableSeparator === "" ? " " : memorableSeparator); 210 | setPassword(pass); 211 | setIsGenerating(false); 212 | resetCopyStatus(); 213 | } 214 | 215 | async function generatePin() { 216 | setPassword(await natives.generatePin(pinLength)); 217 | setIsGenerating(false); 218 | resetCopyStatus(); 219 | } 220 | 221 | async function analyzeAsync() { 222 | setIsAnalyzing(true); 223 | if (analysisPassword) { 224 | let obj: AnalyzedResult = await natives.analyze(analysisPassword); 225 | obj.score = await natives.score(analysisPassword); 226 | obj.crack_times = await natives.crackTimes(analysisPassword); 227 | setAnalysisResult(obj); 228 | } else { 229 | setAnalysisResult(null); 230 | } 231 | setIsAnalyzing(false); 232 | } 233 | 234 | async function bcryptAsync(password: string, rounds: number) { 235 | if (password) { 236 | const value: string = await natives.bcrypt(password, rounds); 237 | setBcryptString(value); 238 | } 239 | } 240 | 241 | async function hashAsync() { 242 | if (hashPassword) { 243 | setMd5String(await natives.md5(hashPassword)); 244 | setBase64String(await natives.base64(hashPassword)); 245 | setBcryptString(await natives.bcrypt(hashPassword, bcryptRounds)); 246 | setSha1String(await natives.sha1(hashPassword)); 247 | setSha224String(await natives.sha224(hashPassword)); 248 | setSha256String(await natives.sha256(hashPassword)); 249 | setSha384String(await natives.sha384(hashPassword)); 250 | setSha512String(await natives.sha512(hashPassword)); 251 | } else { 252 | setMd5String(""); 253 | setBase64String(""); 254 | setBcryptString(""); 255 | setSha1String(""); 256 | setSha224String(""); 257 | setSha256String(""); 258 | setSha384String(""); 259 | setSha512String(""); 260 | } 261 | setIsCalculating(false); 262 | } 263 | 264 | useEffect(() => { 265 | setIsGenerating(true); 266 | generateRandomPassword(); 267 | }, [ 268 | randomLengthDebounce, 269 | randomNumbers, 270 | randomSymbols, 271 | randomUppercase, 272 | randomExcludeSimilarChars, 273 | randomStrict, 274 | ]); 275 | 276 | useEffect(() => { 277 | setIsGenerating(true); 278 | generateWords(); 279 | }, [ 280 | memorableLengthDebounce, 281 | memorableCapitalize, 282 | memorableUppercase, 283 | memorableUseFullWords, 284 | memorableSeparator, 285 | ]); 286 | 287 | useEffect(() => { 288 | setIsGenerating(true); 289 | generatePin(); 290 | }, [pinLengthDebounce]); 291 | 292 | useEffect(() => { 293 | setIsCalculating(true); 294 | hashAsync(); 295 | }, [hashDebounce]); 296 | 297 | useEffect(() => { 298 | analyzeAsync(); 299 | }, [analyzeDebounce]); 300 | 301 | useEffect(() => { 302 | bcryptAsync(hashPassword, bcryptRounds); 303 | setIsCalculating(false); 304 | }, [bcryptRoundsDebounce]); 305 | 306 | const copy = async () => { 307 | await copyToClipboard(password); 308 | }; 309 | 310 | useEffect(() => { 311 | if (passwordType === "random") { 312 | setIsGenerating(true); 313 | generateRandomPassword(); 314 | } else if (passwordType === "memorable") { 315 | setIsGenerating(true); 316 | generateWords(); 317 | } else if (passwordType === "pin") { 318 | setIsGenerating(true); 319 | generatePin(); 320 | } 321 | }, [passwordType]); 322 | 323 | async function setWindowHeight(height: number) { 324 | await getCurrentWindow().setSize(new LogicalSize(480, height)); 325 | } 326 | 327 | const { ref } = useResizeObserver({ 328 | onResize: ({ height }) => { 329 | if (height) { 330 | setWindowHeight(height); 331 | } 332 | }, 333 | }); 334 | 335 | useEffect(() => { 336 | // disable context menu 337 | document.addEventListener( 338 | "contextmenu", 339 | (e) => { 340 | e.preventDefault(); 341 | return false; 342 | }, 343 | { capture: true } 344 | ); 345 | }, []); 346 | 347 | return ( 348 | 355 | 356 | 368 | 369 | 380 | 381 | 382 | 383 | Generator 384 | 385 | 386 | 387 | 388 | 389 | Hasher 390 | 391 | 392 | 393 | 394 | 395 | Analyzer 396 | 397 | 398 | 399 | 400 | 401 | Principles 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | Choose a password type: 410 | 411 | 418 | 419 | 420 | 421 | Random 422 | 423 | 424 | 425 | 426 | 427 | 428 | Memorable 429 | 430 | 431 | 432 | 433 | 434 | PIN 435 | 436 | 437 | 438 | 439 | Customize your password: 440 | 441 | {passwordType === "random" ? ( 442 | 452 | 453 | 454 | 455 | 458 | setRandomNumbers(checked as boolean) 459 | } 460 | /> 461 | Numbers 462 | 463 | 464 | 465 | 466 | 469 | setRandomSymbols(checked as boolean) 470 | } 471 | /> 472 | Symbols 473 | 474 | 475 | 476 | 477 | 480 | setRandomUppercase(checked as boolean) 481 | } 482 | /> 483 | Uppercase 484 | 485 | 486 | 487 | 488 | 491 | setRandomStrict(checked as boolean) 492 | } 493 | /> 494 | Strict 495 | 496 | 497 | 498 | 499 | 502 | setRandomExcludeSimilarChars(checked as boolean) 503 | } 504 | /> 505 | Exclude similar characters 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | Characters 514 | setRandomLength(values[0])} 517 | min={4} 518 | max={128} 519 | /> 520 | 521 | 528 | 529 | 530 | 531 | ) : passwordType === "memorable" ? ( 532 | 542 | 543 | 544 | 545 | { 548 | setMemorableCapitalize(checked as boolean); 549 | if (checked) { 550 | setMemorableUppercase(false); 551 | } 552 | }} 553 | /> 554 | Capitalize 555 | 556 | 557 | 558 | 559 | { 562 | setMemorableUppercase(checked as boolean); 563 | if (checked) { 564 | setMemorableCapitalize(false); 565 | } 566 | }} 567 | /> 568 | Uppercase 569 | 570 | 571 | 572 | 573 | 576 | setMemorableUseFullWords(checked as boolean) 577 | } 578 | /> 579 | Use full words 580 | 581 | 582 | 583 | 584 | 585 | Separator 586 | 587 | 588 | 593 | setMemorableSeparator(e.currentTarget.value) 594 | } 595 | /> 596 | 597 | 598 | 599 | 600 | Characters 601 | 602 | 605 | setMemorableLength(values[0]) 606 | } 607 | min={3} 608 | max={20} 609 | /> 610 | 611 | 618 | 619 | 620 | 621 | ) : ( 622 | 632 | 633 | Characters 634 | setPinLength(values[0])} 637 | min={3} 638 | max={12} 639 | /> 640 | 641 | 648 | 649 | 650 | 651 | )} 652 | 653 | Generated password: 654 | 655 | { 664 | await copy(); 665 | }} 666 | > 667 | 668 | 674 | {[...password].map((char, i) => 675 | char === " " ? ( 676 |   677 | ) : ( 678 | 690 | {char} 691 | 692 | ) 693 | )} 694 | 695 | 696 | {passwordType === "random" ? ( 697 | 704 | {strength} 705 | 706 | Estimated time to crack: {crackTime} 707 | 708 | 709 | ) : null} 710 | 711 | 718 | 736 | 746 | 747 | 748 | 749 | 750 | 751 |