Skip to content

ezotic/passWord

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Password Manager

A self-hosted, encrypted password manager with per-user vaults, admin controls, and JWT authentication. Runs entirely in Docker.


Features

  • Encrypted storage β€” passwords at rest are encrypted with AES-256-GCM
  • Per-user vaults β€” each user sees only their own saved entries
  • JWT authentication β€” stateless, 8-hour sessions with automatic inactivity logout after 2 minutes
  • Password generator β€” cryptographically random 20-character passwords with strength meter
  • Forgot password β€” users can reset their own password from the login page (no email required; requires knowing the username)
  • Admin panel β€” admins can manage all registered users:
    • View all accounts with join date
    • Delete any user (cascades to their vault entries)
    • Force a user to change their password on next login
    • Download a full SQL backup of the database
    • Restore a previously downloaded SQL backup
  • Forced password change β€” default admin and any admin-reset accounts must set a new password before accessing the app
  • Emergency admin reset β€” reset-admin-password.sh script to recover the admin account from the host when the UI is inaccessible
  • Rate limiting β€” brute-force protection on all auth and write endpoints
  • Security headers β€” CSP, X-Frame-Options, X-Content-Type-Options via Nginx + Helmet

Infrastructure

Container Architecture

flowchart TD
    Browser(["🌐 Browser"])

    subgraph host["Host Machine"]
        subgraph frontend["frontend-net"]
            Nginx["nginx:1.27-alpine\n─────────────────\nβ€’ Serves static files HTML/CSS/JS\nβ€’ Proxies /api/* β†’ backend:3000\nβ€’ Security headers CSP, X-Frame-Options"]
        end

        subgraph backend_net["backend-net  (internal β€” no internet access)"]
            Backend["Node.js 20 / Express 4\n─────────────────\nβ€’ JWT auth middleware\nβ€’ bcrypt hashing 12 rounds\nβ€’ AES-256-GCM vault encryption\nβ€’ Rate limiting auth:10/15min write:20/15min\nβ€’ Auto-seeds default admin on first start"]
            DB[("MariaDB LTS\n─────────────────\nβ€’ app_users β€” login credentials\nβ€’ users β€” encrypted vault entries\nβ€’ Volume: mysql_data persistent")]
        end
    end

    Browser -->|"HTTP :8080"| Nginx
    Nginx -->|"/api/* β†’ :3000"| Backend
    Backend -->|"SQL queries"| DB
Loading

backend-net is marked internal: true β€” the backend and database have no direct internet access. Only Nginx is exposed to the host.

Docker Networks

Network Members Internet Access
frontend-net nginx Yes (via host port 8080)
backend-net nginx, backend, mariadb No (internal)

Request Flow

Browser
  β”‚
  β”‚  HTTP :8080
  β–Ό
Nginx (nginx:1.27-alpine)
  β”‚
  β”œβ”€β”€ GET /               β†’ serve login.html (static)
  β”œβ”€β”€ GET /index.html     β†’ serve index.html (static)
  β”œβ”€β”€ GET /style.css      β†’ serve style.css  (static)
  β”‚
  └── /api/*  ──────────► Express Backend (:3000)
                              β”‚
                              β”œβ”€β”€ POST /api/auth/login
                              β”œβ”€β”€ POST /api/auth/register
                              β”œβ”€β”€ POST /api/auth/reset-password
                              β”œβ”€β”€ POST /api/auth/change-password  [JWT required]
                              β”œβ”€β”€ GET  /api/passwords             [JWT required]
                              β”œβ”€β”€ POST /api/passwords             [JWT required]
                              β”œβ”€β”€ DELETE /api/passwords/:id       [JWT required]
                              β”œβ”€β”€ GET  /api/admin/users           [JWT + admin]
                              β”œβ”€β”€ DELETE /api/admin/users/:id     [JWT + admin]
                              β”œβ”€β”€ POST /api/admin/users/:id/reset-password  [JWT + admin]
                              β”œβ”€β”€ GET  /api/admin/backup          [JWT + admin]
                              └── POST /api/admin/restore         [JWT + admin]

Database Schema

app_users                          users
─────────────────────────────      ────────────────────────────────
id            INT UNSIGNED PK      id            INT UNSIGNED PK
username      VARCHAR(64) UNIQUE   user_id       INT UNSIGNED FK ──► app_users.id
is_admin      TINYINT(1)           website       VARCHAR(255)
must_change_password TINYINT(1)    username      VARCHAR(64)
password_hash VARCHAR(255)         password      VARCHAR(255)  ← AES-256-GCM
created_at    TIMESTAMP            created_at    TIMESTAMP

                                   ON DELETE CASCADE

Authentication Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Browser β”‚                  β”‚ Backend β”‚              β”‚    Database      β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜                  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚                             β”‚                            β”‚
     β”‚  POST /api/auth/login       β”‚                            β”‚
     β”‚  { username, password }     β”‚                            β”‚
     │────────────────────────────►│                            β”‚
     β”‚                             β”‚  SELECT id, is_admin,      β”‚
     β”‚                             β”‚  must_change_password,     β”‚
     β”‚                             β”‚  password_hash             β”‚
     β”‚                             β”‚  WHERE username = ?        β”‚
     β”‚                             │───────────────────────────►│
     β”‚                             │◄───────────────────────────│
     β”‚                             β”‚                            β”‚
     β”‚                             β”‚  bcrypt.compare()          β”‚
     β”‚                             β”‚  (always runs β€” timing     β”‚
     β”‚                             β”‚   attack protection)       β”‚
     β”‚                             β”‚                            β”‚
     β”‚  200 { token, isAdmin,      β”‚                            β”‚
     β”‚        mustChangePassword } β”‚                            β”‚
     │◄────────────────────────────│                            β”‚
     β”‚                             β”‚                            β”‚
     β”‚  mustChangePassword=true?   β”‚                            β”‚
     β”‚  β†’ redirect: change-password.html                        β”‚
     β”‚                             β”‚                            β”‚
     β”‚  mustChangePassword=false?  β”‚                            β”‚
     β”‚  β†’ redirect: index.html     β”‚                            β”‚
     β”‚                             β”‚                            β”‚
     β”‚  Subsequent requests:       β”‚                            β”‚
     β”‚  Authorization: Bearer JWT  β”‚                            β”‚
     │────────────────────────────►│                            β”‚
     β”‚                             β”‚  jwt.verify(token,         β”‚
     β”‚                             β”‚    { algorithms: ['HS256']}β”‚
     β”‚                             β”‚  β†’ req.user = { id,        β”‚
     β”‚                             β”‚    username, isAdmin }     β”‚

Quick Start

Prerequisites

  • Docker Engine 24+
  • Docker Compose v2

Install Docker

Linux (Debian / Ubuntu)

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER   # log out and back in after this

macOS Download and install Docker Desktop. Docker Compose is bundled.

Windows Download and install Docker Desktop. Enable WSL 2 backend when prompted. Docker Compose is bundled.

Verify the installation:

docker --version          # Docker version 24.x or later
docker compose version    # Docker Compose version v2.x or later

1 β€” Clone and configure

git clone <repo-url>
cd passWord
cp .env.example .env

Edit .env and set strong values for every variable (see Configuration).

2 β€” Start the stack

docker compose up -d

On first start the backend will log:

[seed] Default admin created β€” username: admin, password: password (must change on first login)

3 β€” Open the app

Navigate to http://localhost:8080

You will be redirected to the login page. Log in with:

Field Value
Username admin
Password password

You will be immediately redirected to the Change Password page. Set a strong password (12–20 characters, must include uppercase, lowercase, number, and special character) before you can access the app.


Configuration

Copy .env.example to .env and fill in all values before starting.

Variable Description Example
MYSQL_ROOT_PASSWORD MariaDB root password (also used by the restore route) ch@ngeMe_r00t!
MYSQL_DATABASE Database name password_app
MYSQL_USER Application DB user appuser
MYSQL_PASSWORD Application DB password ch@ngeMe_app!
ENCRYPTION_KEY 64-hex-char AES-256-GCM key for vault entries openssl rand -hex 32
JWT_SECRET 64-hex-char HMAC-SHA256 signing secret openssl rand -hex 32
NODE_ENV Node environment production

Generate secrets:

openssl rand -hex 32   # for ENCRYPTION_KEY
openssl rand -hex 32   # for JWT_SECRET

Project Structure

passWord/
β”œβ”€β”€ frontend/                  # Static files served by Nginx
β”‚   β”œβ”€β”€ index.html             # Main app UI (vault + admin panel)
β”‚   β”œβ”€β”€ login.html             # Login / Register page
β”‚   β”œβ”€β”€ change-password.html   # Forced password change page
β”‚   β”œβ”€β”€ forgot-password.html   # Self-service password reset page
β”‚   β”œβ”€β”€ app.js                 # Main app logic (CRUD, password generation, admin panel)
β”‚   β”œβ”€β”€ login.js               # Login / register logic
β”‚   β”œβ”€β”€ change-password.js     # Password change logic
β”‚   β”œβ”€β”€ forgot-password.js     # Password reset logic
β”‚   └── style.css              # Dracula dark theme (Bootstrap 5 overrides)
β”‚
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ server.js          # Express app, middleware, routes, admin seed
β”‚   β”‚   β”œβ”€β”€ db.js              # MariaDB connection pool
β”‚   β”‚   β”œβ”€β”€ crypto.js          # AES-256-GCM encrypt/decrypt
β”‚   β”‚   β”œβ”€β”€ middleware/
β”‚   β”‚   β”‚   β”œβ”€β”€ authenticate.js   # JWT verification β†’ req.user
β”‚   β”‚   β”‚   └── requireAdmin.js   # Admin-only guard
β”‚   β”‚   └── routes/
β”‚   β”‚       β”œβ”€β”€ auth.js           # login, register, change-password, reset-password
β”‚   β”‚       β”œβ”€β”€ admin.js          # user management, backup, restore
β”‚   β”‚       └── passwords.js      # CRUD for vault entries
β”‚   β”œβ”€β”€ Dockerfile             # Multi-stage build (node:20-alpine)
β”‚   └── package.json
β”‚
β”œβ”€β”€ mysql/
β”‚   └── init/
β”‚       └── 01_init.sql        # Schema creation (runs once on fresh volume)
β”‚
β”œβ”€β”€ nginx/
β”‚   └── default.conf           # Reverse proxy + security headers
β”‚
β”œβ”€β”€ reset-admin-password.sh    # Emergency CLI script to reset the admin password
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ .env.example
└── README.md

Admin Panel

The admin panel is visible only to users with is_admin = 1. It replaces the standard vault UI for admin accounts.

User Management

Each registered user appears in a table with their ID, username, join date, and two action buttons:

Button Action
Key icon (yellow) Sets must_change_password = 1 β€” user is redirected to the change-password page on their next login
Person-X icon (red) Permanently deletes the user and all their saved vault entries (irreversible)

Admins cannot delete or reset-password their own account from this panel.

Database Backup

Click Backup to download a full SQL dump (backup-<timestamp>.sql) containing both the app_users and users tables. Rate-limited to 5 downloads per 15 minutes.

Database Restore

Click Choose… to select a previously downloaded .sql backup file, then click Restore and confirm. This replays the full SQL dump against the live database β€” all current users and vault entries are overwritten. Rate-limited to 3 restores per 15 minutes.

Only files generated by this application's backup feature are accepted. The restore route validates the file header before executing anything.


Emergency Admin Reset

If the admin account password is lost and the UI is inaccessible, use the bundled script from the project root on the Docker host.

Linux / macOS

./reset-admin-password.sh
# or for a different admin username:
./reset-admin-password.sh someadmin

Windows (PowerShell)

.\reset-admin-password.ps1
# or for a different admin username:
.\reset-admin-password.ps1 someadmin

The script:

  1. Prompts for a new password (same complexity rules as the UI)
  2. Generates the bcrypt hash inside the running backend container (no host-side dependencies)
  3. Updates the database and sets must_change_password = 1

The backend and database services must be running (docker compose up -d).


API Reference

All /api/passwords and /api/admin endpoints require Authorization: Bearer <token>.

Auth (public, rate-limited to 10 req / 15 min)

Method Path Body Response
POST /api/auth/register { username, password } 201 or 409 / 422
POST /api/auth/login { username, password } 200 { token, username, isAdmin, mustChangePassword }
POST /api/auth/reset-password { username, password } 200 or 404 / 422
POST /api/auth/change-password { currentPassword, password } 200 or 401 / 422 β€” requires JWT

Vault (JWT required)

Method Path Description
GET /api/passwords List current user's entries
POST /api/passwords Save a new entry
DELETE /api/passwords/:id Delete an entry (owner only)

Admin (JWT + admin required)

Method Path Description
GET /api/admin/users List all registered users
DELETE /api/admin/users/:id Delete a user and all their entries
POST /api/admin/users/:id/reset-password Force user to change password on next login
GET /api/admin/backup Download a full SQL dump (rate-limited: 5 / 15 min)
POST /api/admin/restore Restore a SQL backup file (rate-limited: 3 / 15 min)

Password Rules (register, change-password, reset-password)

  • 12–20 characters
  • At least one lowercase letter
  • At least one uppercase letter
  • At least one number
  • At least one special character

Security Notes

Concern Mitigation
Vault passwords at rest AES-256-GCM with random IV per entry
Login credential storage bcrypt (cost factor 12)
Username enumeration via timing Constant-time dummy bcrypt compare when user not found
JWT algorithm confusion algorithms: ['HS256'] pinned in jwt.verify
Brute force 10 failed auth attempts per IP per 15 min (skipSuccessfulRequests: true)
Session inactivity Client-side auto-logout after 2 minutes of inactivity
Clickjacking X-Frame-Options: SAMEORIGIN
MIME sniffing X-Content-Type-Options: nosniff
XSS via CDN CSP restricts scripts/styles to self + cdn.jsdelivr.net
DB network exposure backend-net is Docker-internal; MariaDB not reachable from host
Container privilege Backend runs as non-root appuser inside the container
Self-deletion by admin Server rejects DELETE /api/admin/users/<own-id> with 400
Backup restore abuse Restore endpoint rate-limited (3/15 min); only accepts files with the application's backup header
Restore privilege Restore uses a root DB connection scoped to a single transaction β€” MYSQL_ROOT_PASSWORD required in .env

Stopping and Resetting

# Stop containers (data preserved)
docker compose down

# Stop and delete all data (wipe the database volume)
docker compose down -v

After a full wipe, the next docker compose up will re-seed the default admin.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors