Spaces:
Sleeping
Sleeping
Upload 23 files
Browse files- .gitignore +9 -0
- README.md +237 -10
- app.py +521 -0
- extension/README.md +53 -0
- extension/background.js +111 -0
- extension/content.js +192 -0
- extension/crypto-helpers.js +230 -0
- extension/icons/icon128.png +0 -0
- extension/icons/icon16.png +0 -0
- extension/icons/icon32.png +0 -0
- extension/icons/icon48.png +0 -0
- extension/manifest.json +39 -0
- extension/popup.html +406 -0
- extension/popup.js +389 -0
- extension/zxcvbn.js +0 -0
- static/css/style.css +870 -0
- static/js/crypto-helpers.js +230 -0
- static/js/zxcvbn.js +0 -0
- templates/analyse.html +950 -0
- templates/index.html +912 -0
- templates/login.html +248 -0
- templates/register.html +249 -0
- templates/storage.html +502 -0
.gitignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# .vercelignore
|
| 2 |
+
.env
|
| 3 |
+
.venv/
|
| 4 |
+
venv/
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.pyc
|
| 7 |
+
*.pyo
|
| 8 |
+
.git
|
| 9 |
+
.vscode/
|
README.md
CHANGED
|
@@ -1,10 +1,237 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Secure E2EE Password Manager & Analyzer
|
| 2 |
+
|
| 3 |
+
This project combines a Flask web application with a Chrome browser extension to provide a secure password management solution featuring End-to-End Encryption (E2EE), password strength analysis, and breach detection. User credentials are encrypted client-side before being sent to a Supabase backend, ensuring the server never sees plaintext passwords.
|
| 4 |
+
|
| 5 |
+
**Core Principles:**
|
| 6 |
+
|
| 7 |
+
* **End-to-End Encryption:** Passwords and associated data are encrypted/decrypted *only* in the user's browser using a key derived from their Master Password.
|
| 8 |
+
* **Zero Knowledge (Server):** The backend stores only encrypted data and cannot decrypt user passwords.
|
| 9 |
+
* **Client-Side Security Focus:** Critical cryptographic operations happen client-side (Web Crypto API in the browser/extension).
|
| 10 |
+
|
| 11 |
+
## Features
|
| 12 |
+
|
| 13 |
+
* **User Authentication:** Secure registration and login system using Flask-Login and Bcrypt for hashing Master Passwords.
|
| 14 |
+
* **End-to-End Encryption:** AES-GCM encryption using Web Crypto API, with keys derived using PBKDF2HMAC-SHA256.
|
| 15 |
+
* **Supabase Backend:** Utilizes Supabase (PostgreSQL) for user management and encrypted credential storage.
|
| 16 |
+
* **Chrome Extension:**
|
| 17 |
+
* Popup interface for quick access to credentials.
|
| 18 |
+
* Login/Unlock directly within the popup.
|
| 19 |
+
* View, search, copy, and auto-fill credentials.
|
| 20 |
+
* Add new credentials directly from the popup.
|
| 21 |
+
* Integrated password generator.
|
| 22 |
+
* Password strength analysis (ZXCVBN + HIBP breach check) in the "Add New" form.
|
| 23 |
+
* Theme toggle (light/dark).
|
| 24 |
+
* Content script for interacting with web pages (auto-fill).
|
| 25 |
+
* **Web Application (Flask):**
|
| 26 |
+
* User registration and login pages.
|
| 27 |
+
* Interface to add new credentials.
|
| 28 |
+
* Page to view/decrypt stored credentials.
|
| 29 |
+
* Password analysis page:
|
| 30 |
+
* Analyze strength and breach status of a single password.
|
| 31 |
+
* Analyze *all* stored passwords (decrypts client-side for analysis).
|
| 32 |
+
* Optional AI-powered insights via Google Gemini API.
|
| 33 |
+
* **Password Generation:** Secure password generator with customizable options (length, character sets).
|
| 34 |
+
* **Password Strength Analysis:** Uses the robust ZXCVBN library (client-side in popup/web app, server-side via API).
|
| 35 |
+
* **Breach Detection:** Integrates with the **free** Have I Been Pwned (HIBP) Pwned Passwords API (using k-Anonymity for privacy) on the client-side.
|
| 36 |
+
* **"Remember Me" Functionality:** Secure session persistence using Flask-Login.
|
| 37 |
+
* **Theme Toggle:** Light/Dark mode support in the web app and extension popup.
|
| 38 |
+
|
| 39 |
+
## Tech Stack
|
| 40 |
+
|
| 41 |
+
* **Backend:** Python 3, Flask, Supabase (Python Client), Flask-Login, Flask-Bcrypt, python-dotenv, cryptography, zxcvbn-python, google-generativeai (optional)
|
| 42 |
+
* **Frontend (Web App):** HTML, CSS, JavaScript, Jinja2, ZXCVBN.js
|
| 43 |
+
* **Frontend (Extension):** HTML, CSS, JavaScript (Web Crypto API), ZXCVBN.js, Chrome Extension APIs
|
| 44 |
+
* **Database:** Supabase (PostgreSQL backend)
|
| 45 |
+
* **APIs:**
|
| 46 |
+
* Have I Been Pwned (HIBP) Pwned Passwords API (Range Endpoint - Free)
|
| 47 |
+
* Google Gemini API (Optional - Requires API Key)
|
| 48 |
+
|
| 49 |
+
## Prerequisites
|
| 50 |
+
|
| 51 |
+
* Python 3.9+
|
| 52 |
+
* `pip` (Python package installer)
|
| 53 |
+
* A Supabase Account (Free tier is sufficient)
|
| 54 |
+
* Google Chrome (or other Chromium-based browser like Edge, Brave)
|
| 55 |
+
* (Optional) Google Gemini API Key for enhanced password analysis features.
|
| 56 |
+
|
| 57 |
+
## Setup & Installation
|
| 58 |
+
|
| 59 |
+
1. **Clone the Repository:**
|
| 60 |
+
```bash
|
| 61 |
+
git clone <repository-url>
|
| 62 |
+
cd <repository-directory>
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
2. **Set up Supabase:**
|
| 66 |
+
* Go to [Supabase](https://supabase.com/) and create a new project.
|
| 67 |
+
* Navigate to **Project Settings** > **API**.
|
| 68 |
+
* Find your **Project URL** and the **`anon` public API Key**. You'll need these for the `.env` file.
|
| 69 |
+
* Go to the **SQL Editor** in your Supabase dashboard.
|
| 70 |
+
* Create the necessary tables by running the following SQL (or use a schema migration tool):
|
| 71 |
+
|
| 72 |
+
```sql
|
| 73 |
+
-- users table
|
| 74 |
+
CREATE TABLE users (
|
| 75 |
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
| 76 |
+
email character varying UNIQUE NOT NULL,
|
| 77 |
+
password_hash character varying NOT NULL,
|
| 78 |
+
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
|
| 79 |
+
);
|
| 80 |
+
|
| 81 |
+
-- Ensure email is lowercase for consistency
|
| 82 |
+
ALTER TABLE users ADD CONSTRAINT users_email_check CHECK ((email = lower((email)::text)));
|
| 83 |
+
|
| 84 |
+
-- credentials table
|
| 85 |
+
CREATE TABLE credentials (
|
| 86 |
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
| 87 |
+
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
| 88 |
+
encrypted_data text NOT NULL,
|
| 89 |
+
service_hint character varying, -- For easier identification without decryption
|
| 90 |
+
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
|
| 91 |
+
);
|
| 92 |
+
|
| 93 |
+
-- Index for faster lookups by user_id
|
| 94 |
+
CREATE INDEX idx_credentials_user_id ON public.credentials USING btree (user_id);
|
| 95 |
+
|
| 96 |
+
-- Enable Row Level Security (VERY IMPORTANT)
|
| 97 |
+
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
| 98 |
+
ALTER TABLE credentials ENABLE ROW LEVEL SECURITY;
|
| 99 |
+
|
| 100 |
+
-- RLS Policy: Users can only see/manage their own credentials
|
| 101 |
+
CREATE POLICY "Users can manage their own credentials"
|
| 102 |
+
ON credentials
|
| 103 |
+
FOR ALL
|
| 104 |
+
USING (auth.uid() = user_id);
|
| 105 |
+
|
| 106 |
+
-- RLS Policy: Users can see their own user record (e.g., for profile info if needed)
|
| 107 |
+
CREATE POLICY "Users can view their own user record"
|
| 108 |
+
ON users
|
| 109 |
+
FOR SELECT
|
| 110 |
+
USING (auth.uid() = id);
|
| 111 |
+
|
| 112 |
+
-- RLS Policy: Allow users to update their own record (if needed, e.g., change email - requires more logic)
|
| 113 |
+
-- CREATE POLICY "Users can update their own user record"
|
| 114 |
+
-- ON users
|
| 115 |
+
-- FOR UPDATE
|
| 116 |
+
-- USING (auth.uid() = id);
|
| 117 |
+
|
| 118 |
+
-- NOTE: Inserting new users requires the service_role key or disabling RLS temporarily
|
| 119 |
+
-- during backend registration if using anon key, OR handle user creation via Supabase Auth.
|
| 120 |
+
-- The current Python code uses the anon key to insert directly, assuming RLS might
|
| 121 |
+
-- need adjustment or you handle auth differently in production.
|
| 122 |
+
-- For simplicity in this setup, we assume direct insert works, but review RLS for inserts.
|
| 123 |
+
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
3. **Configure Backend (`.env` file):**
|
| 127 |
+
* Create a file named `.env` in the root project directory (where `app.py` is).
|
| 128 |
+
* Add the following environment variables:
|
| 129 |
+
|
| 130 |
+
```dotenv
|
| 131 |
+
SUPABASE_URL=YOUR_SUPABASE_PROJECT_URL
|
| 132 |
+
SUPABASE_KEY=YOUR_SUPABASE_ANON_PUBLIC_KEY
|
| 133 |
+
FLASK_SECRET_KEY=generate_a_very_strong_random_secret_key_here # Use os.urandom(24).hex() in Python to generate one
|
| 134 |
+
GEMINI_API_KEY=YOUR_GOOGLE_GEMINI_API_KEY # Optional: Leave blank or remove if not using Gemini
|
| 135 |
+
|
| 136 |
+
# Optional: Set to 'development' for debug mode, 'production' otherwise
|
| 137 |
+
FLASK_ENV=development
|
| 138 |
+
```
|
| 139 |
+
* **Important:** Replace placeholders with your actual Supabase URL/Key and generate a strong `FLASK_SECRET_KEY`.
|
| 140 |
+
|
| 141 |
+
4. **Install Backend Dependencies:**
|
| 142 |
+
* Open your terminal in the project root directory.
|
| 143 |
+
* (Recommended) Create and activate a virtual environment:
|
| 144 |
+
```bash
|
| 145 |
+
python -m venv venv
|
| 146 |
+
# Windows
|
| 147 |
+
venv\Scripts\activate
|
| 148 |
+
# macOS/Linux
|
| 149 |
+
source venv/bin/activate
|
| 150 |
+
```
|
| 151 |
+
* Install the required packages:
|
| 152 |
+
```bash
|
| 153 |
+
pip install -r requirements.txt
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
5. **Load Chrome Extension:**
|
| 157 |
+
* Open Google Chrome.
|
| 158 |
+
* Go to `chrome://extensions/`.
|
| 159 |
+
* Enable "Developer mode" (usually a toggle in the top-right corner).
|
| 160 |
+
* Click "Load unpacked".
|
| 161 |
+
* Navigate to and select the directory containing the extension's `manifest.json` file (e.g., an `extension` subfolder if you have one).
|
| 162 |
+
* The extension icon should appear in your Chrome toolbar.
|
| 163 |
+
|
| 164 |
+
## Running the Application
|
| 165 |
+
|
| 166 |
+
1. **Start the Flask Backend:**
|
| 167 |
+
* Make sure your virtual environment is activated (if you created one).
|
| 168 |
+
* Run the Flask app from the project root directory:
|
| 169 |
+
```bash
|
| 170 |
+
python app.py
|
| 171 |
+
```
|
| 172 |
+
* The backend should start, typically on `http://127.0.0.1:5000`.
|
| 173 |
+
|
| 174 |
+
2. **Use the Web Application:**
|
| 175 |
+
* Open your web browser and navigate to `http://127.0.0.1:5000`.
|
| 176 |
+
* You can register a new user or log in if you already have an account.
|
| 177 |
+
|
| 178 |
+
3. **Use the Chrome Extension:**
|
| 179 |
+
* Click the extension's icon in your Chrome toolbar.
|
| 180 |
+
* If you are logged into the web application *in the same browser session*, the extension might automatically detect the session (via background script communication). If not, log in via the popup.
|
| 181 |
+
* The popup allows adding, viewing, copying, and filling credentials.
|
| 182 |
+
|
| 183 |
+
## Usage Guide
|
| 184 |
+
|
| 185 |
+
* **Registration/Login:** Use the web interface (`http://127.0.0.1:5000/register` or `/login`) or the extension popup to create an account or log in. Your Master Password is crucial and **cannot be recovered**.
|
| 186 |
+
* **Adding Credentials:**
|
| 187 |
+
* **Web App:** Navigate to `/add` after logging in.
|
| 188 |
+
* **Extension:** Click the extension icon, then click "+ Add New Credential". Fill in the details and confirm with your Master Password.
|
| 189 |
+
* **Generating Passwords:** Use the "Generate" button in the "Add New Credential" forms (web app or extension popup). Customize options as needed.
|
| 190 |
+
* **Viewing/Decrypting:**
|
| 191 |
+
* **Web App:** Navigate to `/storage`. Click "Show" on an entry to decrypt and view details client-side.
|
| 192 |
+
* **Extension:** Click the icon. Click "Show" on an entry. Strength and breach status are checked upon showing.
|
| 193 |
+
* **Copying Passwords:** In the extension popup or `/storage` page, click "Show" first, then a "Copy" button will appear.
|
| 194 |
+
* **Filling Passwords:**
|
| 195 |
+
* **Extension:** When on a login page, click the extension icon. Entries matching the domain appear first. Click the list item (if it's a domain match) or click "Show" then "Fill".
|
| 196 |
+
* **Content Script Icon (Optional):** If enabled/working, an icon may appear in password fields. Clicking it can trigger the popup or directly fill credentials (depending on implementation).
|
| 197 |
+
* **Analyzing Passwords:**
|
| 198 |
+
* Navigate to `/analyse` in the web app.
|
| 199 |
+
* Enter a single password for immediate analysis.
|
| 200 |
+
* Click "Load & Analyse All Stored Credentials" to decrypt (client-side) and analyze all your saved passwords for strength and potential breaches.
|
| 201 |
+
* **Logging Out:** Click the "Logout" link available in the header of the web app pages or within the extension popup. This clears the session key.
|
| 202 |
+
|
| 203 |
+
## Security Considerations
|
| 204 |
+
|
| 205 |
+
* **Master Password:** This is the most critical piece of information. Choose a strong, unique Master Password that you don't use anywhere else. **If you forget it, your encrypted data is irrecoverable.**
|
| 206 |
+
* **End-to-End Encryption:** The design ensures the server (Flask backend and Supabase) only ever handles encrypted data blobs. Decryption keys are derived client-side from the Master Password and ideally only held in memory (sessionStorage or background script memory) during an active session.
|
| 207 |
+
* **Backend Security:** While the backend doesn't handle decryption, it's vital to secure it against unauthorized access, ensure dependencies are up-to-date, and use HTTPS for any real-world deployment. Master Password *hashes* (using Bcrypt) are stored for login verification.
|
| 208 |
+
* **Supabase Row Level Security (RLS):** RLS policies **must** be enabled and correctly configured in Supabase to prevent users from accessing each other's encrypted data. The provided SQL includes basic policies, but review and test them thoroughly.
|
| 209 |
+
* **API Keys (`.env`):** Keep your `.env` file secure and **never commit it to version control**. Ensure `FLASK_SECRET_KEY` is strong and random. The `GEMINI_API_KEY` should also be treated as sensitive.
|
| 210 |
+
* **HTTPS:** For any non-local deployment, HTTPS is **mandatory** to protect login credentials and session cookies during transit.
|
| 211 |
+
* **XSS/CSRF:** Standard web security practices should be followed in the Flask application to prevent Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF) attacks. Use Flask-WTF or similar libraries if forms are more complex. Ensure Jinja2 autoescaping is enabled (default).
|
| 212 |
+
* **Extension Permissions:** The extension requests permissions like `activeTab`, `scripting`, `storage`, and `cookies`. These are necessary for its core functions (filling passwords, storing session state, communicating with the backend). Be aware of what permissions extensions request.
|
| 213 |
+
* **k-Anonymity (HIBP):** Password breach checking via HIBP uses k-Anonymity, meaning your full password hash is *not* sent to the HIBP server, preserving privacy for that specific check.
|
| 214 |
+
* **Session Management:** The Flask session stores the *derived* encryption key (Base64URL encoded). Session security (cookie flags like HttpOnly, Secure, SameSite) is important. The "Remember Me" feature extends session lifetime but relies on secure cookie handling. The background script also holds the key temporarily.
|
| 215 |
+
* **Disclaimer:** This project is primarily for educational/demonstration purposes. While it implements strong E2EE concepts, deploying it for highly sensitive data requires thorough security audits and hardening beyond this basic setup.
|
| 216 |
+
|
| 217 |
+
## Development
|
| 218 |
+
|
| 219 |
+
* **Backend:** Modify `app.py` and related files. The Flask development server usually auto-reloads on changes (`FLASK_ENV=development`).
|
| 220 |
+
* **Extension:**
|
| 221 |
+
* Modify HTML, CSS, or JS files within the extension directory.
|
| 222 |
+
* Go back to `chrome://extensions/`.
|
| 223 |
+
* Find the extension card and click the refresh icon (circular arrow).
|
| 224 |
+
* Close and reopen the popup or refresh the web page where the content script runs to see changes.
|
| 225 |
+
|
| 226 |
+
## Future Improvements
|
| 227 |
+
|
| 228 |
+
* Implement Email Breach Checking (requires HIBP paid API or alternative service).
|
| 229 |
+
* Add Secure Notes storage feature.
|
| 230 |
+
* Implement Two-Factor Authentication (2FA) for login.
|
| 231 |
+
* Add Password History (store previous encrypted versions).
|
| 232 |
+
* More robust error handling and user feedback.
|
| 233 |
+
* UI/UX enhancements for both web app and extension.
|
| 234 |
+
* Option for users to host their own backend easily (e.g., Docker setup).
|
| 235 |
+
* Detailed deployment guide (e.g., Heroku, Render, Docker).
|
| 236 |
+
* Formal security audit.
|
| 237 |
+
* Cross-browser compatibility testing (Firefox support would require manifest/API changes).
|
app.py
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- START OF FILE app.py ---
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import json
|
| 5 |
+
import hashlib
|
| 6 |
+
import time
|
| 7 |
+
import re
|
| 8 |
+
import base64
|
| 9 |
+
import traceback # For detailed error logging
|
| 10 |
+
import secrets # Use secrets for cryptographic randomness
|
| 11 |
+
import string # For character sets
|
| 12 |
+
import random # For shuffling (secrets doesn't have shuffle)
|
| 13 |
+
from datetime import timedelta # ** IMPORT timedelta **
|
| 14 |
+
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session
|
| 15 |
+
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
| 16 |
+
from flask_bcrypt import Bcrypt
|
| 17 |
+
from supabase import create_client, Client # Use v2 import style
|
| 18 |
+
# Note: cryptography library parts are only needed for derive_key now
|
| 19 |
+
from cryptography.hazmat.primitives import hashes
|
| 20 |
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
| 21 |
+
from dotenv import load_dotenv
|
| 22 |
+
import google.generativeai as genai
|
| 23 |
+
from zxcvbn import zxcvbn
|
| 24 |
+
load_dotenv()
|
| 25 |
+
|
| 26 |
+
app = Flask(__name__)
|
| 27 |
+
# Make sure FLASK_SECRET_KEY is set strong in your .env!
|
| 28 |
+
app.config['SECRET_KEY'] = os.environ.get('FLASK_SECRET_KEY', 'default_secret_key_please_change')
|
| 29 |
+
app.config['ENV'] = os.environ.get('FLASK_ENV', 'production')
|
| 30 |
+
app.config['DEBUG'] = app.config['ENV'] == 'development'
|
| 31 |
+
|
| 32 |
+
# ** SET PERMANENT SESSION LIFETIME (e.g., 7 days) **
|
| 33 |
+
# This applies *only* when 'Remember Me' is checked.
|
| 34 |
+
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# --- Supabase Configuration ---
|
| 38 |
+
supabase_url = os.environ.get("SUPABASE_URL")
|
| 39 |
+
supabase_key = os.environ.get("SUPABASE_KEY") # Anon key
|
| 40 |
+
|
| 41 |
+
if not supabase_url or not supabase_key:
|
| 42 |
+
raise ValueError("Supabase URL and Key must be set in .env file")
|
| 43 |
+
|
| 44 |
+
supabase: Client = create_client(supabase_url, supabase_key)
|
| 45 |
+
print("Supabase client initialized.")
|
| 46 |
+
|
| 47 |
+
# --- Authentication Setup ---
|
| 48 |
+
bcrypt = Bcrypt(app)
|
| 49 |
+
login_manager = LoginManager()
|
| 50 |
+
login_manager.init_app(app)
|
| 51 |
+
login_manager.login_view = 'login'
|
| 52 |
+
login_manager.login_message_category = 'info'
|
| 53 |
+
print("Flask-Login initialized.")
|
| 54 |
+
|
| 55 |
+
# --- User Model for Flask-Login ---
|
| 56 |
+
class User(UserMixin):
|
| 57 |
+
def __init__(self, id, email):
|
| 58 |
+
self.id = id
|
| 59 |
+
self.email = email
|
| 60 |
+
|
| 61 |
+
@login_manager.user_loader
|
| 62 |
+
def load_user(user_id):
|
| 63 |
+
user_data = session.get('user_data')
|
| 64 |
+
if user_data and user_data['id'] == user_id:
|
| 65 |
+
return User(id=user_data['id'], email=user_data['email'])
|
| 66 |
+
# print(f"User {user_id} not found in session.") # Reduced verbosity
|
| 67 |
+
return None
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# --- Gemini LLM Configuration ---
|
| 71 |
+
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
|
| 72 |
+
genai_available = False
|
| 73 |
+
if GEMINI_API_KEY and GEMINI_API_KEY.startswith("AIza"):
|
| 74 |
+
try:
|
| 75 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
| 76 |
+
genai_available = True
|
| 77 |
+
print("Gemini API configured successfully.")
|
| 78 |
+
except Exception as e:
|
| 79 |
+
print(f"Warning: Failed to configure Gemini API: {e}")
|
| 80 |
+
else:
|
| 81 |
+
print("Warning: Gemini API key not found or looks invalid. LLM features will use mock/basic analysis.")
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# --- E2EE Helper Functions ---
|
| 85 |
+
def derive_key(master_password: str, salt_or_email: str) -> bytes:
|
| 86 |
+
"""
|
| 87 |
+
Derives a 32-byte key suitable for AES using PBKDF2HMAC.
|
| 88 |
+
Uses email (lowercase) as salt for simplicity - *replace with stored unique salt in production*.
|
| 89 |
+
Returns the key BASE64 URL-SAFE ENCODED (suitable for session/storage).
|
| 90 |
+
"""
|
| 91 |
+
salt = salt_or_email.lower().encode('utf-8')
|
| 92 |
+
iterations = 390000 # Match JS side
|
| 93 |
+
kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=salt, iterations=iterations )
|
| 94 |
+
key_bytes = kdf.derive(master_password.encode('utf-8')) # Raw 32 bytes
|
| 95 |
+
|
| 96 |
+
# --- ADDED LOGGING ---
|
| 97 |
+
# Log the standard Base64 representation for easier comparison with JS console log
|
| 98 |
+
try:
|
| 99 |
+
raw_key_standard_b64 = base64.b64encode(key_bytes).decode('utf-8')
|
| 100 |
+
# print(f"DEBUG: PY Derived Key (Raw -> Standard Base64): {raw_key_standard_b64}") # Keep commented unless debugging
|
| 101 |
+
except Exception as log_e:
|
| 102 |
+
print(f"DEBUG: Error logging derived key: {log_e}")
|
| 103 |
+
# --- END LOGGING ---
|
| 104 |
+
|
| 105 |
+
# Return URL-safe Base64 encoded key for session storage
|
| 106 |
+
key_b64url_encoded = base64.urlsafe_b64encode(key_bytes)
|
| 107 |
+
return key_b64url_encoded
|
| 108 |
+
|
| 109 |
+
# --- Password Analysis Functions ---
|
| 110 |
+
# (get_character_composition, get_llm_password_insights, get_basic_password_analysis_from_chars, get_mock_llm_insights_from_chars)
|
| 111 |
+
def get_character_composition(password):
|
| 112 |
+
"""Generates character composition dict."""
|
| 113 |
+
composition = { 'lowercase': 0, 'uppercase': 0, 'digits': 0, 'special': 0 }
|
| 114 |
+
if not password: return composition # Handle None or empty string
|
| 115 |
+
for char in password:
|
| 116 |
+
if char.islower(): composition['lowercase'] += 1
|
| 117 |
+
elif char.isupper(): composition['uppercase'] += 1
|
| 118 |
+
elif char.isdigit(): composition['digits'] += 1
|
| 119 |
+
elif not char.isalnum() and not char.isspace(): composition['special'] += 1 # More specific special char check
|
| 120 |
+
return composition
|
| 121 |
+
|
| 122 |
+
def get_llm_password_insights(password_characteristics: dict):
|
| 123 |
+
""" Get password analysis insights from Gemini API based on characteristics."""
|
| 124 |
+
global genai_available
|
| 125 |
+
try:
|
| 126 |
+
use_mock = app.config['DEBUG'] or not genai_available
|
| 127 |
+
if use_mock:
|
| 128 |
+
return get_mock_llm_insights_from_chars(password_characteristics)
|
| 129 |
+
|
| 130 |
+
generation_config = { "temperature": 0.2, "top_p": 0.8, "top_k": 40, "max_output_tokens": 512 }
|
| 131 |
+
safety_settings = [
|
| 132 |
+
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
| 133 |
+
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
| 134 |
+
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
| 135 |
+
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
| 136 |
+
]
|
| 137 |
+
model = genai.GenerativeModel('gemini-1.5-flash', generation_config=generation_config, safety_settings=safety_settings)
|
| 138 |
+
|
| 139 |
+
prompt = f"""
|
| 140 |
+
Analyze a password based *only* on the following characteristics. Provide a security assessment.
|
| 141 |
+
DO NOT attempt to guess the password or ask for it.
|
| 142 |
+
Return ONLY a valid JSON object with the exact structure below, no other text or explanations.
|
| 143 |
+
{{
|
| 144 |
+
"strength_score": 1-5 (integer, 1=very weak, 5=very strong),
|
| 145 |
+
"assessment": "Very Weak/Weak/Medium/Strong/Very Strong",
|
| 146 |
+
"insights": ["Specific observations based on characteristics provided"],
|
| 147 |
+
"suggestions": ["Actionable improvement tips based on characteristics"]
|
| 148 |
+
}}
|
| 149 |
+
|
| 150 |
+
Password Characteristics:
|
| 151 |
+
Length: {password_characteristics.get('length', 0)}
|
| 152 |
+
Contains Lowercase: {'Yes' if password_characteristics.get('composition', {}).get('lowercase', 0) > 0 else 'No'}
|
| 153 |
+
Contains Uppercase: {'Yes' if password_characteristics.get('composition', {}).get('uppercase', 0) > 0 else 'No'}
|
| 154 |
+
Contains Digits: {'Yes' if password_characteristics.get('composition', {}).get('digits', 0) > 0 else 'No'}
|
| 155 |
+
Contains Special Chars: {'Yes' if password_characteristics.get('composition', {}).get('special', 0) > 0 else 'No'}
|
| 156 |
+
"""
|
| 157 |
+
response = model.generate_content(prompt)
|
| 158 |
+
|
| 159 |
+
if not response.candidates or not hasattr(response.candidates[0], 'content') or not response.candidates[0].content.parts:
|
| 160 |
+
print(f"Gemini response blocked or empty. Reason: {response.prompt_feedback}")
|
| 161 |
+
return get_basic_password_analysis_from_chars(password_characteristics)
|
| 162 |
+
|
| 163 |
+
response_text = response.text
|
| 164 |
+
json_match = re.search(r'```(?:json)?\s*({.*?})\s*```', response_text, re.DOTALL | re.IGNORECASE)
|
| 165 |
+
if not json_match: json_match = re.search(r'({.*})', response_text, re.DOTALL)
|
| 166 |
+
|
| 167 |
+
if json_match:
|
| 168 |
+
try:
|
| 169 |
+
insights_json = json.loads(json_match.group(1))
|
| 170 |
+
if all(k in insights_json for k in ['strength_score', 'assessment', 'insights', 'suggestions']):
|
| 171 |
+
return { 'strength': insights_json.get('strength_score', 1), 'assessment': insights_json.get('assessment', 'Weak'),
|
| 172 |
+
'insights': insights_json.get('insights', []), 'suggestions': insights_json.get('suggestions', []) }
|
| 173 |
+
else: raise ValueError("Missing keys in JSON response")
|
| 174 |
+
except (json.JSONDecodeError, ValueError) as json_err:
|
| 175 |
+
print(f"LLM JSON processing Error: {json_err}. Falling back.")
|
| 176 |
+
return get_basic_password_analysis_from_chars(password_characteristics)
|
| 177 |
+
else:
|
| 178 |
+
print("LLM JSON pattern not found. Falling back.")
|
| 179 |
+
return get_basic_password_analysis_from_chars(password_characteristics)
|
| 180 |
+
except Exception as e:
|
| 181 |
+
print(f"Error getting Gemini insights: {type(e).__name__} - {e}")
|
| 182 |
+
return get_basic_password_analysis_from_chars(password_characteristics)
|
| 183 |
+
|
| 184 |
+
def get_basic_password_analysis_from_chars(characteristics: dict):
|
| 185 |
+
""" Basic password analysis based on characteristics. Returns same structure as LLM."""
|
| 186 |
+
strength = 0; insights = []; suggestions = []
|
| 187 |
+
length = characteristics.get('length', 0)
|
| 188 |
+
composition = characteristics.get('composition', {})
|
| 189 |
+
# Score Calculation (simple example)
|
| 190 |
+
if length < 8: insights.append("Short (< 8 chars)"); suggestions.append("Use 12+ chars.")
|
| 191 |
+
elif length < 12: strength += 1; insights.append("Okay length (8-11)") ; suggestions.append("Use 12+ chars.")
|
| 192 |
+
else: strength += 2; insights.append("Good length (12+)")
|
| 193 |
+
if composition.get('uppercase', 0) > 0: strength += 1
|
| 194 |
+
else: insights.append("No uppercase"); suggestions.append("Add A-Z.")
|
| 195 |
+
if composition.get('lowercase', 0) > 0: strength += 1
|
| 196 |
+
else: insights.append("No lowercase"); suggestions.append("Add a-z.")
|
| 197 |
+
if composition.get('digits', 0) > 0: strength += 1
|
| 198 |
+
else: insights.append("No numbers"); suggestions.append("Add 0-9.")
|
| 199 |
+
if composition.get('special', 0) > 0: strength += 1
|
| 200 |
+
else: insights.append("No special chars"); suggestions.append("Add !@#$.")
|
| 201 |
+
# Map score (0-6) to 1-5 rating
|
| 202 |
+
score_map = {0: 1, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 5}
|
| 203 |
+
final_score = score_map.get(strength, 1)
|
| 204 |
+
assessment_map = {1: "Very Weak", 2: "Weak", 3: "Medium", 4: "Strong", 5: "Very Strong"}
|
| 205 |
+
assessment = assessment_map.get(final_score, "Weak")
|
| 206 |
+
if final_score == 5 and not insights: insights.append("Meets basic complexity.")
|
| 207 |
+
return {'strength': final_score, 'assessment': assessment, 'insights': insights, 'suggestions': suggestions}
|
| 208 |
+
|
| 209 |
+
def get_mock_llm_insights_from_chars(characteristics: dict):
|
| 210 |
+
""" Generates mock LLM insights based on characteristics."""
|
| 211 |
+
basic_analysis = get_basic_password_analysis_from_chars(characteristics)
|
| 212 |
+
if basic_analysis['strength'] < 4: basic_analysis['suggestions'].append("Consider passphrase.")
|
| 213 |
+
if not basic_analysis['insights'] and basic_analysis['strength'] >= 4: basic_analysis['insights'].append("Good length/variety.")
|
| 214 |
+
return basic_analysis
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
# --- NEW: Password Generation Function ---
|
| 218 |
+
def generate_secure_password(length=16, use_lowercase=True, use_uppercase=True, use_digits=True, use_symbols=True):
|
| 219 |
+
"""Generates a secure password ensuring character set inclusion."""
|
| 220 |
+
character_pool = []
|
| 221 |
+
required_chars = []
|
| 222 |
+
|
| 223 |
+
if use_lowercase:
|
| 224 |
+
character_pool.extend(string.ascii_lowercase)
|
| 225 |
+
required_chars.append(secrets.choice(string.ascii_lowercase))
|
| 226 |
+
if use_uppercase:
|
| 227 |
+
character_pool.extend(string.ascii_uppercase)
|
| 228 |
+
required_chars.append(secrets.choice(string.ascii_uppercase))
|
| 229 |
+
if use_digits:
|
| 230 |
+
character_pool.extend(string.digits)
|
| 231 |
+
required_chars.append(secrets.choice(string.digits))
|
| 232 |
+
if use_symbols:
|
| 233 |
+
# Define a standard set of symbols, avoid potentially problematic ones like `'"\
|
| 234 |
+
symbols = '!@#$%^&*()_-+={}[]|:;<>,.?/~'
|
| 235 |
+
character_pool.extend(symbols)
|
| 236 |
+
required_chars.append(secrets.choice(symbols))
|
| 237 |
+
|
| 238 |
+
if not character_pool:
|
| 239 |
+
raise ValueError("No character types selected for password generation.")
|
| 240 |
+
|
| 241 |
+
if length < len(required_chars):
|
| 242 |
+
raise ValueError(f"Length ({length}) is too short to include all required character types ({len(required_chars)}).")
|
| 243 |
+
|
| 244 |
+
# Fill the rest of the password length
|
| 245 |
+
remaining_length = length - len(required_chars)
|
| 246 |
+
password_chars = required_chars + [secrets.choice(character_pool) for _ in range(remaining_length)]
|
| 247 |
+
|
| 248 |
+
# Shuffle the list to ensure randomness
|
| 249 |
+
random.SystemRandom().shuffle(password_chars) # Use system random for better shuffling
|
| 250 |
+
|
| 251 |
+
return "".join(password_chars)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
# --- Routes ---
|
| 255 |
+
|
| 256 |
+
@app.route('/')
|
| 257 |
+
def home():
|
| 258 |
+
if current_user.is_authenticated: return redirect(url_for('add_password_page'))
|
| 259 |
+
else: return redirect(url_for('login'))
|
| 260 |
+
|
| 261 |
+
@app.route('/login', methods=['GET', 'POST'])
|
| 262 |
+
def login():
|
| 263 |
+
if current_user.is_authenticated: return redirect(url_for('add_password_page'))
|
| 264 |
+
if request.method == 'POST':
|
| 265 |
+
email = request.form.get('email', '').strip(); password = request.form.get('password', '')
|
| 266 |
+
# ** GET REMEMBER ME VALUE **
|
| 267 |
+
remember_me = 'remember' in request.form # True if checkbox was ticked
|
| 268 |
+
|
| 269 |
+
if not email or not password:
|
| 270 |
+
flash('Email and Master Password are required.', 'danger'); return render_template('login.html'), 400
|
| 271 |
+
try:
|
| 272 |
+
response = supabase.table('users').select("id, email, password_hash").eq('email', email).maybe_single().execute()
|
| 273 |
+
if hasattr(response, 'data') and response.data:
|
| 274 |
+
user_data = response.data; stored_hash = user_data['password_hash']
|
| 275 |
+
if bcrypt.check_password_hash(stored_hash, password):
|
| 276 |
+
user_obj = User(id=user_data['id'], email=user_data['email'])
|
| 277 |
+
# ** PASS REMEMBER ME TO login_user **
|
| 278 |
+
login_user(user_obj, remember=remember_me)
|
| 279 |
+
# Flask-Login sets session.permanent based on 'remember' flag
|
| 280 |
+
|
| 281 |
+
session['user_data'] = {'id': user_data['id'], 'email': user_data['email']}
|
| 282 |
+
# Derive key and store URL-safe Base64 version in session
|
| 283 |
+
derived_key_b64url = derive_key(password, user_data['email'])
|
| 284 |
+
session['encryption_key'] = derived_key_b64url.decode('utf-8')
|
| 285 |
+
flash('Logged in successfully!', 'success')
|
| 286 |
+
next_page = request.args.get('next')
|
| 287 |
+
return redirect(next_page or url_for('add_password_page'))
|
| 288 |
+
else: flash('Login failed. Invalid email or password.', 'danger')
|
| 289 |
+
else: flash('Login failed. Invalid email or password.', 'danger')
|
| 290 |
+
return render_template('login.html'), 401
|
| 291 |
+
except Exception as e:
|
| 292 |
+
flash(f'An error occurred during login. Please try again.', 'danger')
|
| 293 |
+
print(f"Login Exception for {email}: {type(e).__name__} - {e}"); traceback.print_exc()
|
| 294 |
+
return render_template('login.html'), 500
|
| 295 |
+
return render_template('login.html')
|
| 296 |
+
|
| 297 |
+
@app.route('/register', methods=['GET', 'POST'])
|
| 298 |
+
def register():
|
| 299 |
+
if current_user.is_authenticated: return redirect(url_for('add_password_page'))
|
| 300 |
+
if request.method == 'POST':
|
| 301 |
+
email = request.form.get('email', '').strip()
|
| 302 |
+
password = request.form.get('password', ''); confirm_password = request.form.get('confirm_password', '')
|
| 303 |
+
if not email or not password or not confirm_password: flash('All fields are required.', 'danger'); return render_template('register.html'), 400
|
| 304 |
+
if password != confirm_password: flash('Passwords do not match.', 'danger'); return render_template('register.html'), 400
|
| 305 |
+
try:
|
| 306 |
+
check_response = supabase.table('users').select("id", count='exact').eq('email', email).execute()
|
| 307 |
+
if hasattr(check_response, 'count') and check_response.count is not None and check_response.count > 0:
|
| 308 |
+
flash('Email address is already registered. Please login.', 'warning'); return redirect(url_for('login'))
|
| 309 |
+
elif not hasattr(check_response, 'count'): print(f"WARNING: Supabase check for {email} lacked 'count': {check_response}")
|
| 310 |
+
|
| 311 |
+
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
|
| 312 |
+
insert_response = supabase.table('users').insert({'email': email, 'password_hash': hashed_password}).execute()
|
| 313 |
+
if hasattr(insert_response, 'data') and insert_response.data:
|
| 314 |
+
flash('Registration successful! Please log in.', 'success'); return redirect(url_for('login'))
|
| 315 |
+
else:
|
| 316 |
+
print(f"ERROR: Registration failed for {email}. Response: {insert_response}")
|
| 317 |
+
flash("Registration failed due to a server error.", 'danger')
|
| 318 |
+
return render_template('register.html'), 500
|
| 319 |
+
except Exception as e:
|
| 320 |
+
flash(f'An error occurred during registration. Please try again.', 'danger')
|
| 321 |
+
print(f"Registration Exception for {email}: {type(e).__name__} - {e}"); traceback.print_exc()
|
| 322 |
+
return render_template('register.html'), 500
|
| 323 |
+
return render_template('register.html')
|
| 324 |
+
|
| 325 |
+
@app.route('/logout')
|
| 326 |
+
@login_required
|
| 327 |
+
def logout():
|
| 328 |
+
session.pop('encryption_key', None); session.pop('user_data', None)
|
| 329 |
+
logout_user()
|
| 330 |
+
flash('You have been logged out successfully.', 'success')
|
| 331 |
+
return redirect(url_for('login'))
|
| 332 |
+
|
| 333 |
+
# --- Main Application Pages (Require Login) ---
|
| 334 |
+
|
| 335 |
+
@app.route('/add')
|
| 336 |
+
@login_required
|
| 337 |
+
def add_password_page(): return render_template('index.html')
|
| 338 |
+
|
| 339 |
+
@app.route('/storage')
|
| 340 |
+
@login_required
|
| 341 |
+
def storage(): return render_template('storage.html')
|
| 342 |
+
|
| 343 |
+
@app.route('/analyse')
|
| 344 |
+
@login_required
|
| 345 |
+
def analyse(): return render_template('analyse.html')
|
| 346 |
+
|
| 347 |
+
# --- API Endpoints (Require Login) ---
|
| 348 |
+
|
| 349 |
+
@app.route('/api/credentials', methods=['GET'])
|
| 350 |
+
@login_required
|
| 351 |
+
def get_credentials():
|
| 352 |
+
user_id = current_user.id
|
| 353 |
+
try:
|
| 354 |
+
response = supabase.table('credentials').select("id, encrypted_data, service_hint, created_at").eq('user_id', user_id).order('created_at', desc=True).execute()
|
| 355 |
+
# Check response structure for Supabase v2 (data attribute is primary)
|
| 356 |
+
if hasattr(response, 'data'):
|
| 357 |
+
return jsonify(response.data)
|
| 358 |
+
else: # Fallback or error logging if structure changes
|
| 359 |
+
print(f"Supabase get credentials unexpected response for user {user_id}. Response: {response}")
|
| 360 |
+
return jsonify({'error': 'Failed to retrieve credentials'}), 500
|
| 361 |
+
except Exception as e:
|
| 362 |
+
print(f"Error in get_credentials for user {user_id}: {e}"); traceback.print_exc()
|
| 363 |
+
return jsonify({'error': 'Server error retrieving credentials'}), 500
|
| 364 |
+
|
| 365 |
+
@app.route('/api/credentials', methods=['POST'])
|
| 366 |
+
@login_required
|
| 367 |
+
def add_credential():
|
| 368 |
+
data = request.json; user_id = current_user.id
|
| 369 |
+
encrypted_data = data.get('encrypted_data'); service_hint = data.get('service_hint')
|
| 370 |
+
if not encrypted_data: return jsonify({'success': False, 'message': 'Encrypted data payload is required.'}), 400
|
| 371 |
+
try:
|
| 372 |
+
insert_response = supabase.table('credentials').insert({'user_id': user_id, 'encrypted_data': encrypted_data, 'service_hint': service_hint}).execute()
|
| 373 |
+
# Check response structure for Supabase v2
|
| 374 |
+
if hasattr(insert_response, 'data') and insert_response.data:
|
| 375 |
+
return jsonify({'success': True, 'message': 'Credential saved successfully.'})
|
| 376 |
+
else:
|
| 377 |
+
print(f"Supabase insert credentials error for user {user_id}. Response: {insert_response}")
|
| 378 |
+
# Attempt to get more specific error if available (structure might vary)
|
| 379 |
+
error_msg = "Failed to save credential"
|
| 380 |
+
if hasattr(insert_response, 'error') and insert_response.error and hasattr(insert_response.error, 'message'):
|
| 381 |
+
error_msg += f": {insert_response.error.message}"
|
| 382 |
+
return jsonify({'success': False, 'message': error_msg}), 500
|
| 383 |
+
except Exception as e:
|
| 384 |
+
print(f"Error in add_credential for user {user_id}: {e}"); traceback.print_exc()
|
| 385 |
+
return jsonify({'success': False, 'message': 'Server error saving credential'}), 500
|
| 386 |
+
|
| 387 |
+
@app.route('/api/analyse_password', methods=['POST'])
|
| 388 |
+
@login_required
|
| 389 |
+
def analyse_password_api():
|
| 390 |
+
data = request.json; characteristics = data.get('characteristics')
|
| 391 |
+
if not characteristics or not isinstance(characteristics, dict):
|
| 392 |
+
return jsonify({'error': 'Password characteristics payload is required.'}), 400
|
| 393 |
+
analysis = get_llm_password_insights(characteristics)
|
| 394 |
+
feedback = []
|
| 395 |
+
if 'insights' in analysis: feedback.extend([f"Issue: {ins}" for ins in analysis['insights'] if ins])
|
| 396 |
+
if 'suggestions' in analysis: feedback.extend([f"Tip: {sug}" for sug in analysis['suggestions'] if sug])
|
| 397 |
+
if not feedback:
|
| 398 |
+
if analysis.get('strength', 0) == 5: feedback.append("Tip: Looks good based on characteristics!")
|
| 399 |
+
else: feedback.append("Issue: Review password based on assessment.")
|
| 400 |
+
return jsonify({ 'strength': analysis.get('strength', 1), 'assessment': analysis.get('assessment', 'Weak'), 'feedback': feedback })
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
# --- NEW: Password Generation API Endpoint ---
|
| 404 |
+
@app.route('/api/generate_password', methods=['POST']) # Using POST for consistency
|
| 405 |
+
@login_required
|
| 406 |
+
def generate_password_api():
|
| 407 |
+
try:
|
| 408 |
+
data = request.get_json(silent=True)
|
| 409 |
+
|
| 410 |
+
if data is None:
|
| 411 |
+
data = request.args.to_dict()
|
| 412 |
+
if not data:
|
| 413 |
+
data = {}
|
| 414 |
+
|
| 415 |
+
# Safely get length
|
| 416 |
+
try:
|
| 417 |
+
length_str = data.get('length', '16') # Default to string '16'
|
| 418 |
+
length = int(length_str)
|
| 419 |
+
if not (4 <= length <= 128):
|
| 420 |
+
return jsonify({'error': 'Length must be between 4 and 128.'}), 400
|
| 421 |
+
except (ValueError, TypeError):
|
| 422 |
+
return jsonify({'error': f'Invalid length parameter: "{length_str}". Must be an integer.'}), 400
|
| 423 |
+
|
| 424 |
+
# Helper for robust boolean parsing from various request inputs
|
| 425 |
+
def parse_bool(key, default_value):
|
| 426 |
+
value = data.get(key, default_value) # Get value or default
|
| 427 |
+
if isinstance(value, bool): return value
|
| 428 |
+
if isinstance(value, str):
|
| 429 |
+
low_val = value.lower()
|
| 430 |
+
if low_val in ['true', '1', 'yes', 'y']: return True
|
| 431 |
+
if low_val in ['false', '0', 'no', 'n']: return False
|
| 432 |
+
# Check for integer 1/0 as well
|
| 433 |
+
if isinstance(value, int) and value in [0, 1]:
|
| 434 |
+
return bool(value)
|
| 435 |
+
# If it's none of the above, return the original default
|
| 436 |
+
# print(f"Warning: Unexpected type for boolean parse '{key}': {type(value)}, using default: {default_value}")
|
| 437 |
+
return default_value
|
| 438 |
+
|
| 439 |
+
use_lowercase = parse_bool('use_lowercase', True)
|
| 440 |
+
use_uppercase = parse_bool('use_uppercase', True)
|
| 441 |
+
use_digits = parse_bool('use_digits', True)
|
| 442 |
+
use_symbols = parse_bool('use_symbols', True)
|
| 443 |
+
|
| 444 |
+
if not any([use_lowercase, use_uppercase, use_digits, use_symbols]):
|
| 445 |
+
return jsonify({'error': 'At least one character type must be selected.'}), 400
|
| 446 |
+
|
| 447 |
+
password = generate_secure_password(length, use_lowercase, use_uppercase, use_digits, use_symbols)
|
| 448 |
+
return jsonify({'password': password})
|
| 449 |
+
|
| 450 |
+
except ValueError as ve:
|
| 451 |
+
print(f"Password generation validation error: {ve}")
|
| 452 |
+
return jsonify({'error': str(ve)}), 400
|
| 453 |
+
except Exception as e:
|
| 454 |
+
print(f"Unexpected error in generate_password_api: {type(e).__name__} - {e}")
|
| 455 |
+
traceback.print_exc()
|
| 456 |
+
return jsonify({'error': 'Server error generating password.'}), 500
|
| 457 |
+
|
| 458 |
+
# --- Helper for zxcvbn analysis ---
|
| 459 |
+
# (Keep the import from zxcvbn at the top)
|
| 460 |
+
# from zxcvbn import zxcvbn # Already should be there
|
| 461 |
+
|
| 462 |
+
def get_zxcvbn_feedback(password):
|
| 463 |
+
"""Analyzes password using zxcvbn and formats feedback."""
|
| 464 |
+
results = zxcvbn(password)
|
| 465 |
+
score = results['score'] # Score 0-4
|
| 466 |
+
feedback_items = []
|
| 467 |
+
|
| 468 |
+
# Map score to assessment
|
| 469 |
+
assessment_map = {
|
| 470 |
+
0: "Very Weak", 1: "Weak", 2: "Medium", 3: "Strong", 4: "Very Strong"
|
| 471 |
+
}
|
| 472 |
+
assessment = assessment_map.get(score, "Unknown")
|
| 473 |
+
|
| 474 |
+
# Add warning if present
|
| 475 |
+
if results['feedback']['warning']:
|
| 476 |
+
feedback_items.append(f"Warning: {results['feedback']['warning']}")
|
| 477 |
+
# Add suggestions
|
| 478 |
+
if results['feedback']['suggestions']:
|
| 479 |
+
feedback_items.extend([f"Suggestion: {s}" for s in results['feedback']['suggestions']])
|
| 480 |
+
|
| 481 |
+
# Add a generic suggestion if feedback is empty but score is low
|
| 482 |
+
if not feedback_items and score < 3:
|
| 483 |
+
feedback_items.append("Suggestion: Add more variety (uppercase, numbers, symbols) or increase length.")
|
| 484 |
+
elif not feedback_items and score >= 3:
|
| 485 |
+
feedback_items.append("Suggestion: Looks reasonably strong!")
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
# Return without calc_time which is not JSON serializable
|
| 489 |
+
return {
|
| 490 |
+
'score': score, # 0-4
|
| 491 |
+
'assessment': assessment,
|
| 492 |
+
'feedback': feedback_items,
|
| 493 |
+
'guesses': results['guesses'], # Optional: include estimated guesses if needed
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
@app.route('/api/strength_check', methods=['POST'])
|
| 497 |
+
@login_required
|
| 498 |
+
def strength_check_api():
|
| 499 |
+
"""Receives a password and returns its zxcvbn strength analysis."""
|
| 500 |
+
data = request.get_json()
|
| 501 |
+
if not data or 'password' not in data:
|
| 502 |
+
return jsonify({'error': 'Password key missing in request body.'}), 400
|
| 503 |
+
|
| 504 |
+
password = data['password']
|
| 505 |
+
if not password: # Handle empty password case
|
| 506 |
+
return jsonify({'score': 0, 'assessment': 'Very Weak', 'feedback': ['Suggestion: Please enter a password.']})
|
| 507 |
+
|
| 508 |
+
try:
|
| 509 |
+
analysis = get_zxcvbn_feedback(password)
|
| 510 |
+
return jsonify(analysis)
|
| 511 |
+
except Exception as e:
|
| 512 |
+
print(f"Error during zxcvbn strength check: {e}")
|
| 513 |
+
traceback.print_exc()
|
| 514 |
+
# Return a generic low score on error, rather than crashing
|
| 515 |
+
return jsonify({'score': 0, 'assessment': 'Error', 'feedback': ['Error analyzing password strength.']}), 500
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
if __name__ == '__main__':
|
| 519 |
+
port = int(os.environ.get('PORT', 5000))
|
| 520 |
+
app.run(host='0.0.0.0', port=port, debug=app.config['DEBUG'])
|
| 521 |
+
# --- END OF FILE app.py ---
|
extension/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Blockchain Password Manager Extension
|
| 2 |
+
|
| 3 |
+
A Chrome extension for the Blockchain Password Manager that allows you to securely store and manage your passwords.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- Secure password storage using blockchain technology
|
| 8 |
+
- Auto-fill passwords on websites
|
| 9 |
+
- Easy password management through browser extension
|
| 10 |
+
- One-click password copying
|
| 11 |
+
- Secure login system
|
| 12 |
+
|
| 13 |
+
## Installation
|
| 14 |
+
|
| 15 |
+
1. Make sure the Blockchain Password Manager backend is running on `http://localhost:5000`
|
| 16 |
+
2. Open Chrome and go to `chrome://extensions/`
|
| 17 |
+
3. Enable "Developer mode" in the top right corner
|
| 18 |
+
4. Click "Load unpacked" and select the `extension` folder
|
| 19 |
+
5. The extension should now be installed and visible in your Chrome toolbar
|
| 20 |
+
|
| 21 |
+
## Usage
|
| 22 |
+
|
| 23 |
+
1. Click the extension icon in your Chrome toolbar
|
| 24 |
+
2. Log in with your credentials
|
| 25 |
+
3. To add a new password:
|
| 26 |
+
- Click "Add New Password"
|
| 27 |
+
- Fill in the service, username, and password
|
| 28 |
+
- Click "Save"
|
| 29 |
+
4. To use a saved password:
|
| 30 |
+
- Click on the password in the list to copy it to clipboard
|
| 31 |
+
- Or visit the website and click the green icon that appears on the login form
|
| 32 |
+
|
| 33 |
+
## Security Notes
|
| 34 |
+
|
| 35 |
+
- All passwords are stored securely in the blockchain
|
| 36 |
+
- Passwords are never stored in plain text
|
| 37 |
+
- The extension only communicates with your local blockchain instance
|
| 38 |
+
- Make sure to keep your login credentials secure
|
| 39 |
+
|
| 40 |
+
## Development
|
| 41 |
+
|
| 42 |
+
To modify the extension:
|
| 43 |
+
|
| 44 |
+
1. Make changes to the files in the `extension` folder
|
| 45 |
+
2. Go to `chrome://extensions/`
|
| 46 |
+
3. Find the extension and click the refresh icon
|
| 47 |
+
4. The changes will be applied
|
| 48 |
+
|
| 49 |
+
## Requirements
|
| 50 |
+
|
| 51 |
+
- Chrome browser
|
| 52 |
+
- Running instance of the Blockchain Password Manager backend
|
| 53 |
+
- Internet connection (for blockchain operations)
|
extension/background.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// --- background.js ---
|
| 2 |
+
|
| 3 |
+
let inMemoryKeyB64 = null; // Store Base64 string
|
| 4 |
+
let userEmailForSession = null;
|
| 5 |
+
let keyExpiryTimer = null;
|
| 6 |
+
const KEY_EXPIRY_MINUTES = 30;
|
| 7 |
+
|
| 8 |
+
// --- Helper Functions ---
|
| 9 |
+
// Necessary for converting back when GETTING the key if needed elsewhere,
|
| 10 |
+
// but not strictly needed for storing/retrieving the B64 string itself.
|
| 11 |
+
// Including for completeness/potential future use.
|
| 12 |
+
function base64ToArrayBuffer(base64) {
|
| 13 |
+
try {
|
| 14 |
+
const binary_string = atob(base64); // Use global atob
|
| 15 |
+
const len = binary_string.length;
|
| 16 |
+
const bytes = new Uint8Array(len);
|
| 17 |
+
for (let i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); }
|
| 18 |
+
return bytes.buffer;
|
| 19 |
+
} catch (e) {
|
| 20 |
+
console.error("[Background] Base64 decoding failed:", e);
|
| 21 |
+
throw new Error("Invalid Base64 string.");
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function clearKeyAndTimer() {
|
| 26 |
+
console.log("[Background] Clearing key and timer...");
|
| 27 |
+
inMemoryKeyB64 = null; // Clear the B64 string
|
| 28 |
+
userEmailForSession = null;
|
| 29 |
+
if (keyExpiryTimer) { clearTimeout(keyExpiryTimer); keyExpiryTimer = null; }
|
| 30 |
+
console.log("[Background] In-memory key cleared.");
|
| 31 |
+
// chrome.action.setIcon({ path: "icons/icon128_locked.png" }).catch(e => console.warn("Error setting icon:", e));
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function resetKeyExpiryTimer() {
|
| 35 |
+
if (keyExpiryTimer) { clearTimeout(keyExpiryTimer); }
|
| 36 |
+
if (inMemoryKeyB64) { // Check if B64 string exists
|
| 37 |
+
keyExpiryTimer = setTimeout(clearKeyAndTimer, KEY_EXPIRY_MINUTES * 60 * 1000);
|
| 38 |
+
console.log(`[Background] Key expiry timer reset (${KEY_EXPIRY_MINUTES} mins). Timer ID: ${keyExpiryTimer}`);
|
| 39 |
+
} else {
|
| 40 |
+
console.log("[Background] No key exists, expiry timer not set.");
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Listener for messages
|
| 45 |
+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
| 46 |
+
const senderType = sender.tab ? `content script (${sender.tab.url})` : `popup/extension (${sender.id})`;
|
| 47 |
+
console.log(`[Background] Received message: ACTION=${request.action} from ${senderType}`);
|
| 48 |
+
|
| 49 |
+
if (!sender.tab) { resetKeyExpiryTimer(); }
|
| 50 |
+
|
| 51 |
+
try {
|
| 52 |
+
if (request.action === 'storeKey') {
|
| 53 |
+
console.log("[Background] Handling 'storeKey'");
|
| 54 |
+
// *** THIS IS THE CORRECTED CHECK ***
|
| 55 |
+
if (typeof request.keyB64 === 'string' && request.keyB64.length > 10 && request.email) { // Check for keyB64 string
|
| 56 |
+
inMemoryKeyB64 = request.keyB64; // Store Base64 string
|
| 57 |
+
userEmailForSession = request.email;
|
| 58 |
+
console.log("[Background] Key (Base64) stored successfully for", userEmailForSession);
|
| 59 |
+
resetKeyExpiryTimer();
|
| 60 |
+
sendResponse({ success: true });
|
| 61 |
+
} else {
|
| 62 |
+
// *** THIS LOG MATCHES THE EXPECTATION OF keyB64 ***
|
| 63 |
+
console.error("[Background] StoreKey request missing valid keyB64 (string) or email.");
|
| 64 |
+
sendResponse({ success: false, error: "Missing valid keyB64 or email" });
|
| 65 |
+
}
|
| 66 |
+
return false; // Synchronous response
|
| 67 |
+
|
| 68 |
+
} else if (request.action === 'getKey') {
|
| 69 |
+
console.log("[Background] Handling 'getKey'. Key (B64) exists?", !!inMemoryKeyB64);
|
| 70 |
+
if (inMemoryKeyB64 && userEmailForSession) {
|
| 71 |
+
console.log("[Background] Key (Base64) found for", userEmailForSession);
|
| 72 |
+
// Send Base64 string back
|
| 73 |
+
sendResponse({ success: true, keyB64: inMemoryKeyB64, email: userEmailForSession });
|
| 74 |
+
} else {
|
| 75 |
+
console.log("[Background] Key not found in memory.");
|
| 76 |
+
sendResponse({ success: false });
|
| 77 |
+
}
|
| 78 |
+
// **MUST return true for potential async response pathway**
|
| 79 |
+
// Although sendResponse is called synchronously here, message listeners
|
| 80 |
+
// should generally return true if they might ever call sendResponse later.
|
| 81 |
+
return true;
|
| 82 |
+
|
| 83 |
+
} else if (request.action === 'clearKey') {
|
| 84 |
+
console.log("[Background] Handling 'clearKey'");
|
| 85 |
+
clearKeyAndTimer();
|
| 86 |
+
sendResponse({ success: true });
|
| 87 |
+
return false; // Synchronous response
|
| 88 |
+
|
| 89 |
+
} else if (request.action === 'getEmail') {
|
| 90 |
+
console.log("[Background] Handling 'getEmail'. Email stored?", userEmailForSession || "No");
|
| 91 |
+
sendResponse({success: true, email: userEmailForSession });
|
| 92 |
+
return false; // Synchronous response
|
| 93 |
+
}
|
| 94 |
+
else {
|
| 95 |
+
console.log("[Background] Unknown action received:", request.action);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
} catch (error) {
|
| 99 |
+
console.error(`[Background] Error handling action '${request.action}':`, error);
|
| 100 |
+
try { sendResponse({ success: false, error: `Background script error: ${error.message}` }); }
|
| 101 |
+
catch (responseError) { console.error("[Background] Failed to send error response:", responseError); }
|
| 102 |
+
return false;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// If no specific handler matched and returned, return false by default.
|
| 106 |
+
// However, the 'getKey' handler MUST return true.
|
| 107 |
+
// The structure above handles this correctly now.
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
// Initial setup log
|
| 111 |
+
console.log("[Background] Service worker script loaded and listener attached.");
|
extension/content.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Listen for messages from the popup (for autofill)
|
| 2 |
+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
| 3 |
+
if (request.action === 'fillPassword' && request.password) {
|
| 4 |
+
fillPassword(request.password, request.username); // Pass username too
|
| 5 |
+
sendResponse({ success: true }); // Acknowledge message received
|
| 6 |
+
} else if (request.action === 'getDomain') {
|
| 7 |
+
// Respond with the current page's domain if requested (e.g., by popup)
|
| 8 |
+
sendResponse({ domain: window.location.hostname });
|
| 9 |
+
}
|
| 10 |
+
// Keep the listener alive for asynchronous responses if needed
|
| 11 |
+
// return true;
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
// Function to fill username and password in the current page
|
| 15 |
+
function fillPassword(password, username) {
|
| 16 |
+
// --- Password Field Detection ---
|
| 17 |
+
// Prioritize specific attributes often used for login
|
| 18 |
+
let passwordInput = document.querySelector('input[type="password"][autocomplete*="current-password"]');
|
| 19 |
+
if (!passwordInput) {
|
| 20 |
+
// More robust fallback: Find any visible password field
|
| 21 |
+
const visiblePasswordFields = Array.from(document.querySelectorAll('input[type="password"]'))
|
| 22 |
+
.filter(el => el.offsetParent !== null && !el.disabled && !el.readOnly);
|
| 23 |
+
if(visiblePasswordFields.length > 0) {
|
| 24 |
+
passwordInput = visiblePasswordFields[0]; // Use the first visible one
|
| 25 |
+
// If multiple, could try to find one near the username field later
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// --- Username Field Detection (More Heuristic) ---
|
| 30 |
+
let usernameInput = null;
|
| 31 |
+
if (passwordInput) { // Try finding username relative to password field
|
| 32 |
+
const form = passwordInput.closest('form');
|
| 33 |
+
if (form) {
|
| 34 |
+
// Look within the same form first
|
| 35 |
+
usernameInput = form.querySelector('input[type="email"], input[type="text"][autocomplete*="username"], input[type="tel"][autocomplete*="username"]');
|
| 36 |
+
if (!usernameInput) {
|
| 37 |
+
// Fallback: Look for any email/text/tel field before the password field in the form
|
| 38 |
+
const inputs = Array.from(form.querySelectorAll('input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"])'));
|
| 39 |
+
const passwordIndex = inputs.indexOf(passwordInput);
|
| 40 |
+
if (passwordIndex > 0) {
|
| 41 |
+
// Look backwards from password field
|
| 42 |
+
for(let i = passwordIndex - 1; i >= 0; i--) {
|
| 43 |
+
if(inputs[i].type === 'text' || inputs[i].type === 'email' || inputs[i].type === 'tel'){
|
| 44 |
+
usernameInput = inputs[i];
|
| 45 |
+
break;
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
// Absolute fallback if no form or no input found yet
|
| 53 |
+
if (!usernameInput) {
|
| 54 |
+
usernameInput = document.querySelector('input[type="email"], input[type="text"][autocomplete*="username"], input[type="tel"][autocomplete*="username"]');
|
| 55 |
+
}
|
| 56 |
+
// Final fallback: first visible text/email/tel input if still nothing
|
| 57 |
+
if(!usernameInput){
|
| 58 |
+
const visibleUserFields = Array.from(document.querySelectorAll('input[type="text"], input[type="email"], input[type="tel"]'))
|
| 59 |
+
.filter(el => el.offsetParent !== null && !el.disabled && !el.readOnly && el.type !== 'search');
|
| 60 |
+
if(visibleUserFields.length > 0){
|
| 61 |
+
// Maybe check if it's *before* the password field in the DOM as a final heuristic?
|
| 62 |
+
const passwordFieldDomOrder = passwordInput ? Array.from(document.querySelectorAll('input')).indexOf(passwordInput) : Infinity;
|
| 63 |
+
const potentialUserField = visibleUserFields.find(uf => Array.from(document.querySelectorAll('input')).indexOf(uf) < passwordFieldDomOrder);
|
| 64 |
+
usernameInput = potentialUserField || visibleUserFields[0];
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
// --- Filling Logic ---
|
| 70 |
+
let filledPassword = false;
|
| 71 |
+
let filledUsername = false;
|
| 72 |
+
|
| 73 |
+
if (passwordInput) {
|
| 74 |
+
setInputValue(passwordInput, password);
|
| 75 |
+
filledPassword = true;
|
| 76 |
+
console.log("Secure PWM: Password field found and filled.");
|
| 77 |
+
} else {
|
| 78 |
+
console.warn("Secure PWM: No suitable password input field found on this page.");
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
if (username && usernameInput) {
|
| 82 |
+
setInputValue(usernameInput, username);
|
| 83 |
+
filledUsername = true;
|
| 84 |
+
console.log("Secure PWM: Username field found and filled.");
|
| 85 |
+
} else if (username && !usernameInput) {
|
| 86 |
+
console.warn("Secure PWM: Username provided but no suitable input field found.");
|
| 87 |
+
} else if (!username) {
|
| 88 |
+
console.log("Secure PWM: No username provided to fill.");
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
// Optional: Attempt to focus the next logical element (e.g., login button)
|
| 93 |
+
// if (filledPassword && filledUsername) {
|
| 94 |
+
// // Try finding a submit button in the same form
|
| 95 |
+
// }
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// Helper to set value and dispatch events for better compatibility
|
| 99 |
+
function setInputValue(inputElement, value) {
|
| 100 |
+
inputElement.focus();
|
| 101 |
+
inputElement.value = value;
|
| 102 |
+
inputElement.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
| 103 |
+
inputElement.dispatchEvent(new Event('change', { bubbles: true }));
|
| 104 |
+
// inputElement.blur(); // Sometimes needed, sometimes breaks things. Test carefully.
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
// --- (Optional but Recommended) Icon Injection ---
|
| 109 |
+
|
| 110 |
+
// Function to detect login forms and add an icon
|
| 111 |
+
function detectAndMarkForms() {
|
| 112 |
+
// Remove existing icons first to avoid duplicates on dynamic updates
|
| 113 |
+
document.querySelectorAll('.secure-pwm-icon').forEach(icon => icon.remove());
|
| 114 |
+
|
| 115 |
+
const forms = document.querySelectorAll('form');
|
| 116 |
+
forms.forEach((form) => {
|
| 117 |
+
const hasPasswordField = form.querySelector('input[type="password"]');
|
| 118 |
+
// Simple check for a potential username/email field nearby
|
| 119 |
+
const hasUsernameField = form.querySelector('input[type="email"], input[type="text"], input[type="tel"]');
|
| 120 |
+
|
| 121 |
+
if (hasPasswordField && hasUsernameField) {
|
| 122 |
+
// Check if form or password field is likely visible
|
| 123 |
+
if(hasPasswordField.offsetParent === null) return; // Skip hidden fields
|
| 124 |
+
|
| 125 |
+
const passwordField = hasPasswordField;
|
| 126 |
+
|
| 127 |
+
// Create and style the icon
|
| 128 |
+
const icon = document.createElement('div');
|
| 129 |
+
icon.className = 'secure-pwm-icon'; // Class for identification and removal
|
| 130 |
+
icon.title = 'Fill with Secure Password Manager';
|
| 131 |
+
icon.style.cssText = `
|
| 132 |
+
position: absolute;
|
| 133 |
+
width: 18px;
|
| 134 |
+
height: 18px;
|
| 135 |
+
background-color: #28a745; /* Green */
|
| 136 |
+
background-image: url('${chrome.runtime.getURL("icons/icon16.png")}'); /* Use extension icon */
|
| 137 |
+
background-size: 12px 12px; /* Adjust size */
|
| 138 |
+
background-repeat: no-repeat;
|
| 139 |
+
background-position: center;
|
| 140 |
+
border: 1px solid #1e7e34;
|
| 141 |
+
border-radius: 50%;
|
| 142 |
+
cursor: pointer;
|
| 143 |
+
z-index: 9999;
|
| 144 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
| 145 |
+
/* Attempt to position inside/near the right edge of the password field */
|
| 146 |
+
top: ${passwordField.offsetTop + (passwordField.offsetHeight / 2) - 9}px;
|
| 147 |
+
left: ${passwordField.offsetLeft + passwordField.offsetWidth - 22}px;
|
| 148 |
+
`;
|
| 149 |
+
|
| 150 |
+
// Ensure the *parent* of the input allows absolute positioning relative to it
|
| 151 |
+
const inputWrapper = passwordField.parentElement;
|
| 152 |
+
if (inputWrapper && getComputedStyle(inputWrapper).position === 'static') {
|
| 153 |
+
inputWrapper.style.position = 'relative'; // Make parent relative if needed
|
| 154 |
+
}
|
| 155 |
+
// Append the icon to the input's parent wrapper if possible, or form
|
| 156 |
+
if (inputWrapper) {
|
| 157 |
+
inputWrapper.appendChild(icon);
|
| 158 |
+
} else if (form && getComputedStyle(form).position !== 'static') {
|
| 159 |
+
form.appendChild(icon); // Fallback to form if parent is tricky
|
| 160 |
+
} else {
|
| 161 |
+
// Less ideal: append to body, requires calculating absolute coords
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
// Add click handler to the icon (optional - could trigger popup)
|
| 166 |
+
icon.addEventListener('click', (e) => {
|
| 167 |
+
e.stopPropagation(); // Prevent form submission if icon is inside button area
|
| 168 |
+
// Send message to background/popup to show relevant entries for this domain
|
| 169 |
+
console.log("Secure PWM icon clicked. Requesting relevant passwords.");
|
| 170 |
+
chrome.runtime.sendMessage({ action: 'showRelevantPasswords', domain: window.location.hostname });
|
| 171 |
+
});
|
| 172 |
+
}
|
| 173 |
+
});
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// Run form detection with debouncing
|
| 177 |
+
let debounceTimer;
|
| 178 |
+
const observer = new MutationObserver(() => {
|
| 179 |
+
clearTimeout(debounceTimer);
|
| 180 |
+
debounceTimer = setTimeout(detectAndMarkForms, 300); // Adjust delay as needed
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
// Initial detection and observation setup
|
| 184 |
+
if (document.readyState === "loading") {
|
| 185 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 186 |
+
detectAndMarkForms();
|
| 187 |
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['type', 'style', 'class', 'hidden', 'disabled'] });
|
| 188 |
+
});
|
| 189 |
+
} else {
|
| 190 |
+
detectAndMarkForms(); // Already loaded
|
| 191 |
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['type', 'style', 'class', 'hidden', 'disabled'] });
|
| 192 |
+
}
|
extension/crypto-helpers.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// --- crypto-helpers.js ---
|
| 2 |
+
// Helper functions for Web Crypto API and Base64 Handling
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Converts a Base64URL encoded string to an ArrayBuffer.
|
| 6 |
+
* Needed for decoding the key stored in Flask session.
|
| 7 |
+
* @param {string} b64url - Base64URL encoded string.
|
| 8 |
+
* @returns {ArrayBuffer}
|
| 9 |
+
*/
|
| 10 |
+
function base64UrlDecode(b64url) {
|
| 11 |
+
try {
|
| 12 |
+
let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
|
| 13 |
+
while (b64.length % 4) {
|
| 14 |
+
b64 += '=';
|
| 15 |
+
}
|
| 16 |
+
return base64ToArrayBuffer(b64);
|
| 17 |
+
} catch (e) {
|
| 18 |
+
console.error("Base64URL decoding failed:", e);
|
| 19 |
+
throw new Error("Invalid Base64URL string for key decoding.");
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/**
|
| 24 |
+
* Converts a standard Base64 string to an ArrayBuffer.
|
| 25 |
+
* @param {string} base64 - Standard Base64 encoded string.
|
| 26 |
+
* @returns {ArrayBuffer}
|
| 27 |
+
*/
|
| 28 |
+
function base64ToArrayBuffer(base64) {
|
| 29 |
+
try {
|
| 30 |
+
const binary_string = window.atob(base64);
|
| 31 |
+
const len = binary_string.length;
|
| 32 |
+
const bytes = new Uint8Array(len);
|
| 33 |
+
for (let i = 0; i < len; i++) {
|
| 34 |
+
bytes[i] = binary_string.charCodeAt(i);
|
| 35 |
+
}
|
| 36 |
+
return bytes.buffer;
|
| 37 |
+
} catch (e) {
|
| 38 |
+
console.error("Base64 decoding failed:", e);
|
| 39 |
+
throw new Error("Invalid Base64 string.");
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Converts an ArrayBuffer to a standard Base64 string.
|
| 45 |
+
* Used for logging raw keys consistently.
|
| 46 |
+
* @param {ArrayBuffer} buffer - The ArrayBuffer to encode.
|
| 47 |
+
* @returns {string}
|
| 48 |
+
*/
|
| 49 |
+
function arrayBufferToBase64(buffer) {
|
| 50 |
+
let binary = '';
|
| 51 |
+
const bytes = new Uint8Array(buffer);
|
| 52 |
+
const len = bytes.byteLength;
|
| 53 |
+
for (let i = 0; i < len; i++) {
|
| 54 |
+
binary += String.fromCharCode(bytes[i]);
|
| 55 |
+
}
|
| 56 |
+
return window.btoa(binary);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* Derives a 32-byte AES key from a password and salt using PBKDF2.
|
| 61 |
+
* Logs the standard Base64 representation of the raw key.
|
| 62 |
+
* @param {string} password - The master password.
|
| 63 |
+
* @param {string} saltString - The salt (e.g., user's email).
|
| 64 |
+
* @returns {Promise<ArrayBuffer>} - Promise resolving to the raw key bytes (ArrayBuffer).
|
| 65 |
+
*/
|
| 66 |
+
async function deriveKeyRawBytes(password, saltString) {
|
| 67 |
+
try {
|
| 68 |
+
const salt = new TextEncoder().encode(saltString.toLowerCase()); // Use consistent casing for salt
|
| 69 |
+
const passwordBuffer = new TextEncoder().encode(password);
|
| 70 |
+
const iterations = 390000; // Match Python backend KDF iterations
|
| 71 |
+
|
| 72 |
+
const keyMaterial = await crypto.subtle.importKey(
|
| 73 |
+
'raw', passwordBuffer, { name: 'PBKDF2' }, false, ['deriveBits']
|
| 74 |
+
);
|
| 75 |
+
|
| 76 |
+
const derivedBits = await crypto.subtle.deriveBits(
|
| 77 |
+
{ name: 'PBKDF2', salt: salt, iterations: iterations, hash: 'SHA-256' },
|
| 78 |
+
keyMaterial,
|
| 79 |
+
256 // Derive 256 bits (32 bytes)
|
| 80 |
+
);
|
| 81 |
+
|
| 82 |
+
// --- ADDED LOGGING ---
|
| 83 |
+
try {
|
| 84 |
+
console.log(`DEBUG: JS Derived Key (Raw -> Standard Base64): ${arrayBufferToBase64(derivedBits)}`);
|
| 85 |
+
} catch(logErr) { console.error("DEBUG: Error logging JS derived key", logErr); }
|
| 86 |
+
// --- END LOGGING ---
|
| 87 |
+
|
| 88 |
+
return derivedBits; // Return raw ArrayBuffer
|
| 89 |
+
} catch (error) {
|
| 90 |
+
console.error("Key derivation failed:", error);
|
| 91 |
+
throw new Error("Could not derive encryption key.");
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/**
|
| 96 |
+
* Encrypts data (object) using AES-GCM with the provided raw key bytes.
|
| 97 |
+
* @param {ArrayBuffer} keyBuffer - The raw 32-byte AES key.
|
| 98 |
+
* @param {object} data - The JavaScript object to encrypt.
|
| 99 |
+
* @returns {Promise<string>} - Promise resolving to the Base64 encoded encrypted data (IV + Ciphertext).
|
| 100 |
+
*/
|
| 101 |
+
async function encryptData(keyBuffer, data) {
|
| 102 |
+
try {
|
| 103 |
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
| 104 |
+
const dataString = JSON.stringify(data);
|
| 105 |
+
const encodedData = new TextEncoder().encode(dataString);
|
| 106 |
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM", length: 256 }, false, ["encrypt"]);
|
| 107 |
+
const encryptedContent = await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, cryptoKey, encodedData);
|
| 108 |
+
const combinedBuffer = new Uint8Array(iv.length + encryptedContent.byteLength);
|
| 109 |
+
combinedBuffer.set(iv, 0);
|
| 110 |
+
combinedBuffer.set(new Uint8Array(encryptedContent), iv.length);
|
| 111 |
+
return arrayBufferToBase64(combinedBuffer); // Return standard Base64
|
| 112 |
+
} catch (error) {
|
| 113 |
+
console.error("Encryption failed:", error);
|
| 114 |
+
throw new Error("Could not encrypt data.");
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/**
|
| 119 |
+
* Decrypts Base64 encoded data using AES-GCM with the provided raw key bytes.
|
| 120 |
+
* @param {ArrayBuffer} keyBuffer - The raw 32-byte AES key.
|
| 121 |
+
* @param {string} encryptedB64Data - Base64 encoded data (IV + Ciphertext).
|
| 122 |
+
* @returns {Promise<object|null>} - Promise resolving to the decrypted object, or null on failure.
|
| 123 |
+
*/
|
| 124 |
+
async function decryptData(keyBuffer, encryptedB64Data) {
|
| 125 |
+
try {
|
| 126 |
+
const combinedData = base64ToArrayBuffer(encryptedB64Data);
|
| 127 |
+
if (combinedData.byteLength < 12) throw new Error("Encrypted data too short.");
|
| 128 |
+
const iv = combinedData.slice(0, 12);
|
| 129 |
+
const ciphertext = combinedData.slice(12);
|
| 130 |
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
|
| 131 |
+
const decryptedContent = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, cryptoKey, ciphertext);
|
| 132 |
+
const decodedString = new TextDecoder().decode(decryptedContent);
|
| 133 |
+
return JSON.parse(decodedString);
|
| 134 |
+
} catch (error) {
|
| 135 |
+
// Log the specific crypto error, helpful for debugging (e.g., Authentication tag mismatch)
|
| 136 |
+
console.error(`Decryption failed: ${error.name} - ${error.message}`);
|
| 137 |
+
return null; // Indicate failure
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/**
|
| 142 |
+
* Escapes HTML special characters in a string.
|
| 143 |
+
* @param {string} unsafe - The potentially unsafe string.
|
| 144 |
+
* @returns {string} - The escaped string.
|
| 145 |
+
*/
|
| 146 |
+
function escapeHtml(unsafe) {
|
| 147 |
+
if (typeof unsafe !== 'string') return '';
|
| 148 |
+
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
/**
|
| 152 |
+
* Computes the SHA-1 hash of a string.
|
| 153 |
+
* @param {string} text - The string to hash.
|
| 154 |
+
* @returns {Promise<string>} - Promise resolving to the SHA-1 hash as an uppercase hex string.
|
| 155 |
+
*/
|
| 156 |
+
async function sha1Hash(text) {
|
| 157 |
+
// ... (implementation as previously provided)
|
| 158 |
+
try {
|
| 159 |
+
const encoder = new TextEncoder();
|
| 160 |
+
const data = encoder.encode(text);
|
| 161 |
+
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
|
| 162 |
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
| 163 |
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
| 164 |
+
return hashHex.toUpperCase(); // HIBP API uses uppercase hex
|
| 165 |
+
} catch (error) {
|
| 166 |
+
console.error("SHA-1 Hashing failed:", error);
|
| 167 |
+
throw new Error("Could not compute SHA-1 hash.");
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
/**
|
| 172 |
+
* Checks if a password has been exposed in known data breaches using HIBP Pwned Passwords API (FREE version).
|
| 173 |
+
* Uses k-Anonymity.
|
| 174 |
+
* @param {string} password - The password to check.
|
| 175 |
+
* @returns {Promise<{isPwned: boolean, count: number | null, error: string | null}>}
|
| 176 |
+
*/
|
| 177 |
+
async function checkHIBPPassword(password) {
|
| 178 |
+
// ... (implementation as previously provided)
|
| 179 |
+
if (!password) {
|
| 180 |
+
return { isPwned: false, count: null, error: null }; // Cannot check empty password
|
| 181 |
+
}
|
| 182 |
+
try {
|
| 183 |
+
const hash = await sha1Hash(password);
|
| 184 |
+
const prefix = hash.substring(0, 5);
|
| 185 |
+
const suffix = hash.substring(5);
|
| 186 |
+
const apiUrl = `https://api.pwnedpasswords.com/range/${prefix}`; // FREE endpoint
|
| 187 |
+
|
| 188 |
+
const controller = new AbortController();
|
| 189 |
+
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
|
| 190 |
+
|
| 191 |
+
const response = await fetch(apiUrl, {
|
| 192 |
+
method: 'GET',
|
| 193 |
+
signal: controller.signal
|
| 194 |
+
});
|
| 195 |
+
|
| 196 |
+
clearTimeout(timeoutId);
|
| 197 |
+
|
| 198 |
+
if (!response.ok) {
|
| 199 |
+
if (response.status === 404) {
|
| 200 |
+
// 404 means the prefix wasn't found (good!)
|
| 201 |
+
return { isPwned: false, count: 0, error: null };
|
| 202 |
+
}
|
| 203 |
+
throw new Error(`HIBP API Error: ${response.status} ${response.statusText}`);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
const text = await response.text();
|
| 207 |
+
const lines = text.split('\r\n');
|
| 208 |
+
|
| 209 |
+
for (const line of lines) {
|
| 210 |
+
const [lineSuffix, lineCountStr] = line.split(':');
|
| 211 |
+
if (lineSuffix === suffix) {
|
| 212 |
+
const count = parseInt(lineCountStr, 10);
|
| 213 |
+
console.warn(`Password found in HIBP database ${count} times!`);
|
| 214 |
+
return { isPwned: true, count: count, error: null };
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
return { isPwned: false, count: 0, error: null };
|
| 218 |
+
|
| 219 |
+
} catch (error) {
|
| 220 |
+
let errorMessage = "Could not check password breach status.";
|
| 221 |
+
if (error.name === 'AbortError') {
|
| 222 |
+
errorMessage = "Breach check timed out.";
|
| 223 |
+
} else if (error instanceof Error) {
|
| 224 |
+
errorMessage = `Breach check failed: ${error.message}`;
|
| 225 |
+
}
|
| 226 |
+
console.error("HIBP Check Error:", error);
|
| 227 |
+
return { isPwned: false, count: null, error: errorMessage };
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
// --- END OF crypto-helpers.js additions ---
|
extension/icons/icon128.png
ADDED
|
|
extension/icons/icon16.png
ADDED
|
|
extension/icons/icon32.png
ADDED
|
|
extension/icons/icon48.png
ADDED
|
|
extension/manifest.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"manifest_version": 3,
|
| 3 |
+
"name": "Secure E2EE Password Manager",
|
| 4 |
+
"version": "1.2",
|
| 5 |
+
"description": "A secure password manager using E2EE and a Supabase backend.",
|
| 6 |
+
"permissions": [
|
| 7 |
+
"storage",
|
| 8 |
+
"activeTab",
|
| 9 |
+
"scripting",
|
| 10 |
+
"cookies",
|
| 11 |
+
"alarms"
|
| 12 |
+
],
|
| 13 |
+
"host_permissions": [
|
| 14 |
+
"http://127.0.0.1:5000/*",
|
| 15 |
+
"http://localhost:5000/*"
|
| 16 |
+
],
|
| 17 |
+
"action": {
|
| 18 |
+
"default_popup": "popup.html",
|
| 19 |
+
"default_icon": {
|
| 20 |
+
"16": "icons/icon16.png",
|
| 21 |
+
"48": "icons/icon48.png",
|
| 22 |
+
"128": "icons/icon128.png"
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"icons": {
|
| 26 |
+
"16": "icons/icon16.png",
|
| 27 |
+
"48": "icons/icon48.png",
|
| 28 |
+
"128": "icons/icon128.png"
|
| 29 |
+
},
|
| 30 |
+
"background": {
|
| 31 |
+
"service_worker": "background.js"
|
| 32 |
+
},
|
| 33 |
+
"content_scripts": [
|
| 34 |
+
{
|
| 35 |
+
"matches": ["<all_urls>"],
|
| 36 |
+
"js": ["content.js", "crypto-helpers.js"]
|
| 37 |
+
}
|
| 38 |
+
]
|
| 39 |
+
}
|
extension/popup.html
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>Secure Manager</title>
|
| 6 |
+
<style>
|
| 7 |
+
/* Define theme variables locally for the popup */
|
| 8 |
+
:root {
|
| 9 |
+
--popup-primary-color: #007bff;
|
| 10 |
+
--popup-primary-darker: #0056b3;
|
| 11 |
+
--popup-secondary-color: #6c757d;
|
| 12 |
+
--popup-secondary-darker: #5a6268;
|
| 13 |
+
--popup-success-color: #28a745;
|
| 14 |
+
--popup-success-darker: #1e7e34;
|
| 15 |
+
--popup-danger-color: #dc3545;
|
| 16 |
+
|
| 17 |
+
--popup-light-bg: #f8f9fa;
|
| 18 |
+
--popup-light-card-bg: #ffffff;
|
| 19 |
+
--popup-light-text: #212529;
|
| 20 |
+
--popup-light-text-secondary: #6c757d;
|
| 21 |
+
--popup-light-border: #dee2e6;
|
| 22 |
+
--popup-light-input-bg: #ffffff;
|
| 23 |
+
--popup-light-input-border: #ced4da;
|
| 24 |
+
--popup-light-focus-shadow: rgba(0, 123, 255, 0.2);
|
| 25 |
+
--popup-light-hover-bg: #e9ecef;
|
| 26 |
+
--popup-light-item-hover: #f1f1f1;
|
| 27 |
+
--popup-strength-bg: #f0f0f0;
|
| 28 |
+
--popup-strength-border: #ddd;
|
| 29 |
+
|
| 30 |
+
--popup-dark-bg: #212529;
|
| 31 |
+
--popup-dark-card-bg: #343a40;
|
| 32 |
+
--popup-dark-text: #f8f9fa;
|
| 33 |
+
--popup-dark-text-secondary: #adb5bd;
|
| 34 |
+
--popup-dark-border: #495057;
|
| 35 |
+
--popup-dark-input-bg: #495057;
|
| 36 |
+
--popup-dark-input-border: #6c757d;
|
| 37 |
+
--popup-dark-focus-shadow: rgba(13, 110, 253, 0.3);
|
| 38 |
+
--popup-dark-hover-bg: #495057;
|
| 39 |
+
--popup-dark-item-hover: #454b51;
|
| 40 |
+
--popup-strength-bg: #495057;
|
| 41 |
+
--popup-strength-border: #6c757d;
|
| 42 |
+
|
| 43 |
+
/* Default to light */
|
| 44 |
+
--popup-bg: var(--popup-light-bg);
|
| 45 |
+
--popup-card-bg: var(--popup-light-card-bg);
|
| 46 |
+
--popup-text: var(--popup-light-text);
|
| 47 |
+
--popup-text-secondary: var(--popup-light-text-secondary);
|
| 48 |
+
--popup-border: var(--popup-light-border);
|
| 49 |
+
--popup-input-bg: var(--popup-light-input-bg);
|
| 50 |
+
--popup-input-border: var(--popup-light-input-border);
|
| 51 |
+
--popup-focus-shadow: var(--popup-light-focus-shadow);
|
| 52 |
+
--popup-hover-bg: var(--popup-light-hover-bg);
|
| 53 |
+
--popup-item-hover: var(--popup-light-item-hover);
|
| 54 |
+
--popup-hdr-bg: var(--popup-primary-color);
|
| 55 |
+
--popup-hdr-text: white;
|
| 56 |
+
--popup-strength-bg-color: var(--popup-strength-bg);
|
| 57 |
+
--popup-strength-border-color: var(--popup-strength-border);
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
--popup-border-radius: 4px;
|
| 61 |
+
--popup-transition: 0.2s ease;
|
| 62 |
+
--popup-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
body[data-theme="dark"] {
|
| 66 |
+
--popup-bg: var(--popup-dark-bg);
|
| 67 |
+
--popup-card-bg: var(--popup-dark-card-bg);
|
| 68 |
+
--popup-text: var(--popup-dark-text);
|
| 69 |
+
--popup-text-secondary: var(--popup-dark-text-secondary);
|
| 70 |
+
--popup-border: var(--popup-dark-border);
|
| 71 |
+
--popup-input-bg: var(--popup-dark-input-bg);
|
| 72 |
+
--popup-input-border: var(--popup-dark-input-border);
|
| 73 |
+
--popup-focus-shadow: var(--popup-dark-focus-shadow);
|
| 74 |
+
--popup-hover-bg: var(--popup-dark-hover-bg);
|
| 75 |
+
--popup-item-hover: var(--popup-dark-item-hover);
|
| 76 |
+
--popup-hdr-bg: #2c3e50; /* Darker header for dark mode */
|
| 77 |
+
--popup-hdr-text: #e9ecef;
|
| 78 |
+
--popup-strength-bg-color: var(--popup-dark-strength-bg);
|
| 79 |
+
--popup-strength-border-color: var(--popup-dark-strength-border);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
body {
|
| 83 |
+
width: 370px; /* Adjusted width */
|
| 84 |
+
padding: 0;
|
| 85 |
+
margin: 0;
|
| 86 |
+
font-family: var(--popup-font);
|
| 87 |
+
font-size: 14px;
|
| 88 |
+
background-color: var(--popup-bg);
|
| 89 |
+
color: var(--popup-text);
|
| 90 |
+
transition: background-color var(--popup-transition), color var(--popup-transition);
|
| 91 |
+
}
|
| 92 |
+
.container { display: flex; flex-direction: column; max-height: 550px; /* Limit total popup height */ }
|
| 93 |
+
|
| 94 |
+
/* Header */
|
| 95 |
+
.popup-header {
|
| 96 |
+
background-color: var(--popup-hdr-bg);
|
| 97 |
+
color: var(--popup-hdr-text);
|
| 98 |
+
padding: 10px 15px; /* Slightly less padding */
|
| 99 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 100 |
+
transition: background-color var(--popup-transition), color var(--popup-transition);
|
| 101 |
+
}
|
| 102 |
+
.popup-header h2 { font-size: 1.05em; margin: 0; font-weight: 500; }
|
| 103 |
+
.popup-header button#logout-btn,
|
| 104 |
+
.popup-header button#popup-theme-toggle {
|
| 105 |
+
background-color: rgba(255, 255, 255, 0.15); color: var(--popup-hdr-text); border: 1px solid rgba(255, 255, 255, 0.3); padding: 4px 8px; font-size: 0.8em; border-radius: var(--popup-border-radius); cursor: pointer; transition: background-color var(--popup-transition); line-height: 1;
|
| 106 |
+
}
|
| 107 |
+
.popup-header button#logout-btn:hover,
|
| 108 |
+
.popup-header button#popup-theme-toggle:hover { background-color: rgba(255, 255, 255, 0.25); }
|
| 109 |
+
body[data-theme="dark"] .popup-header button#logout-btn,
|
| 110 |
+
body[data-theme="dark"] .popup-header button#popup-theme-toggle {
|
| 111 |
+
background-color: rgba(0, 0, 0, 0.2); border-color: rgba(255, 255, 255, 0.2);
|
| 112 |
+
}
|
| 113 |
+
body[data-theme="dark"] .popup-header button#logout-btn:hover,
|
| 114 |
+
body[data-theme="dark"] .popup-header button#popup-theme-toggle:hover {
|
| 115 |
+
background-color: rgba(0, 0, 0, 0.3);
|
| 116 |
+
}
|
| 117 |
+
#theme-toggle-container { /* Container for the theme toggle */
|
| 118 |
+
margin-left: auto; /* Push it right */
|
| 119 |
+
padding-left: 10px;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/* Content Area */
|
| 123 |
+
.content-area { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; overflow-y: auto; }
|
| 124 |
+
|
| 125 |
+
/* Login View */
|
| 126 |
+
#login-view .content-area { background-color: var(--popup-card-bg); border-radius: 0 0 var(--popup-border-radius) var(--popup-border-radius); box-shadow: 0 2px 4px rgba(0,0,0,0.05); padding: 20px;}
|
| 127 |
+
#login-view label { display: block; margin-bottom: 5px; font-weight: 500; color: var(--popup-text-secondary); font-size: 0.9em; }
|
| 128 |
+
#login-view input { width: 100%; padding: 10px; margin-bottom: 12px; border: 1px solid var(--popup-input-border); border-radius: var(--popup-border-radius); box-sizing: border-box; background-color: var(--popup-input-bg); color: var(--popup-text); transition: border-color var(--popup-transition), box-shadow var(--popup-transition); }
|
| 129 |
+
#login-view input:focus { border-color: var(--popup-primary-color); outline: none; box-shadow: 0 0 0 2px var(--popup-focus-shadow); }
|
| 130 |
+
#login-view button { width: 100%; padding: 10px; background-image: linear-gradient(135deg, var(--popup-primary-color), var(--popup-primary-darker)); color: white; border: none; border-radius: var(--popup-border-radius); font-size: 1em; cursor: pointer; transition: all var(--popup-transition); margin-top: 10px; background-size: 200% auto; }
|
| 131 |
+
#login-view button:hover { background-position: right center; box-shadow: 0 2px 5px rgba(0, 123, 255, 0.2); }
|
| 132 |
+
#login-view button:active { transform: translateY(1px); }
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
/* Main View */
|
| 136 |
+
.main-header {
|
| 137 |
+
display: flex;
|
| 138 |
+
justify-content: space-between;
|
| 139 |
+
align-items: center;
|
| 140 |
+
margin-bottom: 10px;
|
| 141 |
+
padding: 0 5px; /* Add padding */
|
| 142 |
+
}
|
| 143 |
+
#current-domain {
|
| 144 |
+
font-size: 0.85em;
|
| 145 |
+
color: var(--popup-text-secondary);
|
| 146 |
+
font-style: italic;
|
| 147 |
+
overflow: hidden;
|
| 148 |
+
text-overflow: ellipsis;
|
| 149 |
+
white-space: nowrap;
|
| 150 |
+
max-width: 220px; /* Adjust as needed */
|
| 151 |
+
}
|
| 152 |
+
#search-input { width: 100%; padding: 9px 12px; border: 1px solid var(--popup-input-border); border-radius: var(--popup-border-radius); box-sizing: border-box; font-size: 0.95em; margin-bottom: 10px; background-color: var(--popup-input-bg); color: var(--popup-text); transition: all var(--popup-transition); }
|
| 153 |
+
#search-input:focus { border-color: var(--popup-primary-color); outline: none; box-shadow: 0 0 0 2px var(--popup-focus-shadow); }
|
| 154 |
+
|
| 155 |
+
.password-list-container { flex-grow: 1; max-height: 220px; /* Max height for scroll */ overflow-y: auto; border: 1px solid var(--popup-border); border-radius: var(--popup-border-radius); background-color: var(--popup-card-bg); margin-bottom: 10px; transition: all var(--popup-transition); }
|
| 156 |
+
.password-list { list-style: none; padding: 0; margin: 0; }
|
| 157 |
+
.password-item { padding: 10px 12px; border-bottom: 1px solid var(--popup-border); display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; transition: background-color 0.15s ease; }
|
| 158 |
+
.password-item:last-child { border-bottom: none; }
|
| 159 |
+
.password-item:hover { background-color: var(--popup-item-hover); }
|
| 160 |
+
.password-item > div:first-child { flex-grow: 1; overflow: hidden; }
|
| 161 |
+
.item-info { display: flex; flex-direction: column; line-height: 1.4; }
|
| 162 |
+
.item-info strong { font-weight: 600; color: var(--popup-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
|
| 163 |
+
.item-info span { color: var(--popup-text-secondary); font-size: 0.9em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
|
| 164 |
+
.item-actions {
|
| 165 |
+
flex-shrink: 0;
|
| 166 |
+
display: flex;
|
| 167 |
+
gap: 5px; /* Adjust gap */
|
| 168 |
+
align-items: center; /* Vertically align buttons */
|
| 169 |
+
}
|
| 170 |
+
.item-actions button {
|
| 171 |
+
font-size: 0.8em;
|
| 172 |
+
padding: 4px 6px; /* Adjust padding */
|
| 173 |
+
background-color: var(--popup-hover-bg);
|
| 174 |
+
color: var(--popup-text-secondary);
|
| 175 |
+
border: 1px solid var(--popup-border);
|
| 176 |
+
border-radius: var(--popup-border-radius);
|
| 177 |
+
cursor: pointer;
|
| 178 |
+
transition: all var(--popup-transition);
|
| 179 |
+
}
|
| 180 |
+
.item-actions button:hover { background-color: #d3d9df; color: var(--popup-text); border-color: var(--popup-secondary-color); }
|
| 181 |
+
body[data-theme="dark"] .item-actions button:hover { background-color: #5a6268; }
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
/* Add Form */
|
| 185 |
+
.add-form { border: 1px solid var(--popup-border); padding: 15px; border-radius: var(--popup-border-radius); background-color: var(--popup-card-bg); margin-top: 12px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: all var(--popup-transition); }
|
| 186 |
+
.add-form h3 { font-size: 1.05em; margin: 0 0 15px 0; text-align: center; color: var(--popup-text-secondary); font-weight: 500; }
|
| 187 |
+
.add-form label { margin-bottom: 4px; font-size: 0.9em; font-weight: 500; display: block; color: var(--popup-text-secondary); }
|
| 188 |
+
.add-form input[type="text"], .add-form input[type="password"] { width: 100%; padding: 9px; margin-bottom: 10px; font-size: 0.95em; border: 1px solid var(--popup-input-border); border-radius: var(--popup-border-radius); box-sizing: border-box; background-color: var(--popup-input-bg); color: var(--popup-text); transition: all var(--popup-transition); }
|
| 189 |
+
.add-form input[type="range"] { width: 100%; padding: 0; height: 18px; margin-bottom: 5px; accent-color: var(--popup-primary-color); }
|
| 190 |
+
.add-form input:focus { border-color: var(--popup-primary-color); outline: none; box-shadow: 0 0 0 2px var(--popup-focus-shadow); }
|
| 191 |
+
.add-form .form-buttons { display: flex; gap: 10px; margin-top: 15px; }
|
| 192 |
+
.add-form .form-buttons button { flex-grow: 1; padding: 9px; }
|
| 193 |
+
|
| 194 |
+
/* Buttons within Add Form */
|
| 195 |
+
.btn { border: none; border-radius: var(--popup-border-radius); cursor: pointer; font-size: 0.95em; font-weight: 500; transition: all var(--popup-transition); }
|
| 196 |
+
.label-button-group { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
| 197 |
+
.label-button-group label { margin-bottom: 0; }
|
| 198 |
+
.label-button-group button { padding: 2px 6px; font-size: 0.8em; }
|
| 199 |
+
.btn-primary { background-image: linear-gradient(135deg, var(--popup-primary-color), var(--popup-primary-darker)); color: white; background-size: 200% auto; }
|
| 200 |
+
.btn-primary:hover { background-position: right center; box-shadow: 0 2px 5px rgba(0, 123, 255, 0.2); }
|
| 201 |
+
.btn-secondary { background-image: linear-gradient(135deg, var(--popup-secondary-color), var(--popup-secondary-darker)); color: white; background-size: 200% auto; }
|
| 202 |
+
.btn-secondary:hover { background-position: right center; box-shadow: 0 2px 5px rgba(108, 117, 125, 0.2); }
|
| 203 |
+
.btn-outline-secondary { background-color: transparent; border: 1px solid var(--popup-secondary-color); color: var(--popup-secondary-color); }
|
| 204 |
+
.btn-outline-secondary:hover { background-color: var(--popup-secondary-color); color: white; }
|
| 205 |
+
.btn-add-toggle { width: 100%; padding: 10px; margin-top: 5px; background-image: linear-gradient(135deg, var(--popup-success-color), var(--popup-success-darker)); color: white; background-size: 200% auto; }
|
| 206 |
+
.btn-add-toggle:hover { background-position: right center; box-shadow: 0 2px 5px rgba(40, 167, 69, 0.2); }
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
/* Popup Generator Options Styles */
|
| 210 |
+
.popup-generator-options { border-top: 1px solid var(--popup-border); margin-top: 15px; padding-top: 15px; }
|
| 211 |
+
.popup-generator-options h4 { margin: 0 0 10px 0; font-size: 0.95em; color: var(--popup-text-secondary); font-weight: 500; }
|
| 212 |
+
.popup-length-control { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
| 213 |
+
.popup-length-control label { margin-bottom: 0; font-size: 0.85em; }
|
| 214 |
+
.popup-length-control input[type="range"] { flex-grow: 1; margin-bottom: 0; }
|
| 215 |
+
.popup-length-display { font-weight: 600; font-size: 0.9em; min-width: 20px; text-align: right; color: var(--popup-primary-color); }
|
| 216 |
+
.popup-char-options { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; }
|
| 217 |
+
.popup-option-group { display: flex; align-items: center; gap: 4px; }
|
| 218 |
+
.popup-option-group label { margin-bottom: 0; font-weight: normal; font-size: 0.85em; color: var(--popup-text); cursor: pointer;}
|
| 219 |
+
.popup-option-group input[type="checkbox"] { cursor: pointer; margin-top: 0px; width: 14px; height: 14px; padding: 0; margin-bottom: 0; accent-color: var(--popup-primary-color);}
|
| 220 |
+
|
| 221 |
+
/* Popup Strength Display Styles */
|
| 222 |
+
.popup-strength-area {
|
| 223 |
+
margin-top: 8px; margin-bottom: 12px; padding: 8px; border-radius: var(--popup-border-radius); background-color: var(--popup-strength-bg-color); border: 1px solid var(--popup-strength-border-color); min-height: 45px; font-size: 0.9em;
|
| 224 |
+
transition: all var(--popup-transition);
|
| 225 |
+
}
|
| 226 |
+
/* Scored Backgrounds (Light Theme) */
|
| 227 |
+
.popup-strength-area.strength-0 { background-color: #f8d7da; border-color: #f5c6cb;}
|
| 228 |
+
.popup-strength-area.strength-1 { background-color: #fff3cd; border-color: #ffeeba;}
|
| 229 |
+
.popup-strength-area.strength-2 { background-color: #d1ecf1; border-color: #bee5eb;}
|
| 230 |
+
.popup-strength-area.strength-3 { background-color: #d4edda; border-color: #c3e6cb;}
|
| 231 |
+
.popup-strength-area.strength-4 { background-color: #c8e6c9; border-color: #a5d6a7;}
|
| 232 |
+
/* Scored Backgrounds (Dark Theme) */
|
| 233 |
+
body[data-theme="dark"] .popup-strength-area.strength-0 { background-color: #58151c; border-color: #842029;}
|
| 234 |
+
body[data-theme="dark"] .popup-strength-area.strength-1 { background-color: #664d03; border-color: #997404;}
|
| 235 |
+
body[data-theme="dark"] .popup-strength-area.strength-2 { background-color: #055160; border-color: #087990;}
|
| 236 |
+
body[data-theme="dark"] .popup-strength-area.strength-3 { background-color: #0f5132; border-color: #146c43;}
|
| 237 |
+
body[data-theme="dark"] .popup-strength-area.strength-4 { background-color: #1f6322; border-color: #28832c;}
|
| 238 |
+
|
| 239 |
+
.popup-strength-meter-container { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
|
| 240 |
+
.popup-strength-label { font-weight: 600; font-size: 0.9em; width: 85px; flex-shrink: 0; color: var(--popup-text-secondary); }
|
| 241 |
+
.popup-strength-bar { height: 8px; background-color: var(--popup-hover-bg); border-radius: 4px; overflow: hidden; flex-grow: 1; transition: background-color var(--popup-transition); }
|
| 242 |
+
.popup-strength-indicator { height: 100%; width: 0%; transition: width 0.4s ease, background-image 0.4s ease; border-radius: 4px; background-image: linear-gradient(to right, #e74c3c, #dc3545); /* Default gradient */ }
|
| 243 |
+
/* Strength Colors - Use gradient */
|
| 244 |
+
.popup-strength-indicator.very-weak { background-image: linear-gradient(to right, #dc3545, #c82333); width: 10%; }
|
| 245 |
+
.popup-strength-indicator.weak { background-image: linear-gradient(to right, #fd7e14, #e67e22); width: 30%; }
|
| 246 |
+
.popup-strength-indicator.medium { background-image: linear-gradient(to right, #ffc107, #dda600); width: 55%; }
|
| 247 |
+
.popup-strength-indicator.strong { background-image: linear-gradient(to right, #20c997, #1aa07f); width: 80%; }
|
| 248 |
+
.popup-strength-indicator.very-strong { background-image: linear-gradient(to right, #198754, #13653f); width: 100%; }
|
| 249 |
+
|
| 250 |
+
#popup-strength-feedback { font-size: 0.85em; color: var(--popup-text-secondary); line-height: 1.3; transition: color var(--popup-transition); }
|
| 251 |
+
#popup-strength-feedback ul { list-style: none; padding-left: 0; margin: 3px 0 0 0; }
|
| 252 |
+
#popup-strength-feedback li { margin-bottom: 2px; padding-left: 12px; position: relative; }
|
| 253 |
+
#popup-strength-feedback li::before { content: ''; position: absolute; left: 0; top: 6px; width: 5px; height: 5px; border-radius: 50%; background-color: currentColor; opacity: 0.7; }
|
| 254 |
+
/* Light Theme Feedback */
|
| 255 |
+
#popup-strength-feedback li.warning { color: #856404; font-weight: 500;}
|
| 256 |
+
#popup-strength-feedback li.suggestion { color: #0c5460; }
|
| 257 |
+
/* Dark Theme Feedback */
|
| 258 |
+
body[data-theme="dark"] #popup-strength-feedback li.warning { color: var(--warning-color); }
|
| 259 |
+
body[data-theme="dark"] #popup-strength-feedback li.suggestion { color: var(--info-color); }
|
| 260 |
+
|
| 261 |
+
/* Style for strength in list items */
|
| 262 |
+
.strength-display-popup {
|
| 263 |
+
font-size: 0.8em; text-align: right; margin-top: 4px; padding-right: 5px; display: none; /* Hide initially */
|
| 264 |
+
font-weight: 500;
|
| 265 |
+
transition: color var(--popup-transition);
|
| 266 |
+
}
|
| 267 |
+
.strength-display-popup.score-0 { color: var(--popup-danger-color); }
|
| 268 |
+
.strength-display-popup.score-1 { color: #fd7e14; }
|
| 269 |
+
.strength-display-popup.score-2 { color: #ffc107; }
|
| 270 |
+
.strength-display-popup.score-3 { color: #20c997; }
|
| 271 |
+
.strength-display-popup.score-4 { color: var(--popup-success-color); }
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
/* Status/Error Messages */
|
| 275 |
+
.status-message { padding: 8px 10px; margin: 10px 0 5px 0; border-radius: var(--popup-border-radius); text-align: center; font-size: 0.9em; border: 1px solid transparent; transition: all var(--popup-transition); }
|
| 276 |
+
.status-error { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; }
|
| 277 |
+
.status-success { background-color: #d4edda; color: #155724; border-color: #c3e6cb; }
|
| 278 |
+
.status-info { background-color: #d1ecf1; color: #0c5460; border-color: #bee5eb;}
|
| 279 |
+
body[data-theme="dark"] .status-error { background-color: #58151c; color: #f1aeb5; border-color: #842029; }
|
| 280 |
+
body[data-theme="dark"] .status-success { background-color: #0f5132; color: #75b798; border-color: #146c43; }
|
| 281 |
+
body[data-theme="dark"] .status-info { background-color: #055160; color: #6edff6; border-color: #087990;}
|
| 282 |
+
|
| 283 |
+
.hidden { display: none !important; } /* Use important to ensure override */
|
| 284 |
+
|
| 285 |
+
/* Scrollbar styling (optional, webkit only) */
|
| 286 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 287 |
+
::-webkit-scrollbar-track { background: var(--popup-hover-bg); border-radius: 3px; }
|
| 288 |
+
::-webkit-scrollbar-thumb { background: var(--popup-secondary-color); border-radius: 3px; }
|
| 289 |
+
::-webkit-scrollbar-thumb:hover { background: var(--popup-secondary-darker); }
|
| 290 |
+
</style>
|
| 291 |
+
</head>
|
| 292 |
+
<body>
|
| 293 |
+
<div class="container">
|
| 294 |
+
|
| 295 |
+
<!-- Login View -->
|
| 296 |
+
<div id="login-view">
|
| 297 |
+
<div class="popup-header">
|
| 298 |
+
<h2>Secure Login</h2>
|
| 299 |
+
</div>
|
| 300 |
+
<div class="content-area">
|
| 301 |
+
<div id="login-error" class="status-message status-error hidden"></div>
|
| 302 |
+
<label for="email">Email:</label>
|
| 303 |
+
<input type="email" id="email" placeholder="Enter your email" required>
|
| 304 |
+
<label for="masterPasswordLogin">Master Password:</label>
|
| 305 |
+
<input type="password" id="masterPasswordLogin" placeholder="Enter Master Password" required>
|
| 306 |
+
<button id="login-btn" class="btn btn-primary">Login</button> <!-- Applied general button style -->
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
|
| 310 |
+
<!-- Main Logged-In View -->
|
| 311 |
+
<div id="main-view" class="hidden">
|
| 312 |
+
<div class="popup-header">
|
| 313 |
+
<h2>Credentials</h2>
|
| 314 |
+
<!-- Add Theme Toggle Button -->
|
| 315 |
+
<div id="theme-toggle-container">
|
| 316 |
+
<button id="popup-theme-toggle" title="Toggle light/dark theme">☀️</button>
|
| 317 |
+
</div>
|
| 318 |
+
<button id="logout-btn">Logout</button>
|
| 319 |
+
</div>
|
| 320 |
+
<div class="content-area">
|
| 321 |
+
<!-- Display Current Domain -->
|
| 322 |
+
<div class="main-header">
|
| 323 |
+
<span id="current-domain" title="Current Tab Domain">Domain: N/A</span>
|
| 324 |
+
</div>
|
| 325 |
+
<input type="search" id="search-input" placeholder="Filter by service name...">
|
| 326 |
+
<div id="status-indicator" class="status-message status-info hidden"></div>
|
| 327 |
+
<div class="password-list-container">
|
| 328 |
+
<ul id="password-list">
|
| 329 |
+
<!-- List items are generated by popup.js -->
|
| 330 |
+
<!-- Example Structure (for reference):
|
| 331 |
+
<li class="password-item" data-encrypted="..." data-service-hint="...">
|
| 332 |
+
<div style="flex-grow: 1; overflow: hidden;">
|
| 333 |
+
<div class="item-info">
|
| 334 |
+
<strong>Service Hint</strong>
|
| 335 |
+
<span class="username-display">(Username Hidden)</span>
|
| 336 |
+
</div>
|
| 337 |
+
<div class="strength-display-popup score-x">Strength: ...</div>
|
| 338 |
+
<div class="breach-display-popup">Breach: ...</div> // <-- Add this
|
| 339 |
+
</div>
|
| 340 |
+
<div class="item-actions">
|
| 341 |
+
<button>Show</button>
|
| 342 |
+
<button style="display: none;">Copy</button>
|
| 343 |
+
<button style="display: none;">Fill</button>
|
| 344 |
+
</div>
|
| 345 |
+
</li>
|
| 346 |
+
-->
|
| 347 |
+
</ul>
|
| 348 |
+
</div>
|
| 349 |
+
<button id="add-password-btn" class="btn btn-secondary btn-add-toggle">+ Add New Credential</button>
|
| 350 |
+
|
| 351 |
+
<!-- Add/Edit Form -->
|
| 352 |
+
<div id="add-password-form" class="add-form hidden">
|
| 353 |
+
<h3>Add New Credential</h3>
|
| 354 |
+
<div id="save-error" class="status-message status-error hidden"></div>
|
| 355 |
+
<label for="service">Service/Website:</label>
|
| 356 |
+
<input type="text" id="service" placeholder="e.g., Google, Example.com" required>
|
| 357 |
+
<label for="new-username">Username/Email:</label>
|
| 358 |
+
<input type="text" id="new-username" placeholder="Your username or email" required>
|
| 359 |
+
|
| 360 |
+
<div class="label-button-group">
|
| 361 |
+
<label for="new-password">Password:</label>
|
| 362 |
+
<button type="button" id="generate-popup-password-btn" class="btn btn-outline-secondary">Generate</button>
|
| 363 |
+
</div>
|
| 364 |
+
<input type="password" id="new-password" placeholder="Enter or generate password" required>
|
| 365 |
+
|
| 366 |
+
<!-- Popup Strength Display -->
|
| 367 |
+
<div id="popup-strength-area" class="popup-strength-area" style="display: none;">
|
| 368 |
+
<!-- ... strength meter ... -->
|
| 369 |
+
<div id="popup-strength-feedback"></div>
|
| 370 |
+
<!-- ** NEW: Popup Breach Status ** -->
|
| 371 |
+
<div id="popup-breach-status" class="breach-status-area" style="display: none; margin-top: 5px; font-size: 0.85em;">
|
| 372 |
+
<span class="breach-label">Breach:</span>
|
| 373 |
+
<span id="popup-breach-indicator" class="breach-indicator">Checking...</span>
|
| 374 |
+
</div>
|
| 375 |
+
<!-- ** END Popup Breach Status ** -->
|
| 376 |
+
</div>
|
| 377 |
+
|
| 378 |
+
<!-- Generator Options -->
|
| 379 |
+
<div class="popup-generator-options">
|
| 380 |
+
<h4>Generator Options</h4>
|
| 381 |
+
<div class="popup-length-control"> <label for="popup-gen-length">Length:</label> <input type="range" id="popup-gen-length" name="popup-gen-length" min="8" max="64" value="16"> <span class="popup-length-display" id="popup-gen-length-value">16</span> </div>
|
| 382 |
+
<div class="popup-char-options">
|
| 383 |
+
<div class="popup-option-group"> <input type="checkbox" id="popup-gen-lowercase" checked> <label for="popup-gen-lowercase">a-z</label> </div>
|
| 384 |
+
<div class="popup-option-group"> <input type="checkbox" id="popup-gen-uppercase" checked> <label for="popup-gen-uppercase">A-Z</label> </div>
|
| 385 |
+
<div class="popup-option-group"> <input type="checkbox" id="popup-gen-digits" checked> <label for="popup-gen-digits">0-9</label> </div>
|
| 386 |
+
<div class="popup-option-group"> <input type="checkbox" id="popup-gen-symbols" checked> <label for="popup-gen-symbols">!@#</label> </div>
|
| 387 |
+
</div>
|
| 388 |
+
</div>
|
| 389 |
+
|
| 390 |
+
<label for="masterPasswordSave">Confirm Master Password:</label>
|
| 391 |
+
<input type="password" id="masterPasswordSave" placeholder="Enter Master Password to save" required>
|
| 392 |
+
<div class="form-buttons">
|
| 393 |
+
<button id="save-password-btn" class="btn btn-primary">Encrypt & Save</button>
|
| 394 |
+
<button type="button" id="cancel-add-btn" class="btn btn-secondary">Cancel</button>
|
| 395 |
+
</div>
|
| 396 |
+
</div>
|
| 397 |
+
</div>
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
<!-- Include zxcvbn library FIRST -->
|
| 401 |
+
<script src="zxcvbn.js"></script>
|
| 402 |
+
<!-- Then other helpers and main popup script -->
|
| 403 |
+
<script src="crypto-helpers.js"></script>
|
| 404 |
+
<script src="popup.js"></script>
|
| 405 |
+
</body>
|
| 406 |
+
</html>
|
extension/popup.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// --- START OF FILE popup.js ---
|
| 2 |
+
|
| 3 |
+
// --- Global Variables ---
|
| 4 |
+
const API_BASE_URL = 'http://127.0.0.1:5000'; // Adjust if your backend runs elsewhere
|
| 5 |
+
let derivedEncryptionKey = null; // ArrayBuffer | null - Key used for crypto ops after login/unlock
|
| 6 |
+
let userEmail = null; // string | null - Email of the logged-in/remembered user
|
| 7 |
+
let allCredentials = []; // Array to hold fetched credential objects {id, encrypted_data, service_hint}
|
| 8 |
+
let currentDomain = 'N/A'; // Domain of the active browser tab
|
| 9 |
+
|
| 10 |
+
// --- DOM Elements (Define references early, check existence before use) ---
|
| 11 |
+
const loginView = document.getElementById('login-view');
|
| 12 |
+
const mainView = document.getElementById('main-view');
|
| 13 |
+
const emailInput = document.getElementById('email');
|
| 14 |
+
const masterPasswordLoginInput = document.getElementById('masterPasswordLogin');
|
| 15 |
+
const loginBtn = document.getElementById('login-btn');
|
| 16 |
+
const loginError = document.getElementById('login-error');
|
| 17 |
+
const statusIndicator = document.getElementById('status-indicator');
|
| 18 |
+
const passwordList = document.getElementById('password-list');
|
| 19 |
+
const searchInput = document.getElementById('search-input');
|
| 20 |
+
const addPasswordBtn = document.getElementById('add-password-btn');
|
| 21 |
+
const addPasswordForm = document.getElementById('add-password-form');
|
| 22 |
+
const savePasswordBtn = document.getElementById('save-password-btn');
|
| 23 |
+
const cancelAddBtn = document.getElementById('cancel-add-btn');
|
| 24 |
+
const serviceInput = document.getElementById('service');
|
| 25 |
+
const newUsernameInput = document.getElementById('new-username');
|
| 26 |
+
const newPasswordInput = document.getElementById('new-password'); // Reference needed early
|
| 27 |
+
const masterPasswordSaveInput = document.getElementById('masterPasswordSave');
|
| 28 |
+
const saveError = document.getElementById('save-error');
|
| 29 |
+
const logoutBtn = document.getElementById('logout-btn');
|
| 30 |
+
const currentDomainSpan = document.getElementById('current-domain');
|
| 31 |
+
const generatePopupPasswordBtn = document.getElementById('generate-popup-password-btn');
|
| 32 |
+
const popupThemeToggleBtn = document.getElementById('popup-theme-toggle');
|
| 33 |
+
// Generator Options Elements
|
| 34 |
+
const popupGenLengthSlider = document.getElementById('popup-gen-length');
|
| 35 |
+
const popupGenLengthValueSpan = document.getElementById('popup-gen-length-value');
|
| 36 |
+
const popupGenLowercaseCheckbox = document.getElementById('popup-gen-lowercase');
|
| 37 |
+
const popupGenUppercaseCheckbox = document.getElementById('popup-gen-uppercase');
|
| 38 |
+
const popupGenDigitsCheckbox = document.getElementById('popup-gen-digits');
|
| 39 |
+
const popupGenSymbolsCheckbox = document.getElementById('popup-gen-symbols');
|
| 40 |
+
// Popup Add Form Analysis Elements
|
| 41 |
+
const popupStrengthArea = document.getElementById('popup-strength-area');
|
| 42 |
+
const popupStrengthIndicator = document.getElementById('popup-strength-indicator');
|
| 43 |
+
const popupStrengthTextLabel = document.getElementById('popup-strength-text-label');
|
| 44 |
+
const popupStrengthFeedbackDiv = document.getElementById('popup-strength-feedback');
|
| 45 |
+
const popupBreachStatusArea = document.getElementById('popup-breach-status');
|
| 46 |
+
const popupBreachIndicator = document.getElementById('popup-breach-indicator');
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
// --- Utility & Helper Functions (Defined Outside DOMContentLoaded) ---
|
| 50 |
+
|
| 51 |
+
// Debounce function specific to popup analysis
|
| 52 |
+
let debounceTimerPopup;
|
| 53 |
+
function debouncePopup(func, delay) {
|
| 54 |
+
return function(...args) {
|
| 55 |
+
clearTimeout(debounceTimerPopup);
|
| 56 |
+
debounceTimerPopup = setTimeout(() => {
|
| 57 |
+
func.apply(this, args); // Pass 'this' and arguments
|
| 58 |
+
}, delay);
|
| 59 |
+
};
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// Helper to format zxcvbn feedback for the popup
|
| 63 |
+
function formatZxcvbnFeedbackPopup(result) {
|
| 64 |
+
if (!result || !result.feedback) return '';
|
| 65 |
+
let html = '<ul>';
|
| 66 |
+
if (result.feedback.warning) { html += `<li class="warning">${escapeHtml(result.feedback.warning)}</li>`; }
|
| 67 |
+
if (result.feedback.suggestions && result.feedback.suggestions.length > 0) { result.feedback.suggestions.forEach(s => { html += `<li class="suggestion">${escapeHtml(s)}</li>`; }); }
|
| 68 |
+
else if (!result.feedback.warning) { if (result.score < 3) { html += '<li class="suggestion">Add length or variety (caps, nums, symbols).</li>'; } }
|
| 69 |
+
html += '</ul>';
|
| 70 |
+
return html;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// Helper to map zxcvbn score (0-4) to CSS class
|
| 74 |
+
function getStrengthClassFromScore(score) {
|
| 75 |
+
const classes = ['very-weak', 'weak', 'medium', 'strong', 'very-strong'];
|
| 76 |
+
const validScore = Math.max(0, Math.min(score ?? 0, 4));
|
| 77 |
+
return classes[validScore];
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Helper to compare two ArrayBuffers
|
| 81 |
+
function compareArrayBuffers(buf1, buf2) {
|
| 82 |
+
if (!buf1 || !buf2 || buf1.byteLength !== buf2.byteLength) return false;
|
| 83 |
+
const view1 = new Uint8Array(buf1);
|
| 84 |
+
const view2 = new Uint8Array(buf2);
|
| 85 |
+
for (let i = 0; i < buf1.byteLength; i++) { if (view1[i] !== view2[i]) return false; }
|
| 86 |
+
return true;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// --- Theme Handling ---
|
| 90 |
+
function applyPopupTheme() {
|
| 91 |
+
chrome.storage.local.get(['popupTheme'], (result) => {
|
| 92 |
+
const theme = result.popupTheme || 'light';
|
| 93 |
+
document.body.setAttribute('data-theme', theme);
|
| 94 |
+
if (popupThemeToggleBtn) {
|
| 95 |
+
popupThemeToggleBtn.textContent = theme === 'dark' ? '☀️' : '🌙';
|
| 96 |
+
popupThemeToggleBtn.title = `Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`;
|
| 97 |
+
}
|
| 98 |
+
});
|
| 99 |
+
}
|
| 100 |
+
function togglePopupTheme() {
|
| 101 |
+
const currentTheme = document.body.getAttribute('data-theme') || 'light';
|
| 102 |
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
| 103 |
+
chrome.storage.local.set({ popupTheme: newTheme }, () => { applyPopupTheme(); });
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// --- Communication with Background Script ---
|
| 107 |
+
async function sendMessageToBackground(message) {
|
| 108 |
+
console.log("[Popup] Sending message to background ->", message.action, message);
|
| 109 |
+
return new Promise((resolve, reject) => {
|
| 110 |
+
chrome.runtime.sendMessage(message, (response) => {
|
| 111 |
+
const lastError = chrome.runtime.lastError;
|
| 112 |
+
if (lastError) {
|
| 113 |
+
console.error(`[Popup] Error sending '${message.action}':`, lastError.message);
|
| 114 |
+
let errorMsg = `Error contacting background: ${lastError.message}. Reload extension?`;
|
| 115 |
+
if (lastError.message.includes("Receiving end does not exist")) {
|
| 116 |
+
errorMsg = "Background service not running. Try reloading the extension.";
|
| 117 |
+
}
|
| 118 |
+
reject(new Error(errorMsg));
|
| 119 |
+
} else {
|
| 120 |
+
console.log("[Popup] Received response from background for ->", message.action, response);
|
| 121 |
+
if (response && response.success === false && response.error) {
|
| 122 |
+
reject(new Error(response.error));
|
| 123 |
+
} else {
|
| 124 |
+
resolve(response);
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
});
|
| 128 |
+
});
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// --- Authentication Functions ---
|
| 132 |
+
async function checkLoginStatus() { /* ... (code as before) ... */
|
| 133 |
+
console.log("Popup: Checking login status...");
|
| 134 |
+
try {
|
| 135 |
+
const response = await sendMessageToBackground({ action: 'getKey' });
|
| 136 |
+
if (response && response.success && typeof response.keyB64 === 'string' && response.email) {
|
| 137 |
+
derivedEncryptionKey = base64ToArrayBuffer(response.keyB64);
|
| 138 |
+
userEmail = response.email;
|
| 139 |
+
showMainView(true); updateDomainDisplay();
|
| 140 |
+
} else {
|
| 141 |
+
const emailCheckResponse = await sendMessageToBackground({ action: 'getEmail' });
|
| 142 |
+
if (emailCheckResponse?.success && emailCheckResponse.email) {
|
| 143 |
+
userEmail = emailCheckResponse.email;
|
| 144 |
+
showLoginView(`Unlock Manager for ${userEmail}`);
|
| 145 |
+
if(emailInput) emailInput.value = userEmail;
|
| 146 |
+
if(masterPasswordLoginInput) masterPasswordLoginInput.focus();
|
| 147 |
+
} else {
|
| 148 |
+
userEmail = null; derivedEncryptionKey = null; showLoginView(); if(emailInput) emailInput.focus();
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
} catch (error) { console.error("Popup: checkLoginStatus failed:", error); showLoginView(error.message || 'Background error.'); userEmail = null; derivedEncryptionKey = null; }
|
| 152 |
+
}
|
| 153 |
+
async function handleLoginAttempt() { /* ... (code as before) ... */
|
| 154 |
+
const emailToUse = userEmail || (emailInput ? emailInput.value.trim() : '');
|
| 155 |
+
const masterPassword = masterPasswordLoginInput ? masterPasswordLoginInput.value : '';
|
| 156 |
+
if (!emailToUse || !masterPassword) { showLoginError("Email and Master Password required."); return; }
|
| 157 |
+
if (loginBtn) { loginBtn.disabled = true; loginBtn.textContent = userEmail ? 'Unlocking...' : 'Logging in...'; }
|
| 158 |
+
hideLoginError();
|
| 159 |
+
let keyBuffer = null;
|
| 160 |
+
try {
|
| 161 |
+
keyBuffer = await deriveKeyRawBytes(masterPassword, emailToUse);
|
| 162 |
+
const keyB64 = arrayBufferToBase64(keyBuffer);
|
| 163 |
+
await sendMessageToBackground({ action: 'storeKey', keyB64: keyB64, email: emailToUse });
|
| 164 |
+
derivedEncryptionKey = keyBuffer; userEmail = emailToUse;
|
| 165 |
+
showMainView(true); updateDomainDisplay();
|
| 166 |
+
} catch (error) {
|
| 167 |
+
console.error('Popup: Login/Unlock error:', error); derivedEncryptionKey = null; showLoginError(`Error: ${error.message}`);
|
| 168 |
+
if(userEmail && !derivedEncryptionKey) { showLoginView(`Unlock Failed: ${userEmail}`); if (emailInput) emailInput.value = userEmail; }
|
| 169 |
+
else { showLoginView(); if (emailInput) emailInput.value = emailToUse; }
|
| 170 |
+
if (masterPasswordLoginInput) masterPasswordLoginInput.value = '';
|
| 171 |
+
} finally { if (loginBtn) { loginBtn.disabled = false; loginBtn.textContent = (userEmail && !derivedEncryptionKey) ? 'Unlock' : 'Login'; } if (masterPasswordLoginInput) masterPasswordLoginInput.value = ''; }
|
| 172 |
+
}
|
| 173 |
+
async function handleLogout() { /* ... (code as before) ... */
|
| 174 |
+
console.log("Popup: Logging out."); const emailBefore = userEmail;
|
| 175 |
+
derivedEncryptionKey = null; userEmail = null; allCredentials = [];
|
| 176 |
+
try { await sendMessageToBackground({ action: 'clearKey' }); } catch (error) { console.error("Popup: Error clearing background key:", error.message); }
|
| 177 |
+
try { await fetch(`${API_BASE_URL}/logout`, { method: 'GET', credentials: 'include' }); } catch (error) { console.warn("Popup: Backend logout failed:", error); }
|
| 178 |
+
showLoginView(`Logged out: ${emailBefore || 'session'}.`); if(emailInput) emailInput.value = ''; if(masterPasswordLoginInput) masterPasswordLoginInput.value = '';
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// --- UI View Management Functions ---
|
| 182 |
+
function showLoginView(message = null) { /* ... (code as before) ... */
|
| 183 |
+
if (loginView) loginView.classList.remove('hidden'); if (mainView) mainView.classList.add('hidden');
|
| 184 |
+
if (message) showLoginError(message); else hideLoginError();
|
| 185 |
+
if (!userEmail && emailInput) emailInput.focus(); else if (userEmail && masterPasswordLoginInput) masterPasswordLoginInput.focus();
|
| 186 |
+
}
|
| 187 |
+
function showLoginError(message) { if(loginError) { loginError.textContent = message; loginError.classList.remove('hidden'); } }
|
| 188 |
+
function hideLoginError() { if(loginError) { loginError.classList.add('hidden'); loginError.textContent = ''; } }
|
| 189 |
+
function showMainView(fetchData = false) { /* ... (code as before) ... */
|
| 190 |
+
if (loginView) loginView.classList.add('hidden'); if (mainView) mainView.classList.remove('hidden'); hideAddForm();
|
| 191 |
+
if (fetchData && derivedEncryptionKey) loadAndDisplayCredentials();
|
| 192 |
+
else if (!derivedEncryptionKey) { console.error("showMainView: Key missing!"); showLoginView("Error: Key missing. Unlock."); }
|
| 193 |
+
updateDomainDisplay();
|
| 194 |
+
}
|
| 195 |
+
function updateDomainDisplay() { /* ... (code as before) ... */
|
| 196 |
+
if (currentDomainSpan) { currentDomainSpan.textContent = `Domain: ${currentDomain}`; currentDomainSpan.title = `Current Tab: ${currentDomain}`; }
|
| 197 |
+
}
|
| 198 |
+
function showAddForm() { /* ... (code as before - CORRECTED CALL) ... */
|
| 199 |
+
if (addPasswordForm) addPasswordForm.classList.remove('hidden');
|
| 200 |
+
if (addPasswordBtn) addPasswordBtn.classList.add('hidden');
|
| 201 |
+
hideSaveError();
|
| 202 |
+
if (currentDomain !== 'N/A' && serviceInput && !serviceInput.value) { let dN = currentDomain.replace(/^www\./, ''); serviceInput.value = dN.charAt(0).toUpperCase() + dN.slice(1); }
|
| 203 |
+
if (newPasswordInput) newPasswordInput.value = '';
|
| 204 |
+
// ***** CORRECTED: Dispatch event instead of calling function directly *****
|
| 205 |
+
if (newPasswordInput) {
|
| 206 |
+
newPasswordInput.dispatchEvent(new Event('input', { bubbles: true }));
|
| 207 |
+
}
|
| 208 |
+
// ***** END CORRECTION *****
|
| 209 |
+
if (masterPasswordSaveInput) masterPasswordSaveInput.value = '';
|
| 210 |
+
if(serviceInput) serviceInput.focus();
|
| 211 |
+
}
|
| 212 |
+
function hideAddForm() { /* ... (code as before) ... */
|
| 213 |
+
if (addPasswordForm) addPasswordForm.classList.add('hidden'); if (addPasswordBtn) addPasswordBtn.classList.remove('hidden');
|
| 214 |
+
if (serviceInput) serviceInput.value = ''; if (newUsernameInput) newUsernameInput.value = ''; if (newPasswordInput) newPasswordInput.value = ''; if (masterPasswordSaveInput) masterPasswordSaveInput.value = '';
|
| 215 |
+
hideSaveError();
|
| 216 |
+
if (popupGenLengthSlider) popupGenLengthSlider.value = 16; if (popupGenLengthValueSpan) popupGenLengthValueSpan.textContent = '16';
|
| 217 |
+
if (popupGenLowercaseCheckbox) popupGenLowercaseCheckbox.checked = true; if (popupGenUppercaseCheckbox) popupGenUppercaseCheckbox.checked = true;
|
| 218 |
+
if (popupGenDigitsCheckbox) popupGenDigitsCheckbox.checked = true; if (popupGenSymbolsCheckbox) popupGenSymbolsCheckbox.checked = true;
|
| 219 |
+
if (popupStrengthArea) popupStrengthArea.style.display = 'none'; if (popupBreachStatusArea) popupBreachStatusArea.style.display = 'none';
|
| 220 |
+
}
|
| 221 |
+
function showSaveError(message) { /* ... (code as before) ... */ if(saveError) { saveError.textContent = message; saveError.classList.remove('hidden'); } }
|
| 222 |
+
function hideSaveError() { /* ... (code as before) ... */ if(saveError) { saveError.textContent = ''; saveError.classList.add('hidden'); } }
|
| 223 |
+
function setPopupStatus(message, isError = false, duration = 0) { /* ... (code as before) ... */
|
| 224 |
+
if (statusIndicator) { statusIndicator.textContent = message; statusIndicator.className = `status-message ${isError ? 'status-error' : (message.includes("copied") || message.includes("saved") ? 'status-success' : 'status-info')}`; statusIndicator.classList.remove('hidden'); if (duration > 0) { if (window.statusClearTimer) clearTimeout(window.statusClearTimer); window.statusClearTimer = setTimeout(clearPopupStatus, duration); }}
|
| 225 |
+
}
|
| 226 |
+
function clearPopupStatus() { /* ... (code as before) ... */ if(statusIndicator) { statusIndicator.classList.add('hidden'); statusIndicator.textContent = ''; statusIndicator.className = 'status-message status-info'; }}
|
| 227 |
+
|
| 228 |
+
// --- Credential Handling Functions ---
|
| 229 |
+
async function loadAndDisplayCredentials() { /* ... (code as before) ... */
|
| 230 |
+
if (!derivedEncryptionKey) { console.error("loadDisplay: Key missing."); setPopupStatus("Key missing.", true); return; }
|
| 231 |
+
setPopupStatus("Loading...", false); if (passwordList) passwordList.innerHTML = '';
|
| 232 |
+
try { const resp = await fetch(`${API_BASE_URL}/api/credentials`, { method: 'GET', credentials: 'include' }); if (!resp.ok) { if (resp.status === 401) { await handleLogout(); showLoginView("Session expired."); } else { throw new Error(`Fetch failed (${resp.status})`); } return; } const txt = await resp.text(); try { allCredentials = JSON.parse(txt) || []; clearPopupStatus(); if (allCredentials.length === 0) { if (passwordList) passwordList.innerHTML = `<li style="padding: 10px; text-align: center; color: var(--popup-text-secondary); list-style: none;">None saved.</li>`; } else { filterAndRenderList(); } } catch (jsonErr) { console.error("JSON Err:", jsonErr, "Resp:", txt); throw new Error(`Invalid server response (${resp.status})`); } }
|
| 233 |
+
catch (error) { setPopupStatus(`Load Error: ${error.message}`, true); console.error('Load cred error:', error); }
|
| 234 |
+
}
|
| 235 |
+
function filterAndRenderList() { /* ... (code as before) ... */
|
| 236 |
+
if (!passwordList) return; passwordList.innerHTML = ''; const term = searchInput ? searchInput.value.toLowerCase().trim() : ''; let filtered = []; let domainMatches = [];
|
| 237 |
+
if (currentDomain !== 'N/A' && !term) { const lcDomain = currentDomain.replace(/^www\./, '').toLowerCase(); domainMatches = allCredentials.filter(c => c.service_hint?.toLowerCase().includes(lcDomain)); const others = allCredentials.filter(c => !domainMatches.includes(c)); domainMatches.sort((a, b) => (a.service_hint || '').localeCompare(b.service_hint || '')); others.sort((a, b) => (a.service_hint || '').localeCompare(b.service_hint || '')); filtered = [...domainMatches, ...others]; }
|
| 238 |
+
else { filtered = allCredentials.filter(c => term ? (c.service_hint?.toLowerCase().includes(term)) : true); filtered.sort((a, b) => (a.service_hint || '').localeCompare(b.service_hint || '')); }
|
| 239 |
+
if (filtered.length === 0) { const msg = term ? "No match." : (allCredentials.length === 0 ? "None saved." : "No credentials."); passwordList.innerHTML = `<li style="padding: 10px; text-align: center; color: var(--popup-text-secondary); list-style: none;">${msg}</li>`; return; }
|
| 240 |
+
filtered.forEach(cred => renderCredentialItem(cred, domainMatches.includes(cred)));
|
| 241 |
+
}
|
| 242 |
+
function renderCredentialItem(credential, isDomainMatch = false) { /* ... (code as before) ... */
|
| 243 |
+
if (!passwordList) return; const li = document.createElement('li'); li.className = 'password-item'; li.dataset.encrypted = credential.encrypted_data; li.dataset.serviceHint = credential.service_hint || '(No Hint)';
|
| 244 |
+
if (isDomainMatch) { li.style.setProperty('--popup-item-hover', 'rgba(0, 123, 255, 0.1)'); li.style.borderLeft = '3px solid var(--popup-primary-color)'; li.style.paddingLeft = '9px'; }
|
| 245 |
+
const itemCont = document.createElement('div'); itemCont.style.flexGrow = '1'; itemCont.style.overflow = 'hidden';
|
| 246 |
+
const infoDiv = document.createElement('div'); infoDiv.className = 'item-info'; infoDiv.innerHTML = `<strong>${escapeHtml(credential.service_hint || '(No Hint)')}</strong><span class="username-display">(Username Hidden)</span>`;
|
| 247 |
+
const strDiv = document.createElement('div'); strDiv.className = 'strength-display-popup'; strDiv.style.display = 'none';
|
| 248 |
+
const brchDiv = document.createElement('div'); brchDiv.className = 'breach-display-popup'; brchDiv.style.display = 'none'; brchDiv.style.fontSize = '0.8em'; brchDiv.style.marginTop = '4px'; brchDiv.style.paddingRight = '5px';
|
| 249 |
+
itemCont.append(infoDiv, strDiv, brchDiv);
|
| 250 |
+
const actsDiv = document.createElement('div'); actsDiv.className = 'item-actions';
|
| 251 |
+
const showBtn = document.createElement('button'); showBtn.textContent = 'Show'; showBtn.title = 'Decrypt/show details'; showBtn.onclick = (e) => { e.stopPropagation(); handleShowDetails(li); };
|
| 252 |
+
const copyBtn = document.createElement('button'); copyBtn.textContent = 'Copy'; copyBtn.title = 'Copy password'; copyBtn.style.display = 'none'; copyBtn.onclick = (e) => { e.stopPropagation(); handleCopyPassword(li); };
|
| 253 |
+
const fillBtn = document.createElement('button'); fillBtn.textContent = 'Fill'; fillBtn.title = 'Fill on current page'; fillBtn.style.display = 'none'; fillBtn.onclick = (e) => { e.stopPropagation(); handleFillPassword(li); };
|
| 254 |
+
if (isDomainMatch) { li.title = `Click to fill ${escapeHtml(credential.service_hint || '')}`; li.style.cursor = 'pointer'; li.addEventListener('click', async (e) => { if (e.target === li || infoDiv.contains(e.target)) { if (!derivedEncryptionKey) { alert("Unlock first."); return; } const dec = await decryptData(derivedEncryptionKey, credential.encrypted_data); if (dec?.password) { handleDirectFill(dec.username || '', dec.password); } else { alert("Decrypt failed."); } } }); }
|
| 255 |
+
actsDiv.append(showBtn, copyBtn, fillBtn); li.append(itemCont, actsDiv); passwordList.appendChild(li);
|
| 256 |
+
}
|
| 257 |
+
async function handleShowDetails(listItem) { /* ... (code as before) ... */
|
| 258 |
+
if (!derivedEncryptionKey) { alert("Unlock first."); return; }
|
| 259 |
+
const encData = listItem.dataset.encrypted; const itemCont = listItem.querySelector('div[style*="flex-grow"]'); if (!itemCont) return;
|
| 260 |
+
const infoD = itemCont.querySelector('.item-info'); const userSpan = infoD?.querySelector('.username-display'); const strDiv = itemCont.querySelector('.strength-display-popup'); const brchDiv = itemCont.querySelector('.breach-display-popup'); const actsDiv = listItem.querySelector('.item-actions');
|
| 261 |
+
const showBtn = actsDiv?.querySelector('button:nth-child(1)'); const copyBtn = actsDiv?.querySelector('button:nth-child(2)'); const fillBtn = actsDiv?.querySelector('button:nth-child(3)');
|
| 262 |
+
if (!infoD || !userSpan || !strDiv || !brchDiv || !actsDiv || !showBtn || !copyBtn || !fillBtn) { console.error("Missing list item elements."); return; }
|
| 263 |
+
const assessmentMap = ["Very Weak", "Weak", "Medium", "Strong", "Very Strong"];
|
| 264 |
+
if (showBtn.textContent === 'Show') {
|
| 265 |
+
showBtn.textContent = '...'; showBtn.disabled = true; strDiv.style.display = 'none'; brchDiv.style.display = 'none'; brchDiv.textContent = 'Breach: Checking...'; brchDiv.className = 'breach-display-popup loading';
|
| 266 |
+
const dec = await decryptData(derivedEncryptionKey, encData); showBtn.disabled = false;
|
| 267 |
+
if (dec) {
|
| 268 |
+
const svc = dec.service || '(No Service)'; const user = dec.username || '(No Username)'; const pass = dec.password || '';
|
| 269 |
+
listItem.dataset.decryptedPassword = pass; listItem.dataset.decryptedUsername = user;
|
| 270 |
+
infoD.querySelector('strong').textContent = escapeHtml(svc); userSpan.textContent = escapeHtml(user); userSpan.style.color = '';
|
| 271 |
+
showBtn.textContent = 'Hide'; copyBtn.style.display = 'inline-block'; fillBtn.style.display = 'inline-block';
|
| 272 |
+
let strScore = -1;
|
| 273 |
+
if (pass && typeof zxcvbn === 'function') { try { const r = zxcvbn(pass); strScore = r.score; strDiv.textContent = `Strength: ${assessmentMap[strScore]}`; strDiv.className = `strength-display-popup score-${strScore}`; strDiv.style.display = 'block'; } catch(e){ strDiv.textContent = 'Strength: Error'; strDiv.className = 'strength-display-popup score-0'; strDiv.style.display = 'block';} } else { strDiv.style.display = 'none'; }
|
| 274 |
+
if (pass && typeof checkHIBPPassword === 'function') {
|
| 275 |
+
brchDiv.style.display = 'block'; try { const br = await checkHIBPPassword(pass); if (br.error) { brchDiv.textContent = `Breach: Error`; brchDiv.className = 'breach-display-popup error'; brchDiv.title = escapeHtml(br.error); } else if (br.isPwned) { brchDiv.textContent = `Breach: Compromised! (${br.count})`; brchDiv.className = 'breach-display-popup pwned'; brchDiv.title = `Found in ${br.count} breach(es).`; } else { brchDiv.textContent = 'Breach: Not Found'; brchDiv.className = 'breach-display-popup safe'; brchDiv.title = 'Not found.'; } } catch (be) { brchDiv.textContent = `Breach: Error`; brchDiv.className = 'breach-display-popup error'; brchDiv.title = 'Check failed.'; } }
|
| 276 |
+
else { brchDiv.style.display = 'none'; }
|
| 277 |
+
} else { userSpan.textContent = '(Decrypt Failed)'; userSpan.style.color = 'var(--popup-danger-color)'; showBtn.textContent = 'Error'; strDiv.style.display = 'none'; brchDiv.style.display = 'none'; copyBtn.style.display = 'none'; fillBtn.style.display = 'none'; delete listItem.dataset.decryptedPassword; delete listItem.dataset.decryptedUsername; }
|
| 278 |
+
} else { const origHint = listItem.dataset.serviceHint; infoD.querySelector('strong').textContent = escapeHtml(origHint); userSpan.textContent = '(Username Hidden)'; userSpan.style.color = ''; showBtn.textContent = 'Show'; copyBtn.style.display = 'none'; fillBtn.style.display = 'none'; strDiv.style.display = 'none'; brchDiv.style.display = 'none'; delete listItem.dataset.decryptedPassword; delete listItem.dataset.decryptedUsername; }
|
| 279 |
+
}
|
| 280 |
+
async function handleCopyPassword(listItem) { /* ... (code as before) ... */
|
| 281 |
+
let pass = listItem.dataset.decryptedPassword; if (!pass) { setPopupStatus("Decrypting...", false); await handleShowDetails(listItem); pass = listItem.dataset.decryptedPassword; clearPopupStatus(); if (!pass) { alert("Decrypt failed."); return; } } try { await navigator.clipboard.writeText(pass); setPopupStatus("Password copied!", false, 1500); } catch (err) { setPopupStatus("Copy failed.", true); console.error('Clipboard err:', err); alert("Copy failed."); }
|
| 282 |
+
}
|
| 283 |
+
async function handleFillPassword(listItem) { /* ... (code as before) ... */
|
| 284 |
+
let pass = listItem.dataset.decryptedPassword; let user = listItem.dataset.decryptedUsername; if (!pass) { setPopupStatus("Decrypting...", false); await handleShowDetails(listItem); pass = listItem.dataset.decryptedPassword; user = listItem.dataset.decryptedUsername; clearPopupStatus(); if (!pass) { alert("Decrypt failed."); return; } } handleDirectFill(user || '', pass);
|
| 285 |
+
}
|
| 286 |
+
async function handleDirectFill(username, password) { /* ... (code as before) ... */
|
| 287 |
+
setPopupStatus(`Filling ${currentDomain}...`, false); try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (tab?.id) { chrome.tabs.sendMessage(tab.id, { action: 'fillPassword', username: username, password: password }, (resp) => { if (chrome.runtime.lastError) { console.error("Fill Send Error:", chrome.runtime.lastError.message); setPopupStatus("Error sending fill.", true, 3000); } else if (resp?.success) { window.close(); } else { console.warn("Fill failed/not ack."); setPopupStatus("Fill sent.", false, 2000); } }); } else { throw new Error("No active tab."); } } catch (error) { setPopupStatus(`Fill Error: ${error.message}`, true); console.error("Fill error:", error); }
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
// --- Password Generation ---
|
| 291 |
+
async function handleGeneratePopupPassword() { /* ... (code as before - uses dispatchEvent now) ... */
|
| 292 |
+
hideSaveError(); if (generatePopupPasswordBtn) { generatePopupPasswordBtn.disabled = true; generatePopupPasswordBtn.textContent = '...'; }
|
| 293 |
+
const opts = { length: popupGenLengthSlider ? parseInt(popupGenLengthSlider.value, 10) : 16, use_lowercase: popupGenLowercaseCheckbox?.checked ?? true, use_uppercase: popupGenUppercaseCheckbox?.checked ?? true, use_digits: popupGenDigitsCheckbox?.checked ?? true, use_symbols: popupGenSymbolsCheckbox?.checked ?? true };
|
| 294 |
+
if (!opts.use_lowercase && !opts.use_uppercase && !opts.use_digits && !opts.use_symbols) { showSaveError("Select character type."); if (generatePopupPasswordBtn) { generatePopupPasswordBtn.disabled = false; generatePopupPasswordBtn.textContent = 'Generate'; } return; }
|
| 295 |
+
try { const resp = await fetch(`${API_BASE_URL}/api/generate_password`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(opts) }); const res = await resp.json(); if (resp.ok && res.password) { if (newPasswordInput) { newPasswordInput.value = res.password; newPasswordInput.type = 'text'; newPasswordInput.dispatchEvent(new Event('input', { bubbles: true })); setTimeout(() => { if (newPasswordInput?.type === 'text') newPasswordInput.type = 'password'; }, 2000); } } else { throw new Error(res.error || 'API gen failed.'); } } catch (error) { showSaveError(`Generate Error: ${error.message}`); console.error('Generate Err:', error); } finally { if (generatePopupPasswordBtn) { generatePopupPasswordBtn.disabled = false; generatePopupPasswordBtn.textContent = 'Generate'; } }
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
// --- Save New Password ---
|
| 299 |
+
async function handleSavePassword() { /* ... (code as before) ... */
|
| 300 |
+
const service = serviceInput ? serviceInput.value.trim() : ''; const username = newUsernameInput ? newUsernameInput.value.trim() : ''; const password = newPasswordInput ? newPasswordInput.value : ''; const masterPassword = masterPasswordSaveInput ? masterPasswordSaveInput.value : '';
|
| 301 |
+
if (!service || !username || !password || !masterPassword) { showSaveError("All fields & Master PW required."); return; }
|
| 302 |
+
hideSaveError(); if (savePasswordBtn) { savePasswordBtn.disabled = true; savePasswordBtn.textContent = 'Saving...'; }
|
| 303 |
+
try { if (!userEmail || !derivedEncryptionKey) throw new Error("Session key missing."); const confirmKey = await deriveKeyRawBytes(masterPassword, userEmail); if (!compareArrayBuffers(confirmKey, derivedEncryptionKey)) throw new Error("Master PW mismatch."); const dataToEnc = { service, username, password }; const encData = await encryptData(derivedEncryptionKey, dataToEnc); const resp = await fetch(`${API_BASE_URL}/api/credentials`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ encrypted_data: encData, service_hint: service }) }); const res = await resp.json(); if (resp.ok && res.success) { setPopupStatus("Saved!", false, 2000); hideAddForm(); await loadAndDisplayCredentials(); } else { throw new Error(res.message || `Save failed (${resp.status})`); } }
|
| 304 |
+
catch (error) { showSaveError(`Save failed: ${error.message}`); console.error('Save error:', error); }
|
| 305 |
+
finally { if (savePasswordBtn) { savePasswordBtn.disabled = false; savePasswordBtn.textContent = 'Encrypt & Save'; } if (masterPasswordSaveInput) masterPasswordSaveInput.value = ''; }
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
// --- Search/Filter ---
|
| 309 |
+
function handleSearchFilter() { /* ... (code as before) ... */
|
| 310 |
+
if (window.searchDebounceTimer) clearTimeout(window.searchDebounceTimer);
|
| 311 |
+
window.searchDebounceTimer = setTimeout(filterAndRenderList, 250);
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
// --- Utility: Get Current Tab Domain ---
|
| 315 |
+
async function getCurrentTabDomain() { /* ... (code as before) ... */
|
| 316 |
+
try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (tab?.id && tab.url?.startsWith('http')) { currentDomain = await new Promise((resolve) => { let resolved = false; const timer = setTimeout(() => { if (!resolved) { resolved = true; resolve(new URL(tab.url).hostname); } }, 350); chrome.tabs.sendMessage(tab.id, { action: 'getDomain' }, (resp) => { clearTimeout(timer); if (resolved) return; resolved = true; if (chrome.runtime.lastError || !resp?.domain) { resolve(new URL(tab.url).hostname); } else { resolve(resp.domain); } }); }); } else if (tab?.url?.startsWith('http')) { currentDomain = new URL(tab.url).hostname; } else { currentDomain = 'N/A'; } } catch (error) { console.warn("Get domain error:", error); currentDomain = 'Error'; } console.log("Domain:", currentDomain); updateDomainDisplay();
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
// --- DOMContentLoaded Listener (Main Entry Point) ---
|
| 321 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
| 322 |
+
console.log("Popup: DOMContentLoaded - Initializing...");
|
| 323 |
+
applyPopupTheme();
|
| 324 |
+
|
| 325 |
+
// --- Check Dependencies ---
|
| 326 |
+
let cryptoHelpersLoaded = typeof arrayBufferToBase64 !== 'undefined' && typeof base64ToArrayBuffer !== 'undefined' && typeof base64UrlDecode !== 'undefined' && typeof deriveKeyRawBytes !== 'undefined' && typeof decryptData !== 'undefined' && typeof encryptData !== 'undefined' && typeof sha1Hash !== 'undefined' && typeof checkHIBPPassword !== 'undefined' && typeof escapeHtml !== 'undefined';
|
| 327 |
+
let zxcvbnLoaded = typeof zxcvbn !== 'undefined';
|
| 328 |
+
|
| 329 |
+
if (!cryptoHelpersLoaded) { console.error("CRITICAL: Crypto helpers missing!"); showLoginView("Error: Missing functions."); return; }
|
| 330 |
+
if (!zxcvbnLoaded) { console.warn("zxcvbn missing."); if(popupStrengthArea) popupStrengthArea.style.display = 'none'; }
|
| 331 |
+
|
| 332 |
+
// --- Define Debounced Analysis Function (INSIDE DOMContentLoaded) ---
|
| 333 |
+
const debouncePopupAnalysis = debouncePopup(async function() {
|
| 334 |
+
const password = this.value; // 'this' is the input
|
| 335 |
+
const assessmentMap = ["Very Weak", "Weak", "Medium", "Strong", "Very Strong"];
|
| 336 |
+
|
| 337 |
+
if (!password) {
|
| 338 |
+
if (popupStrengthArea) popupStrengthArea.style.display = 'none';
|
| 339 |
+
if (popupBreachStatusArea) popupBreachStatusArea.style.display = 'none';
|
| 340 |
+
return;
|
| 341 |
+
}
|
| 342 |
+
if (popupStrengthArea) popupStrengthArea.style.display = 'block';
|
| 343 |
+
if (popupBreachStatusArea) popupBreachStatusArea.style.display = 'flex';
|
| 344 |
+
if (popupStrengthIndicator) popupStrengthIndicator.className = 'popup-strength-indicator';
|
| 345 |
+
if (popupStrengthTextLabel) popupStrengthTextLabel.textContent = 'Strength: Checking...';
|
| 346 |
+
if (popupStrengthFeedbackDiv) popupStrengthFeedbackDiv.innerHTML = '';
|
| 347 |
+
if (popupStrengthArea) popupStrengthArea.className = 'popup-strength-area';
|
| 348 |
+
if (popupBreachIndicator) { popupBreachIndicator.textContent = 'Checking...'; popupBreachIndicator.className = 'breach-indicator loading'; }
|
| 349 |
+
|
| 350 |
+
let strengthResult = null; let breachResultPromise = null;
|
| 351 |
+
if (zxcvbnLoaded) { try { strengthResult = zxcvbn(password); } catch(e) { console.error(e);}}
|
| 352 |
+
else { if(popupStrengthArea) popupStrengthArea.style.display = 'none'; }
|
| 353 |
+
if (cryptoHelpersLoaded) { breachResultPromise = checkHIBPPassword(password).catch(e => ({error: "Check failed"})); }
|
| 354 |
+
else { if(popupBreachStatusArea) popupBreachStatusArea.style.display = 'none'; breachResultPromise = Promise.resolve({ error: "Checker N/A" }); }
|
| 355 |
+
|
| 356 |
+
// Process Strength
|
| 357 |
+
if (strengthResult && popupStrengthIndicator && popupStrengthTextLabel && popupStrengthFeedbackDiv) {
|
| 358 |
+
const score = strengthResult.score;
|
| 359 |
+
popupStrengthIndicator.className = `popup-strength-indicator ${getStrengthClassFromScore(score)}`;
|
| 360 |
+
popupStrengthTextLabel.textContent = `Strength: ${assessmentMap[score]}`;
|
| 361 |
+
popupStrengthFeedbackDiv.innerHTML = formatZxcvbnFeedbackPopup(strengthResult);
|
| 362 |
+
popupStrengthArea.className = `popup-strength-area strength-${score}`;
|
| 363 |
+
}
|
| 364 |
+
// Process Breach
|
| 365 |
+
if (breachResultPromise && popupBreachIndicator) {
|
| 366 |
+
try { const br = await breachResultPromise; if (br.error) { popupBreachIndicator.textContent = `Error: ${escapeHtml(br.error)}`; popupBreachIndicator.className = 'breach-indicator error'; } else if (br.isPwned) { popupBreachIndicator.textContent = `Compromised! (${br.count})`; popupBreachIndicator.className = 'breach-indicator pwned'; } else { popupBreachIndicator.textContent = 'Not in breaches.'; popupBreachIndicator.className = 'breach-indicator safe'; } }
|
| 367 |
+
catch (error) { popupBreachIndicator.textContent = 'Error checking.'; popupBreachIndicator.className = 'breach-indicator error'; }
|
| 368 |
+
}
|
| 369 |
+
}, 600); // Debounce time
|
| 370 |
+
|
| 371 |
+
// --- Attach Event Listeners (INSIDE DOMContentLoaded) ---
|
| 372 |
+
if (loginBtn) loginBtn.addEventListener('click', handleLoginAttempt);
|
| 373 |
+
if (logoutBtn) logoutBtn.addEventListener('click', handleLogout);
|
| 374 |
+
if (addPasswordBtn) addPasswordBtn.addEventListener('click', showAddForm);
|
| 375 |
+
if (cancelAddBtn) cancelAddBtn.addEventListener('click', hideAddForm);
|
| 376 |
+
if (savePasswordBtn) savePasswordBtn.addEventListener('click', handleSavePassword);
|
| 377 |
+
if (searchInput) searchInput.addEventListener('input', handleSearchFilter);
|
| 378 |
+
if (generatePopupPasswordBtn) generatePopupPasswordBtn.addEventListener('click', handleGeneratePopupPassword);
|
| 379 |
+
if (popupGenLengthSlider && popupGenLengthValueSpan) { popupGenLengthSlider.addEventListener('input', () => { popupGenLengthValueSpan.textContent = popupGenLengthSlider.value; }); }
|
| 380 |
+
if (newPasswordInput) { newPasswordInput.addEventListener('input', debouncePopupAnalysis); } // Attach listener here
|
| 381 |
+
if (popupThemeToggleBtn) popupThemeToggleBtn.addEventListener('click', togglePopupTheme);
|
| 382 |
+
|
| 383 |
+
// --- Initial Actions ---
|
| 384 |
+
await getCurrentTabDomain();
|
| 385 |
+
await checkLoginStatus();
|
| 386 |
+
|
| 387 |
+
}); // --- END OF DOMContentLoaded ---
|
| 388 |
+
|
| 389 |
+
// --- END OF FILE popup.js ---
|
extension/zxcvbn.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/css/style.css
ADDED
|
@@ -0,0 +1,870 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* --- START OF FILE static/style.css --- */
|
| 2 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 3 |
+
|
| 4 |
+
:root {
|
| 5 |
+
--primary-color: #007bff;
|
| 6 |
+
--primary-color-darker: #0056b3;
|
| 7 |
+
--primary-gradient: linear-gradient(135deg, #007bff, #0056b3);
|
| 8 |
+
--secondary-color: #6c757d;
|
| 9 |
+
--secondary-color-darker: #5a6268;
|
| 10 |
+
--success-color: #28a745;
|
| 11 |
+
--success-color-darker: #1e7e34;
|
| 12 |
+
--danger-color: #dc3545;
|
| 13 |
+
--danger-color-darker: #c82333;
|
| 14 |
+
--info-color: #17a2b8;
|
| 15 |
+
--warning-color: #ffc107;
|
| 16 |
+
--warning-text-color: #333; /* Darker text for yellow background */
|
| 17 |
+
|
| 18 |
+
--light-bg: #f8f9fa;
|
| 19 |
+
--light-card-bg: #ffffff;
|
| 20 |
+
--light-text: #212529;
|
| 21 |
+
--light-text-secondary: #6c757d;
|
| 22 |
+
--light-border: #dee2e6;
|
| 23 |
+
--light-input-bg: #ffffff;
|
| 24 |
+
--light-input-border: #ced4da;
|
| 25 |
+
--light-focus-shadow: rgba(0, 123, 255, 0.25);
|
| 26 |
+
--light-hover-bg: #e9ecef;
|
| 27 |
+
--light-active-bg: #cfe2ff; /* Active nav background */
|
| 28 |
+
--light-strength-bg: #f8f9fa; /* Default strength area bg */
|
| 29 |
+
--light-strength-border: #e9ecef;
|
| 30 |
+
|
| 31 |
+
--dark-bg: #1a1a1a; /* Darker background */
|
| 32 |
+
--dark-card-bg: #2c2c2c; /* Slightly lighter dark for cards */
|
| 33 |
+
--dark-text: #f8f9fa;
|
| 34 |
+
--dark-text-secondary: #adb5bd;
|
| 35 |
+
--dark-border: #495057;
|
| 36 |
+
--dark-input-bg: #343a40;
|
| 37 |
+
--dark-input-border: #495057;
|
| 38 |
+
--dark-focus-shadow: rgba(13, 110, 253, 0.35); /* Brighter shadow */
|
| 39 |
+
--dark-hover-bg: #343a40;
|
| 40 |
+
--dark-active-bg: #003c80; /* Darker active nav background */
|
| 41 |
+
--dark-strength-bg: #343a40; /* Dark strength area bg */
|
| 42 |
+
--dark-strength-border: #495057;
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
/* Default to light theme */
|
| 46 |
+
--bg-color: var(--light-bg);
|
| 47 |
+
--card-bg-color: var(--light-card-bg);
|
| 48 |
+
--text-color: var(--light-text);
|
| 49 |
+
--text-secondary-color: var(--light-text-secondary);
|
| 50 |
+
--border-color: var(--light-border);
|
| 51 |
+
--input-bg-color: var(--light-input-bg);
|
| 52 |
+
--input-border-color: var(--light-input-border);
|
| 53 |
+
--focus-shadow-color: var(--light-focus-shadow);
|
| 54 |
+
--hover-bg-color: var(--light-hover-bg);
|
| 55 |
+
--active-bg-color: var(--light-active-bg);
|
| 56 |
+
--strength-bg-color: var(--light-strength-bg);
|
| 57 |
+
--strength-border-color: var(--light-strength-border);
|
| 58 |
+
--nav-bg-color: var(--light-card-bg);
|
| 59 |
+
|
| 60 |
+
--border-radius: 6px;
|
| 61 |
+
--box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
| 62 |
+
--transition-speed: 0.25s;
|
| 63 |
+
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* Apply dark theme variables when data-theme is 'dark' */
|
| 67 |
+
body[data-theme="dark"] {
|
| 68 |
+
--bg-color: var(--dark-bg);
|
| 69 |
+
--card-bg-color: var(--dark-card-bg);
|
| 70 |
+
--text-color: var(--dark-text);
|
| 71 |
+
--text-secondary-color: var(--dark-text-secondary);
|
| 72 |
+
--border-color: var(--dark-border);
|
| 73 |
+
--input-bg-color: var(--dark-input-bg);
|
| 74 |
+
--input-border-color: var(--dark-input-border);
|
| 75 |
+
--focus-shadow-color: var(--dark-focus-shadow);
|
| 76 |
+
--hover-bg-color: var(--dark-hover-bg);
|
| 77 |
+
--active-bg-color: var(--dark-active-bg);
|
| 78 |
+
--strength-bg-color: var(--dark-strength-bg);
|
| 79 |
+
--strength-border-color: var(--dark-strength-border);
|
| 80 |
+
--nav-bg-color: var(--dark-card-bg);
|
| 81 |
+
|
| 82 |
+
/* Adjust specific component colors for dark mode */
|
| 83 |
+
--warning-bg: #332a00; /* Darker warning bg */
|
| 84 |
+
--warning-border: #665100;
|
| 85 |
+
--warning-text: #ffeeba; /* Lighter text for dark warning */
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/* Basic Reset & Global Styles */
|
| 89 |
+
* {
|
| 90 |
+
margin: 0;
|
| 91 |
+
padding: 0;
|
| 92 |
+
box-sizing: border-box;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
body {
|
| 96 |
+
font-family: var(--font-family);
|
| 97 |
+
line-height: 1.6;
|
| 98 |
+
color: var(--text-color);
|
| 99 |
+
background-color: var(--bg-color);
|
| 100 |
+
padding-bottom: 80px; /* Footer space */
|
| 101 |
+
transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.container {
|
| 105 |
+
max-width: 1100px;
|
| 106 |
+
margin: 0 auto;
|
| 107 |
+
padding: 25px;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* Header & Navigation */
|
| 111 |
+
header {
|
| 112 |
+
margin-bottom: 40px;
|
| 113 |
+
position: relative; /* For positioning toggle */
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
h1 {
|
| 117 |
+
color: var(--text-color);
|
| 118 |
+
margin-bottom: 25px;
|
| 119 |
+
text-align: center;
|
| 120 |
+
font-weight: 600;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.llm-badge {
|
| 124 |
+
display: inline-block;
|
| 125 |
+
background-color: var(--info-color);
|
| 126 |
+
color: white;
|
| 127 |
+
padding: 3px 8px;
|
| 128 |
+
border-radius: var(--border-radius);
|
| 129 |
+
font-size: 12px;
|
| 130 |
+
font-weight: 500;
|
| 131 |
+
margin-left: 10px;
|
| 132 |
+
vertical-align: middle;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
nav ul {
|
| 136 |
+
display: flex;
|
| 137 |
+
justify-content: center;
|
| 138 |
+
list-style: none;
|
| 139 |
+
background-color: var(--nav-bg-color);
|
| 140 |
+
border-radius: var(--border-radius);
|
| 141 |
+
box-shadow: var(--box-shadow);
|
| 142 |
+
overflow: hidden;
|
| 143 |
+
padding: 0;
|
| 144 |
+
transition: background-color var(--transition-speed) ease;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
nav li {
|
| 148 |
+
flex: 1;
|
| 149 |
+
text-align: center;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
nav a {
|
| 153 |
+
display: block;
|
| 154 |
+
padding: 16px 0;
|
| 155 |
+
color: var(--text-secondary-color);
|
| 156 |
+
text-decoration: none;
|
| 157 |
+
transition: all var(--transition-speed) ease;
|
| 158 |
+
font-weight: 500;
|
| 159 |
+
border-bottom: 3px solid transparent;
|
| 160 |
+
position: relative;
|
| 161 |
+
overflow: hidden;
|
| 162 |
+
}
|
| 163 |
+
nav a::before { /* Add subtle hover effect */
|
| 164 |
+
content: '';
|
| 165 |
+
position: absolute;
|
| 166 |
+
bottom: 0;
|
| 167 |
+
left: 50%;
|
| 168 |
+
width: 0;
|
| 169 |
+
height: 3px;
|
| 170 |
+
background-color: var(--primary-color);
|
| 171 |
+
transition: all var(--transition-speed) ease-out;
|
| 172 |
+
transform: translateX(-50%);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
nav a:hover {
|
| 176 |
+
background-color: var(--hover-bg-color);
|
| 177 |
+
color: var(--text-color);
|
| 178 |
+
}
|
| 179 |
+
nav a:hover::before {
|
| 180 |
+
width: 60%; /* Animate underline on hover */
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
nav a.active {
|
| 184 |
+
background-color: var(--active-bg-color);
|
| 185 |
+
color: var(--primary-color-darker);
|
| 186 |
+
border-bottom-color: var(--primary-color);
|
| 187 |
+
font-weight: 600;
|
| 188 |
+
}
|
| 189 |
+
body[data-theme="dark"] nav a.active {
|
| 190 |
+
color: var(--light-bg); /* Make active text lighter in dark mode */
|
| 191 |
+
}
|
| 192 |
+
nav a.active::before {
|
| 193 |
+
width: 100%; /* Full underline for active */
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
/* Logout & Theme Toggle */
|
| 198 |
+
.header-controls {
|
| 199 |
+
position: absolute;
|
| 200 |
+
top: -10px; /* Adjust as needed */
|
| 201 |
+
right: 0;
|
| 202 |
+
display: flex;
|
| 203 |
+
align-items: center;
|
| 204 |
+
gap: 15px;
|
| 205 |
+
}
|
| 206 |
+
.logout-link {
|
| 207 |
+
color: var(--primary-color);
|
| 208 |
+
text-decoration: none;
|
| 209 |
+
font-weight: 500;
|
| 210 |
+
transition: color var(--transition-speed) ease;
|
| 211 |
+
font-size: 0.9em;
|
| 212 |
+
}
|
| 213 |
+
.logout-link:hover {
|
| 214 |
+
color: var(--primary-color-darker);
|
| 215 |
+
text-decoration: underline;
|
| 216 |
+
}
|
| 217 |
+
#theme-toggle {
|
| 218 |
+
background: none;
|
| 219 |
+
border: 1px solid var(--border-color);
|
| 220 |
+
color: var(--text-secondary-color);
|
| 221 |
+
padding: 5px 8px;
|
| 222 |
+
border-radius: var(--border-radius);
|
| 223 |
+
cursor: pointer;
|
| 224 |
+
font-size: 1.1em; /* Slightly larger icon */
|
| 225 |
+
line-height: 1;
|
| 226 |
+
transition: all var(--transition-speed) ease;
|
| 227 |
+
}
|
| 228 |
+
#theme-toggle:hover {
|
| 229 |
+
background-color: var(--hover-bg-color);
|
| 230 |
+
border-color: var(--text-secondary-color);
|
| 231 |
+
color: var(--text-color);
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
/* Card Styles */
|
| 235 |
+
.card {
|
| 236 |
+
background-color: var(--card-bg-color);
|
| 237 |
+
border-radius: var(--border-radius);
|
| 238 |
+
box-shadow: var(--box-shadow);
|
| 239 |
+
padding: 35px;
|
| 240 |
+
margin-bottom: 35px;
|
| 241 |
+
border: 1px solid var(--border-color);
|
| 242 |
+
transition: background-color var(--transition-speed) ease, border-color var(--transition-speed) ease;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
h2, h3 {
|
| 246 |
+
color: var(--text-color);
|
| 247 |
+
margin-bottom: 25px;
|
| 248 |
+
border-bottom: 1px solid var(--border-color);
|
| 249 |
+
padding-bottom: 12px;
|
| 250 |
+
font-weight: 600;
|
| 251 |
+
transition: color var(--transition-speed) ease, border-color var(--transition-speed) ease;
|
| 252 |
+
}
|
| 253 |
+
h3 {
|
| 254 |
+
margin-top: 30px;
|
| 255 |
+
font-size: 1.35em;
|
| 256 |
+
}
|
| 257 |
+
p {
|
| 258 |
+
color: var(--text-secondary-color);
|
| 259 |
+
margin-bottom: 20px;
|
| 260 |
+
transition: color var(--transition-speed) ease;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
/* Form Styles */
|
| 264 |
+
.form-group {
|
| 265 |
+
margin-bottom: 28px;
|
| 266 |
+
position: relative;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
label {
|
| 270 |
+
display: block;
|
| 271 |
+
margin-bottom: 8px;
|
| 272 |
+
font-weight: 500;
|
| 273 |
+
color: var(--text-secondary-color);
|
| 274 |
+
font-size: 0.95em;
|
| 275 |
+
transition: color var(--transition-speed) ease;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
input[type="text"],
|
| 279 |
+
input[type="password"],
|
| 280 |
+
input[type="email"],
|
| 281 |
+
input[type="number"],
|
| 282 |
+
.search-bar input /* Apply to search too */ {
|
| 283 |
+
width: 100%;
|
| 284 |
+
padding: 12px 15px;
|
| 285 |
+
border: 1px solid var(--input-border-color);
|
| 286 |
+
border-radius: var(--border-radius);
|
| 287 |
+
font-size: 1rem;
|
| 288 |
+
transition: border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease, background-color var(--transition-speed) ease, color var(--transition-speed) ease;
|
| 289 |
+
box-sizing: border-box;
|
| 290 |
+
background-color: var(--input-bg-color);
|
| 291 |
+
color: var(--text-color);
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
input[type="range"] {
|
| 295 |
+
width: 100%;
|
| 296 |
+
cursor: pointer;
|
| 297 |
+
accent-color: var(--primary-color); /* Modern way to color range slider */
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
input:focus {
|
| 301 |
+
border-color: var(--primary-color);
|
| 302 |
+
outline: none;
|
| 303 |
+
box-shadow: 0 0 0 3px var(--focus-shadow-color);
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/* Button Styles */
|
| 307 |
+
.btn {
|
| 308 |
+
border: none;
|
| 309 |
+
border-radius: var(--border-radius);
|
| 310 |
+
padding: 12px 25px;
|
| 311 |
+
font-size: 1rem;
|
| 312 |
+
cursor: pointer;
|
| 313 |
+
transition: all var(--transition-speed) ease;
|
| 314 |
+
font-weight: 500;
|
| 315 |
+
background-image: var(--primary-gradient);
|
| 316 |
+
color: white;
|
| 317 |
+
background-size: 200% auto; /* For gradient animation */
|
| 318 |
+
text-align: center;
|
| 319 |
+
display: inline-block; /* Ensure it behaves well */
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.btn:hover {
|
| 323 |
+
background-position: right center; /* Change gradient direction on hover */
|
| 324 |
+
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
|
| 325 |
+
transform: translateY(-1px);
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.btn:active {
|
| 329 |
+
transform: translateY(0px);
|
| 330 |
+
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.btn:disabled {
|
| 334 |
+
background-image: none;
|
| 335 |
+
background-color: var(--secondary-color);
|
| 336 |
+
opacity: 0.65;
|
| 337 |
+
cursor: not-allowed;
|
| 338 |
+
box-shadow: none;
|
| 339 |
+
transform: none;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.btn-secondary {
|
| 343 |
+
background-image: linear-gradient(135deg, var(--secondary-color), var(--secondary-color-darker));
|
| 344 |
+
}
|
| 345 |
+
.btn-secondary:hover {
|
| 346 |
+
box-shadow: 0 4px 8px rgba(108, 117, 125, 0.3);
|
| 347 |
+
}
|
| 348 |
+
.btn-secondary:active {
|
| 349 |
+
box-shadow: 0 2px 4px rgba(108, 117, 125, 0.2);
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.btn-outline-secondary {
|
| 353 |
+
background-color: transparent;
|
| 354 |
+
background-image: none;
|
| 355 |
+
border: 1px solid var(--secondary-color);
|
| 356 |
+
color: var(--secondary-color);
|
| 357 |
+
}
|
| 358 |
+
.btn-outline-secondary:hover {
|
| 359 |
+
background-color: var(--secondary-color);
|
| 360 |
+
color: white;
|
| 361 |
+
background-image: none; /* Ensure no gradient */
|
| 362 |
+
box-shadow: none;
|
| 363 |
+
transform: none;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.btn-sm {
|
| 367 |
+
padding: 0.3rem 0.6rem;
|
| 368 |
+
font-size: 0.875rem;
|
| 369 |
+
line-height: 1.5;
|
| 370 |
+
border-radius: 0.2rem;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.password-input-group {
|
| 374 |
+
display: flex;
|
| 375 |
+
align-items: center;
|
| 376 |
+
gap: 10px;
|
| 377 |
+
position: relative; /* For absolute positioning of toggle */
|
| 378 |
+
}
|
| 379 |
+
.password-input-group input[type="password"],
|
| 380 |
+
.password-input-group input[type="text"] { /* Handle toggled state */
|
| 381 |
+
flex-grow: 1;
|
| 382 |
+
padding-right: 100px; /* Space for both buttons */
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.password-input-group .toggle-button {
|
| 386 |
+
position: absolute;
|
| 387 |
+
right: 100px; /* Adjust based on generate button width */
|
| 388 |
+
top: 50%;
|
| 389 |
+
transform: translateY(-50%);
|
| 390 |
+
background: none;
|
| 391 |
+
border: none;
|
| 392 |
+
color: var(--primary-color);
|
| 393 |
+
cursor: pointer;
|
| 394 |
+
font-size: 14px;
|
| 395 |
+
padding: 5px;
|
| 396 |
+
transition: color var(--transition-speed) ease;
|
| 397 |
+
}
|
| 398 |
+
.password-input-group .toggle-button:hover {
|
| 399 |
+
color: var(--primary-color-darker);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.password-input-group .generate-button {
|
| 403 |
+
position: absolute;
|
| 404 |
+
right: 10px;
|
| 405 |
+
top: 50%;
|
| 406 |
+
transform: translateY(-50%);
|
| 407 |
+
flex-shrink: 0;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
/* Generator Options */
|
| 411 |
+
.generator-options {
|
| 412 |
+
border: 1px dashed var(--border-color);
|
| 413 |
+
padding: 20px 25px;
|
| 414 |
+
margin-top: -10px;
|
| 415 |
+
margin-bottom: 25px;
|
| 416 |
+
border-radius: var(--border-radius);
|
| 417 |
+
background-color: rgba(0,0,0,0.02); /* Subtle background */
|
| 418 |
+
transition: border-color var(--transition-speed) ease, background-color var(--transition-speed) ease;
|
| 419 |
+
}
|
| 420 |
+
body[data-theme="dark"] .generator-options {
|
| 421 |
+
background-color: rgba(255,255,255,0.03);
|
| 422 |
+
}
|
| 423 |
+
.generator-options h4 {
|
| 424 |
+
margin-top: 0;
|
| 425 |
+
margin-bottom: 18px;
|
| 426 |
+
font-size: 1.05em;
|
| 427 |
+
color: var(--text-secondary-color);
|
| 428 |
+
font-weight: 600;
|
| 429 |
+
border-bottom: none; /* Remove border */
|
| 430 |
+
padding-bottom: 0;
|
| 431 |
+
}
|
| 432 |
+
.length-control {
|
| 433 |
+
display: flex;
|
| 434 |
+
align-items: center;
|
| 435 |
+
gap: 15px;
|
| 436 |
+
margin-bottom: 18px;
|
| 437 |
+
}
|
| 438 |
+
.length-control label {
|
| 439 |
+
margin-bottom: 0;
|
| 440 |
+
flex-basis: 60px; /* Shorter label */
|
| 441 |
+
flex-shrink: 0;
|
| 442 |
+
font-size: 0.9em;
|
| 443 |
+
}
|
| 444 |
+
.length-control input[type="range"] {
|
| 445 |
+
flex-grow: 1;
|
| 446 |
+
}
|
| 447 |
+
.length-control .length-display {
|
| 448 |
+
font-weight: 600;
|
| 449 |
+
min-width: 30px;
|
| 450 |
+
text-align: right;
|
| 451 |
+
color: var(--primary-color);
|
| 452 |
+
}
|
| 453 |
+
.char-options {
|
| 454 |
+
display: flex;
|
| 455 |
+
flex-wrap: wrap;
|
| 456 |
+
gap: 15px 25px; /* Row and column gap */
|
| 457 |
+
}
|
| 458 |
+
.option-group {
|
| 459 |
+
display: flex;
|
| 460 |
+
align-items: center;
|
| 461 |
+
gap: 8px;
|
| 462 |
+
}
|
| 463 |
+
.option-group label {
|
| 464 |
+
margin-bottom: 0;
|
| 465 |
+
font-weight: normal;
|
| 466 |
+
color: var(--text-color);
|
| 467 |
+
cursor: pointer;
|
| 468 |
+
font-size: 0.9em;
|
| 469 |
+
transition: color var(--transition-speed) ease;
|
| 470 |
+
}
|
| 471 |
+
.option-group input[type="checkbox"] {
|
| 472 |
+
cursor: pointer;
|
| 473 |
+
accent-color: var(--primary-color);
|
| 474 |
+
width: 16px;
|
| 475 |
+
height: 16px;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
/* Password Strength Display */
|
| 479 |
+
.password-strength-area {
|
| 480 |
+
margin-top: 10px;
|
| 481 |
+
margin-bottom: 25px; /* Add space before next element */
|
| 482 |
+
padding: 15px;
|
| 483 |
+
border-radius: var(--border-radius);
|
| 484 |
+
background-color: var(--strength-bg-color);
|
| 485 |
+
border: 1px solid var(--strength-border-color);
|
| 486 |
+
min-height: 65px; /* Ensure space */
|
| 487 |
+
transition: background-color 0.3s ease, border-color 0.3s ease;
|
| 488 |
+
}
|
| 489 |
+
/* Scored Backgrounds (Light Theme) */
|
| 490 |
+
.password-strength-area.strength-0 { background-color: #f8d7da; border-color: #f5c6cb;} /* Very Weak */
|
| 491 |
+
.password-strength-area.strength-1 { background-color: #fff3cd; border-color: #ffeeba;} /* Weak */
|
| 492 |
+
.password-strength-area.strength-2 { background-color: #d1ecf1; border-color: #bee5eb;} /* Medium */
|
| 493 |
+
.password-strength-area.strength-3 { background-color: #d4edda; border-color: #c3e6cb;} /* Strong */
|
| 494 |
+
.password-strength-area.strength-4 { background-color: #c8e6c9; border-color: #a5d6a7;} /* Very Strong */
|
| 495 |
+
/* Scored Backgrounds (Dark Theme) */
|
| 496 |
+
body[data-theme="dark"] .password-strength-area.strength-0 { background-color: #58151c; border-color: #842029;}
|
| 497 |
+
body[data-theme="dark"] .password-strength-area.strength-1 { background-color: #664d03; border-color: #997404;}
|
| 498 |
+
body[data-theme="dark"] .password-strength-area.strength-2 { background-color: #055160; border-color: #087990;}
|
| 499 |
+
body[data-theme="dark"] .password-strength-area.strength-3 { background-color: #0f5132; border-color: #146c43;}
|
| 500 |
+
body[data-theme="dark"] .password-strength-area.strength-4 { background-color: #1f6322; border-color: #28832c;}
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
.strength-meter-container {
|
| 504 |
+
display: flex;
|
| 505 |
+
align-items: center;
|
| 506 |
+
gap: 12px;
|
| 507 |
+
margin-bottom: 10px;
|
| 508 |
+
}
|
| 509 |
+
.strength-label {
|
| 510 |
+
font-weight: 600;
|
| 511 |
+
font-size: 0.95em;
|
| 512 |
+
width: 110px; /* Fixed width */
|
| 513 |
+
flex-shrink: 0;
|
| 514 |
+
color: var(--text-secondary-color);
|
| 515 |
+
}
|
| 516 |
+
.strength-bar {
|
| 517 |
+
height: 10px;
|
| 518 |
+
background-color: var(--hover-bg-color);
|
| 519 |
+
border-radius: 5px;
|
| 520 |
+
overflow: hidden;
|
| 521 |
+
flex-grow: 1;
|
| 522 |
+
transition: background-color var(--transition-speed) ease;
|
| 523 |
+
}
|
| 524 |
+
.strength-indicator {
|
| 525 |
+
height: 100%;
|
| 526 |
+
width: 0%;
|
| 527 |
+
transition: width 0.4s ease, background-color 0.4s ease;
|
| 528 |
+
border-radius: 5px;
|
| 529 |
+
background-image: linear-gradient(to right, #e74c3c, #dc3545); /* Default gradient */
|
| 530 |
+
}
|
| 531 |
+
/* Strength Colors - Use gradient for smoother look */
|
| 532 |
+
.strength-indicator.very-weak { background-image: linear-gradient(to right, #e74c3c, #dc3545); width: 10%; } /* Score 0 */
|
| 533 |
+
.strength-indicator.weak { background-image: linear-gradient(to right, #fd7e14, #f39c12); width: 30%; } /* Score 1 */
|
| 534 |
+
.strength-indicator.medium { background-image: linear-gradient(to right, #ffc107, #f1c40f); width: 55%; } /* Score 2 */
|
| 535 |
+
.strength-indicator.strong { background-image: linear-gradient(to right, #20c997, #28a745); width: 80%; } /* Score 3 */
|
| 536 |
+
.strength-indicator.very-strong { background-image: linear-gradient(to right, #198754, #146c43); width: 100%; } /* Score 4 */
|
| 537 |
+
|
| 538 |
+
#password-strength-feedback {
|
| 539 |
+
font-size: 0.9em;
|
| 540 |
+
color: var(--text-secondary-color);
|
| 541 |
+
line-height: 1.5;
|
| 542 |
+
transition: color var(--transition-speed) ease;
|
| 543 |
+
}
|
| 544 |
+
#password-strength-feedback ul {
|
| 545 |
+
list-style: none;
|
| 546 |
+
padding-left: 0;
|
| 547 |
+
margin: 5px 0 0 0;
|
| 548 |
+
}
|
| 549 |
+
#password-strength-feedback li {
|
| 550 |
+
margin-bottom: 4px;
|
| 551 |
+
padding-left: 15px;
|
| 552 |
+
position: relative;
|
| 553 |
+
}
|
| 554 |
+
#password-strength-feedback li::before {
|
| 555 |
+
content: '';
|
| 556 |
+
position: absolute;
|
| 557 |
+
left: 0;
|
| 558 |
+
top: 7px;
|
| 559 |
+
width: 6px;
|
| 560 |
+
height: 6px;
|
| 561 |
+
border-radius: 50%;
|
| 562 |
+
background-color: currentColor; /* Use text color */
|
| 563 |
+
opacity: 0.6;
|
| 564 |
+
}
|
| 565 |
+
/* Adjust colors based on theme */
|
| 566 |
+
body[data-theme="light"] #password-strength-feedback li.warning { color: #856404; font-weight: 500;}
|
| 567 |
+
body[data-theme="light"] #password-strength-feedback li.suggestion { color: #0c5460; }
|
| 568 |
+
body[data-theme="dark"] #password-strength-feedback li.warning { color: var(--warning-color); font-weight: 500;}
|
| 569 |
+
body[data-theme="dark"] #password-strength-feedback li.suggestion { color: var(--info-color); }
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
/* Messages (Flash messages, API responses) */
|
| 573 |
+
.message, .alert {
|
| 574 |
+
padding: 15px 20px;
|
| 575 |
+
border-radius: var(--border-radius);
|
| 576 |
+
margin-top: 20px;
|
| 577 |
+
font-size: 0.95rem;
|
| 578 |
+
text-align: center;
|
| 579 |
+
display: none; /* Hidden by default, shown by JS/Flask */
|
| 580 |
+
border: 1px solid transparent;
|
| 581 |
+
transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease, border-color var(--transition-speed) ease;
|
| 582 |
+
}
|
| 583 |
+
.message.success, .alert-success { background-color: #d4edda; color: #155724; border-color: #c3e6cb; display: block; }
|
| 584 |
+
.message.error, .alert-danger { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; display: block; }
|
| 585 |
+
.alert-warning { background-color: #fff3cd; color: var(--warning-text-color); border-color: #ffeeba; display: block; }
|
| 586 |
+
.alert-info { background-color: #d1ecf1; color: #0c5460; border-color: #bee5eb; display: block; }
|
| 587 |
+
|
| 588 |
+
body[data-theme="dark"] .message.success,
|
| 589 |
+
body[data-theme="dark"] .alert-success { background-color: #0f5132; color: #75b798; border-color: #146c43; }
|
| 590 |
+
body[data-theme="dark"] .message.error,
|
| 591 |
+
body[data-theme="dark"] .alert-danger { background-color: #58151c; color: #f1aeb5; border-color: #842029; }
|
| 592 |
+
body[data-theme="dark"] .alert-warning { background-color: #664d03; color: #ffda6a; border-color: #997404; }
|
| 593 |
+
body[data-theme="dark"] .alert-info { background-color: #055160; color: #6edff6; border-color: #087990; }
|
| 594 |
+
|
| 595 |
+
|
| 596 |
+
/* Footer */
|
| 597 |
+
footer {
|
| 598 |
+
text-align: center;
|
| 599 |
+
padding: 25px 0;
|
| 600 |
+
margin-top: 40px;
|
| 601 |
+
color: var(--text-secondary-color);
|
| 602 |
+
font-size: 14px;
|
| 603 |
+
border-top: 1px solid var(--border-color);
|
| 604 |
+
transition: color var(--transition-speed) ease, border-color var(--transition-speed) ease;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
/* Loading Spinner (Enhanced) */
|
| 608 |
+
.loading-indicator { display: none; text-align: center; margin: 30px 0; }
|
| 609 |
+
.spinner {
|
| 610 |
+
width: 40px;
|
| 611 |
+
height: 40px;
|
| 612 |
+
border: 4px solid rgba(0, 123, 255, 0.2); /* Lighter border */
|
| 613 |
+
border-left-color: var(--primary-color);
|
| 614 |
+
border-radius: 50%;
|
| 615 |
+
display: inline-block;
|
| 616 |
+
animation: spin 0.8s linear infinite;
|
| 617 |
+
}
|
| 618 |
+
@keyframes spin {
|
| 619 |
+
to { transform: rotate(360deg); }
|
| 620 |
+
}
|
| 621 |
+
.loading-indicator p { margin-top: 15px; color: var(--text-secondary-color); font-style: italic; transition: color var(--transition-speed) ease;}
|
| 622 |
+
.status-message { text-align: center; padding: 15px; color: var(--text-secondary-color); font-style: italic; transition: color var(--transition-speed) ease;}
|
| 623 |
+
|
| 624 |
+
/* Table Styles (Storage & Analyse) */
|
| 625 |
+
.table-container,
|
| 626 |
+
.analysis-table-container /* Generic container for analysis table */ {
|
| 627 |
+
overflow-x: auto;
|
| 628 |
+
margin-top: 20px;
|
| 629 |
+
border: 1px solid var(--border-color);
|
| 630 |
+
border-radius: var(--border-radius);
|
| 631 |
+
transition: border-color var(--transition-speed) ease;
|
| 632 |
+
}
|
| 633 |
+
table, .analysis-table {
|
| 634 |
+
width: 100%;
|
| 635 |
+
border-collapse: collapse;
|
| 636 |
+
table-layout: fixed;
|
| 637 |
+
}
|
| 638 |
+
th, td {
|
| 639 |
+
padding: 14px 16px;
|
| 640 |
+
text-align: left;
|
| 641 |
+
border-bottom: 1px solid var(--border-color);
|
| 642 |
+
vertical-align: middle;
|
| 643 |
+
transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease, border-color var(--transition-speed) ease;
|
| 644 |
+
word-wrap: break-word;
|
| 645 |
+
}
|
| 646 |
+
th {
|
| 647 |
+
background-color: var(--hover-bg-color);
|
| 648 |
+
font-weight: 600;
|
| 649 |
+
color: var(--text-color);
|
| 650 |
+
white-space: nowrap;
|
| 651 |
+
}
|
| 652 |
+
tr:last-child td {
|
| 653 |
+
border-bottom: none;
|
| 654 |
+
}
|
| 655 |
+
tr:hover td {
|
| 656 |
+
background-color: var(--hover-bg-color);
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
/* Specific Table Cell Styles */
|
| 660 |
+
.password-cell span { display: inline-block; min-width: 80px; word-break: break-all; font-family: monospace; }
|
| 661 |
+
.password-hidden { letter-spacing: 2px; color: var(--text-secondary-color); }
|
| 662 |
+
.password-cell .toggle-button {
|
| 663 |
+
background: none; border: none; color: var(--primary-color); cursor: pointer; font-size: 0.9em; margin-left: 10px; padding: 2px 5px; vertical-align: middle;
|
| 664 |
+
transition: color var(--transition-speed) ease;
|
| 665 |
+
}
|
| 666 |
+
.password-cell .toggle-button:hover { text-decoration: underline; color: var(--primary-color-darker); }
|
| 667 |
+
.password-cell .toggle-button:disabled { color: var(--secondary-color); cursor: default; text-decoration: none; opacity: 0.6; }
|
| 668 |
+
|
| 669 |
+
/* Analyse Page Specifics */
|
| 670 |
+
.analysis-table th:nth-child(1) { width: 20%; } /* Service */
|
| 671 |
+
.analysis-table th:nth-child(2) { width: 20%; } /* Username */
|
| 672 |
+
.analysis-table th:nth-child(3) { width: 15%; } /* Strength Bar */
|
| 673 |
+
.analysis-table th:nth-child(4) { width: 15%; } /* Assessment */
|
| 674 |
+
.analysis-table th:nth-child(5) { width: 30%; } /* Feedback */
|
| 675 |
+
|
| 676 |
+
.strength-bar-table { /* Style for strength bar within table */
|
| 677 |
+
height: 10px; background-color: var(--hover-bg-color); border-radius: 5px; overflow: hidden; margin: 5px 0; display: inline-block; width: 100%; position: relative;
|
| 678 |
+
}
|
| 679 |
+
.strength-indicator-table { /* Style for indicator within table */
|
| 680 |
+
height: 100%; width: 0%; transition: width 0.5s ease, background-color 0.5s ease; border-radius: 5px; background-image: linear-gradient(to right, #e74c3c, #dc3545); /* Default */
|
| 681 |
+
}
|
| 682 |
+
/* Apply strength classes directly to the indicator div */
|
| 683 |
+
.strength-indicator-table.very-weak { background-image: linear-gradient(to right, #e74c3c, #dc3545); width: 20%; }
|
| 684 |
+
.strength-indicator-table.weak { background-image: linear-gradient(to right, #e67e22, #f39c12); width: 40%; }
|
| 685 |
+
.strength-indicator-table.medium { background-image: linear-gradient(to right, #f1c40f, #e6b800); width: 60%; }
|
| 686 |
+
.strength-indicator-table.strong { background-image: linear-gradient(to right, #2ecc71, #28a745); width: 80%; }
|
| 687 |
+
.strength-indicator-table.very-strong { background-image: linear-gradient(to right, #27ae60, #1e7e34); width: 100%; }
|
| 688 |
+
|
| 689 |
+
.feedback-section { margin-top: 5px; }
|
| 690 |
+
.feedback-item {
|
| 691 |
+
margin-bottom: 6px; padding: 6px 10px; border-radius: var(--border-radius); font-size: 0.9rem; border-left: 3px solid; line-height: 1.4;
|
| 692 |
+
transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease, border-color var(--transition-speed) ease;
|
| 693 |
+
}
|
| 694 |
+
/* Light Theme Feedback Items */
|
| 695 |
+
.feedback-item.issue { background-color: #fff0f0; border-left-color: #e74c3c; color: #721c24; }
|
| 696 |
+
.feedback-item.tip { background-color: #f0f8ff; border-left-color: #3498db; color: #0c5460; }
|
| 697 |
+
/* Dark Theme Feedback Items */
|
| 698 |
+
body[data-theme="dark"] .feedback-item.issue { background-color: #4a1e23; border-left-color: #dc3545; color: #f1aeb5; }
|
| 699 |
+
body[data-theme="dark"] .feedback-item.tip { background-color: #0e3842; border-left-color: #3498db; color: #9eeaf9; }
|
| 700 |
+
|
| 701 |
+
|
| 702 |
+
/* Login/Register Specific Styles */
|
| 703 |
+
.auth-container {
|
| 704 |
+
max-width: 420px; margin: 50px auto; padding: 40px; background-color: var(--card-bg-color); border-radius: var(--border-radius); box-shadow: var(--box-shadow); border: 1px solid var(--border-color);
|
| 705 |
+
transition: background-color var(--transition-speed) ease, border-color var(--transition-speed) ease;
|
| 706 |
+
}
|
| 707 |
+
.auth-container h2 { text-align: center; color: var(--text-color); margin-bottom: 30px; font-weight: 600; border-bottom: none; padding-bottom: 0; }
|
| 708 |
+
.auth-container .form-group { margin-bottom: 22px; }
|
| 709 |
+
.auth-container label { display: block; margin-bottom: 7px; font-weight: 500; color: var(--text-secondary-color); font-size: 0.9em; }
|
| 710 |
+
.auth-container input[type="email"], .auth-container input[type="password"] {
|
| 711 |
+
width: 100%; padding: 12px; border: 1px solid var(--input-border-color); border-radius: var(--border-radius); box-sizing: border-box; font-size: 1rem; background-color: var(--input-bg-color); color: var(--text-color);
|
| 712 |
+
transition: all var(--transition-speed) ease;
|
| 713 |
+
}
|
| 714 |
+
.auth-container input:focus { border-color: var(--primary-color); outline: 0; box-shadow: 0 0 0 3px var(--focus-shadow-color); }
|
| 715 |
+
.auth-container .btn { display: block; width: 100%; padding: 12px 15px; font-size: 1rem; font-weight: 500; margin-top: 10px; }
|
| 716 |
+
.auth-container .btn-register { background-image: linear-gradient(135deg, var(--success-color), var(--success-color-darker)); }
|
| 717 |
+
.auth-container .btn-register:hover { box-shadow: 0 4px 8px rgba(40, 167, 69, 0.3); }
|
| 718 |
+
.auth-container .btn-register:active { box-shadow: 0 2px 4px rgba(40, 167, 69, 0.2); }
|
| 719 |
+
.text-center { text-align: center; }
|
| 720 |
+
.mt-3 { margin-top: 1.5rem !important; }
|
| 721 |
+
.link { color: var(--primary-color); text-decoration: none; transition: color var(--transition-speed) ease; }
|
| 722 |
+
.link:hover { text-decoration: underline; color: var(--primary-color-darker); }
|
| 723 |
+
.password-rules { font-size: 0.85rem; color: var(--text-secondary-color); margin-top: -10px; margin-bottom: 15px; transition: color var(--transition-speed) ease; }
|
| 724 |
+
/* Remember Me Checkbox */
|
| 725 |
+
.form-group.remember-me { display: flex; align-items: center; gap: 8px; margin-bottom: 25px; }
|
| 726 |
+
.form-group.remember-me label { margin-bottom: 0; font-weight: normal; color: var(--text-secondary-color); cursor: pointer; transition: color var(--transition-speed) ease;}
|
| 727 |
+
.form-group.remember-me input[type="checkbox"] { width: auto; margin-top: 0px; cursor: pointer; accent-color: var(--primary-color); }
|
| 728 |
+
|
| 729 |
+
|
| 730 |
+
/* Responsive Styles */
|
| 731 |
+
@media (max-width: 768px) {
|
| 732 |
+
.container { padding: 15px; }
|
| 733 |
+
header { margin-bottom: 25px; }
|
| 734 |
+
.header-controls { top: 5px; right: 5px; gap: 10px;}
|
| 735 |
+
nav ul { flex-direction: column; border-radius: var(--border-radius); }
|
| 736 |
+
nav a { border-bottom: 1px solid var(--border-color); padding: 14px 0; }
|
| 737 |
+
nav a:hover::before { width: 0; } /* Disable underline animation on mobile */
|
| 738 |
+
nav a.active { border-bottom-color: var(--primary-color); }
|
| 739 |
+
.logout-link { font-size: 0.85em; }
|
| 740 |
+
#theme-toggle { padding: 4px 6px; font-size: 1em; }
|
| 741 |
+
|
| 742 |
+
.card { padding: 25px; margin-bottom: 25px; }
|
| 743 |
+
h2, h3 { margin-bottom: 20px; padding-bottom: 10px; font-size: 1.4em;}
|
| 744 |
+
h3 { margin-top: 25px; font-size: 1.2em;}
|
| 745 |
+
|
| 746 |
+
.password-input-group input[type="password"],
|
| 747 |
+
.password-input-group input[type="text"] { padding-right: 85px; } /* Less space needed on mobile */
|
| 748 |
+
.password-input-group .toggle-button { right: 85px; /* Adjust generate button pos */ }
|
| 749 |
+
.password-input-group .generate-button { right: 5px; }
|
| 750 |
+
|
| 751 |
+
.length-control { flex-wrap: wrap; }
|
| 752 |
+
.length-control label { flex-basis: auto; width: 100%; margin-bottom: 5px; }
|
| 753 |
+
.char-options { gap: 10px 15px; }
|
| 754 |
+
|
| 755 |
+
.analysis-table { display: block; overflow-x: auto; white-space: nowrap; }
|
| 756 |
+
.analysis-table th, .analysis-table td { white-space: normal; font-size: 0.9em; } /* Allow wrapping */
|
| 757 |
+
.analysis-table th:nth-child(n), .analysis-table td:nth-child(n) { width: auto; } /* Reset fixed widths */
|
| 758 |
+
.analysis-table td:last-child { min-width: 180px; } /* Give feedback some space */
|
| 759 |
+
|
| 760 |
+
.auth-container { margin: 20px auto; padding: 30px; }
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
@media (max-width: 480px) {
|
| 764 |
+
.header-controls { position: static; justify-content: space-between; margin-top: 15px; }
|
| 765 |
+
.container { padding: 10px; }
|
| 766 |
+
.card { padding: 20px; }
|
| 767 |
+
.btn { padding: 10px 20px; font-size: 0.95rem;}
|
| 768 |
+
.password-input-group input[type="password"],
|
| 769 |
+
.password-input-group input[type="text"] { padding: 10px 12px; padding-right: 75px; font-size: 0.95rem;}
|
| 770 |
+
.password-input-group .generate-button { padding: 0.2rem 0.4rem; font-size: 0.8rem; }
|
| 771 |
+
.password-input-group .toggle-button { right: 70px; font-size: 13px;}
|
| 772 |
+
.generator-options { padding: 15px 20px; }
|
| 773 |
+
.char-options { grid-template-columns: 1fr; gap: 8px; } /* Stack checkboxes */
|
| 774 |
+
.table-container, .analysis-table-container { margin-top: 15px; }
|
| 775 |
+
th, td { padding: 10px 12px; font-size: 0.85em; }
|
| 776 |
+
.auth-container { padding: 25px; }
|
| 777 |
+
}
|
| 778 |
+
/* --- Add to static/style.css --- */
|
| 779 |
+
|
| 780 |
+
/* Breach Status Area (General) */
|
| 781 |
+
.breach-status-area {
|
| 782 |
+
display: flex; /* Use flex for alignment */
|
| 783 |
+
align-items: center;
|
| 784 |
+
gap: 8px;
|
| 785 |
+
font-size: 0.9em; /* Slightly smaller font */
|
| 786 |
+
margin-top: 10px;
|
| 787 |
+
padding-top: 10px;
|
| 788 |
+
border-top: 1px dashed var(--strength-border-color); /* Separator line */
|
| 789 |
+
}
|
| 790 |
+
.breach-label {
|
| 791 |
+
font-weight: 600;
|
| 792 |
+
color: var(--text-secondary-color);
|
| 793 |
+
flex-shrink: 0; /* Prevent label from shrinking */
|
| 794 |
+
}
|
| 795 |
+
.breach-indicator {
|
| 796 |
+
font-weight: 500;
|
| 797 |
+
transition: color 0.3s ease;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
/* Breach Indicator States */
|
| 801 |
+
.breach-indicator.loading {
|
| 802 |
+
color: var(--text-secondary-color);
|
| 803 |
+
font-style: italic;
|
| 804 |
+
}
|
| 805 |
+
.breach-indicator.safe {
|
| 806 |
+
color: var(--success-color); /* Green for safe */
|
| 807 |
+
}
|
| 808 |
+
body[data-theme="dark"] .breach-indicator.safe {
|
| 809 |
+
color: #75b798; /* Lighter green for dark mode */
|
| 810 |
+
}
|
| 811 |
+
.breach-indicator.pwned {
|
| 812 |
+
color: var(--danger-color); /* Red for pwned */
|
| 813 |
+
font-weight: 700; /* Make it bold */
|
| 814 |
+
}
|
| 815 |
+
body[data-theme="dark"] .breach-indicator.pwned {
|
| 816 |
+
color: #f1aeb5; /* Lighter red for dark mode */
|
| 817 |
+
}
|
| 818 |
+
.breach-indicator.error {
|
| 819 |
+
color: var(--warning-color); /* Orange/yellow for error */
|
| 820 |
+
font-style: italic;
|
| 821 |
+
}
|
| 822 |
+
body[data-theme="dark"] .breach-indicator.error {
|
| 823 |
+
color: #ffda6a; /* Lighter warning color */
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
/* Popup Specific Breach Styles */
|
| 828 |
+
#popup-breach-status {
|
| 829 |
+
margin-top: 5px;
|
| 830 |
+
padding-top: 5px;
|
| 831 |
+
font-size: 0.85em; /* Even smaller in popup */
|
| 832 |
+
border-top-style: dotted; /* Dotted line in popup */
|
| 833 |
+
}
|
| 834 |
+
#popup-breach-status .breach-label {
|
| 835 |
+
width: 50px; /* Fixed width for label in popup */
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
/* Breach Status in List Items (Popup) */
|
| 839 |
+
.breach-display-popup {
|
| 840 |
+
font-size: 0.8em;
|
| 841 |
+
text-align: right;
|
| 842 |
+
margin-top: 4px;
|
| 843 |
+
padding-right: 5px;
|
| 844 |
+
font-weight: 500;
|
| 845 |
+
transition: color var(--popup-transition);
|
| 846 |
+
}
|
| 847 |
+
.breach-display-popup.loading { color: var(--popup-text-secondary); font-style: italic;}
|
| 848 |
+
.breach-display-popup.safe { color: var(--popup-success-color); }
|
| 849 |
+
.breach-display-popup.pwned { color: var(--popup-danger-color); font-weight: 700;}
|
| 850 |
+
.breach-display-popup.error { color: #fd7e14; /* Orange for error in popup list */ font-style: italic;} /* Use a distinct error color */
|
| 851 |
+
body[data-theme="dark"] .breach-display-popup.safe { color: #75b798; }
|
| 852 |
+
body[data-theme="dark"] .breach-display-popup.pwned { color: #f1aeb5; }
|
| 853 |
+
body[data-theme="dark"] .breach-display-popup.error { color: #ffac4d; }
|
| 854 |
+
|
| 855 |
+
|
| 856 |
+
/* Analyse Page Table Breach Cell */
|
| 857 |
+
.analysis-table th:last-child { width: 15%; } /* Adjust width for new column */
|
| 858 |
+
.analysis-table td.breach-cell {
|
| 859 |
+
font-weight: 500;
|
| 860 |
+
text-align: center;
|
| 861 |
+
}
|
| 862 |
+
.analysis-table td.breach-safe span { color: var(--success-color); }
|
| 863 |
+
.analysis-table td.breach-pwned span { color: var(--danger-color); font-weight: 700; }
|
| 864 |
+
.analysis-table td.breach-error span { color: #856404; font-style: italic; } /* Muted warning color for table */
|
| 865 |
+
|
| 866 |
+
body[data-theme="dark"] .analysis-table td.breach-safe span { color: #75b798; }
|
| 867 |
+
body[data-theme="dark"] .analysis-table td.breach-pwned span { color: #f1aeb5; }
|
| 868 |
+
body[data-theme="dark"] .analysis-table td.breach-error span { color: #ffda6a; }
|
| 869 |
+
|
| 870 |
+
/* --- End of style.css additions --- */
|
static/js/crypto-helpers.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// --- crypto-helpers.js ---
|
| 2 |
+
// Helper functions for Web Crypto API and Base64 Handling
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Converts a Base64URL encoded string to an ArrayBuffer.
|
| 6 |
+
* Needed for decoding the key stored in Flask session.
|
| 7 |
+
* @param {string} b64url - Base64URL encoded string.
|
| 8 |
+
* @returns {ArrayBuffer}
|
| 9 |
+
*/
|
| 10 |
+
function base64UrlDecode(b64url) {
|
| 11 |
+
try {
|
| 12 |
+
let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
|
| 13 |
+
while (b64.length % 4) {
|
| 14 |
+
b64 += '=';
|
| 15 |
+
}
|
| 16 |
+
return base64ToArrayBuffer(b64);
|
| 17 |
+
} catch (e) {
|
| 18 |
+
console.error("Base64URL decoding failed:", e);
|
| 19 |
+
throw new Error("Invalid Base64URL string for key decoding.");
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/**
|
| 24 |
+
* Converts a standard Base64 string to an ArrayBuffer.
|
| 25 |
+
* @param {string} base64 - Standard Base64 encoded string.
|
| 26 |
+
* @returns {ArrayBuffer}
|
| 27 |
+
*/
|
| 28 |
+
function base64ToArrayBuffer(base64) {
|
| 29 |
+
try {
|
| 30 |
+
const binary_string = window.atob(base64);
|
| 31 |
+
const len = binary_string.length;
|
| 32 |
+
const bytes = new Uint8Array(len);
|
| 33 |
+
for (let i = 0; i < len; i++) {
|
| 34 |
+
bytes[i] = binary_string.charCodeAt(i);
|
| 35 |
+
}
|
| 36 |
+
return bytes.buffer;
|
| 37 |
+
} catch (e) {
|
| 38 |
+
console.error("Base64 decoding failed:", e);
|
| 39 |
+
throw new Error("Invalid Base64 string.");
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Converts an ArrayBuffer to a standard Base64 string.
|
| 45 |
+
* Used for logging raw keys consistently.
|
| 46 |
+
* @param {ArrayBuffer} buffer - The ArrayBuffer to encode.
|
| 47 |
+
* @returns {string}
|
| 48 |
+
*/
|
| 49 |
+
function arrayBufferToBase64(buffer) {
|
| 50 |
+
let binary = '';
|
| 51 |
+
const bytes = new Uint8Array(buffer);
|
| 52 |
+
const len = bytes.byteLength;
|
| 53 |
+
for (let i = 0; i < len; i++) {
|
| 54 |
+
binary += String.fromCharCode(bytes[i]);
|
| 55 |
+
}
|
| 56 |
+
return window.btoa(binary);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* Derives a 32-byte AES key from a password and salt using PBKDF2.
|
| 61 |
+
* Logs the standard Base64 representation of the raw key.
|
| 62 |
+
* @param {string} password - The master password.
|
| 63 |
+
* @param {string} saltString - The salt (e.g., user's email).
|
| 64 |
+
* @returns {Promise<ArrayBuffer>} - Promise resolving to the raw key bytes (ArrayBuffer).
|
| 65 |
+
*/
|
| 66 |
+
async function deriveKeyRawBytes(password, saltString) {
|
| 67 |
+
try {
|
| 68 |
+
const salt = new TextEncoder().encode(saltString.toLowerCase()); // Use consistent casing for salt
|
| 69 |
+
const passwordBuffer = new TextEncoder().encode(password);
|
| 70 |
+
const iterations = 390000; // Match Python backend KDF iterations
|
| 71 |
+
|
| 72 |
+
const keyMaterial = await crypto.subtle.importKey(
|
| 73 |
+
'raw', passwordBuffer, { name: 'PBKDF2' }, false, ['deriveBits']
|
| 74 |
+
);
|
| 75 |
+
|
| 76 |
+
const derivedBits = await crypto.subtle.deriveBits(
|
| 77 |
+
{ name: 'PBKDF2', salt: salt, iterations: iterations, hash: 'SHA-256' },
|
| 78 |
+
keyMaterial,
|
| 79 |
+
256 // Derive 256 bits (32 bytes)
|
| 80 |
+
);
|
| 81 |
+
|
| 82 |
+
// --- ADDED LOGGING ---
|
| 83 |
+
try {
|
| 84 |
+
console.log(`DEBUG: JS Derived Key (Raw -> Standard Base64): ${arrayBufferToBase64(derivedBits)}`);
|
| 85 |
+
} catch(logErr) { console.error("DEBUG: Error logging JS derived key", logErr); }
|
| 86 |
+
// --- END LOGGING ---
|
| 87 |
+
|
| 88 |
+
return derivedBits; // Return raw ArrayBuffer
|
| 89 |
+
} catch (error) {
|
| 90 |
+
console.error("Key derivation failed:", error);
|
| 91 |
+
throw new Error("Could not derive encryption key.");
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/**
|
| 96 |
+
* Encrypts data (object) using AES-GCM with the provided raw key bytes.
|
| 97 |
+
* @param {ArrayBuffer} keyBuffer - The raw 32-byte AES key.
|
| 98 |
+
* @param {object} data - The JavaScript object to encrypt.
|
| 99 |
+
* @returns {Promise<string>} - Promise resolving to the Base64 encoded encrypted data (IV + Ciphertext).
|
| 100 |
+
*/
|
| 101 |
+
async function encryptData(keyBuffer, data) {
|
| 102 |
+
try {
|
| 103 |
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
| 104 |
+
const dataString = JSON.stringify(data);
|
| 105 |
+
const encodedData = new TextEncoder().encode(dataString);
|
| 106 |
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM", length: 256 }, false, ["encrypt"]);
|
| 107 |
+
const encryptedContent = await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, cryptoKey, encodedData);
|
| 108 |
+
const combinedBuffer = new Uint8Array(iv.length + encryptedContent.byteLength);
|
| 109 |
+
combinedBuffer.set(iv, 0);
|
| 110 |
+
combinedBuffer.set(new Uint8Array(encryptedContent), iv.length);
|
| 111 |
+
return arrayBufferToBase64(combinedBuffer); // Return standard Base64
|
| 112 |
+
} catch (error) {
|
| 113 |
+
console.error("Encryption failed:", error);
|
| 114 |
+
throw new Error("Could not encrypt data.");
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/**
|
| 119 |
+
* Decrypts Base64 encoded data using AES-GCM with the provided raw key bytes.
|
| 120 |
+
* @param {ArrayBuffer} keyBuffer - The raw 32-byte AES key.
|
| 121 |
+
* @param {string} encryptedB64Data - Base64 encoded data (IV + Ciphertext).
|
| 122 |
+
* @returns {Promise<object|null>} - Promise resolving to the decrypted object, or null on failure.
|
| 123 |
+
*/
|
| 124 |
+
async function decryptData(keyBuffer, encryptedB64Data) {
|
| 125 |
+
try {
|
| 126 |
+
const combinedData = base64ToArrayBuffer(encryptedB64Data);
|
| 127 |
+
if (combinedData.byteLength < 12) throw new Error("Encrypted data too short.");
|
| 128 |
+
const iv = combinedData.slice(0, 12);
|
| 129 |
+
const ciphertext = combinedData.slice(12);
|
| 130 |
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
|
| 131 |
+
const decryptedContent = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, cryptoKey, ciphertext);
|
| 132 |
+
const decodedString = new TextDecoder().decode(decryptedContent);
|
| 133 |
+
return JSON.parse(decodedString);
|
| 134 |
+
} catch (error) {
|
| 135 |
+
// Log the specific crypto error, helpful for debugging (e.g., Authentication tag mismatch)
|
| 136 |
+
console.error(`Decryption failed: ${error.name} - ${error.message}`);
|
| 137 |
+
return null; // Indicate failure
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/**
|
| 142 |
+
* Escapes HTML special characters in a string.
|
| 143 |
+
* @param {string} unsafe - The potentially unsafe string.
|
| 144 |
+
* @returns {string} - The escaped string.
|
| 145 |
+
*/
|
| 146 |
+
function escapeHtml(unsafe) {
|
| 147 |
+
if (typeof unsafe !== 'string') return '';
|
| 148 |
+
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
/**
|
| 152 |
+
* Computes the SHA-1 hash of a string.
|
| 153 |
+
* @param {string} text - The string to hash.
|
| 154 |
+
* @returns {Promise<string>} - Promise resolving to the SHA-1 hash as an uppercase hex string.
|
| 155 |
+
*/
|
| 156 |
+
async function sha1Hash(text) {
|
| 157 |
+
// ... (implementation as previously provided)
|
| 158 |
+
try {
|
| 159 |
+
const encoder = new TextEncoder();
|
| 160 |
+
const data = encoder.encode(text);
|
| 161 |
+
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
|
| 162 |
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
| 163 |
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
| 164 |
+
return hashHex.toUpperCase(); // HIBP API uses uppercase hex
|
| 165 |
+
} catch (error) {
|
| 166 |
+
console.error("SHA-1 Hashing failed:", error);
|
| 167 |
+
throw new Error("Could not compute SHA-1 hash.");
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
/**
|
| 172 |
+
* Checks if a password has been exposed in known data breaches using HIBP Pwned Passwords API (FREE version).
|
| 173 |
+
* Uses k-Anonymity.
|
| 174 |
+
* @param {string} password - The password to check.
|
| 175 |
+
* @returns {Promise<{isPwned: boolean, count: number | null, error: string | null}>}
|
| 176 |
+
*/
|
| 177 |
+
async function checkHIBPPassword(password) {
|
| 178 |
+
// ... (implementation as previously provided)
|
| 179 |
+
if (!password) {
|
| 180 |
+
return { isPwned: false, count: null, error: null }; // Cannot check empty password
|
| 181 |
+
}
|
| 182 |
+
try {
|
| 183 |
+
const hash = await sha1Hash(password);
|
| 184 |
+
const prefix = hash.substring(0, 5);
|
| 185 |
+
const suffix = hash.substring(5);
|
| 186 |
+
const apiUrl = `https://api.pwnedpasswords.com/range/${prefix}`; // FREE endpoint
|
| 187 |
+
|
| 188 |
+
const controller = new AbortController();
|
| 189 |
+
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
|
| 190 |
+
|
| 191 |
+
const response = await fetch(apiUrl, {
|
| 192 |
+
method: 'GET',
|
| 193 |
+
signal: controller.signal
|
| 194 |
+
});
|
| 195 |
+
|
| 196 |
+
clearTimeout(timeoutId);
|
| 197 |
+
|
| 198 |
+
if (!response.ok) {
|
| 199 |
+
if (response.status === 404) {
|
| 200 |
+
// 404 means the prefix wasn't found (good!)
|
| 201 |
+
return { isPwned: false, count: 0, error: null };
|
| 202 |
+
}
|
| 203 |
+
throw new Error(`HIBP API Error: ${response.status} ${response.statusText}`);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
const text = await response.text();
|
| 207 |
+
const lines = text.split('\r\n');
|
| 208 |
+
|
| 209 |
+
for (const line of lines) {
|
| 210 |
+
const [lineSuffix, lineCountStr] = line.split(':');
|
| 211 |
+
if (lineSuffix === suffix) {
|
| 212 |
+
const count = parseInt(lineCountStr, 10);
|
| 213 |
+
console.warn(`Password found in HIBP database ${count} times!`);
|
| 214 |
+
return { isPwned: true, count: count, error: null };
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
return { isPwned: false, count: 0, error: null };
|
| 218 |
+
|
| 219 |
+
} catch (error) {
|
| 220 |
+
let errorMessage = "Could not check password breach status.";
|
| 221 |
+
if (error.name === 'AbortError') {
|
| 222 |
+
errorMessage = "Breach check timed out.";
|
| 223 |
+
} else if (error instanceof Error) {
|
| 224 |
+
errorMessage = `Breach check failed: ${error.message}`;
|
| 225 |
+
}
|
| 226 |
+
console.error("HIBP Check Error:", error);
|
| 227 |
+
return { isPwned: false, count: null, error: errorMessage };
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
// --- END OF crypto-helpers.js additions ---
|
static/js/zxcvbn.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
templates/analyse.html
ADDED
|
@@ -0,0 +1,950 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Analyse Passwords - Secure Password Manager</title>
|
| 7 |
+
<!-- Ensure correct path to your CSS file -->
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 9 |
+
<!-- Add Google Fonts -->
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 11 |
+
<style>
|
| 12 |
+
:root {
|
| 13 |
+
--primary-color: #2563eb;
|
| 14 |
+
--primary-hover: #1d4ed8;
|
| 15 |
+
--success-color: #10b981;
|
| 16 |
+
--warning-color: #f59e0b;
|
| 17 |
+
--danger-color: #ef4444;
|
| 18 |
+
--text-primary: #1f2937;
|
| 19 |
+
--text-secondary: #4b5563;
|
| 20 |
+
--bg-card: #ffffff;
|
| 21 |
+
--border-color: #e5e7eb;
|
| 22 |
+
--transition: all 0.3s ease;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
body {
|
| 26 |
+
font-family: 'Inter', sans-serif;
|
| 27 |
+
line-height: 1.6;
|
| 28 |
+
color: var(--text-primary);
|
| 29 |
+
background: #f3f4f6;
|
| 30 |
+
margin: 0;
|
| 31 |
+
padding: 0;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.container {
|
| 35 |
+
max-width: 1200px;
|
| 36 |
+
margin: 0 auto;
|
| 37 |
+
padding: 2rem;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
header {
|
| 41 |
+
background: var(--bg-card);
|
| 42 |
+
border-radius: 1rem;
|
| 43 |
+
padding: 1.5rem;
|
| 44 |
+
margin-bottom: 2rem;
|
| 45 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
h1 {
|
| 49 |
+
font-size: 2rem;
|
| 50 |
+
font-weight: 700;
|
| 51 |
+
color: var(--text-primary);
|
| 52 |
+
margin: 0;
|
| 53 |
+
display: flex;
|
| 54 |
+
align-items: center;
|
| 55 |
+
gap: 1rem;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.llm-badge {
|
| 59 |
+
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
| 60 |
+
color: white;
|
| 61 |
+
padding: 0.25rem 0.75rem;
|
| 62 |
+
border-radius: 1rem;
|
| 63 |
+
font-size: 0.875rem;
|
| 64 |
+
font-weight: 500;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
nav ul {
|
| 68 |
+
display: flex;
|
| 69 |
+
gap: 1rem;
|
| 70 |
+
padding: 0;
|
| 71 |
+
margin: 1rem 0 0;
|
| 72 |
+
list-style: none;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
nav a {
|
| 76 |
+
color: var(--text-secondary);
|
| 77 |
+
text-decoration: none;
|
| 78 |
+
padding: 0.5rem 1rem;
|
| 79 |
+
border-radius: 0.5rem;
|
| 80 |
+
transition: var(--transition);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
nav a:hover, nav a.active {
|
| 84 |
+
color: var(--primary-color);
|
| 85 |
+
background: #f0f9ff;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.card {
|
| 89 |
+
background: var(--bg-card);
|
| 90 |
+
border-radius: 1rem;
|
| 91 |
+
padding: 2rem;
|
| 92 |
+
margin-bottom: 2rem;
|
| 93 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 94 |
+
transition: var(--transition);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.card:hover {
|
| 98 |
+
transform: translateY(-2px);
|
| 99 |
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.form-group {
|
| 103 |
+
margin-bottom: 1.5rem;
|
| 104 |
+
position: relative;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
input[type="password"] {
|
| 108 |
+
width: 100%;
|
| 109 |
+
padding: 0.75rem 1rem;
|
| 110 |
+
border: 2px solid var(--border-color);
|
| 111 |
+
border-radius: 0.5rem;
|
| 112 |
+
font-size: 1rem;
|
| 113 |
+
transition: var(--transition);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
input[type="password"]:focus {
|
| 117 |
+
border-color: var(--primary-color);
|
| 118 |
+
outline: none;
|
| 119 |
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.toggle-button {
|
| 123 |
+
position: absolute;
|
| 124 |
+
right: 1rem;
|
| 125 |
+
top: 50%;
|
| 126 |
+
transform: translateY(-50%);
|
| 127 |
+
background: none;
|
| 128 |
+
border: none;
|
| 129 |
+
color: var(--text-secondary);
|
| 130 |
+
cursor: pointer;
|
| 131 |
+
font-size: 0.875rem;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.btn {
|
| 135 |
+
background: var(--primary-color);
|
| 136 |
+
color: white;
|
| 137 |
+
padding: 0.75rem 1.5rem;
|
| 138 |
+
border: none;
|
| 139 |
+
border-radius: 0.5rem;
|
| 140 |
+
font-size: 1rem;
|
| 141 |
+
font-weight: 500;
|
| 142 |
+
cursor: pointer;
|
| 143 |
+
transition: var(--transition);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.btn:hover {
|
| 147 |
+
background: var(--primary-hover);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.btn:disabled {
|
| 151 |
+
opacity: 0.7;
|
| 152 |
+
cursor: not-allowed;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.loading-indicator {
|
| 156 |
+
display: flex;
|
| 157 |
+
flex-direction: column;
|
| 158 |
+
align-items: center;
|
| 159 |
+
gap: 1rem;
|
| 160 |
+
padding: 2rem;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.spinner {
|
| 164 |
+
width: 2.5rem;
|
| 165 |
+
height: 2.5rem;
|
| 166 |
+
border: 3px solid #f3f3f3;
|
| 167 |
+
border-top: 3px solid var(--primary-color);
|
| 168 |
+
border-radius: 50%;
|
| 169 |
+
animation: spin 1s linear infinite;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
@keyframes spin {
|
| 173 |
+
0% { transform: rotate(0deg); }
|
| 174 |
+
100% { transform: rotate(360deg); }
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.analysis-table {
|
| 178 |
+
width: 100%;
|
| 179 |
+
border-collapse: separate;
|
| 180 |
+
border-spacing: 0;
|
| 181 |
+
margin: 1.5rem 0;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.analysis-table th,
|
| 185 |
+
.analysis-table td {
|
| 186 |
+
padding: 1rem;
|
| 187 |
+
text-align: left;
|
| 188 |
+
border-bottom: 1px solid var(--border-color);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.analysis-table th {
|
| 192 |
+
background: #f8fafc;
|
| 193 |
+
font-weight: 600;
|
| 194 |
+
color: var(--text-secondary);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.strength-bar {
|
| 198 |
+
background: #e5e7eb;
|
| 199 |
+
height: 0.5rem;
|
| 200 |
+
border-radius: 1rem;
|
| 201 |
+
overflow: hidden;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.strength-indicator {
|
| 205 |
+
height: 100%;
|
| 206 |
+
transition: width 0.3s ease;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.strength-indicator.very-weak { background: var(--danger-color); }
|
| 210 |
+
.strength-indicator.weak { background: #f97316; }
|
| 211 |
+
.strength-indicator.medium { background: var(--warning-color); }
|
| 212 |
+
.strength-indicator.strong { background: #84cc16; }
|
| 213 |
+
.strength-indicator.very-strong { background: var(--success-color); }
|
| 214 |
+
|
| 215 |
+
.feedback-item {
|
| 216 |
+
padding: 0.75rem;
|
| 217 |
+
margin-bottom: 0.5rem;
|
| 218 |
+
border-radius: 0.5rem;
|
| 219 |
+
font-size: 0.875rem;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.feedback-item.issue {
|
| 223 |
+
background: #fee2e2;
|
| 224 |
+
color: #991b1b;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.feedback-item.tip {
|
| 228 |
+
background: #e0f2fe;
|
| 229 |
+
color: #075985;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.breach-indicator {
|
| 233 |
+
display: inline-flex;
|
| 234 |
+
align-items: center;
|
| 235 |
+
padding: 0.5rem 1rem;
|
| 236 |
+
border-radius: 0.5rem;
|
| 237 |
+
font-weight: 500;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.breach-indicator.safe {
|
| 241 |
+
background: #dcfce7;
|
| 242 |
+
color: #166534;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.breach-indicator.pwned {
|
| 246 |
+
background: #fee2e2;
|
| 247 |
+
color: #991b1b;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.breach-indicator.error {
|
| 251 |
+
background: #fef3c7;
|
| 252 |
+
color: #92400e;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
footer {
|
| 256 |
+
text-align: center;
|
| 257 |
+
padding: 2rem;
|
| 258 |
+
color: var(--text-secondary);
|
| 259 |
+
font-size: 0.875rem;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.logout-link {
|
| 263 |
+
float: right;
|
| 264 |
+
color: var(--text-secondary);
|
| 265 |
+
text-decoration: none;
|
| 266 |
+
font-size: 0.875rem;
|
| 267 |
+
transition: var(--transition);
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.logout-link:hover {
|
| 271 |
+
color: var(--primary-color);
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
/* Responsive Design */
|
| 275 |
+
@media (max-width: 768px) {
|
| 276 |
+
.container {
|
| 277 |
+
padding: 1rem;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.card {
|
| 281 |
+
padding: 1.5rem;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.analysis-table {
|
| 285 |
+
display: block;
|
| 286 |
+
overflow-x: auto;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
nav ul {
|
| 290 |
+
flex-direction: column;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
nav a {
|
| 294 |
+
display: block;
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.header-content {
|
| 299 |
+
width: 100%;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.header-top {
|
| 303 |
+
display: flex;
|
| 304 |
+
justify-content: space-between;
|
| 305 |
+
align-items: center;
|
| 306 |
+
margin-bottom: 1rem;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.user-email {
|
| 310 |
+
color: var(--text-secondary);
|
| 311 |
+
margin-right: 0.5rem;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.logout-text {
|
| 315 |
+
color: var(--danger-color);
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.card-header {
|
| 319 |
+
margin-bottom: 2rem;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.card-description {
|
| 323 |
+
color: var(--text-secondary);
|
| 324 |
+
margin: 0.5rem 0 0;
|
| 325 |
+
font-size: 0.95rem;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.analysis-form {
|
| 329 |
+
max-width: 600px;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.password-input-wrapper {
|
| 333 |
+
position: relative;
|
| 334 |
+
display: flex;
|
| 335 |
+
align-items: center;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.form-actions {
|
| 339 |
+
display: flex;
|
| 340 |
+
justify-content: flex-start;
|
| 341 |
+
gap: 1rem;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.btn-primary {
|
| 345 |
+
background: var(--primary-color);
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.btn-secondary {
|
| 349 |
+
background: #4b5563;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.card-actions {
|
| 353 |
+
margin: 1.5rem 0;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.result-container {
|
| 357 |
+
background: #f8fafc;
|
| 358 |
+
border-radius: 0.5rem;
|
| 359 |
+
padding: 1rem;
|
| 360 |
+
margin-top: 1rem;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.table-container {
|
| 364 |
+
overflow-x: auto;
|
| 365 |
+
background: #f8fafc;
|
| 366 |
+
border-radius: 0.5rem;
|
| 367 |
+
padding: 1rem;
|
| 368 |
+
margin-top: 1rem;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.password-cell {
|
| 372 |
+
font-family: monospace;
|
| 373 |
+
font-size: 1rem;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.assessment-cell {
|
| 377 |
+
font-weight: 500;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.feedback-list {
|
| 381 |
+
display: flex;
|
| 382 |
+
flex-direction: column;
|
| 383 |
+
gap: 0.5rem;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.status-message {
|
| 387 |
+
margin: 1rem 0;
|
| 388 |
+
padding: 0.75rem;
|
| 389 |
+
border-radius: 0.5rem;
|
| 390 |
+
font-size: 0.875rem;
|
| 391 |
+
background: #f0f9ff;
|
| 392 |
+
color: var(--primary-color);
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
/* Enhanced Loading Animation */
|
| 396 |
+
.loading-indicator {
|
| 397 |
+
background: rgba(255, 255, 255, 0.9);
|
| 398 |
+
border-radius: 0.5rem;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.loading-indicator p {
|
| 402 |
+
color: var(--text-secondary);
|
| 403 |
+
font-size: 0.875rem;
|
| 404 |
+
margin: 0;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
/* Improved Table Styles */
|
| 408 |
+
.analysis-table th:first-child,
|
| 409 |
+
.analysis-table td:first-child {
|
| 410 |
+
border-top-left-radius: 0.5rem;
|
| 411 |
+
border-bottom-left-radius: 0.5rem;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
.analysis-table th:last-child,
|
| 415 |
+
.analysis-table td:last-child {
|
| 416 |
+
border-top-right-radius: 0.5rem;
|
| 417 |
+
border-bottom-right-radius: 0.5rem;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
/* Strength Bar Enhancements */
|
| 421 |
+
.strength-bar-table {
|
| 422 |
+
width: 100%;
|
| 423 |
+
height: 0.375rem;
|
| 424 |
+
background: #e5e7eb;
|
| 425 |
+
border-radius: 0.25rem;
|
| 426 |
+
overflow: hidden;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.strength-indicator-table {
|
| 430 |
+
height: 100%;
|
| 431 |
+
transition: width 0.3s ease, background-color 0.3s ease;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
/* Responsive Enhancements */
|
| 435 |
+
@media (max-width: 768px) {
|
| 436 |
+
.header-top {
|
| 437 |
+
flex-direction: column;
|
| 438 |
+
align-items: flex-start;
|
| 439 |
+
gap: 1rem;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.logout-link {
|
| 443 |
+
float: none;
|
| 444 |
+
display: inline-block;
|
| 445 |
+
margin-top: 0.5rem;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.card {
|
| 449 |
+
padding: 1.25rem;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.form-actions {
|
| 453 |
+
flex-direction: column;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.btn {
|
| 457 |
+
width: 100%;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.analysis-table {
|
| 461 |
+
font-size: 0.875rem;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.analysis-table th,
|
| 465 |
+
.analysis-table td {
|
| 466 |
+
padding: 0.75rem;
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
/* Dark Mode Support (if needed) */
|
| 471 |
+
@media (prefers-color-scheme: dark) {
|
| 472 |
+
:root {
|
| 473 |
+
--bg-card: #1f2937;
|
| 474 |
+
--text-primary: #f3f4f6;
|
| 475 |
+
--text-secondary: #9ca3af;
|
| 476 |
+
--border-color: #374151;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
body {
|
| 480 |
+
background: #111827;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
.card {
|
| 484 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
input[type="password"] {
|
| 488 |
+
background: #374151;
|
| 489 |
+
color: #f3f4f6;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.analysis-table th {
|
| 493 |
+
background: #374151;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
.result-container,
|
| 497 |
+
.table-container {
|
| 498 |
+
background: #1f2937;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
.status-message {
|
| 502 |
+
background: #1e3a8a;
|
| 503 |
+
color: #93c5fd;
|
| 504 |
+
}
|
| 505 |
+
}
|
| 506 |
+
</style>
|
| 507 |
+
</head>
|
| 508 |
+
<body>
|
| 509 |
+
<div class="container">
|
| 510 |
+
<header>
|
| 511 |
+
<div class="header-content">
|
| 512 |
+
<div class="header-top">
|
| 513 |
+
<h1>Password Analysis <span class="llm-badge">AI Insights</span></h1>
|
| 514 |
+
{% if current_user.is_authenticated %}
|
| 515 |
+
<a href="{{ url_for('logout') }}" class="logout-link">
|
| 516 |
+
<span class="user-email">{{ current_user.email }}</span>
|
| 517 |
+
<span class="logout-text">Logout</span>
|
| 518 |
+
</a>
|
| 519 |
+
{% endif %}
|
| 520 |
+
</div>
|
| 521 |
+
<nav>
|
| 522 |
+
<ul>
|
| 523 |
+
<li><a href="{{ url_for('add_password_page') }}">Add Password</a></li>
|
| 524 |
+
<li><a href="{{ url_for('storage') }}">View Passwords</a></li>
|
| 525 |
+
<li><a href="{{ url_for('analyse') }}" class="active">Analyse Passwords</a></li>
|
| 526 |
+
</ul>
|
| 527 |
+
</nav>
|
| 528 |
+
</div>
|
| 529 |
+
</header>
|
| 530 |
+
|
| 531 |
+
<main>
|
| 532 |
+
<!-- Single Password Analysis Card -->
|
| 533 |
+
<section class="card analysis-card">
|
| 534 |
+
<div class="card-header">
|
| 535 |
+
<h2>Analyse a Single Password</h2>
|
| 536 |
+
<p class="card-description">Enter a password below to get an instant strength analysis using AI insights and check if it has been compromised in known data breaches. The password itself is not stored or sent after analysis.</p>
|
| 537 |
+
</div>
|
| 538 |
+
<form id="analyse-form" class="analysis-form">
|
| 539 |
+
<div class="form-group">
|
| 540 |
+
<label for="analyse-password">Password to Analyse:</label>
|
| 541 |
+
<div class="password-input-wrapper">
|
| 542 |
+
<input type="password" id="analyse-password" name="analyse-password" required autocomplete="new-password" placeholder="Enter password to analyse">
|
| 543 |
+
<button type="button" id="toggle-analyse-password" class="toggle-button" title="Show/hide password">Show</button>
|
| 544 |
+
</div>
|
| 545 |
+
</div>
|
| 546 |
+
<div class="form-actions">
|
| 547 |
+
<button type="submit" class="btn btn-primary">Analyse Password</button>
|
| 548 |
+
</div>
|
| 549 |
+
</form>
|
| 550 |
+
|
| 551 |
+
<div id="single-loading" class="loading-indicator" style="display: none;">
|
| 552 |
+
<div class="spinner"></div>
|
| 553 |
+
<p>Analysing your password...</p>
|
| 554 |
+
</div>
|
| 555 |
+
|
| 556 |
+
<div id="single-analysis-result" class="analysis-result" style="display: none;">
|
| 557 |
+
<h3>Analysis Result</h3>
|
| 558 |
+
<div class="result-container">
|
| 559 |
+
<table class="analysis-table">
|
| 560 |
+
<thead>
|
| 561 |
+
<tr>
|
| 562 |
+
<th>Password</th>
|
| 563 |
+
<th>Strength</th>
|
| 564 |
+
<th>Assessment</th>
|
| 565 |
+
<th>AI Insights & Suggestions</th>
|
| 566 |
+
<th>Breach Status</th>
|
| 567 |
+
</tr>
|
| 568 |
+
</thead>
|
| 569 |
+
<tbody>
|
| 570 |
+
<tr>
|
| 571 |
+
<td id="password-value" class="password-cell">*****</td>
|
| 572 |
+
<td>
|
| 573 |
+
<div class="strength-bar">
|
| 574 |
+
<div id="strength-indicator" class="strength-indicator"></div>
|
| 575 |
+
</div>
|
| 576 |
+
</td>
|
| 577 |
+
<td id="strength-text" class="assessment-cell">Medium</td>
|
| 578 |
+
<td id="feedback-cell">
|
| 579 |
+
<div id="feedback-list" class="feedback-list"></div>
|
| 580 |
+
</td>
|
| 581 |
+
<td class="breach-cell">
|
| 582 |
+
<span id="single-breach-indicator" class="breach-indicator"></span>
|
| 583 |
+
</td>
|
| 584 |
+
</tr>
|
| 585 |
+
</tbody>
|
| 586 |
+
</table>
|
| 587 |
+
</div>
|
| 588 |
+
</div>
|
| 589 |
+
</section>
|
| 590 |
+
|
| 591 |
+
<!-- All Stored Passwords Analysis Card -->
|
| 592 |
+
<section class="card analysis-card">
|
| 593 |
+
<div class="card-header">
|
| 594 |
+
<h2>Analyse All Stored Credentials</h2>
|
| 595 |
+
<p class="card-description">Decrypt and analyse all your stored credentials to identify potential weaknesses and check for compromises across your accounts. This may take some time depending on the number of credentials.</p>
|
| 596 |
+
</div>
|
| 597 |
+
<div class="card-actions">
|
| 598 |
+
<button id="analyse-all-btn" class="btn btn-secondary">Load & Analyse All Stored Credentials</button>
|
| 599 |
+
</div>
|
| 600 |
+
|
| 601 |
+
<div id="all-passwords-loading" class="loading-indicator" style="display: none;">
|
| 602 |
+
<div class="spinner"></div>
|
| 603 |
+
<p>Loading, decrypting, and analysing all credentials...</p>
|
| 604 |
+
</div>
|
| 605 |
+
|
| 606 |
+
<div id="all-analysis-status" class="status-message"></div>
|
| 607 |
+
|
| 608 |
+
<div id="all-passwords-analysis" class="analysis-result" style="display: none;">
|
| 609 |
+
<h3>Stored Credentials Analysis</h3>
|
| 610 |
+
<div class="table-container">
|
| 611 |
+
<table class="analysis-table" id="password-analysis-table">
|
| 612 |
+
<thead>
|
| 613 |
+
<tr>
|
| 614 |
+
<th>Service/Website</th>
|
| 615 |
+
<th>Username/Email</th>
|
| 616 |
+
<th>Strength</th>
|
| 617 |
+
<th>Assessment</th>
|
| 618 |
+
<th>AI Insights & Suggestions</th>
|
| 619 |
+
<th>Breach Status</th>
|
| 620 |
+
</tr>
|
| 621 |
+
</thead>
|
| 622 |
+
<tbody id="password-analysis-list"></tbody>
|
| 623 |
+
</table>
|
| 624 |
+
</div>
|
| 625 |
+
</div>
|
| 626 |
+
</section>
|
| 627 |
+
</main>
|
| 628 |
+
|
| 629 |
+
<footer>
|
| 630 |
+
<p>Secure Password Manager - End-to-End Encrypted Password Management</p>
|
| 631 |
+
</footer>
|
| 632 |
+
</div>
|
| 633 |
+
|
| 634 |
+
<script src="{{ url_for('static', filename='js/crypto-helpers.js') }}"></script>
|
| 635 |
+
|
| 636 |
+
<script>
|
| 637 |
+
// --- E2EE Helper Functions (Assume they are loaded from crypto-helpers.js) ---
|
| 638 |
+
// Includes: base64UrlDecode, decryptData, base64ToArrayBuffer,
|
| 639 |
+
// arrayBufferToBase64, escapeHtml, sha1Hash, checkHIBPPassword
|
| 640 |
+
|
| 641 |
+
// --- Client-Side Password Analysis Helpers ---
|
| 642 |
+
function getPasswordCharacteristics(password) {
|
| 643 |
+
let comp = { lowercase: 0, uppercase: 0, digits: 0, special: 0 };
|
| 644 |
+
if (!password || typeof password !== 'string') return { length: 0, composition: comp };
|
| 645 |
+
for (let i = 0; i < password.length; i++) {
|
| 646 |
+
const char = password[i];
|
| 647 |
+
if (char >= 'a' && char <= 'z') comp.lowercase++; else if (char >= 'A' && char <= 'Z') comp.uppercase++;
|
| 648 |
+
else if (char >= '0' && char <= '9') comp.digits++; else if (!char.match(/^[a-zA-Z0-9\s]$/)) comp.special++;
|
| 649 |
+
} return { length: password.length, composition: comp };
|
| 650 |
+
}
|
| 651 |
+
function getStrengthClass(score) { // Maps 1-5 score to CSS class
|
| 652 |
+
if (score <= 1) return 'very-weak'; if (score === 2) return 'weak'; if (score === 3) return 'medium';
|
| 653 |
+
if (score === 4) return 'strong'; return 'very-strong';
|
| 654 |
+
}
|
| 655 |
+
function formatFeedbackHtml(feedbackArray) { // Formats LLM/Basic feedback
|
| 656 |
+
if (!feedbackArray || feedbackArray.length === 0) return '<div class="feedback-item tip">No specific issues found.</div>';
|
| 657 |
+
let html = '<div class="feedback-section">';
|
| 658 |
+
feedbackArray.forEach(fb => {
|
| 659 |
+
let itemClass = 'feedback-item'; let text = fb;
|
| 660 |
+
if (fb.toLowerCase().startsWith('issue:') || fb.toLowerCase().startsWith('warning:')) { itemClass += ' issue'; text = fb.substring(fb.indexOf(':') + 1).trim(); }
|
| 661 |
+
else if (fb.toLowerCase().startsWith('tip:') || fb.toLowerCase().startsWith('suggestion:')) { itemClass += ' tip'; text = fb.substring(fb.indexOf(':') + 1).trim(); }
|
| 662 |
+
html += `<div class="${itemClass}">${escapeHtml(text)}</div>`;
|
| 663 |
+
});
|
| 664 |
+
html += '</div>';
|
| 665 |
+
return html;
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
// --- DOMContentLoaded ---
|
| 669 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 670 |
+
const flaskKey = "{{ session.get('encryption_key', 'null') }}";
|
| 671 |
+
if (flaskKey !== 'null' && !sessionStorage.getItem('encryptionKey')) sessionStorage.setItem('encryptionKey', flaskKey);
|
| 672 |
+
else if (!sessionStorage.getItem('encryptionKey')) { console.warn("Key missing, redirecting."); window.location.href = "{{ url_for('login') }}"; return; }
|
| 673 |
+
|
| 674 |
+
// Single Analysis Elements
|
| 675 |
+
const analyseForm = document.getElementById('analyse-form');
|
| 676 |
+
const togglePasswordBtn = document.getElementById('toggle-analyse-password');
|
| 677 |
+
const passwordInput = document.getElementById('analyse-password');
|
| 678 |
+
const singleAnalysisResultEl = document.getElementById('single-analysis-result');
|
| 679 |
+
const singleLoadingEl = document.getElementById('single-loading');
|
| 680 |
+
const strengthIndicator = document.getElementById('strength-indicator');
|
| 681 |
+
const strengthText = document.getElementById('strength-text');
|
| 682 |
+
const feedbackList = document.getElementById('feedback-list');
|
| 683 |
+
const passwordValueCell = document.getElementById('password-value');
|
| 684 |
+
const singleBreachIndicator = document.getElementById('single-breach-indicator'); // Get breach element
|
| 685 |
+
const singleSubmitBtn = analyseForm.querySelector('button[type="submit"]');
|
| 686 |
+
|
| 687 |
+
// All Analysis Elements
|
| 688 |
+
const analyseAllBtn = document.getElementById('analyse-all-btn');
|
| 689 |
+
const allPasswordsLoadingEl = document.getElementById('all-passwords-loading');
|
| 690 |
+
const allPasswordsAnalysisEl = document.getElementById('all-passwords-analysis');
|
| 691 |
+
const passwordAnalysisListBody = document.getElementById('password-analysis-list');
|
| 692 |
+
const allAnalysisStatus = document.getElementById('all-analysis-status');
|
| 693 |
+
|
| 694 |
+
// --- Single Analysis Logic ---
|
| 695 |
+
togglePasswordBtn.addEventListener('click', function() {
|
| 696 |
+
const type = passwordInput.type === 'password' ? 'text' : 'password';
|
| 697 |
+
passwordInput.type = type; this.textContent = type === 'password' ? 'Show' : 'Hide';
|
| 698 |
+
});
|
| 699 |
+
|
| 700 |
+
analyseForm.addEventListener('submit', async function(e) {
|
| 701 |
+
e.preventDefault();
|
| 702 |
+
const password = passwordInput.value;
|
| 703 |
+
if (!password) return;
|
| 704 |
+
|
| 705 |
+
singleLoadingEl.style.display = 'block';
|
| 706 |
+
singleAnalysisResultEl.style.display = 'none';
|
| 707 |
+
singleSubmitBtn.disabled = true;
|
| 708 |
+
passwordValueCell.textContent = password.replace(/./g, '*'); // Mask password in table
|
| 709 |
+
|
| 710 |
+
// Reset previous results
|
| 711 |
+
strengthIndicator.style.width = '0%';
|
| 712 |
+
strengthIndicator.className = 'strength-indicator';
|
| 713 |
+
strengthText.textContent = 'Checking...';
|
| 714 |
+
feedbackList.innerHTML = '';
|
| 715 |
+
singleBreachIndicator.textContent = 'Checking...';
|
| 716 |
+
singleBreachIndicator.className = 'breach-indicator loading';
|
| 717 |
+
|
| 718 |
+
try {
|
| 719 |
+
const characteristics = getPasswordCharacteristics(password);
|
| 720 |
+
|
| 721 |
+
// --- Perform Checks Concurrently ---
|
| 722 |
+
const analysisPromise = fetch("{{ url_for('analyse_password_api') }}", {
|
| 723 |
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
| 724 |
+
body: JSON.stringify({ characteristics: characteristics })
|
| 725 |
+
});
|
| 726 |
+
// Ensure crypto-helpers is loaded and function exists
|
| 727 |
+
const breachPromise = typeof checkHIBPPassword === 'function'
|
| 728 |
+
? checkHIBPPassword(password)
|
| 729 |
+
: Promise.resolve({ error: "Breach check function not available." }); // Fallback
|
| 730 |
+
|
| 731 |
+
// --- Process Analysis Results ---
|
| 732 |
+
const analysisResponse = await analysisPromise;
|
| 733 |
+
if (!analysisResponse.ok) {
|
| 734 |
+
const err = await analysisResponse.json().catch(() => ({}));
|
| 735 |
+
console.error(`Analysis API failed: ${analysisResponse.status} ${err.error || ''}`);
|
| 736 |
+
strengthText.textContent = 'Error';
|
| 737 |
+
feedbackList.innerHTML = `<div class="feedback-item issue">Analysis Failed</div>`;
|
| 738 |
+
strengthIndicator.className = 'strength-indicator very-weak';
|
| 739 |
+
} else {
|
| 740 |
+
const analysisResult = await analysisResponse.json();
|
| 741 |
+
updateSingleAnalysisUI(analysisResult); // Update strength part of UI
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
// --- Process Breach Results ---
|
| 745 |
+
const breachResult = await breachPromise;
|
| 746 |
+
if (breachResult.error) {
|
| 747 |
+
singleBreachIndicator.textContent = `Error: ${escapeHtml(breachResult.error)}`;
|
| 748 |
+
singleBreachIndicator.className = 'breach-indicator error';
|
| 749 |
+
} else if (breachResult.isPwned) {
|
| 750 |
+
singleBreachIndicator.textContent = `Compromised! Found in ${breachResult.count} breach${breachResult.count > 1 ? 'es' : ''}.`;
|
| 751 |
+
singleBreachIndicator.className = 'breach-indicator pwned';
|
| 752 |
+
} else {
|
| 753 |
+
singleBreachIndicator.textContent = 'Not found in known breaches.';
|
| 754 |
+
singleBreachIndicator.className = 'breach-indicator safe';
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
} catch (error) {
|
| 758 |
+
console.error('Single analysis processing error:', error);
|
| 759 |
+
// Display generic error if fetch/processing fails broadly
|
| 760 |
+
strengthText.textContent = 'Error';
|
| 761 |
+
feedbackList.innerHTML = `<div class="feedback-item issue">Failed: ${escapeHtml(error.message)}</div>`;
|
| 762 |
+
singleBreachIndicator.textContent = 'Error checking.';
|
| 763 |
+
singleBreachIndicator.className = 'breach-indicator error';
|
| 764 |
+
strengthIndicator.className = 'strength-indicator very-weak';
|
| 765 |
+
|
| 766 |
+
} finally {
|
| 767 |
+
singleLoadingEl.style.display = 'none';
|
| 768 |
+
singleAnalysisResultEl.style.display = 'block'; // Show results table
|
| 769 |
+
singleSubmitBtn.disabled = false;
|
| 770 |
+
}
|
| 771 |
+
});
|
| 772 |
+
|
| 773 |
+
function updateSingleAnalysisUI(data) { // Only updates strength part now
|
| 774 |
+
const score = data.strength || 1;
|
| 775 |
+
strengthIndicator.style.width = (score / 5) * 100 + '%';
|
| 776 |
+
strengthIndicator.className = 'strength-indicator ' + getStrengthClass(score);
|
| 777 |
+
strengthText.textContent = data.assessment || 'Unknown';
|
| 778 |
+
if (feedbackList) feedbackList.innerHTML = formatFeedbackHtml(data.feedback);
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
// --- All Analysis Logic ---
|
| 782 |
+
analyseAllBtn.addEventListener('click', async function() {
|
| 783 |
+
allPasswordsLoadingEl.style.display = 'block'; allPasswordsAnalysisEl.style.display = 'none';
|
| 784 |
+
passwordAnalysisListBody.innerHTML = ''; allAnalysisStatus.textContent = ''; allAnalysisStatus.style.color = '';
|
| 785 |
+
analyseAllBtn.disabled = true;
|
| 786 |
+
try {
|
| 787 |
+
// ***** CORRECTED KEY RETRIEVAL *****
|
| 788 |
+
const keyB64Url = sessionStorage.getItem('encryptionKey');
|
| 789 |
+
if (!keyB64Url) {
|
| 790 |
+
console.error("Encryption key missing from sessionStorage.");
|
| 791 |
+
throw new Error("Key missing. Please login again.");
|
| 792 |
+
}
|
| 793 |
+
let encryptionKey = null;
|
| 794 |
+
try {
|
| 795 |
+
// Assuming base64UrlDecode is loaded from crypto-helpers.js
|
| 796 |
+
encryptionKey = base64UrlDecode(keyB64Url);
|
| 797 |
+
} catch (e) {
|
| 798 |
+
console.error("Failed to decode encryption key:", e);
|
| 799 |
+
throw new Error("Invalid encryption key format. Please login again.");
|
| 800 |
+
}
|
| 801 |
+
if (!encryptionKey) { // Should not happen if decode worked, but for safety
|
| 802 |
+
throw new Error("Key decoding failed unexpectedly. Please login again.");
|
| 803 |
+
}
|
| 804 |
+
// ***** END KEY RETRIEVAL CORRECTION *****
|
| 805 |
+
|
| 806 |
+
allAnalysisStatus.textContent = 'Fetching credentials...';
|
| 807 |
+
const credResponse = await fetch("{{ url_for('get_credentials') }}");
|
| 808 |
+
if (!credResponse.ok) {
|
| 809 |
+
if (credResponse.status === 401) {
|
| 810 |
+
throw new Error('Unauthorized (401). Session likely expired. Please re-login.');
|
| 811 |
+
}
|
| 812 |
+
throw new Error(`Failed to fetch credentials (${credResponse.status})`);
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
const encryptedCredentials = await credResponse.json();
|
| 816 |
+
if (!encryptedCredentials || encryptedCredentials.length === 0) {
|
| 817 |
+
allAnalysisStatus.textContent = 'No stored credentials found.'; allPasswordsLoadingEl.style.display = 'none'; analyseAllBtn.disabled = false; return;
|
| 818 |
+
}
|
| 819 |
+
allAnalysisStatus.textContent = `Found ${encryptedCredentials.length}. Analysing...`;
|
| 820 |
+
let analysedCount = 0; const totalCount = encryptedCredentials.length;
|
| 821 |
+
allPasswordsAnalysisEl.style.display = 'block'; // Show table header
|
| 822 |
+
|
| 823 |
+
// Use Promise.allSettled for concurrency
|
| 824 |
+
const analysisPromises = encryptedCredentials.map(async (cred) => {
|
| 825 |
+
const decrypted = await decryptData(encryptionKey, cred.encrypted_data); // Assumes decryptData is loaded
|
| 826 |
+
let analysisResult = { strength: 0, assessment: 'Error', feedback: ['Issue: Processing Error'] };
|
| 827 |
+
let breachResult = { isPwned: false, count: null, error: "Not checked" };
|
| 828 |
+
let serviceName = cred.service_hint || `(Hint: ${cred.id.substring(0,8)})`;
|
| 829 |
+
let userName = '(Unknown)';
|
| 830 |
+
let passwordForChecks = null;
|
| 831 |
+
|
| 832 |
+
if (decrypted) {
|
| 833 |
+
serviceName = decrypted.service || serviceName;
|
| 834 |
+
userName = decrypted.username || userName;
|
| 835 |
+
passwordForChecks = decrypted.password || null;
|
| 836 |
+
|
| 837 |
+
if (passwordForChecks) {
|
| 838 |
+
// Perform LLM/Basic Analysis (async)
|
| 839 |
+
const characteristics = getPasswordCharacteristics(passwordForChecks);
|
| 840 |
+
const analysisApiPromise = fetch("{{ url_for('analyse_password_api') }}", {
|
| 841 |
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ characteristics: characteristics })
|
| 842 |
+
}).then(async resp => {
|
| 843 |
+
if (resp.ok) return await resp.json();
|
| 844 |
+
console.warn(`Analysis API failed for ${serviceName}: ${resp.status}`);
|
| 845 |
+
return { strength: 0, assessment: 'API Error', feedback: ['Issue: Analysis service failed.'] };
|
| 846 |
+
}).catch(apiError => {
|
| 847 |
+
console.error(`Fetch error for analysis API (${serviceName}):`, apiError);
|
| 848 |
+
return { strength: 0, assessment: 'Fetch Error', feedback: [`Issue: ${apiError.message}`] };
|
| 849 |
+
});
|
| 850 |
+
|
| 851 |
+
// Perform HIBP Breach Check (async)
|
| 852 |
+
const breachCheckPromise = (typeof checkHIBPPassword === 'function')
|
| 853 |
+
? checkHIBPPassword(passwordForChecks)
|
| 854 |
+
.catch(hibpError => {
|
| 855 |
+
console.error("Error during HIBP check for", serviceName, hibpError);
|
| 856 |
+
return { isPwned: false, count: null, error: "Check failed" };
|
| 857 |
+
})
|
| 858 |
+
: Promise.resolve({ isPwned: false, count: null, error: "Checker fn missing" });
|
| 859 |
+
|
| 860 |
+
// Await both results
|
| 861 |
+
[analysisResult, breachResult] = await Promise.all([analysisApiPromise, breachCheckPromise]);
|
| 862 |
+
|
| 863 |
+
} else {
|
| 864 |
+
analysisResult = { strength: 0, assessment: 'No Password', feedback: ['Issue: No password found.'] };
|
| 865 |
+
breachResult = { isPwned: false, count: null, error: "No password" };
|
| 866 |
+
}
|
| 867 |
+
} else {
|
| 868 |
+
analysisResult = { strength: 0, assessment: 'Decrypt Error', feedback: ['Issue: Could not decrypt data.'] };
|
| 869 |
+
userName = '(Decryption Failed)';
|
| 870 |
+
breachResult = { isPwned: false, count: null, error: "Decryption failed" };
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
analysedCount++;
|
| 874 |
+
allAnalysisStatus.textContent = `Analysed ${analysedCount} of ${totalCount}...`;
|
| 875 |
+
|
| 876 |
+
return { service: serviceName, username: userName, analysis: analysisResult, breach: breachResult };
|
| 877 |
+
}); // End of map
|
| 878 |
+
|
| 879 |
+
// Wait for all analyses to settle
|
| 880 |
+
const settledResults = await Promise.allSettled(analysisPromises);
|
| 881 |
+
const finalResults = settledResults
|
| 882 |
+
.filter(result => result.status === 'fulfilled')
|
| 883 |
+
.map(result => result.value);
|
| 884 |
+
|
| 885 |
+
settledResults.forEach((result, index) => {
|
| 886 |
+
if(result.status === 'rejected') { console.error(`Analysis promise rejected for index ${index}:`, result.reason); }
|
| 887 |
+
});
|
| 888 |
+
|
| 889 |
+
displayAllAnalysisResults(finalResults);
|
| 890 |
+
allAnalysisStatus.textContent = `Analysis complete for ${finalResults.length} credentials.`;
|
| 891 |
+
|
| 892 |
+
} catch (error) {
|
| 893 |
+
console.error('All analysis processing error:', error); // Log the full error object
|
| 894 |
+
allAnalysisStatus.textContent = `Error: ${error.message || 'An unknown error occurred'}`; // Display message
|
| 895 |
+
allAnalysisStatus.style.color = 'red';
|
| 896 |
+
if (error.message.includes('Unauthorized (401)')) {
|
| 897 |
+
setTimeout(() => window.location.href = "{{ url_for('login') }}", 3000);
|
| 898 |
+
}
|
| 899 |
+
} finally {
|
| 900 |
+
allPasswordsLoadingEl.style.display = 'none';
|
| 901 |
+
analyseAllBtn.disabled = false;
|
| 902 |
+
}
|
| 903 |
+
});
|
| 904 |
+
|
| 905 |
+
// --- Modify displayAllAnalysisResults ---
|
| 906 |
+
function displayAllAnalysisResults(results) {
|
| 907 |
+
passwordAnalysisListBody.innerHTML = '';
|
| 908 |
+
allPasswordsAnalysisEl.style.display = 'block';
|
| 909 |
+
if (results.length === 0) {
|
| 910 |
+
passwordAnalysisListBody.innerHTML = '<tr><td colspan="6">No results to display.</td></tr>'; // Colspan is 6
|
| 911 |
+
return;
|
| 912 |
+
}
|
| 913 |
+
results.forEach((item) => {
|
| 914 |
+
const row = passwordAnalysisListBody.insertRow();
|
| 915 |
+
const analysis = item.analysis || { strength: 0, assessment: 'N/A', feedback: [] };
|
| 916 |
+
const breach = item.breach || { isPwned: false, count: null, error: "Unknown" };
|
| 917 |
+
const score = analysis.strength || 0;
|
| 918 |
+
const strengthClass = getStrengthClass(score);
|
| 919 |
+
const feedbackHtml = formatFeedbackHtml(analysis.feedback);
|
| 920 |
+
|
| 921 |
+
// Breach Status Cell Content
|
| 922 |
+
let breachHtml = ''; let breachClass = ''; let breachTitle = '';
|
| 923 |
+
if (breach.error) {
|
| 924 |
+
breachHtml = `Error`;
|
| 925 |
+
breachClass = 'error';
|
| 926 |
+
breachTitle = escapeHtml(breach.error);
|
| 927 |
+
} else if (breach.isPwned) {
|
| 928 |
+
breachHtml = `Compromised! (${breach.count})`;
|
| 929 |
+
breachClass = 'pwned';
|
| 930 |
+
breachTitle = `Found in ${breach.count} known breach(es).`;
|
| 931 |
+
} else {
|
| 932 |
+
breachHtml = `Not Found`;
|
| 933 |
+
breachClass = 'safe';
|
| 934 |
+
breachTitle = 'Not found in known breaches.';
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
row.innerHTML = `
|
| 938 |
+
<td>${escapeHtml(item.service)}</td>
|
| 939 |
+
<td>${escapeHtml(item.username)}</td>
|
| 940 |
+
<td><div class="strength-bar-table" title="Strength: ${score}/5"><div class="strength-indicator-table ${strengthClass}" style="width: ${(score/5)*100}%"></div></div></td>
|
| 941 |
+
<td>${escapeHtml(analysis.assessment || 'N/A')}</td>
|
| 942 |
+
<td>${feedbackHtml}</td>
|
| 943 |
+
<td class="breach-cell breach-${breachClass}" title="${breachTitle}"><span>${breachHtml}</span></td> <!-- Added Breach Cell with title -->
|
| 944 |
+
`;
|
| 945 |
+
});
|
| 946 |
+
}
|
| 947 |
+
}); // End DOMContentLoaded
|
| 948 |
+
</script>
|
| 949 |
+
</body>
|
| 950 |
+
</html>
|
templates/index.html
ADDED
|
@@ -0,0 +1,912 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Add Password - Secure Password Manager</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<style>
|
| 10 |
+
:root {
|
| 11 |
+
--primary-color: #2563eb;
|
| 12 |
+
--primary-hover: #1d4ed8;
|
| 13 |
+
--success-color: #10b981;
|
| 14 |
+
--warning-color: #f59e0b;
|
| 15 |
+
--danger-color: #ef4444;
|
| 16 |
+
--text-primary: #1f2937;
|
| 17 |
+
--text-secondary: #4b5563;
|
| 18 |
+
--bg-card: #ffffff;
|
| 19 |
+
--border-color: #e5e7eb;
|
| 20 |
+
--transition: all 0.3s ease;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
body {
|
| 24 |
+
font-family: 'Inter', sans-serif;
|
| 25 |
+
line-height: 1.6;
|
| 26 |
+
color: var(--text-primary);
|
| 27 |
+
background: #f3f4f6;
|
| 28 |
+
margin: 0;
|
| 29 |
+
padding: 0;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.container {
|
| 33 |
+
max-width: 1200px;
|
| 34 |
+
margin: 0 auto;
|
| 35 |
+
padding: 2rem;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
header {
|
| 39 |
+
background: var(--bg-card);
|
| 40 |
+
border-radius: 1rem;
|
| 41 |
+
padding: 1.5rem;
|
| 42 |
+
margin-bottom: 2rem;
|
| 43 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.header-content {
|
| 47 |
+
width: 100%;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.header-top {
|
| 51 |
+
display: flex;
|
| 52 |
+
justify-content: space-between;
|
| 53 |
+
align-items: center;
|
| 54 |
+
margin-bottom: 1rem;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.header-controls {
|
| 58 |
+
display: flex;
|
| 59 |
+
align-items: center;
|
| 60 |
+
gap: 1rem;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
h1 {
|
| 64 |
+
font-size: 2rem;
|
| 65 |
+
font-weight: 700;
|
| 66 |
+
color: var(--text-primary);
|
| 67 |
+
margin: 0;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
nav ul {
|
| 71 |
+
display: flex;
|
| 72 |
+
gap: 1rem;
|
| 73 |
+
padding: 0;
|
| 74 |
+
margin: 1rem 0 0;
|
| 75 |
+
list-style: none;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
nav a {
|
| 79 |
+
color: var(--text-secondary);
|
| 80 |
+
text-decoration: none;
|
| 81 |
+
padding: 0.5rem 1rem;
|
| 82 |
+
border-radius: 0.5rem;
|
| 83 |
+
transition: var(--transition);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
nav a:hover, nav a.active {
|
| 87 |
+
color: var(--primary-color);
|
| 88 |
+
background: #f0f9ff;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.card {
|
| 92 |
+
background: var(--bg-card);
|
| 93 |
+
border-radius: 1rem;
|
| 94 |
+
padding: 2rem;
|
| 95 |
+
margin-bottom: 2rem;
|
| 96 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 97 |
+
transition: var(--transition);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.card:hover {
|
| 101 |
+
transform: translateY(-2px);
|
| 102 |
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.form-group {
|
| 106 |
+
margin-bottom: 1.5rem;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
label {
|
| 110 |
+
display: block;
|
| 111 |
+
color: var(--text-primary);
|
| 112 |
+
font-weight: 500;
|
| 113 |
+
margin-bottom: 0.5rem;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
input[type="text"],
|
| 117 |
+
input[type="password"] {
|
| 118 |
+
width: 100%;
|
| 119 |
+
padding: 0.75rem 1rem;
|
| 120 |
+
border: 2px solid var(--border-color);
|
| 121 |
+
border-radius: 0.5rem;
|
| 122 |
+
font-size: 1rem;
|
| 123 |
+
transition: var(--transition);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
input[type="text"]:focus,
|
| 127 |
+
input[type="password"]:focus {
|
| 128 |
+
border-color: var(--primary-color);
|
| 129 |
+
outline: none;
|
| 130 |
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.password-input-group {
|
| 134 |
+
position: relative;
|
| 135 |
+
display: flex;
|
| 136 |
+
align-items: center;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.toggle-button {
|
| 140 |
+
position: absolute;
|
| 141 |
+
right: 7.5rem;
|
| 142 |
+
padding: 0.5rem;
|
| 143 |
+
background: none;
|
| 144 |
+
border: none;
|
| 145 |
+
color: var(--text-secondary);
|
| 146 |
+
cursor: pointer;
|
| 147 |
+
font-size: 0.875rem;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.generate-button {
|
| 151 |
+
position: absolute;
|
| 152 |
+
right: 0.5rem;
|
| 153 |
+
padding: 0.5rem 1rem;
|
| 154 |
+
background: none;
|
| 155 |
+
border: 1px solid var(--border-color);
|
| 156 |
+
border-radius: 0.5rem;
|
| 157 |
+
color: var(--text-secondary);
|
| 158 |
+
cursor: pointer;
|
| 159 |
+
font-size: 0.875rem;
|
| 160 |
+
transition: var(--transition);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.generate-button:hover {
|
| 164 |
+
border-color: var(--primary-color);
|
| 165 |
+
color: var(--primary-color);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.password-strength-area {
|
| 169 |
+
margin-top: 1rem;
|
| 170 |
+
padding: 1rem;
|
| 171 |
+
background: #f8fafc;
|
| 172 |
+
border-radius: 0.5rem;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.strength-meter-container {
|
| 176 |
+
display: flex;
|
| 177 |
+
align-items: center;
|
| 178 |
+
gap: 1rem;
|
| 179 |
+
margin-bottom: 1rem;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.strength-label {
|
| 183 |
+
color: var(--text-secondary);
|
| 184 |
+
font-size: 0.875rem;
|
| 185 |
+
min-width: 4rem;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.strength-bar {
|
| 189 |
+
flex-grow: 1;
|
| 190 |
+
height: 0.5rem;
|
| 191 |
+
background: #e5e7eb;
|
| 192 |
+
border-radius: 1rem;
|
| 193 |
+
overflow: hidden;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.strength-indicator {
|
| 197 |
+
height: 100%;
|
| 198 |
+
width: 0;
|
| 199 |
+
transition: width 0.3s ease, background-color 0.3s ease;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.strength-indicator.very-weak { background: var(--danger-color); }
|
| 203 |
+
.strength-indicator.weak { background: #f97316; }
|
| 204 |
+
.strength-indicator.medium { background: var(--warning-color); }
|
| 205 |
+
.strength-indicator.strong { background: #84cc16; }
|
| 206 |
+
.strength-indicator.very-strong { background: var(--success-color); }
|
| 207 |
+
|
| 208 |
+
.generator-options {
|
| 209 |
+
margin-top: 1.5rem;
|
| 210 |
+
padding: 1.5rem;
|
| 211 |
+
background: #f8fafc;
|
| 212 |
+
border-radius: 0.5rem;
|
| 213 |
+
border: 1px solid var(--border-color);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.generator-options h4 {
|
| 217 |
+
margin: 0 0 1rem;
|
| 218 |
+
color: var(--text-primary);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.length-control {
|
| 222 |
+
display: flex;
|
| 223 |
+
align-items: center;
|
| 224 |
+
gap: 1rem;
|
| 225 |
+
margin-bottom: 1rem;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.length-control input[type="range"] {
|
| 229 |
+
flex-grow: 1;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.length-display {
|
| 233 |
+
min-width: 2.5rem;
|
| 234 |
+
text-align: center;
|
| 235 |
+
font-variant-numeric: tabular-nums;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.char-options {
|
| 239 |
+
display: grid;
|
| 240 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 241 |
+
gap: 1rem;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.option-group {
|
| 245 |
+
display: flex;
|
| 246 |
+
align-items: center;
|
| 247 |
+
gap: 0.5rem;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.option-group input[type="checkbox"] {
|
| 251 |
+
width: 1rem;
|
| 252 |
+
height: 1rem;
|
| 253 |
+
border-radius: 0.25rem;
|
| 254 |
+
border: 2px solid var(--border-color);
|
| 255 |
+
cursor: pointer;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.option-group label {
|
| 259 |
+
margin: 0;
|
| 260 |
+
cursor: pointer;
|
| 261 |
+
font-size: 0.875rem;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.btn {
|
| 265 |
+
display: inline-flex;
|
| 266 |
+
align-items: center;
|
| 267 |
+
justify-content: center;
|
| 268 |
+
padding: 0.75rem 1.5rem;
|
| 269 |
+
background: var(--primary-color);
|
| 270 |
+
color: white;
|
| 271 |
+
border: none;
|
| 272 |
+
border-radius: 0.5rem;
|
| 273 |
+
font-size: 1rem;
|
| 274 |
+
font-weight: 500;
|
| 275 |
+
cursor: pointer;
|
| 276 |
+
transition: var(--transition);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.btn:hover {
|
| 280 |
+
background: var(--primary-hover);
|
| 281 |
+
transform: translateY(-1px);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.message {
|
| 285 |
+
margin-top: 1rem;
|
| 286 |
+
padding: 1rem;
|
| 287 |
+
border-radius: 0.5rem;
|
| 288 |
+
font-size: 0.875rem;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.message.success {
|
| 292 |
+
background: #dcfce7;
|
| 293 |
+
color: #166534;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.message.error {
|
| 297 |
+
background: #fee2e2;
|
| 298 |
+
color: #991b1b;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.breach-status-area {
|
| 302 |
+
margin-top: 1rem;
|
| 303 |
+
padding: 0.75rem;
|
| 304 |
+
border-radius: 0.5rem;
|
| 305 |
+
background: #f8fafc;
|
| 306 |
+
display: flex;
|
| 307 |
+
align-items: center;
|
| 308 |
+
gap: 0.5rem;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.breach-label {
|
| 312 |
+
color: var(--text-secondary);
|
| 313 |
+
font-size: 0.875rem;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.breach-indicator {
|
| 317 |
+
padding: 0.25rem 0.75rem;
|
| 318 |
+
border-radius: 1rem;
|
| 319 |
+
font-size: 0.875rem;
|
| 320 |
+
font-weight: 500;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.breach-indicator.safe {
|
| 324 |
+
background: #dcfce7;
|
| 325 |
+
color: #166534;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.breach-indicator.pwned {
|
| 329 |
+
background: #fee2e2;
|
| 330 |
+
color: #991b1b;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.breach-indicator.checking {
|
| 334 |
+
background: #dbeafe;
|
| 335 |
+
color: #1e40af;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
#theme-toggle {
|
| 339 |
+
padding: 0.5rem;
|
| 340 |
+
background: none;
|
| 341 |
+
border: 1px solid var(--border-color);
|
| 342 |
+
border-radius: 0.5rem;
|
| 343 |
+
cursor: pointer;
|
| 344 |
+
transition: var(--transition);
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
#theme-toggle:hover {
|
| 348 |
+
border-color: var(--primary-color);
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.logout-link {
|
| 352 |
+
color: var(--text-secondary);
|
| 353 |
+
text-decoration: none;
|
| 354 |
+
font-size: 0.875rem;
|
| 355 |
+
transition: var(--transition);
|
| 356 |
+
display: flex;
|
| 357 |
+
align-items: center;
|
| 358 |
+
gap: 0.5rem;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.logout-link:hover {
|
| 362 |
+
color: var(--primary-color);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
footer {
|
| 366 |
+
text-align: center;
|
| 367 |
+
padding: 2rem;
|
| 368 |
+
color: var(--text-secondary);
|
| 369 |
+
font-size: 0.875rem;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
@media (max-width: 768px) {
|
| 373 |
+
.container {
|
| 374 |
+
padding: 1rem;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.card {
|
| 378 |
+
padding: 1.5rem;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.header-top {
|
| 382 |
+
flex-direction: column;
|
| 383 |
+
align-items: flex-start;
|
| 384 |
+
gap: 1rem;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
nav ul {
|
| 388 |
+
flex-direction: column;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
nav a {
|
| 392 |
+
display: block;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.char-options {
|
| 396 |
+
grid-template-columns: 1fr;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.password-input-group {
|
| 400 |
+
flex-direction: column;
|
| 401 |
+
align-items: stretch;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
.toggle-button,
|
| 405 |
+
.generate-button {
|
| 406 |
+
position: static;
|
| 407 |
+
margin-top: 0.5rem;
|
| 408 |
+
}
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
@media (prefers-color-scheme: dark) {
|
| 412 |
+
:root {
|
| 413 |
+
--bg-card: #1f2937;
|
| 414 |
+
--text-primary: #f3f4f6;
|
| 415 |
+
--text-secondary: #9ca3af;
|
| 416 |
+
--border-color: #374151;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
body {
|
| 420 |
+
background: #111827;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
input[type="text"],
|
| 424 |
+
input[type="password"] {
|
| 425 |
+
background: #374151;
|
| 426 |
+
color: #f3f4f6;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.password-strength-area,
|
| 430 |
+
.generator-options,
|
| 431 |
+
.breach-status-area {
|
| 432 |
+
background: #374151;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.card {
|
| 436 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.message.success {
|
| 440 |
+
background: #064e3b;
|
| 441 |
+
color: #6ee7b7;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.message.error {
|
| 445 |
+
background: #7f1d1d;
|
| 446 |
+
color: #fecaca;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.breach-indicator.safe {
|
| 450 |
+
background: #064e3b;
|
| 451 |
+
color: #6ee7b7;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.breach-indicator.pwned {
|
| 455 |
+
background: #7f1d1d;
|
| 456 |
+
color: #fecaca;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.breach-indicator.checking {
|
| 460 |
+
background: #1e3a8a;
|
| 461 |
+
color: #93c5fd;
|
| 462 |
+
}
|
| 463 |
+
}
|
| 464 |
+
</style>
|
| 465 |
+
</head>
|
| 466 |
+
<body>
|
| 467 |
+
<div class="container">
|
| 468 |
+
<header>
|
| 469 |
+
<div class="header-content">
|
| 470 |
+
<div class="header-top">
|
| 471 |
+
<h1>Add New Password</h1>
|
| 472 |
+
<div class="header-controls">
|
| 473 |
+
<button id="theme-toggle" title="Toggle light/dark theme">
|
| 474 |
+
<span class="icon-sun">☀️</span>
|
| 475 |
+
<span class="icon-moon" style="display:none;">🌙</span>
|
| 476 |
+
</button>
|
| 477 |
+
{% if current_user.is_authenticated %}
|
| 478 |
+
<a href="{{ url_for('logout') }}" class="logout-link">
|
| 479 |
+
<span class="user-email">{{ current_user.email }}</span>
|
| 480 |
+
<span class="logout-text">Logout</span>
|
| 481 |
+
</a>
|
| 482 |
+
{% endif %}
|
| 483 |
+
</div>
|
| 484 |
+
</div>
|
| 485 |
+
<nav>
|
| 486 |
+
<ul>
|
| 487 |
+
<li><a href="{{ url_for('add_password_page') }}" class="active">Add Password</a></li>
|
| 488 |
+
<li><a href="{{ url_for('storage') }}">View Passwords</a></li>
|
| 489 |
+
<li><a href="{{ url_for('analyse') }}">Analyse Passwords</a></li>
|
| 490 |
+
</ul>
|
| 491 |
+
</nav>
|
| 492 |
+
</div>
|
| 493 |
+
</header>
|
| 494 |
+
|
| 495 |
+
<main>
|
| 496 |
+
<section class="card">
|
| 497 |
+
<h2>Add New Credential</h2>
|
| 498 |
+
<form id="password-form">
|
| 499 |
+
<div class="form-group">
|
| 500 |
+
<label for="service">Service/Website</label>
|
| 501 |
+
<input type="text" id="service" name="service" required placeholder="Enter service or website name">
|
| 502 |
+
</div>
|
| 503 |
+
<div class="form-group">
|
| 504 |
+
<label for="username">Username/Email</label>
|
| 505 |
+
<input type="text" id="username" name="username" required placeholder="Enter username or email">
|
| 506 |
+
</div>
|
| 507 |
+
<div class="form-group">
|
| 508 |
+
<label for="password">Password</label>
|
| 509 |
+
<div class="password-input-group">
|
| 510 |
+
<input type="password" id="password" name="password" required autocomplete="new-password" placeholder="Enter password">
|
| 511 |
+
<button type="button" id="toggle-password" class="toggle-button" title="Show/hide password">Show</button>
|
| 512 |
+
<button type="button" id="generate-password-btn" class="generate-button">Generate</button>
|
| 513 |
+
</div>
|
| 514 |
+
</div>
|
| 515 |
+
|
| 516 |
+
<div id="password-strength-area" class="password-strength-area" style="display: none;">
|
| 517 |
+
<div class="strength-meter-container">
|
| 518 |
+
<span class="strength-label">Strength:</span>
|
| 519 |
+
<div class="strength-bar">
|
| 520 |
+
<div id="strength-indicator" class="strength-indicator"></div>
|
| 521 |
+
</div>
|
| 522 |
+
</div>
|
| 523 |
+
<div id="password-strength-feedback"></div>
|
| 524 |
+
<div id="password-breach-status" class="breach-status-area" style="display: none;">
|
| 525 |
+
<span class="breach-label">Breach Check:</span>
|
| 526 |
+
<span id="breach-status-indicator" class="breach-indicator checking">Checking...</span>
|
| 527 |
+
</div>
|
| 528 |
+
</div>
|
| 529 |
+
|
| 530 |
+
<div id="generator-options" class="generator-options" style="display: none;">
|
| 531 |
+
<h4>Password Generator Options</h4>
|
| 532 |
+
<div class="length-control">
|
| 533 |
+
<label for="gen-length">Length:</label>
|
| 534 |
+
<input type="range" id="gen-length" name="gen-length" min="8" max="64" value="16">
|
| 535 |
+
<span class="length-display" id="gen-length-value">16</span>
|
| 536 |
+
</div>
|
| 537 |
+
<div class="char-options">
|
| 538 |
+
<div class="option-group">
|
| 539 |
+
<input type="checkbox" id="gen-lowercase" name="gen-lowercase" checked>
|
| 540 |
+
<label for="gen-lowercase">Lowercase (a-z)</label>
|
| 541 |
+
</div>
|
| 542 |
+
<div class="option-group">
|
| 543 |
+
<input type="checkbox" id="gen-uppercase" name="gen-uppercase" checked>
|
| 544 |
+
<label for="gen-uppercase">Uppercase (A-Z)</label>
|
| 545 |
+
</div>
|
| 546 |
+
<div class="option-group">
|
| 547 |
+
<input type="checkbox" id="gen-digits" name="gen-digits" checked>
|
| 548 |
+
<label for="gen-digits">Digits (0-9)</label>
|
| 549 |
+
</div>
|
| 550 |
+
<div class="option-group">
|
| 551 |
+
<input type="checkbox" id="gen-symbols" name="gen-symbols" checked>
|
| 552 |
+
<label for="gen-symbols">Symbols (!@#...)</label>
|
| 553 |
+
</div>
|
| 554 |
+
</div>
|
| 555 |
+
</div>
|
| 556 |
+
|
| 557 |
+
<div class="form-group">
|
| 558 |
+
<button type="submit" class="btn">Encrypt & Save</button>
|
| 559 |
+
</div>
|
| 560 |
+
</form>
|
| 561 |
+
<div id="message" class="message"></div>
|
| 562 |
+
</section>
|
| 563 |
+
</main>
|
| 564 |
+
|
| 565 |
+
<footer>
|
| 566 |
+
<p>Secure Password Manager - End-to-End Encrypted Password Management</p>
|
| 567 |
+
</footer>
|
| 568 |
+
</div>
|
| 569 |
+
|
| 570 |
+
<script src="{{ url_for('static', filename='js/zxcvbn.js') }}"></script>
|
| 571 |
+
<script src="{{ url_for('static', filename='js/crypto-helpers.js') }}"></script>
|
| 572 |
+
<script>
|
| 573 |
+
// --- E2EE Helper Functions (Encryption/Decryption - Keep for saving) ---
|
| 574 |
+
function base64UrlDecode(b64url){let b64=b64url.replace(/-/g,'+').replace(/_/g,'/');while(b64.length%4){b64+='=';}return base64ToArrayBuffer(b64);}
|
| 575 |
+
// Ensure base64ToArrayBuffer and arrayBufferToBase64 are loaded from crypto-helpers.js
|
| 576 |
+
// async function getEncryptionKeyRawBytes(){ ... } // Keep this function if needed elsewhere, but primarily use sessionStorage key
|
| 577 |
+
async function getEncryptionKeyRawBytesFromSession(){ const k=sessionStorage.getItem('encryptionKey'); if(!k){console.error("Key missing.");alert("Key missing. Login.");window.location.href="{{ url_for('login') }}"; return null;} try{ const rawKey = base64UrlDecode(k); console.log("Key from session (B64URL Decoded):", arrayBufferToBase64(rawKey)); return rawKey; } catch(e){console.error("Key decode fail:",e);alert("Invalid key. Login.");window.location.href="{{ url_for('login') }}";return null;}}
|
| 578 |
+
// Ensure encryptData is loaded from crypto-helpers.js
|
| 579 |
+
|
| 580 |
+
// --- Theme Toggler ---
|
| 581 |
+
const themeToggleBtn = document.getElementById('theme-toggle');
|
| 582 |
+
const sunIcon = themeToggleBtn?.querySelector('.icon-sun');
|
| 583 |
+
const moonIcon = themeToggleBtn?.querySelector('.icon-moon');
|
| 584 |
+
const currentTheme = localStorage.getItem('theme') || 'light'; // Default to light
|
| 585 |
+
|
| 586 |
+
function applyTheme(theme) {
|
| 587 |
+
document.body.setAttribute('data-theme', theme);
|
| 588 |
+
localStorage.setItem('theme', theme);
|
| 589 |
+
if (sunIcon && moonIcon) {
|
| 590 |
+
if (theme === 'dark') {
|
| 591 |
+
sunIcon.style.display = 'none';
|
| 592 |
+
moonIcon.style.display = 'inline';
|
| 593 |
+
} else {
|
| 594 |
+
sunIcon.style.display = 'inline';
|
| 595 |
+
moonIcon.style.display = 'none';
|
| 596 |
+
}
|
| 597 |
+
}
|
| 598 |
+
}
|
| 599 |
+
applyTheme(currentTheme); // Apply initial theme on load
|
| 600 |
+
if (themeToggleBtn) {
|
| 601 |
+
themeToggleBtn.addEventListener('click', () => {
|
| 602 |
+
const newTheme = document.body.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
| 603 |
+
applyTheme(newTheme);
|
| 604 |
+
});
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
// --- Strength & Breach Meter Helpers ---
|
| 608 |
+
function getStrengthClassFromScore(score) {
|
| 609 |
+
const classes = ['very-weak', 'weak', 'medium', 'strong', 'very-strong'];
|
| 610 |
+
return classes[score] || 'very-weak';
|
| 611 |
+
}
|
| 612 |
+
function formatBackendFeedback(feedbackArray) {
|
| 613 |
+
let html = '<ul>';
|
| 614 |
+
if (!feedbackArray || feedbackArray.length === 0) {
|
| 615 |
+
html += '<li class="suggestion">Analysis complete.</li>';
|
| 616 |
+
} else {
|
| 617 |
+
feedbackArray.forEach(fb => {
|
| 618 |
+
let itemClass = 'suggestion'; // Default
|
| 619 |
+
if (fb.toLowerCase().includes('warning:') || fb.toLowerCase().includes('issue:')) {
|
| 620 |
+
itemClass = 'warning';
|
| 621 |
+
}
|
| 622 |
+
html += `<li class="${itemClass}">${escapeHtml(fb)}</li>`;
|
| 623 |
+
});
|
| 624 |
+
}
|
| 625 |
+
html += '</ul>';
|
| 626 |
+
return html;
|
| 627 |
+
}
|
| 628 |
+
// Ensure escapeHtml is loaded from crypto-helpers.js
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
// --- DOMContentLoaded Event Listener ---
|
| 632 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 633 |
+
// Ensure key is in sessionStorage from Flask session or redirect
|
| 634 |
+
const flaskProvidedKey = "{{ session.get('encryption_key', 'null') }}";
|
| 635 |
+
if (flaskProvidedKey && flaskProvidedKey !== 'null' && !sessionStorage.getItem('encryptionKey')) {
|
| 636 |
+
sessionStorage.setItem('encryptionKey', flaskProvidedKey);
|
| 637 |
+
console.log("Encryption key loaded into sessionStorage from Flask.");
|
| 638 |
+
} else if (!sessionStorage.getItem('encryptionKey')) {
|
| 639 |
+
console.warn("Encryption key missing from Flask session and sessionStorage. Redirecting to login.");
|
| 640 |
+
window.location.href = "{{ url_for('login') }}";
|
| 641 |
+
return; // Stop script execution if no key
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
// --- Get DOM Elements ---
|
| 645 |
+
const form = document.getElementById('password-form');
|
| 646 |
+
const messageEl = document.getElementById('message');
|
| 647 |
+
const togglePasswordBtn = document.getElementById('toggle-password');
|
| 648 |
+
const passwordInput = document.getElementById('password');
|
| 649 |
+
const submitButton = form.querySelector('button[type="submit"]');
|
| 650 |
+
const generatePasswordBtn = document.getElementById('generate-password-btn');
|
| 651 |
+
const genLengthSlider = document.getElementById('gen-length');
|
| 652 |
+
const genLengthValueSpan = document.getElementById('gen-length-value');
|
| 653 |
+
const genLowercaseCheckbox = document.getElementById('gen-lowercase');
|
| 654 |
+
const genUppercaseCheckbox = document.getElementById('gen-uppercase');
|
| 655 |
+
const genDigitsCheckbox = document.getElementById('gen-digits');
|
| 656 |
+
const genSymbolsCheckbox = document.getElementById('gen-symbols');
|
| 657 |
+
|
| 658 |
+
// Strength & Breach Display Elements
|
| 659 |
+
const strengthArea = document.getElementById('password-strength-area');
|
| 660 |
+
const strengthIndicator = document.getElementById('strength-indicator');
|
| 661 |
+
const strengthTextLabel = document.getElementById('strength-text-label');
|
| 662 |
+
const strengthFeedbackDiv = document.getElementById('password-strength-feedback');
|
| 663 |
+
const breachStatusArea = document.getElementById('password-breach-status');
|
| 664 |
+
const breachStatusIndicator = document.getElementById('breach-status-indicator');
|
| 665 |
+
|
| 666 |
+
// --- Debounce Function ---
|
| 667 |
+
let debounceTimerStrength;
|
| 668 |
+
function debounce(func, delay) {
|
| 669 |
+
return function(...args) {
|
| 670 |
+
clearTimeout(debounceTimerStrength);
|
| 671 |
+
debounceTimerStrength = setTimeout(() => {
|
| 672 |
+
func.apply(this, args);
|
| 673 |
+
}, delay);
|
| 674 |
+
};
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
// --- Function to Update UI for Both Strength and Breach ---
|
| 678 |
+
async function updatePasswordAnalysisUI(password) {
|
| 679 |
+
if (!password) {
|
| 680 |
+
// Hide strength meter and breach status if password is empty
|
| 681 |
+
strengthArea.style.display = 'none';
|
| 682 |
+
strengthIndicator.className = 'strength-indicator';
|
| 683 |
+
strengthTextLabel.textContent = 'Strength:';
|
| 684 |
+
strengthFeedbackDiv.innerHTML = '';
|
| 685 |
+
strengthArea.className = 'password-strength-area';
|
| 686 |
+
breachStatusArea.style.display = 'none'; // Hide breach area
|
| 687 |
+
breachStatusIndicator.textContent = 'Checking...';
|
| 688 |
+
breachStatusIndicator.className = 'breach-indicator'; // Reset breach style
|
| 689 |
+
return;
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
// --- Show elements and indicate loading ---
|
| 693 |
+
strengthArea.style.display = 'block'; // Show combined area
|
| 694 |
+
breachStatusArea.style.display = 'flex'; // Show breach area (use flex for alignment)
|
| 695 |
+
strengthTextLabel.textContent = 'Strength: Checking...';
|
| 696 |
+
strengthIndicator.className = 'strength-indicator'; // Reset bar
|
| 697 |
+
strengthFeedbackDiv.innerHTML = '<ul><li>Checking...</li></ul>';
|
| 698 |
+
strengthArea.className = 'password-strength-area'; // Reset background
|
| 699 |
+
breachStatusIndicator.textContent = 'Checking...';
|
| 700 |
+
breachStatusIndicator.className = 'breach-indicator loading'; // Style for loading
|
| 701 |
+
|
| 702 |
+
// --- Perform Checks Concurrently ---
|
| 703 |
+
const strengthPromise = fetch("{{ url_for('strength_check_api') }}", {
|
| 704 |
+
method: 'POST',
|
| 705 |
+
headers: { 'Content-Type': 'application/json' },
|
| 706 |
+
body: JSON.stringify({ password: password })
|
| 707 |
+
});
|
| 708 |
+
// Use the FREE HIBP check directly from crypto-helpers.js
|
| 709 |
+
const breachPromise = checkHIBPPassword(password); // Assumes checkHIBPPassword is loaded
|
| 710 |
+
|
| 711 |
+
// --- Process Strength Results ---
|
| 712 |
+
try {
|
| 713 |
+
const response = await strengthPromise;
|
| 714 |
+
if (!response.ok) {
|
| 715 |
+
const errorData = await response.json().catch(() => ({ error: 'Unknown server error' }));
|
| 716 |
+
throw new Error(errorData.error || `Server error: ${response.status}`);
|
| 717 |
+
}
|
| 718 |
+
const result = await response.json();
|
| 719 |
+
const score = result.score; // Score 0-4
|
| 720 |
+
const strengthClass = getStrengthClassFromScore(score);
|
| 721 |
+
const assessment = result.assessment || "Unknown";
|
| 722 |
+
|
| 723 |
+
strengthIndicator.className = `strength-indicator ${strengthClass}`;
|
| 724 |
+
strengthTextLabel.textContent = `Strength: ${assessment}`;
|
| 725 |
+
strengthFeedbackDiv.innerHTML = formatBackendFeedback(result.feedback);
|
| 726 |
+
strengthArea.className = `password-strength-area strength-${score}`;
|
| 727 |
+
|
| 728 |
+
} catch (error) {
|
| 729 |
+
console.error('Strength Check Error:', error);
|
| 730 |
+
strengthTextLabel.textContent = 'Strength: Error';
|
| 731 |
+
strengthFeedbackDiv.innerHTML = `<ul><li class="warning">Error checking strength: ${escapeHtml(error.message)}</li></ul>`;
|
| 732 |
+
strengthArea.className = 'password-strength-area strength-0'; // Show error style
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
// --- Process Breach Results ---
|
| 736 |
+
try {
|
| 737 |
+
const breachResult = await breachPromise;
|
| 738 |
+
if (breachResult.error) {
|
| 739 |
+
breachStatusIndicator.textContent = `Error: ${escapeHtml(breachResult.error)}`;
|
| 740 |
+
breachStatusIndicator.className = 'breach-indicator error';
|
| 741 |
+
} else if (breachResult.isPwned) {
|
| 742 |
+
breachStatusIndicator.textContent = `Compromised! Found in ${breachResult.count} breach${breachResult.count > 1 ? 'es' : ''}.`;
|
| 743 |
+
breachStatusIndicator.className = 'breach-indicator pwned';
|
| 744 |
+
} else {
|
| 745 |
+
breachStatusIndicator.textContent = 'Not found in known breaches.';
|
| 746 |
+
breachStatusIndicator.className = 'breach-indicator safe';
|
| 747 |
+
}
|
| 748 |
+
} catch (error) { // Should not happen if checkHIBPPassword handles errors, but for safety
|
| 749 |
+
console.error("Error processing breach result:", error);
|
| 750 |
+
breachStatusIndicator.textContent = 'Error checking breach status.';
|
| 751 |
+
breachStatusIndicator.className = 'breach-indicator error';
|
| 752 |
+
}
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
// --- Password Input Listener ---
|
| 756 |
+
passwordInput.addEventListener('input', debounce(function() {
|
| 757 |
+
if (typeof checkHIBPPassword === 'function' && typeof zxcvbn === 'function') { // Check helpers are loaded
|
| 758 |
+
updatePasswordAnalysisUI(this.value);
|
| 759 |
+
} else {
|
| 760 |
+
console.error("Required analysis functions (checkHIBPPassword or zxcvbn) not found. Ensure scripts are loaded correctly.");
|
| 761 |
+
// Basic fallback or visual error indication could go here
|
| 762 |
+
strengthTextLabel.textContent = 'Strength: Error';
|
| 763 |
+
strengthArea.style.display = 'block';
|
| 764 |
+
strengthFeedbackDiv.innerHTML = `<ul><li class="warning">Error: Analysis script missing.</li></ul>`;
|
| 765 |
+
breachStatusIndicator.textContent = 'Error: Checker missing.';
|
| 766 |
+
breachStatusIndicator.className = 'breach-indicator error';
|
| 767 |
+
breachStatusArea.style.display = 'flex';
|
| 768 |
+
}
|
| 769 |
+
}, 600)); // Debounce API calls by 600ms
|
| 770 |
+
|
| 771 |
+
|
| 772 |
+
// --- Generator Logic ---
|
| 773 |
+
genLengthSlider.addEventListener('input', function() { genLengthValueSpan.textContent = this.value; });
|
| 774 |
+
togglePasswordBtn.addEventListener('click', function() { const t=passwordInput.type==='password'?'text':'password'; passwordInput.type=t; this.textContent=t==='password'?'Show':'Hide'; });
|
| 775 |
+
generatePasswordBtn.addEventListener('click', async function() {
|
| 776 |
+
messageEl.textContent = ''; messageEl.className = 'message';
|
| 777 |
+
generatePasswordBtn.disabled = true; generatePasswordBtn.textContent = '...';
|
| 778 |
+
const options = {
|
| 779 |
+
length: parseInt(genLengthSlider.value, 10),
|
| 780 |
+
use_lowercase: genLowercaseCheckbox.checked,
|
| 781 |
+
use_uppercase: genUppercaseCheckbox.checked,
|
| 782 |
+
use_digits: genDigitsCheckbox.checked,
|
| 783 |
+
use_symbols: genSymbolsCheckbox.checked
|
| 784 |
+
};
|
| 785 |
+
try {
|
| 786 |
+
// Fetch generated password from backend
|
| 787 |
+
const response = await fetch("{{ url_for('generate_password_api') }}", {
|
| 788 |
+
method: 'POST',
|
| 789 |
+
headers: { 'Content-Type': 'application/json' },
|
| 790 |
+
body: JSON.stringify(options)
|
| 791 |
+
});
|
| 792 |
+
const result = await response.json();
|
| 793 |
+
if (response.ok && result.password) {
|
| 794 |
+
passwordInput.value = result.password;
|
| 795 |
+
passwordInput.type = 'text'; // Show generated password briefly
|
| 796 |
+
togglePasswordBtn.textContent = 'Hide';
|
| 797 |
+
// *** Trigger the analysis immediately after generation ***
|
| 798 |
+
if (typeof checkHIBPPassword === 'function' && typeof zxcvbn === 'function') {
|
| 799 |
+
updatePasswordAnalysisUI(result.password); // Call analysis function
|
| 800 |
+
} else {
|
| 801 |
+
console.error("Analysis functions not available after generation.");
|
| 802 |
+
}
|
| 803 |
+
// Hide after a delay
|
| 804 |
+
setTimeout(() => {
|
| 805 |
+
if (passwordInput.type === 'text') {
|
| 806 |
+
passwordInput.type = 'password';
|
| 807 |
+
togglePasswordBtn.textContent = 'Show';
|
| 808 |
+
}
|
| 809 |
+
}, 2500); // Show for 2.5 seconds
|
| 810 |
+
} else {
|
| 811 |
+
throw new Error(result.error || 'Failed to generate password.');
|
| 812 |
+
}
|
| 813 |
+
} catch (error) {
|
| 814 |
+
messageEl.textContent = `Generation Error: ${escapeHtml(error.message)}`;
|
| 815 |
+
messageEl.className = 'message error';
|
| 816 |
+
console.error('Generate Password Error:', error);
|
| 817 |
+
} finally {
|
| 818 |
+
generatePasswordBtn.disabled = false;
|
| 819 |
+
generatePasswordBtn.textContent = 'Generate';
|
| 820 |
+
}
|
| 821 |
+
});
|
| 822 |
+
|
| 823 |
+
// --- Form Submission Logic ---
|
| 824 |
+
form.addEventListener('submit', async function(e) {
|
| 825 |
+
e.preventDefault();
|
| 826 |
+
messageEl.textContent = ''; messageEl.className = 'message';
|
| 827 |
+
submitButton.disabled = true; submitButton.textContent = 'Saving...';
|
| 828 |
+
|
| 829 |
+
const service = document.getElementById('service').value.trim();
|
| 830 |
+
const username = document.getElementById('username').value.trim();
|
| 831 |
+
const password = document.getElementById('password').value; // Get password from input
|
| 832 |
+
|
| 833 |
+
if (!service || !username || !password) {
|
| 834 |
+
messageEl.textContent = 'Service, Username, and Password are required.';
|
| 835 |
+
messageEl.className = 'message error';
|
| 836 |
+
submitButton.disabled = false;
|
| 837 |
+
submitButton.textContent = 'Encrypt & Save';
|
| 838 |
+
return;
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
try {
|
| 842 |
+
// Get the raw key bytes from session storage
|
| 843 |
+
const keyBuffer = await getEncryptionKeyRawBytesFromSession();
|
| 844 |
+
if (!keyBuffer) {
|
| 845 |
+
// Error handled within getEncryptionKeyRawBytesFromSession (alert/redirect)
|
| 846 |
+
submitButton.disabled = false;
|
| 847 |
+
submitButton.textContent = 'Encrypt & Save';
|
| 848 |
+
return;
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
// Encrypt the data
|
| 852 |
+
const dataToEncrypt = { service: service, username: username, password: password };
|
| 853 |
+
const encryptedB64Data = await encryptData(keyBuffer, dataToEncrypt); // Assumes encryptData is loaded
|
| 854 |
+
|
| 855 |
+
// Send to backend
|
| 856 |
+
const response = await fetch("{{ url_for('add_credential') }}", {
|
| 857 |
+
method: 'POST',
|
| 858 |
+
headers: { 'Content-Type': 'application/json' },
|
| 859 |
+
body: JSON.stringify({
|
| 860 |
+
encrypted_data: encryptedB64Data,
|
| 861 |
+
service_hint: service // Use service name as hint
|
| 862 |
+
})
|
| 863 |
+
});
|
| 864 |
+
const result = await response.json();
|
| 865 |
+
|
| 866 |
+
if (response.ok && result.success) {
|
| 867 |
+
messageEl.textContent = 'Credential encrypted and saved successfully!';
|
| 868 |
+
messageEl.className = 'message success';
|
| 869 |
+
form.reset(); // Clear the form
|
| 870 |
+
|
| 871 |
+
// Reset generator options to default
|
| 872 |
+
genLengthSlider.value = 16;
|
| 873 |
+
genLengthValueSpan.textContent = '16';
|
| 874 |
+
genLowercaseCheckbox.checked = true;
|
| 875 |
+
genUppercaseCheckbox.checked = true;
|
| 876 |
+
genDigitsCheckbox.checked = true;
|
| 877 |
+
genSymbolsCheckbox.checked = true;
|
| 878 |
+
passwordInput.type = 'password';
|
| 879 |
+
togglePasswordBtn.textContent = 'Show';
|
| 880 |
+
|
| 881 |
+
// Reset strength & breach display on successful save
|
| 882 |
+
strengthArea.style.display = 'none'; // Hide the whole area
|
| 883 |
+
breachStatusArea.style.display = 'none';
|
| 884 |
+
strengthIndicator.className = 'strength-indicator';
|
| 885 |
+
strengthTextLabel.textContent = 'Strength:';
|
| 886 |
+
strengthFeedbackDiv.innerHTML = '';
|
| 887 |
+
strengthArea.className = 'password-strength-area';
|
| 888 |
+
breachStatusIndicator.textContent = 'Checking...';
|
| 889 |
+
breachStatusIndicator.className = 'breach-indicator';
|
| 890 |
+
|
| 891 |
+
} else {
|
| 892 |
+
messageEl.textContent = `Error: ${escapeHtml(result.message || 'Failed to save credential.')}`;
|
| 893 |
+
messageEl.className = 'message error';
|
| 894 |
+
}
|
| 895 |
+
} catch (error) {
|
| 896 |
+
messageEl.textContent = `An error occurred: ${escapeHtml(error.message)}`;
|
| 897 |
+
messageEl.className = 'message error';
|
| 898 |
+
console.error('Save Credential Error:', error);
|
| 899 |
+
// Log specific crypto errors if they occur
|
| 900 |
+
if (error.message.includes("encrypt") || error.message.includes("Base64")) {
|
| 901 |
+
console.error("Potential encryption or encoding error during save.");
|
| 902 |
+
}
|
| 903 |
+
} finally {
|
| 904 |
+
submitButton.disabled = false;
|
| 905 |
+
submitButton.textContent = 'Encrypt & Save';
|
| 906 |
+
}
|
| 907 |
+
}); // End form submit listener
|
| 908 |
+
|
| 909 |
+
}); // End DOMContentLoaded
|
| 910 |
+
</script>
|
| 911 |
+
</body>
|
| 912 |
+
</html>
|
templates/login.html
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- START OF FILE login.html -->
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html lang="en">
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Login - Secure Password Manager</title>
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 10 |
+
<style>
|
| 11 |
+
:root {
|
| 12 |
+
--primary-color: #2563eb;
|
| 13 |
+
--primary-hover: #1d4ed8;
|
| 14 |
+
--success-color: #10b981;
|
| 15 |
+
--warning-color: #f59e0b;
|
| 16 |
+
--danger-color: #ef4444;
|
| 17 |
+
--text-primary: #1f2937;
|
| 18 |
+
--text-secondary: #4b5563;
|
| 19 |
+
--bg-card: #ffffff;
|
| 20 |
+
--border-color: #e5e7eb;
|
| 21 |
+
--transition: all 0.3s ease;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
body {
|
| 25 |
+
font-family: 'Inter', sans-serif;
|
| 26 |
+
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
| 27 |
+
min-height: 100vh;
|
| 28 |
+
display: flex;
|
| 29 |
+
align-items: center;
|
| 30 |
+
justify-content: center;
|
| 31 |
+
margin: 0;
|
| 32 |
+
padding: 20px;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.auth-container {
|
| 36 |
+
background: var(--bg-card);
|
| 37 |
+
max-width: 400px;
|
| 38 |
+
width: 100%;
|
| 39 |
+
padding: 2rem;
|
| 40 |
+
border-radius: 1rem;
|
| 41 |
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.auth-header {
|
| 45 |
+
text-align: center;
|
| 46 |
+
margin-bottom: 2rem;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.auth-header h2 {
|
| 50 |
+
color: var(--text-primary);
|
| 51 |
+
font-size: 1.75rem;
|
| 52 |
+
font-weight: 700;
|
| 53 |
+
margin: 0;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.form-group {
|
| 57 |
+
margin-bottom: 1.5rem;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
label {
|
| 61 |
+
display: block;
|
| 62 |
+
color: var(--text-primary);
|
| 63 |
+
font-weight: 500;
|
| 64 |
+
margin-bottom: 0.5rem;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
input[type="email"],
|
| 68 |
+
input[type="password"] {
|
| 69 |
+
width: 100%;
|
| 70 |
+
padding: 0.75rem 1rem;
|
| 71 |
+
border: 2px solid var(--border-color);
|
| 72 |
+
border-radius: 0.5rem;
|
| 73 |
+
font-size: 1rem;
|
| 74 |
+
transition: var(--transition);
|
| 75 |
+
background: white;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
input[type="email"]:focus,
|
| 79 |
+
input[type="password"]:focus {
|
| 80 |
+
border-color: var(--primary-color);
|
| 81 |
+
outline: none;
|
| 82 |
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.remember-me {
|
| 86 |
+
display: flex;
|
| 87 |
+
align-items: center;
|
| 88 |
+
gap: 0.5rem;
|
| 89 |
+
margin-bottom: 1.5rem;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.remember-me input[type="checkbox"] {
|
| 93 |
+
width: 1rem;
|
| 94 |
+
height: 1rem;
|
| 95 |
+
border-radius: 0.25rem;
|
| 96 |
+
border: 2px solid var(--border-color);
|
| 97 |
+
cursor: pointer;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.remember-me label {
|
| 101 |
+
margin: 0;
|
| 102 |
+
cursor: pointer;
|
| 103 |
+
color: var(--text-secondary);
|
| 104 |
+
font-size: 0.875rem;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.btn {
|
| 108 |
+
display: block;
|
| 109 |
+
width: 100%;
|
| 110 |
+
padding: 0.875rem;
|
| 111 |
+
background: var(--primary-color);
|
| 112 |
+
color: white;
|
| 113 |
+
border: none;
|
| 114 |
+
border-radius: 0.5rem;
|
| 115 |
+
font-size: 1rem;
|
| 116 |
+
font-weight: 500;
|
| 117 |
+
cursor: pointer;
|
| 118 |
+
transition: var(--transition);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.btn:hover {
|
| 122 |
+
background: var(--primary-hover);
|
| 123 |
+
transform: translateY(-1px);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.alert {
|
| 127 |
+
padding: 1rem;
|
| 128 |
+
margin-bottom: 1.5rem;
|
| 129 |
+
border-radius: 0.5rem;
|
| 130 |
+
font-size: 0.875rem;
|
| 131 |
+
display: flex;
|
| 132 |
+
align-items: center;
|
| 133 |
+
gap: 0.5rem;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.alert-danger {
|
| 137 |
+
background: #fee2e2;
|
| 138 |
+
color: #991b1b;
|
| 139 |
+
border: 1px solid #fecaca;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.alert-success {
|
| 143 |
+
background: #dcfce7;
|
| 144 |
+
color: #166534;
|
| 145 |
+
border: 1px solid #bbf7d0;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.alert-warning {
|
| 149 |
+
background: #fef3c7;
|
| 150 |
+
color: #92400e;
|
| 151 |
+
border: 1px solid #fde68a;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.alert-info {
|
| 155 |
+
background: #dbeafe;
|
| 156 |
+
color: #1e40af;
|
| 157 |
+
border: 1px solid #bfdbfe;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.auth-footer {
|
| 161 |
+
text-align: center;
|
| 162 |
+
margin-top: 2rem;
|
| 163 |
+
color: var(--text-secondary);
|
| 164 |
+
font-size: 0.875rem;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.auth-footer a {
|
| 168 |
+
color: var(--primary-color);
|
| 169 |
+
text-decoration: none;
|
| 170 |
+
font-weight: 500;
|
| 171 |
+
transition: var(--transition);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.auth-footer a:hover {
|
| 175 |
+
text-decoration: underline;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
@media (max-width: 480px) {
|
| 179 |
+
.auth-container {
|
| 180 |
+
padding: 1.5rem;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.auth-header h2 {
|
| 184 |
+
font-size: 1.5rem;
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
@media (prefers-color-scheme: dark) {
|
| 189 |
+
:root {
|
| 190 |
+
--bg-card: #1f2937;
|
| 191 |
+
--text-primary: #f3f4f6;
|
| 192 |
+
--text-secondary: #9ca3af;
|
| 193 |
+
--border-color: #374151;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
body {
|
| 197 |
+
background: linear-gradient(135deg, #111827 0%, #1f2937 100%);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
input[type="email"],
|
| 201 |
+
input[type="password"] {
|
| 202 |
+
background: #374151;
|
| 203 |
+
color: #f3f4f6;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.auth-container {
|
| 207 |
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
</style>
|
| 211 |
+
</head>
|
| 212 |
+
<body>
|
| 213 |
+
<div class="auth-container">
|
| 214 |
+
<div class="auth-header">
|
| 215 |
+
<h2>Welcome Back</h2>
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 219 |
+
{% if messages %}
|
| 220 |
+
{% for category, message in messages %}
|
| 221 |
+
{% set alert_class = 'alert-' + category if category in ['danger', 'success', 'warning'] else 'alert-info' %}
|
| 222 |
+
<div class="alert {{ alert_class }}">{{ message }}</div>
|
| 223 |
+
{% endfor %}
|
| 224 |
+
{% endif %}
|
| 225 |
+
{% endwith %}
|
| 226 |
+
|
| 227 |
+
<form method="POST" action="{{ url_for('login', next=request.args.get('next')) }}">
|
| 228 |
+
<div class="form-group">
|
| 229 |
+
<label for="email">Email Address</label>
|
| 230 |
+
<input type="email" id="email" name="email" required autocomplete="username" placeholder="Enter your email">
|
| 231 |
+
</div>
|
| 232 |
+
<div class="form-group">
|
| 233 |
+
<label for="password">Master Password</label>
|
| 234 |
+
<input type="password" id="password" name="password" required autocomplete="current-password" placeholder="Enter your master password">
|
| 235 |
+
</div>
|
| 236 |
+
<div class="remember-me">
|
| 237 |
+
<input type="checkbox" id="remember" name="remember" value="yes">
|
| 238 |
+
<label for="remember">Remember me</label>
|
| 239 |
+
</div>
|
| 240 |
+
<button type="submit" class="btn">Sign In</button>
|
| 241 |
+
</form>
|
| 242 |
+
<div class="auth-footer">
|
| 243 |
+
<p>Don't have an account? <a href="{{ url_for('register') }}">Create one now</a></p>
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
</body>
|
| 247 |
+
</html>
|
| 248 |
+
<!-- END OF FILE login.html -->
|
templates/register.html
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Register - Secure Password Manager</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<style>
|
| 10 |
+
:root {
|
| 11 |
+
--primary-color: #2563eb;
|
| 12 |
+
--primary-hover: #1d4ed8;
|
| 13 |
+
--success-color: #10b981;
|
| 14 |
+
--warning-color: #f59e0b;
|
| 15 |
+
--danger-color: #ef4444;
|
| 16 |
+
--text-primary: #1f2937;
|
| 17 |
+
--text-secondary: #4b5563;
|
| 18 |
+
--bg-card: #ffffff;
|
| 19 |
+
--border-color: #e5e7eb;
|
| 20 |
+
--transition: all 0.3s ease;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
body {
|
| 24 |
+
font-family: 'Inter', sans-serif;
|
| 25 |
+
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
| 26 |
+
min-height: 100vh;
|
| 27 |
+
display: flex;
|
| 28 |
+
align-items: center;
|
| 29 |
+
justify-content: center;
|
| 30 |
+
margin: 0;
|
| 31 |
+
padding: 20px;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.auth-container {
|
| 35 |
+
background: var(--bg-card);
|
| 36 |
+
max-width: 400px;
|
| 37 |
+
width: 100%;
|
| 38 |
+
padding: 2rem;
|
| 39 |
+
border-radius: 1rem;
|
| 40 |
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.auth-header {
|
| 44 |
+
text-align: center;
|
| 45 |
+
margin-bottom: 2rem;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.auth-header h2 {
|
| 49 |
+
color: var(--text-primary);
|
| 50 |
+
font-size: 1.75rem;
|
| 51 |
+
font-weight: 700;
|
| 52 |
+
margin: 0;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.form-group {
|
| 56 |
+
margin-bottom: 1.5rem;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
label {
|
| 60 |
+
display: block;
|
| 61 |
+
color: var(--text-primary);
|
| 62 |
+
font-weight: 500;
|
| 63 |
+
margin-bottom: 0.5rem;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
input[type="email"],
|
| 67 |
+
input[type="password"] {
|
| 68 |
+
width: 100%;
|
| 69 |
+
padding: 0.75rem 1rem;
|
| 70 |
+
border: 2px solid var(--border-color);
|
| 71 |
+
border-radius: 0.5rem;
|
| 72 |
+
font-size: 1rem;
|
| 73 |
+
transition: var(--transition);
|
| 74 |
+
background: white;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
input[type="email"]:focus,
|
| 78 |
+
input[type="password"]:focus {
|
| 79 |
+
border-color: var(--primary-color);
|
| 80 |
+
outline: none;
|
| 81 |
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.password-rules {
|
| 85 |
+
margin-top: 0.5rem;
|
| 86 |
+
padding: 0.75rem;
|
| 87 |
+
background: #f8fafc;
|
| 88 |
+
border-radius: 0.5rem;
|
| 89 |
+
font-size: 0.875rem;
|
| 90 |
+
color: var(--text-secondary);
|
| 91 |
+
border-left: 3px solid var(--warning-color);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.password-rules strong {
|
| 95 |
+
color: var(--text-primary);
|
| 96 |
+
display: block;
|
| 97 |
+
margin-bottom: 0.25rem;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.btn {
|
| 101 |
+
display: block;
|
| 102 |
+
width: 100%;
|
| 103 |
+
padding: 0.875rem;
|
| 104 |
+
background: var(--success-color);
|
| 105 |
+
color: white;
|
| 106 |
+
border: none;
|
| 107 |
+
border-radius: 0.5rem;
|
| 108 |
+
font-size: 1rem;
|
| 109 |
+
font-weight: 500;
|
| 110 |
+
cursor: pointer;
|
| 111 |
+
transition: var(--transition);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.btn:hover {
|
| 115 |
+
background: #059669;
|
| 116 |
+
transform: translateY(-1px);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.alert {
|
| 120 |
+
padding: 1rem;
|
| 121 |
+
margin-bottom: 1.5rem;
|
| 122 |
+
border-radius: 0.5rem;
|
| 123 |
+
font-size: 0.875rem;
|
| 124 |
+
display: flex;
|
| 125 |
+
align-items: center;
|
| 126 |
+
gap: 0.5rem;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.alert-danger {
|
| 130 |
+
background: #fee2e2;
|
| 131 |
+
color: #991b1b;
|
| 132 |
+
border: 1px solid #fecaca;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.alert-success {
|
| 136 |
+
background: #dcfce7;
|
| 137 |
+
color: #166534;
|
| 138 |
+
border: 1px solid #bbf7d0;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.alert-warning {
|
| 142 |
+
background: #fef3c7;
|
| 143 |
+
color: #92400e;
|
| 144 |
+
border: 1px solid #fde68a;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.alert-info {
|
| 148 |
+
background: #dbeafe;
|
| 149 |
+
color: #1e40af;
|
| 150 |
+
border: 1px solid #bfdbfe;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.auth-footer {
|
| 154 |
+
text-align: center;
|
| 155 |
+
margin-top: 2rem;
|
| 156 |
+
color: var(--text-secondary);
|
| 157 |
+
font-size: 0.875rem;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.auth-footer a {
|
| 161 |
+
color: var(--primary-color);
|
| 162 |
+
text-decoration: none;
|
| 163 |
+
font-weight: 500;
|
| 164 |
+
transition: var(--transition);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.auth-footer a:hover {
|
| 168 |
+
text-decoration: underline;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
@media (max-width: 480px) {
|
| 172 |
+
.auth-container {
|
| 173 |
+
padding: 1.5rem;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.auth-header h2 {
|
| 177 |
+
font-size: 1.5rem;
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
@media (prefers-color-scheme: dark) {
|
| 182 |
+
:root {
|
| 183 |
+
--bg-card: #1f2937;
|
| 184 |
+
--text-primary: #f3f4f6;
|
| 185 |
+
--text-secondary: #9ca3af;
|
| 186 |
+
--border-color: #374151;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
body {
|
| 190 |
+
background: linear-gradient(135deg, #111827 0%, #1f2937 100%);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
input[type="email"],
|
| 194 |
+
input[type="password"] {
|
| 195 |
+
background: #374151;
|
| 196 |
+
color: #f3f4f6;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.password-rules {
|
| 200 |
+
background: #374151;
|
| 201 |
+
border-left-color: var(--warning-color);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.auth-container {
|
| 205 |
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
</style>
|
| 209 |
+
</head>
|
| 210 |
+
<body>
|
| 211 |
+
<div class="auth-container">
|
| 212 |
+
<div class="auth-header">
|
| 213 |
+
<h2>Create Account</h2>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 217 |
+
{% if messages %}
|
| 218 |
+
{% for category, message in messages %}
|
| 219 |
+
{% set alert_class = 'alert-' + category if category in ['danger', 'success', 'warning'] else 'alert-info' %}
|
| 220 |
+
<div class="alert {{ alert_class }}">{{ message }}</div>
|
| 221 |
+
{% endfor %}
|
| 222 |
+
{% endif %}
|
| 223 |
+
{% endwith %}
|
| 224 |
+
|
| 225 |
+
<form method="POST" action="{{ url_for('register') }}">
|
| 226 |
+
<div class="form-group">
|
| 227 |
+
<label for="email">Email Address</label>
|
| 228 |
+
<input type="email" id="email" name="email" required autocomplete="email" placeholder="Enter your email">
|
| 229 |
+
</div>
|
| 230 |
+
<div class="form-group">
|
| 231 |
+
<label for="password">Master Password</label>
|
| 232 |
+
<input type="password" id="password" name="password" required autocomplete="new-password" placeholder="Create a strong master password" aria-describedby="passwordHelp">
|
| 233 |
+
<div id="passwordHelp" class="password-rules">
|
| 234 |
+
<strong>Important Security Note:</strong>
|
| 235 |
+
Choose a strong, unique Master Password. This password will be used to encrypt all your data and cannot be recovered if forgotten.
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
<div class="form-group">
|
| 239 |
+
<label for="confirm_password">Confirm Master Password</label>
|
| 240 |
+
<input type="password" id="confirm_password" name="confirm_password" required autocomplete="new-password" placeholder="Confirm your master password">
|
| 241 |
+
</div>
|
| 242 |
+
<button type="submit" class="btn">Create Account</button>
|
| 243 |
+
</form>
|
| 244 |
+
<div class="auth-footer">
|
| 245 |
+
<p>Already have an account? <a href="{{ url_for('login') }}">Sign in</a></p>
|
| 246 |
+
</div>
|
| 247 |
+
</div>
|
| 248 |
+
</body>
|
| 249 |
+
</html>
|
templates/storage.html
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>View Passwords - Secure Password Manager</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<style>
|
| 10 |
+
:root {
|
| 11 |
+
--primary-color: #2563eb;
|
| 12 |
+
--primary-hover: #1d4ed8;
|
| 13 |
+
--success-color: #10b981;
|
| 14 |
+
--warning-color: #f59e0b;
|
| 15 |
+
--danger-color: #ef4444;
|
| 16 |
+
--text-primary: #1f2937;
|
| 17 |
+
--text-secondary: #4b5563;
|
| 18 |
+
--bg-card: #ffffff;
|
| 19 |
+
--border-color: #e5e7eb;
|
| 20 |
+
--transition: all 0.3s ease;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
body {
|
| 24 |
+
font-family: 'Inter', sans-serif;
|
| 25 |
+
line-height: 1.6;
|
| 26 |
+
color: var(--text-primary);
|
| 27 |
+
background: #f3f4f6;
|
| 28 |
+
margin: 0;
|
| 29 |
+
padding: 0;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.container {
|
| 33 |
+
max-width: 1200px;
|
| 34 |
+
margin: 0 auto;
|
| 35 |
+
padding: 2rem;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
header {
|
| 39 |
+
background: var(--bg-card);
|
| 40 |
+
border-radius: 1rem;
|
| 41 |
+
padding: 1.5rem;
|
| 42 |
+
margin-bottom: 2rem;
|
| 43 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
h1 {
|
| 47 |
+
font-size: 2rem;
|
| 48 |
+
font-weight: 700;
|
| 49 |
+
color: var(--text-primary);
|
| 50 |
+
margin: 0;
|
| 51 |
+
display: flex;
|
| 52 |
+
align-items: center;
|
| 53 |
+
gap: 1rem;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
nav ul {
|
| 57 |
+
display: flex;
|
| 58 |
+
gap: 1rem;
|
| 59 |
+
padding: 0;
|
| 60 |
+
margin: 1rem 0 0;
|
| 61 |
+
list-style: none;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
nav a {
|
| 65 |
+
color: var(--text-secondary);
|
| 66 |
+
text-decoration: none;
|
| 67 |
+
padding: 0.5rem 1rem;
|
| 68 |
+
border-radius: 0.5rem;
|
| 69 |
+
transition: var(--transition);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
nav a:hover, nav a.active {
|
| 73 |
+
color: var(--primary-color);
|
| 74 |
+
background: #f0f9ff;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.card {
|
| 78 |
+
background: var(--bg-card);
|
| 79 |
+
border-radius: 1rem;
|
| 80 |
+
padding: 2rem;
|
| 81 |
+
margin-bottom: 2rem;
|
| 82 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 83 |
+
transition: var(--transition);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.card:hover {
|
| 87 |
+
transform: translateY(-2px);
|
| 88 |
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.search-bar {
|
| 92 |
+
margin-bottom: 1.5rem;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.search-bar input {
|
| 96 |
+
width: 100%;
|
| 97 |
+
padding: 0.75rem 1rem;
|
| 98 |
+
border: 2px solid var(--border-color);
|
| 99 |
+
border-radius: 0.5rem;
|
| 100 |
+
font-size: 1rem;
|
| 101 |
+
transition: var(--transition);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.search-bar input:focus {
|
| 105 |
+
border-color: var(--primary-color);
|
| 106 |
+
outline: none;
|
| 107 |
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.table-container {
|
| 111 |
+
overflow-x: auto;
|
| 112 |
+
margin: 1.5rem -1rem;
|
| 113 |
+
padding: 0 1rem;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
table {
|
| 117 |
+
width: 100%;
|
| 118 |
+
border-collapse: separate;
|
| 119 |
+
border-spacing: 0;
|
| 120 |
+
margin: 1.5rem 0;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
th, td {
|
| 124 |
+
padding: 1rem;
|
| 125 |
+
text-align: left;
|
| 126 |
+
border-bottom: 1px solid var(--border-color);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
th {
|
| 130 |
+
background: #f8fafc;
|
| 131 |
+
font-weight: 600;
|
| 132 |
+
color: var(--text-secondary);
|
| 133 |
+
position: sticky;
|
| 134 |
+
top: 0;
|
| 135 |
+
z-index: 10;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
tr:hover td {
|
| 139 |
+
background: #f8fafc;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.password-cell {
|
| 143 |
+
position: relative;
|
| 144 |
+
min-width: 200px;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.password-hidden {
|
| 148 |
+
font-family: monospace;
|
| 149 |
+
letter-spacing: 0.1em;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.toggle-button {
|
| 153 |
+
padding: 0.5rem 1rem;
|
| 154 |
+
background: none;
|
| 155 |
+
border: 1px solid var(--border-color);
|
| 156 |
+
border-radius: 0.5rem;
|
| 157 |
+
color: var(--text-secondary);
|
| 158 |
+
cursor: pointer;
|
| 159 |
+
font-size: 0.875rem;
|
| 160 |
+
transition: var(--transition);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.toggle-button:hover {
|
| 164 |
+
background: #f8fafc;
|
| 165 |
+
border-color: var(--primary-color);
|
| 166 |
+
color: var(--primary-color);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.toggle-button:disabled {
|
| 170 |
+
opacity: 0.5;
|
| 171 |
+
cursor: not-allowed;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.status-message {
|
| 175 |
+
padding: 1rem;
|
| 176 |
+
margin: 1rem 0;
|
| 177 |
+
border-radius: 0.5rem;
|
| 178 |
+
background: #f0f9ff;
|
| 179 |
+
color: var(--primary-color);
|
| 180 |
+
text-align: center;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.error-message {
|
| 184 |
+
background: #fee2e2;
|
| 185 |
+
color: var(--danger-color);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.logout-link {
|
| 189 |
+
float: right;
|
| 190 |
+
color: var(--text-secondary);
|
| 191 |
+
text-decoration: none;
|
| 192 |
+
font-size: 0.875rem;
|
| 193 |
+
transition: var(--transition);
|
| 194 |
+
display: flex;
|
| 195 |
+
align-items: center;
|
| 196 |
+
gap: 0.5rem;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.logout-link:hover {
|
| 200 |
+
color: var(--primary-color);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
footer {
|
| 204 |
+
text-align: center;
|
| 205 |
+
padding: 2rem;
|
| 206 |
+
color: var(--text-secondary);
|
| 207 |
+
font-size: 0.875rem;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
@media (max-width: 768px) {
|
| 211 |
+
.container {
|
| 212 |
+
padding: 1rem;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.card {
|
| 216 |
+
padding: 1.5rem;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
nav ul {
|
| 220 |
+
flex-direction: column;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
nav a {
|
| 224 |
+
display: block;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
th, td {
|
| 228 |
+
padding: 0.75rem;
|
| 229 |
+
font-size: 0.875rem;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.toggle-button {
|
| 233 |
+
padding: 0.375rem 0.75rem;
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
@media (prefers-color-scheme: dark) {
|
| 238 |
+
:root {
|
| 239 |
+
--bg-card: #1f2937;
|
| 240 |
+
--text-primary: #f3f4f6;
|
| 241 |
+
--text-secondary: #9ca3af;
|
| 242 |
+
--border-color: #374151;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
body {
|
| 246 |
+
background: #111827;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.card {
|
| 250 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.search-bar input {
|
| 254 |
+
background: #374151;
|
| 255 |
+
color: #f3f4f6;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
th {
|
| 259 |
+
background: #374151;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
tr:hover td {
|
| 263 |
+
background: #374151;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.toggle-button {
|
| 267 |
+
background: #374151;
|
| 268 |
+
color: #f3f4f6;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.toggle-button:hover {
|
| 272 |
+
background: #4b5563;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.status-message {
|
| 276 |
+
background: #1e3a8a;
|
| 277 |
+
color: #93c5fd;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.error-message {
|
| 281 |
+
background: #7f1d1d;
|
| 282 |
+
color: #fecaca;
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
</style>
|
| 286 |
+
</head>
|
| 287 |
+
<body>
|
| 288 |
+
<div class="container">
|
| 289 |
+
<header>
|
| 290 |
+
<div class="header-content">
|
| 291 |
+
<div class="header-top">
|
| 292 |
+
<h1>Password Storage</h1>
|
| 293 |
+
{% if current_user.is_authenticated %}
|
| 294 |
+
<a href="{{ url_for('logout') }}" class="logout-link">
|
| 295 |
+
<span class="user-email">{{ current_user.email }}</span>
|
| 296 |
+
<span class="logout-text">Logout</span>
|
| 297 |
+
</a>
|
| 298 |
+
{% endif %}
|
| 299 |
+
</div>
|
| 300 |
+
<nav>
|
| 301 |
+
<ul>
|
| 302 |
+
<li><a href="{{ url_for('add_password_page') }}">Add Password</a></li>
|
| 303 |
+
<li><a href="{{ url_for('storage') }}" class="active">View Passwords</a></li>
|
| 304 |
+
<li><a href="{{ url_for('analyse') }}">Analyse Passwords</a></li>
|
| 305 |
+
</ul>
|
| 306 |
+
</nav>
|
| 307 |
+
</div>
|
| 308 |
+
</header>
|
| 309 |
+
|
| 310 |
+
<main>
|
| 311 |
+
<section class="card">
|
| 312 |
+
<h2>Stored Credentials</h2>
|
| 313 |
+
<div class="search-bar">
|
| 314 |
+
<input type="text" id="search-input" placeholder="Search by service name..." title="Search works on the unencrypted service hint">
|
| 315 |
+
</div>
|
| 316 |
+
<div class="table-container">
|
| 317 |
+
<table id="passwords-table">
|
| 318 |
+
<thead>
|
| 319 |
+
<tr>
|
| 320 |
+
<th>Service/Website</th>
|
| 321 |
+
<th>Username/Email</th>
|
| 322 |
+
<th>Password</th>
|
| 323 |
+
<th>Date Added</th>
|
| 324 |
+
</tr>
|
| 325 |
+
</thead>
|
| 326 |
+
<tbody id="passwords-list">
|
| 327 |
+
<!-- Password entries will be populated here by JS -->
|
| 328 |
+
</tbody>
|
| 329 |
+
</table>
|
| 330 |
+
</div>
|
| 331 |
+
<div id="status-indicator" class="status-message">Loading credentials...</div>
|
| 332 |
+
</section>
|
| 333 |
+
</main>
|
| 334 |
+
|
| 335 |
+
<footer>
|
| 336 |
+
<p>Secure Password Manager - End-to-End Encrypted Password Management</p>
|
| 337 |
+
</footer>
|
| 338 |
+
</div>
|
| 339 |
+
|
| 340 |
+
<script src="{{ url_for('static', filename='js/crypto-helpers.js') }}"></script>
|
| 341 |
+
<script>
|
| 342 |
+
// --- E2EE Helper Functions (JavaScript using Web Crypto API) ---
|
| 343 |
+
function base64UrlDecode(b64url) {
|
| 344 |
+
let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
|
| 345 |
+
while (b64.length % 4) { b64 += '='; }
|
| 346 |
+
return base64ToArrayBuffer(b64);
|
| 347 |
+
}
|
| 348 |
+
async function getEncryptionKeyRawBytes() {
|
| 349 |
+
const keyB64Url = sessionStorage.getItem('encryptionKey');
|
| 350 |
+
if (!keyB64Url) {
|
| 351 |
+
console.error("Key missing"); alert("Key missing. Login again."); window.location.href = "{{ url_for('login') }}"; return null;
|
| 352 |
+
}
|
| 353 |
+
try { return base64UrlDecode(keyB64Url); }
|
| 354 |
+
catch (e) { console.error("Key decode failed:", e); alert("Invalid key. Login again."); window.location.href = "{{ url_for('login') }}"; return null; }
|
| 355 |
+
}
|
| 356 |
+
async function decryptData(keyBuffer, encryptedB64Data) {
|
| 357 |
+
try {
|
| 358 |
+
const combinedData = base64ToArrayBuffer(encryptedB64Data);
|
| 359 |
+
if (combinedData.byteLength < 12) throw new Error("Encrypted data too short.");
|
| 360 |
+
const iv = combinedData.slice(0, 12); const ciphertext = combinedData.slice(12);
|
| 361 |
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
|
| 362 |
+
const decryptedContent = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, cryptoKey, ciphertext);
|
| 363 |
+
return JSON.parse(new TextDecoder().decode(decryptedContent));
|
| 364 |
+
} catch (error) { console.error("Decryption failed:", error); return null; }
|
| 365 |
+
}
|
| 366 |
+
function arrayBufferToBase64(buffer) {
|
| 367 |
+
let binary = ''; const bytes = new Uint8Array(buffer); const len = bytes.byteLength;
|
| 368 |
+
for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary);
|
| 369 |
+
}
|
| 370 |
+
function base64ToArrayBuffer(base64) {
|
| 371 |
+
try {
|
| 372 |
+
const binary_string = window.atob(base64); const len = binary_string.length; const bytes = new Uint8Array(len);
|
| 373 |
+
for (let i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } return bytes.buffer;
|
| 374 |
+
} catch (e) { console.error("Base64 decoding failed:", e); throw new Error("Invalid Base64 string"); }
|
| 375 |
+
}
|
| 376 |
+
function escapeHtml(unsafe) {
|
| 377 |
+
// Corrected escaping
|
| 378 |
+
return unsafe
|
| 379 |
+
.replace(/&/g, "&")
|
| 380 |
+
.replace(/</g, "<")
|
| 381 |
+
.replace(/>/g, ">")
|
| 382 |
+
.replace(/"/g, "\"")
|
| 383 |
+
.replace(/'/g, "'");
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
// --- Main Logic ---
|
| 387 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 388 |
+
const flaskProvidedKey = "{{ session.get('encryption_key', 'null') }}";
|
| 389 |
+
if (flaskProvidedKey && flaskProvidedKey !== 'null' && !sessionStorage.getItem('encryptionKey')) {
|
| 390 |
+
sessionStorage.setItem('encryptionKey', flaskProvidedKey);
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
const passwordsListBody = document.getElementById('passwords-list');
|
| 394 |
+
const statusIndicator = document.getElementById('status-indicator');
|
| 395 |
+
const searchInput = document.getElementById('search-input');
|
| 396 |
+
|
| 397 |
+
let allCredentialsData = [];
|
| 398 |
+
let encryptionKey = null;
|
| 399 |
+
|
| 400 |
+
async function initialize() {
|
| 401 |
+
statusIndicator.textContent = 'Retrieving key...';
|
| 402 |
+
try {
|
| 403 |
+
encryptionKey = await getEncryptionKeyRawBytes();
|
| 404 |
+
if (!encryptionKey) { statusIndicator.textContent = 'Key missing. Login.'; statusIndicator.classList.add('error-message'); return; }
|
| 405 |
+
fetchAndDisplayCredentials();
|
| 406 |
+
} catch (error) { statusIndicator.textContent = `Init Error: ${error.message}`; statusIndicator.classList.add('error-message'); }
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
async function fetchAndDisplayCredentials() {
|
| 410 |
+
statusIndicator.textContent = 'Loading credentials...'; statusIndicator.classList.remove('error-message');
|
| 411 |
+
passwordsListBody.innerHTML = '';
|
| 412 |
+
try {
|
| 413 |
+
const response = await fetch("{{ url_for('get_credentials') }}");
|
| 414 |
+
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
| 415 |
+
const data = await response.json();
|
| 416 |
+
if (data && Array.isArray(data)) {
|
| 417 |
+
allCredentialsData = data; statusIndicator.textContent = '';
|
| 418 |
+
if (data.length === 0) { statusIndicator.textContent = 'No credentials stored yet.'; }
|
| 419 |
+
else { displayPlaceholders(data); }
|
| 420 |
+
} else { throw new Error("Invalid data received."); }
|
| 421 |
+
} catch (error) { console.error('Fetch error:', error); statusIndicator.textContent = `Load Error: ${error.message}`; statusIndicator.classList.add('error-message'); }
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
function displayPlaceholders(credentials) {
|
| 425 |
+
passwordsListBody.innerHTML = ''; statusIndicator.textContent = '';
|
| 426 |
+
if (credentials.length === 0){ statusIndicator.textContent = 'No credentials match search.'; return; }
|
| 427 |
+
credentials.forEach(item => {
|
| 428 |
+
const row = passwordsListBody.insertRow();
|
| 429 |
+
const date = new Date(item.created_at);
|
| 430 |
+
const formattedDate = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 431 |
+
const serviceDisplay = item.service_hint ? escapeHtml(item.service_hint) : '(Service Hidden)';
|
| 432 |
+
const usernameDisplay = '(Username Hidden)';
|
| 433 |
+
row.innerHTML = `
|
| 434 |
+
<td class="service-cell">${serviceDisplay}</td>
|
| 435 |
+
<td class="username-cell">${usernameDisplay}</td>
|
| 436 |
+
<td class="password-cell">
|
| 437 |
+
<span class="password-hidden" data-encrypted="${item.encrypted_data}" data-id="${item.id}">••••••••</span>
|
| 438 |
+
<button class="toggle-button password-toggle" title="Show/hide password">Show</button>
|
| 439 |
+
</td>
|
| 440 |
+
<td>${formattedDate}</td>
|
| 441 |
+
`;
|
| 442 |
+
});
|
| 443 |
+
addToggleListeners();
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
function addToggleListeners() {
|
| 447 |
+
document.querySelectorAll('.password-toggle').forEach(button => {
|
| 448 |
+
const newButton = button.cloneNode(true); button.parentNode.replaceChild(newButton, button);
|
| 449 |
+
newButton.addEventListener('click', handleToggleClick);
|
| 450 |
+
});
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
async function handleToggleClick(event) {
|
| 454 |
+
if (!encryptionKey) { alert("Key not available. Login again."); return; }
|
| 455 |
+
const button = event.target;
|
| 456 |
+
const passwordSpan = button.previousElementSibling;
|
| 457 |
+
const encryptedDataB64 = passwordSpan.dataset.encrypted;
|
| 458 |
+
const credentialId = passwordSpan.dataset.id;
|
| 459 |
+
const row = button.closest('tr');
|
| 460 |
+
const serviceCell = row.querySelector('.service-cell');
|
| 461 |
+
const usernameCell = row.querySelector('.username-cell');
|
| 462 |
+
button.disabled = true;
|
| 463 |
+
|
| 464 |
+
if (passwordSpan.classList.contains('password-hidden')) {
|
| 465 |
+
button.textContent = 'Decrypting...';
|
| 466 |
+
const decryptedData = await decryptData(encryptionKey, encryptedDataB64);
|
| 467 |
+
if (decryptedData) {
|
| 468 |
+
passwordSpan.textContent = escapeHtml(decryptedData.password);
|
| 469 |
+
passwordSpan.classList.remove('password-hidden');
|
| 470 |
+
serviceCell.textContent = escapeHtml(decryptedData.service);
|
| 471 |
+
usernameCell.textContent = escapeHtml(decryptedData.username);
|
| 472 |
+
button.textContent = 'Hide';
|
| 473 |
+
} else {
|
| 474 |
+
passwordSpan.textContent = 'Decrypt Error';
|
| 475 |
+
passwordSpan.classList.remove('password-hidden'); passwordSpan.style.color = 'red';
|
| 476 |
+
button.textContent = 'Error'; // Keep disabled or allow retry?
|
| 477 |
+
}
|
| 478 |
+
} else {
|
| 479 |
+
passwordSpan.textContent = '••••••••';
|
| 480 |
+
passwordSpan.classList.add('password-hidden'); passwordSpan.style.color = '';
|
| 481 |
+
button.textContent = 'Show';
|
| 482 |
+
const originalCredential = allCredentialsData.find(c => c.id === credentialId);
|
| 483 |
+
const serviceDisplay = originalCredential?.service_hint ? escapeHtml(originalCredential.service_hint) : '(Service Hidden)';
|
| 484 |
+
serviceCell.textContent = serviceDisplay; usernameCell.textContent = '(Username Hidden)';
|
| 485 |
+
}
|
| 486 |
+
button.disabled = false;
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
searchInput.addEventListener('input', function() {
|
| 490 |
+
const searchTerm = this.value.toLowerCase().trim();
|
| 491 |
+
if (!searchTerm) { displayPlaceholders(allCredentialsData); return; }
|
| 492 |
+
const filteredCredentials = allCredentialsData.filter(item =>
|
| 493 |
+
item.service_hint && item.service_hint.toLowerCase().includes(searchTerm)
|
| 494 |
+
);
|
| 495 |
+
displayPlaceholders(filteredCredentials);
|
| 496 |
+
});
|
| 497 |
+
|
| 498 |
+
initialize(); // Start
|
| 499 |
+
});
|
| 500 |
+
</script>
|
| 501 |
+
</body>
|
| 502 |
+
</html>
|