Zero-Knowledge secure vault for private data storage built with Python
Built with Python and focused on cryptography, python, security, zero-knowledge.
This repository is part of Neeraj Sai's growing collection of software projects, experiments, and learning builds. It reflects a practical, curious approach to creating useful products and understanding how they work under the hood.
Clone the repository and follow the setup instructions for the project's framework or language:
git clone https://github.com/neerajsait/ZK-Vault.git
cd ZK-VaultCheck the project files for the available run commands and configuration requirements.
Tiruveedhi Neeraj Venkata Sai
- GitHub: @neerajsait
- Portfolio: neeraj's portfolio
The vault divides its operations into three distinct flows to guarantee that raw passwords never touch the wire.
Establishes the user identity, generates the client salt, and registers the server-side login verifiers.
sequenceDiagram
autonumber
actor User as π€ User
participant Browser as π Browser (Client-side JS)
participant Server as π₯οΈ Server (Flask App)
participant DB as ποΈ MySQL Database
User->>Browser: Enters Name & Email
Browser->>Server: POST /request_signup_otp
Server->>User: Sends OTP Code via Email
User->>Browser: Enters OTP Code
Browser->>Server: POST /verify_signup_otp
Server-->>Browser: Session Verified!
Browser->>Server: GET /set_password/get_salt
Server-->>Browser: Returns Random Salt (16-byte base64)
User->>Browser: Enters Master Password
Browser->>Browser: Derive key1 = Argon2id(Password, Salt)
Browser->>Browser: Derive Login Verifier = HKDF(key1, "login-verifier")
Browser->>Server: POST /create_account (verifier)
Server->>Server: verifier_hash = Argon2id(HMAC(server_key, verifier))
Server->>DB: INSERT USER (salt, verifier_hash, encrypted_email)
Logs the user into the server session and establishes the local cryptographic key inside browser memory.
sequenceDiagram
autonumber
actor User as π€ User
participant Browser as π Browser (Client-side JS)
participant Server as π₯οΈ Server (Flask App)
participant DB as ποΈ MySQL Database
User->>Browser: Enters Email
Browser->>Server: POST /send_login_otp
Server->>User: Sends OTP Code via Email
User->>Browser: Enters OTP Code
Browser->>Server: POST /verify_login
Server-->>Browser: OTP check passes. Sets temp session.
Browser->>Browser: Retrieve user salt from page load config
User->>Browser: Enters Master Password
Browser->>Browser: Derive key1 = Argon2id(Password, Salt)
Browser->>Browser: Derive Login Verifier = HKDF(key1, "login-verifier")
Browser->>Server: POST /unlock (verifier)
Server->>Server: Compare verifier_hash using Argon2id
Server-->>Browser: Match! Sets session['vault_unlocked'] = True
Note over Browser: key1 resides strictly in-memory (never written to disk or LocalStorage)
Handles reading and writing of records. Payton data contains both metadata and file attachments.
sequenceDiagram
autonumber
participant Browser as π Browser (Client-side JS)
participant Server as π₯οΈ Server (Flask App)
participant DB as ποΈ MySQL Database
Note over Browser: To Save or Update a Record:
Browser->>Browser: Serialize JSON(title, notes, file_payloads)
Browser->>Browser: Encrypt JSON payload using key1 with AES-GCM
Browser->>Server: POST /api/records (ciphertext payload, size)
Server->>Server: Validate quota limits (records & storage size)
Server->>DB: Save Record (encrypted_payload, size)
Note over Browser: To List or Load Records:
Browser->>Server: GET /api/records
Server->>DB: Query records for user
DB-->>Server: Return encrypted record set
Server-->>Browser: JSON response (list of ciphertexts)
Browser->>Browser: Decrypt payload using local key1 (AES-GCM)
Browser->>Browser: Render cleartext credentials in UI
This application implements rigorous security safeguards to counter a wide array of web vulnerabilities:
- Zero-Knowledge Architecture: Cryptographic encryption and key derivation occur strictly inside the browser. No plaintext secrets or keys are sent to or stored on the server.
- Double-Layer Password Protection: The database stores password verifiers hashed using Argon2id. Furthermore, a server-side
VERIFIER_HMAC_KEYis mixed into the verifier hash, meaning an attacker who steals only the database cannot run offline dictionary attacks. - Email Address Encryption (At Rest): User email addresses are encrypted at rest in the database using the server's
EMAIL_ENCRYPTION_KEY, and indexed via a saltedEMAIL_INDEX_KEYHMAC. This prevents mass email leaks. - Brute-Force Lockouts: Accounts are locked automatically for increasing durations (30 minutes, 24 hours, up to 1 year/permanent lockout) after multiple incorrect password attempts.
- Form CSRF Protection: All input forms are secured with token validation using
Flask-WTFto block Cross-Site Request Forgery. - Strict Content Security Policy (CSP): Employs strict CSP headers via
Flask-Talismanwith dynamic scripts nonces to block XSS and code injection, and disables remote CDN script loading. - Server-Side Sessions (Redis): Login session data is kept in memory on Redis rather than in browser cookies, preventing session hijacking or manipulation.
- SSRF (Server-Side Request Forgery) Hardening: Restricts disposable email domain checking to a hardcoded domain with zero redirects and short timeouts, blocking SSRF vulnerabilities.
- Rate Limiting: Protects sensitive server routes (like OTP generation and logins) using
Flask-Limiterto prevent automated scraping or denial of service.
To run this project, make sure you have the following services and software installed locally:
- Python 3.8+
- MySQL 8.0+ or MariaDB
- Redis (Used for session storage and rate limiting)
- Node.js & npm (For executing local frontend components if utilizing dev tools)
Log into your local MySQL CLI or desktop client and run:
CREATE DATABASE secure_vault CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Navigate to the s directory and run:
# Create venv
python -m venv .venv
# Activate venv (PowerShell)
.venv\Scripts\Activate.ps1
# Activate venv (bash/mac)
source .venv/bin/activate
# Install dependencies
pip install -r requirements.txtGenerate cryptographically strong keys for your .env configuration file by running:
python -c "import os, base64; print(base64.b64encode(os.urandom(32)).decode())"Run this command 3 times to get 3 unique keys for the configuration.
Create a .env file in the root of the s folder. Copy the parameters from your newly generated keys and configure your local settings:
# Flask Settings
SECRET_KEY=ReplaceWithRandomString32CharsOrMore!
WTF_CSRF_SECRET_KEY=ReplaceWithAnotherLongRandomString!
FORCE_HTTPS=false
# Base64 Encoded Cryptographic Secret Keys (32 bytes)
VERIFIER_HMAC_KEY=base64_generated_key_1_here==
EMAIL_ENCRYPTION_KEY=base64_generated_key_2_here==
EMAIL_INDEX_KEY=base64_generated_key_3_here==
# Redis Session Store URL
REDIS_URL=redis://localhost:6379/0
# Database Settings
MYSQL_HOST=localhost
MYSQL_USER=your_mysql_user
MYSQL_PASSWORD=your_mysql_password
MYSQL_DB=secure_vault
# Mail/SMTP Configuration (for OTP delivery)
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=your_sender_account@gmail.com
MAIL_PASSWORD=your_gmail_app_password
# Quotas
MAX_RECORDS_PER_USER=1000
MAX_STORAGE_PER_USER_MB=100Verify that the MySQL and Redis servers are running:
- Windows (Redis Service): Ensure the
redis-servercommand or Windows Service is running. - MySQL: Ensure the MySQL daemon is listening.
python app.pyThe server will bind to 127.0.0.1:5000 by default. Open http://127.0.0.1:5000 in your web browser.
If you need to drop all tables and recreate the clean schema, run:
python wipe_db.py(This command will prompt you for confirmation and is disabled in production).
For manual inspection, the physical database tables mapped by SQLAlchemy models are defined as follows:
usersTable: Holds user credentials metadata, client salts, verifier hashes, failed login counters, and locks.normal_recordsTable: Stores the client-side AES-GCM encrypted payload and total byte sizes of normal vault items.secret_recordsTable: Houses records residing in the secondary "Secret Vault" partition.
The full SQL script is stored in mysql.txt.
A full integration testing script is provided in test_app.py. This script simulates a client browser executing key derivation (Argon2id) and requesting API tokens to verify the complete vault signup and login cycle.
To run the integration tests:
- Ensure your Flask server is running locally (
python app.py). - Run the test script in a separate terminal window:
python test_app.py
All endpoints prefixed with /api/ require a valid, authenticated user session where session['vault_unlocked'] == True.
| Method | Endpoint | Description | Request Payload | Response Code & Output |
|---|---|---|---|---|
POST |
/request_signup_otp |
Dispatches signup OTP code to target email | {"email": "...", "name": "..."} |
200 OK or redirects to OTP step |
POST |
/verify_signup_otp |
Validates signup OTP | {"email": "...", "otp": "..."} |
200 OK |
GET |
/set_password/get_salt |
Fetches signup salt | None | 200 OK, {"salt": "base64_salt"} |
POST |
/create_account |
Registers new user verifiers | {"verifier": "base64_verifier"} |
200 OK |
POST |
/send_login_otp |
Sends login OTP code | {"email": "..."} |
Redirects to OTP verification |
POST |
/verify_login |
Checks login OTP code | {"email": "...", "otp": "..."} |
Sets user session, redirects to password unlock |
POST |
/unlock |
Verifies login verifier and unlocks vault | {"verifier": "base64_verifier"} |
200 OK |
| Method | Endpoint | Description | Request Payload | Response Code & Output |
|---|---|---|---|---|
GET |
/api/records |
Returns all records for logged-in user | None | 200 OK, [{"id": "...", "payload": "...", "size": 123}] |
POST |
/api/records |
Creates a new vault record | {"payload": "...", "size": 123} |
200 OK, {"status": "success", "id": "rec_id"} |
PUT |
/api/records/<record_id> |
Updates an existing vault record | {"payload": "...", "size": 123} |
200 OK, {"status": "success"} |
DELETE |
/api/records/<record_id> |
Deletes a record from the database | None | 200 OK, {"status": "deleted"} |
GET |
/api/records/<record_id>/file/<int:file_index> |
Returns file payload within record | None | 200 OK, {"ciphertext": "...", "file_index": index} |
| Method | Endpoint | Description | Request Payload | Response Code & Output |
|---|---|---|---|---|
GET |
/api/secret/get_salt |
Retrieves the second secret vault salt | None | 200 OK, {"salt": "base64_salt"} |
POST |
/secret/setup |
Sets up the secret vault credentials | {"secret_salt": "...", "secret_verifier": "..."} |
Redirects to secret home |
POST |
/secret/unlock |
Unlocks secret vault partition | {"secret_verifier": "..."} |
200 OK |
GET |
/api/secret/records |
Lists all secret vault records | None | 200 OK, [{"id": "...", "payload": "...", "size": 123}] |
POST |
/api/secret/records |
Saves new secret record | {"payload": "...", "size": 123} |
200 OK |
DELETE |
/api/secret/records/<record_id> |
Deletes a secret vault record | None | 200 OK |
| Method | Endpoint | Description | Request Payload | Response Code & Output |
|---|---|---|---|---|
POST |
/api/change_password |
Performs local re-encryption batch update | {"new_salt": "...", "new_verifier": "...", "records": [...]} |
200 OK |
GET |
/api/user_quota |
Queries storage quota consumption | None | 200 OK, {"records": 10, "storage": 10240} |
POST |
/delete_account |
Deletes user record and clears database | None | Redirects to home page |
GET |
/logout |
Clears local sessions and ends transaction | None | Redirects to login page |
- Cause: The Flask server started successfully, but the local Redis server is inactive.
- Solution: Confirm your Redis instance is running. On Windows, open a terminal and run
redis-serveror check Windows services.
- Cause: The database target does not exist.
- Solution: Create the schema in MySQL manually:
CREATE DATABASE secure_vault;
- Cause: SMTP authentication failure or Gmail security blocker.
- Solution: Ensure your
MAIL_USERNAMEandMAIL_PASSWORDare valid. If you are using Gmail, you must use an App Password rather than your primary Google Account password.