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.
- Master passwords are hashed (never stored in plaintext).
- Each user has a unique
enc_saltfor 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.
Create a new user account.
Request:
{
"email": "user@example.com",
"master_password": "MyStrongPassword123"
}What happens:
- Validates email uniqueness
- Hashes master password β stores as
auth_hash - Generates random
enc_saltfor encryption key derivation - Creates user in database
Response (201):
{
"message": "User registered successfully"
}Login and unlock vault.
Request:
{
"email": "user@example.com",
"master_password": "MyStrongPassword123"
}What happens:
- Verifies credentials (checks
auth_hash) - Derives encryption key using user's
enc_salt+ master password - Stores encryption key in session (not database!)
- Returns JWT token
Response (200):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Use this token in Authorization: Bearer <token> header.
All vault endpoints require:
- JWT token (authenticated)
- Unlocked vault (encryption key in session)
Save a new password to vault (encrypted).
Request:
{
"title": "GitHub",
"username": "myuser",
"email": "myuser@example.com",
"password": "supersecret123"
}What happens:
- Validates input
- Encrypts password with AES-GCM using session key
- 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 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.
Decrypt and retrieve password for a specific account.
Response (200):
{
"password": "supersecret123"
}What happens:
- Checks account ownership
- Retrieves ciphertext from database
- Decrypts using session key
- Returns plaintext password
Security:
- Only owner can decrypt
- Requires session key (vault unlocked)
- Each request decrypts on-demand
Update saved account metadata or password.
Request (all fields optional):
{
"title": "GitHub Work",
"username": "workuser",
"email": "work@example.com",
"password": "newpassword456"
}What happens:
- Updates metadata (title, username, email)
- If password changed β re-encrypts with current session key
- Saves to database
Response (200):
{
"message": "Account updated successfully"
}Delete a saved account permanently.
Response (200):
{
"message": "Account \"GitHub\" deleted successfully"
}What happens:
- Checks account ownership
- Deletes from database
- 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"
}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
- 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
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=cachelibOptional (for production with Redis sessions):
SESSION_TYPE=redis
REDIS_URL=redis://localhost:6379/0Generate secure keys:
python -c "import secrets; print(secrets.token_hex(32))"1. Install dependencies:
pip install -r requirements.txt2. Create .env file (see above)
3. Apply migrations:
flask db upgrade4. (Optional) Start Redis if using Redis sessions:
docker run -p 6379:6379 redis5. Run:
python run.pyAPI will be at http://localhost:5000
# Run all tests
pytestCurrent tests: 5 tests covering basic auth and vault operations
βββββββββββββββββββββββ
β 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
βββββββββββββββββββββββ
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!)
Plaintext Password
β
AES-GCM-256 (with random IV)
β
Ciphertext + Auth Tag
β
Store in Database
- Zero-knowledge: Server never knows master password plaintext
- Per-user keys: Each user has unique
enc_saltβ unique encryption key - Session-based: Encryption key lives in session, not database
- Authenticated encryption: AES-GCM prevents tampering (AAD protection)
- Key stretching: Scrypt makes brute-force attacks expensive
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)
.envfile (contains app secrets)
For production:
- Use Redis for sessions (more secure than filesystem)
- Enable HTTPS
- Set strong
SECRET_KEYandJWT_SECRET_KEY - Configure session timeout
- Add rate limiting on auth endpoints
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.