├── 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 |
2 |
3 | # Passwords
4 |
5 | A random password generator
6 |
7 | 
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 |
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 |
794 | }
795 | />
796 |
802 |
803 | Rounds
804 |
805 | {
811 | setIsCalculating(true);
812 | setBcryptRounds(value[0]);
813 | }}
814 | />
815 |
816 |
821 |
822 |
823 | }
824 | />
825 |
856 |
857 | 1
858 | 224
859 | 256
860 | 384
861 | 512
862 |
863 |
864 | }
865 | />
866 |
872 |
879 | {isCalculating ? (
880 | <>
881 |
882 |
883 | calculating...
884 |
885 | >
886 | ) : null}
887 |
888 |
889 |
890 |
891 |
892 |
899 |
900 |
912 |
915 |
916 |
917 |
918 |
919 |
920 |
921 | Number of characters
922 |
923 |
924 | {analysisResult?.length}
925 |
926 |
927 |
928 |
929 | Lowercase letters
930 |
931 |
932 | {analysisResult?.lowercase_letters_count}
933 |
934 |
935 |
936 |
937 | Uppercase letters
938 |
939 |
940 | {analysisResult?.uppercase_letters_count}
941 |
942 |
943 |
944 |
945 | Numbers
946 |
947 |
948 | {analysisResult?.numbers_count}
949 |
950 |
951 |
952 |
953 | Spaces
954 |
955 |
956 | {analysisResult?.spaces_count}
957 |
958 |
959 |
960 |
961 | Symbols
962 |
963 |
964 | {analysisResult?.symbols_count}
965 |
966 |
967 |
968 |
969 | Other characters
970 |
971 |
972 | {analysisResult?.other_characters_count}
973 |
974 |
975 |
976 |
977 |
978 | Consecutive repeated characters
979 |
980 |
981 |
982 | {analysisResult?.consecutive_count}
983 |
984 |
985 |
986 |
987 |
988 | Non consecutive repeated characters
989 |
990 |
991 |
992 | {analysisResult?.non_consecutive_count}
993 |
994 |
995 |
996 |
997 | Progressive characters
998 |
999 |
1000 | {analysisResult?.progressive_count}
1001 |
1002 |
1003 |
1004 |
1005 | Strength
1006 |
1007 |
1008 | {analysisResult ? (
1009 |
1010 | {getStrengthString(analysisResult.score)}
1011 |
1012 | ) : (
1013 | ""
1014 | )}
1015 |
1016 |
1017 |
1018 |
1019 | Common password
1020 |
1021 |
1022 | {analysisResult ? (
1023 | analysisResult.is_common ? (
1024 | YES
1025 | ) : (
1026 | NO
1027 | )
1028 | ) : (
1029 | ""
1030 | )}
1031 |
1032 |
1033 |
1034 |
1035 | Estimated time to crack
1036 |
1037 |
1038 | {analysisResult?.crack_times}
1039 |
1040 |
1041 |
1042 |
1043 |
1044 |
1045 | {isAnalyzing ? (
1046 | <>
1047 |
1048 |
1049 | analyzing...
1050 |
1051 | >
1052 | ) : null}
1053 |
1054 |
1055 |
1056 |
1057 | The principles of generating a strong password
1058 |
1059 |
1060 |
1061 | Make it unique
1062 |
1063 |
1064 | Passwords should be unique to different accounts. This reduces
1065 | the likelihood that multiple accounts of yours could be hacked
1066 | if one of your passwords is exposed in a data breach.
1067 |
1068 |
1069 | Make it random
1070 |
1071 |
1072 | The password has a combination of uppercase and lowercase
1073 | letters, numbers, special characters, and words with no
1074 | discernable pattern, unrelated to your personal information.
1075 |
1076 |
1077 | Make it long
1078 |
1079 |
1080 | The password consists of 14 characters or longer. An
1081 | 8-character password will take a hacker 39 minutes to crack
1082 | while a 16-character password will take a hacker a billion
1083 | years to crack.
1084 |
1085 |
1086 |
1087 | {COLORS.map((color) => (
1088 | {
1094 | setThemeColorValue(e.currentTarget.value);
1095 | }}
1096 | >
1097 | ))}
1098 |
1099 |
1100 |
1101 |
1102 |
1103 |
1104 | );
1105 | }
1106 |
1107 | function TextBox({
1108 | label,
1109 | text,
1110 | placeholder,
1111 | rows,
1112 | toolbar,
1113 | }: {
1114 | label: string;
1115 | text: string;
1116 | placeholder?: string;
1117 | rows?: number;
1118 | toolbar?: ReactNode;
1119 | }) {
1120 | const { isCopied, copyToClipboard } = useCopy();
1121 | const { hovered, ref } = useHover();
1122 |
1123 | return (
1124 |
1125 |
1126 | {label}
1127 | {toolbar}
1128 |
1129 |
1130 |
1136 |
1137 | {(hovered || isCopied) && text ? (
1138 | copyToClipboard(text)}
1143 | variant="solid"
1144 | >
1145 | {isCopied ? : }
1146 |
1147 | ) : null}
1148 |
1149 |
1150 |
1151 | );
1152 | }
1153 |
1154 | export default App;
1155 |
--------------------------------------------------------------------------------