Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

4 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ” Flask Password Manager

A backend-only password manager API with AES-GCM encryption and zero-knowledge architecture.
Built with Flask, PostgreSQL, and cryptography best practices.

This is a password manager API I built to learn proper encryption techniques and security architecture. It uses AES-GCM for encryption, Scrypt+HKDF for key derivation, and stores encryption keys in sessions (not in the database). Each user has a unique encryption key, so even if the database is compromised, passwords remain encrypted. It's not perfect but taught me a lot about cryptography and secure API design.


πŸ›‘οΈ Security & Architecture

  • Master passwords are hashed (never stored in plaintext).
  • Each user has a unique enc_salt for key derivation.
  • Encryption key is derived per user using Scrypt + HKDF.
  • All vault passwords are encrypted with AES-GCM (authenticated encryption).
  • AAD (Additional Authenticated Data) prevents ciphertext tampering.
  • Encryption key stored in session (not database) - vault must be unlocked each session.
  • Zero-knowledge architecture - server never knows the master password in plaintext.

πŸ”— API Routes

πŸ” Authentication


POST /register

Create a new user account.

Request:

{
  "email": "user@example.com",
  "master_password": "MyStrongPassword123"
}

What happens:

  1. Validates email uniqueness
  2. Hashes master password β†’ stores as auth_hash
  3. Generates random enc_salt for encryption key derivation
  4. Creates user in database

Response (201):

{
  "message": "User registered successfully"
}

POST /login

Login and unlock vault.

Request:

{
  "email": "user@example.com",
  "master_password": "MyStrongPassword123"
}

What happens:

  1. Verifies credentials (checks auth_hash)
  2. Derives encryption key using user's enc_salt + master password
  3. Stores encryption key in session (not database!)
  4. Returns JWT token

Response (200):

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Use this token in Authorization: Bearer <token> header.


πŸ—„οΈ Vault (Saved Accounts)

All vault endpoints require:

  • JWT token (authenticated)
  • Unlocked vault (encryption key in session)

POST /accounts/add

Save a new password to vault (encrypted).

Request:

{
  "title": "GitHub",
  "username": "myuser",
  "email": "myuser@example.com",
  "password": "supersecret123"
}

What happens:

  1. Validates input
  2. Encrypts password with AES-GCM using session key
  3. Stores encrypted ciphertext in database

Response (201):

{
  "message": "Account saved successfully",
  "id": 1
}

Restrictions:

  • Vault must be unlocked (must have logged in)
  • Title is required
  • Username or email required (at least one)

GET /accounts

Get list of saved accounts (metadata only, no passwords).

Response (200):

[
  {
    "id": 1,
    "title": "GitHub",
    "username": "myuser",
    "email": "myuser@example.com"
  },
  {
    "id": 2,
    "title": "Gmail",
    "username": null,
    "email": "myemail@gmail.com"
  }
]

Note: Passwords are not included in list - must be fetched individually.


GET /accounts/{account_id}/password

Decrypt and retrieve password for a specific account.

Response (200):

{
  "password": "supersecret123"
}

What happens:

  1. Checks account ownership
  2. Retrieves ciphertext from database
  3. Decrypts using session key
  4. Returns plaintext password

Security:

  • Only owner can decrypt
  • Requires session key (vault unlocked)
  • Each request decrypts on-demand

PUT /accounts/{account_id}/update

Update saved account metadata or password.

Request (all fields optional):

{
  "title": "GitHub Work",
  "username": "workuser",
  "email": "work@example.com",
  "password": "newpassword456"
}

What happens:

  1. Updates metadata (title, username, email)
  2. If password changed β†’ re-encrypts with current session key
  3. Saves to database

Response (200):

{
  "message": "Account updated successfully"
}

DELETE /accounts/{account_id}

Delete a saved account permanently.

Response (200):

{
  "message": "Account \"GitHub\" deleted successfully"
}

What happens:

  1. Checks account ownership
  2. Deletes from database
  3. Cannot be undone

Restrictions:

  • Only owner can delete
  • Vault must be unlocked
  • Deletion is permanent

Errors:

// 404 - Account not found
{
  "error": "Account not found"
}

// 403 - Not owner
{
  "error": "Forbidden"
}

πŸ“ Project Structure

flaskpasswordmanager/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ routes/              
β”‚   β”‚   β”œβ”€β”€ auth.py          
β”‚   β”‚   └── passwords.py
β”‚   β”œβ”€β”€ utils/
β”‚   β”‚   β”œβ”€β”€ encryption.py
β”‚   β”‚   β”œβ”€β”€ kdf.py
β”‚   β”‚   └── vault.py
β”‚   β”œβ”€β”€ __init__.py 
β”‚   β”œβ”€β”€ config.py           
β”‚   β”œβ”€β”€ logging.py          
β”‚   β”œβ”€β”€ models.py           
β”‚   └── schemas.py   
β”œβ”€β”€ tests/  
β”‚   β”œβ”€β”€ conftest.py
β”‚   └── t/
β”‚       β”œβ”€β”€ test_auth.py
β”‚       └── test_passwords.py
β”œβ”€β”€ migrations/     
β”œβ”€β”€ .env       
β”œβ”€β”€ requirements.txt        
β”œβ”€β”€ pytest.ini               
└── run.py                   

🧰 Technologies

  • Backend: Flask 3.0, Python 3.12
  • Database: PostgreSQL, SQLAlchemy
  • Authentication: JWT (Flask-JWT-Extended), Werkzeug password hashing
  • Encryption: Cryptography library (AES-GCM-256)
  • Key Derivation: Scrypt (N=32768, r=8, p=1) + HKDF
  • Session: Flask-Session (cachelib or Redis)
  • Validation: Marshmallow
  • Testing: Pytest

βš™οΈ Environment Variables

Create a .env file:

SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://username:password@localhost:5432/password_manager
JWT_SECRET_KEY=your-jwt-secret-here

SESSION_TYPE=cachelib

Optional (for production with Redis sessions):

SESSION_TYPE=redis
REDIS_URL=redis://localhost:6379/0

Generate secure keys:

python -c "import secrets; print(secrets.token_hex(32))"

πŸš€ Running the Project

1. Install dependencies:

pip install -r requirements.txt

2. Create .env file (see above)

3. Apply migrations:

flask db upgrade

4. (Optional) Start Redis if using Redis sessions:

docker run -p 6379:6379 redis

5. Run:

python run.py

API will be at http://localhost:5000


πŸ§ͺ Testing

# Run all tests
pytest

Current tests: 5 tests covering basic auth and vault operations


πŸ“Š Database Schema

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚       users         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ id (PK)             β”‚
β”‚ email (unique)      β”‚
β”‚ auth_hash           β”‚  ← Hashed master password
β”‚ enc_salt            β”‚  ← Random salt for key derivation
β”‚ kdf_version         β”‚  ← KDF algorithm version
β”‚ kdf_params          β”‚  ← Scrypt parameters
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        β”‚ (1:N)
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   saved_accounts    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ id (PK)             β”‚
β”‚ user_id (FK)        β”‚
β”‚ title               β”‚
β”‚ username            β”‚
β”‚ email               β”‚
β”‚ password_ciphertext β”‚  ← AES-GCM encrypted password
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ” How Encryption Works

Key Derivation Flow:

Master Password + enc_salt
         ↓
    Scrypt (N=32768, r=8, p=1)
         ↓
    Base Key (256 bits)
         ↓
    HKDF (with context "encryption-key")
         ↓
    Encryption Key (256 bits)
         ↓
    Store in Session (not database!)

Encryption Flow:

Plaintext Password
         ↓
    AES-GCM-256 (with random IV)
         ↓
    Ciphertext + Auth Tag
         ↓
    Store in Database

Why This Is Secure:

  1. Zero-knowledge: Server never knows master password plaintext
  2. Per-user keys: Each user has unique enc_salt β†’ unique encryption key
  3. Session-based: Encryption key lives in session, not database
  4. Authenticated encryption: AES-GCM prevents tampering (AAD protection)
  5. Key stretching: Scrypt makes brute-force attacks expensive

πŸ”’ Security Notes

What protects passwords:

  • Passwords encrypted with AES-GCM-256
  • Encryption key derived from master password + unique salt
  • Key stored in session (cleared on logout/timeout)
  • Even with database access, passwords remain encrypted

What to protect:

  • Master password (only user knows it)
  • Session cookies (contain encryption key)
  • .env file (contains app secrets)

For production:

  • Use Redis for sessions (more secure than filesystem)
  • Enable HTTPS
  • Set strong SECRET_KEY and JWT_SECRET_KEY
  • Configure session timeout
  • Add rate limiting on auth endpoints

πŸ“ What I Learned

Building this taught me a lot about cryptography and security:

  • How to properly implement AES-GCM encryption with authenticated data
  • Key derivation using Scrypt and HKDF
  • Zero-knowledge architecture (server never knows plaintext)
  • Session-based security for sensitive data
  • The difference between authentication (hashing) and encryption
  • Why per-user encryption keys matter

The hardest part was understanding key derivation properly - I had to learn about KDFs, why Scrypt is better than PBKDF2 for passwords, and how to use HKDF to derive multiple keys from a master key. Reading the cryptography library docs and security best practices took a lot of time, but now I understand how password managers like 1Password actually work under the hood.

If I rebuild this, I'd add password generation, password strength checker, and export/import functionality. Also would add more tests for encryption edge cases.


That's it! This is a learning project focused on understanding encryption and secure API design.

About

Secure Flask-based password manager API with user authentication, per-user encryption, AES-GCM vault protection, and clean modular architecture.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages