A lightweight, self-hosted password vault API inspired by Vaultwarden.
Welcome to another official UVF IT beginner build. Today, we are constructing a minimal, working clone of a password manager backend, inspired by the lightweight efficiency of Vaultwarden.
While the real Vaultwarden is a complex Rust application compatible with Bitwarden clients, our goal is to understand the core data flow: secure storage, user authentication, and encrypted item retrieval, using a simplified Python and SQLite stack.
Create a new directory for your project and initialize a Python virtual environment to keep dependencies isolated.
Install Flask, the micro web framework we will use to build the API, and sqlite3 is included in the standard library.
mkdir vaultwarden-clone
cd vaultwarden-clone
python3 -m venv venv
source venv/bin/activate
pip install flaskCreate a simple SQLite database with two tables: users for authentication and items for storing password data.
In a real Vaultwarden setup, passwords are encrypted client-side. Here, we store them as plain text for simplicity, but we will structure the JSON to mimic the Bitwarden API format.
db_setup.py
import sqlite3
def init_db():
conn = sqlite3.connect('vault.db')
c = conn.cursor()
# Create users table
c.execute('''CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
api_key TEXT UNIQUE NOT NULL
)''')
# Create ciphers (password items) table
c.execute('''CREATE TABLE IF NOT EXISTS ciphers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
username TEXT,
password TEXT,
FOREIGN KEY(user_id) REFERENCES users(id)
)''')
# Insert a dummy user for testing
try:
c.execute("INSERT INTO users (email, password, api_key) VALUES (?, ?, ?)",
('test@example.com', 'hashed_password_placeholder', 'test-api-key-123'))
c.execute("INSERT INTO ciphers (user_id, name, username, password) VALUES (?, ?, ?, ?)",
(1, 'Test Account', 'admin', 'supersecret'))
except sqlite3.IntegrityError:
print("Data already exists.")
conn.commit()
conn.close()
print("Database initialized with test data.")
if __name__ == '__main__':
init_db()Create the main application file app.py with Flask routes.
Implement three core endpoints: /register to create a user, /login to authenticate (returning a fake token), and /api/ciphers to list stored passwords for the authenticated user.
app.py
import sqlite3
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
DB_NAME = 'vault.db'
def get_db():
conn = sqlite3.connect(DB_NAME)
conn.row_factory = sqlite3.Row
return conn
@app.route('/register', methods=['POST'])
def register():
data = request.get_json()
email = data.get('email')
password = data.get('password')
if not email or not password:
return jsonify({'error': 'Email and password required'}), 400
db = get_db()
try:
db.execute('INSERT INTO users (email, password) VALUES (?, ?)', (email, password))
db.commit()
return jsonify({'success': True}), 201
except sqlite3.IntegrityError:
return jsonify({'error': 'User already exists'}), 409
finally:
db.close()
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
email = data.get('email')
password = data.get('password')
db = get_db()
user = db.execute('SELECT * FROM users WHERE email = ? AND password = ?', (email, password)).fetchone()
db.close()
if user:
# In a real app, this would be a JWT or session token
return jsonify({'access_token': f'fake-token-{user["id"]}', 'user_id': user['id']})
else:
return jsonify({'error': 'Invalid credentials'}), 401
@app.route('/api/ciphers', methods=['GET'])
def get_ciphers():
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'Unauthorized'}), 401
# Extract fake user ID from token for demo purposes
token = auth_header.split(' ')[1]
try:
user_id = int(token.split('-')[-1])
except (IndexError, ValueError):
return jsonify({'error': 'Invalid token'}), 401
db = get_db()
ciphers = db.execute('SELECT * FROM items WHERE user_id = ?', (user_id, )).fetchall()
db.close()
# Convert to list of dicts
cipher_list = [dict(row) for row in ciphers]
return jsonify(cipher_list)
if __name__ == '__main__':
app.run(debug=True, port=5000)First, run the database setup script to create the tables.
Then, start the Flask server. It will run on http://localhost:5000.
Use curl to test the endpoints: register a user, login to get a token, and then fetch ciphers.
python db_setup.py
python app.py# Register a user
curl -X POST http://localhost:5000/register -H "Content-Type: application/json" -d '{"email": "test@example.com", "password": "securepass123"}'
# Login to get token
TOKEN=$(curl -s -X POST http://localhost:5000/login -H "Content-Type: application/json" -d '{"email": "test@example.com", "password": "securepass123"}' | python3 -c "import sys, json; print(json.load(sys.stdin)['access_token'])")
# Fetch ciphers (should be empty initially)
curl -X GET http://localhost:5000/api/ciphers -H "Authorization: Bearer $TOKEN"| Check | Command / Why it matters |
|---|---|
| Database initialized | Run python db_setup.py and verify vault.db file exists in the directory. |
| Server running | Run python app.py and see 'Running on http://127.0.0.1:5000' in the terminal. |
| User registration works | Curl the /register endpoint and receive a 201 status with '{"success": true}'. |
| Cipher retrieval works | Curl the /api/ciphers endpoint with the Authorization header and receive a JSON array containing the test item. |
You have built a minimal backend that mimics the core data structure of a password manager. To extend this, try adding a POST endpoint for /api/ciphers to allow creating new items via the API, and implement proper JWT token validation instead of the fake token check.