Self-hosted, zero-knowledge password manager. The server never sees your master password and never sees a plaintext secret. All encryption happens client-side; the API stores an opaque AES-256-GCM blob.
Status: experimental. Built and self-hosted by the author. Audit before trusting it with anything you can't afford to lose. Pull requests welcome.
Because the existing self-hosted options either trust the server with your master password, lock you into a single client, or stop being maintained.
ZeroPass is intentionally small:
- One Postgres + one Node API + one static web app, behind any reverse proxy.
- Master password → Argon2id → master key → HKDF → vault key + auth key. The auth key is what hits the server; the vault key never leaves the device.
- Vault blob is encrypted before upload. The DB row is meaningless without the user's master password.
┌──────────────┐ ┌──────────────┐ ┌────────────┐
│ Web (SPA) │ │ Chrome Ext. │ │ Android │
│ React+Vite │ │ MV3 + React │ │ Compose │
└──────┬───────┘ └──────┬───────┘ └─────┬──────┘
│ │ │
│ HTTPS + JWT │ │
└─────────────┬────┴─────────────────┘
▼
┌─────────────┐ ┌────────────┐
│ ZeroPass │──────▶│ PostgreSQL │
│ API (Node) │ │ 16 │
│ Fastify+Zod │ └────────────┘
└─────────────┘
▲
│ E2EE blob
▼
┌─────────────┐
│ Importer │ CLI - 1Password .1pux, Bitwarden CSV
│ (Node CLI) │
└─────────────┘
| Component | Path | Tech |
|---|---|---|
| API | apps/api |
Fastify, Prisma, Postgres, Argon2id-on-server (auth key bcrypt) |
| Web client | apps/web |
React, Vite, Tailwind, Zustand, @noble/hashes |
| Chrome extension | apps/extension |
MV3, side panel + autofill, hash-wasm |
| Android client | apps/android |
Kotlin, Compose, Autofill Service, Argon2 via JNI |
| Importer CLI | apps/importer |
TypeScript, parses 1Password .1pux + CSV |
| Shared types | packages/shared-types |
Zod + TS interfaces |
master password
│
▼
Argon2id (params per-user, returned by /auth/kdf-params)
│
▼
master key (32 B)
│
├── HKDF-SHA256(info="vault") → vault key (AES-GCM, client only)
└── HKDF-SHA256(info="auth") → auth key (sent over TLS, server bcrypt)
- The server stores
bcrypt(authKey)and per-user KDF params - nothing more. - The vault is one big
Bytescolumn: AES-256-GCM(JSON, vaultKey). - A monotonically increasing
vaultVersionlets clients detect concurrent edits. - Refresh tokens are stored as SHA-256 hashes; raw tokens only live on the device.
The same KDF logic is implemented three times - in apps/web/src/crypto,
apps/extension/src/crypto, and apps/android/.../crypto - and they MUST
produce byte-identical keys for the same inputs.
git clone https://github.com/lukasschwarz/zeropass.git
cd zeropass
cp .env.example .env
# edit .env: set POSTGRES_PASSWORD, JWT_ACCESS_SECRET, JWT_REFRESH_SECRET
docker compose up -dThat brings up:
| Service | Port | Notes |
|---|---|---|
zeropass-db |
internal | Postgres 16, named volume zeropass-db-data |
zeropass-api |
8200 | Fastify API on /api/v1 |
zeropass-web |
8201 | Nginx serving the static React build |
Open http://localhost:8201, register, and you're in.
For production, put it behind your favourite reverse proxy (Traefik / Caddy / nginx) and terminate TLS there.
pnpm install
pnpm --filter @zeropass/shared-types build
pnpm --filter @zeropass/api db:migrate:dev
pnpm dev:api # http://localhost:3000
pnpm dev:web # http://localhost:5173 (proxies /api to :3000)Run the test suite with pnpm test.
pnpm --filter @zeropass/extension build
# load apps/extension/dist as unpacked extension in chrome://extensionsapps/android is a standard Gradle project. Open it in Android Studio,
sync Gradle, then run on a device. The default API URL points to
http://10.0.2.2:8200 (emulator → host) - change it in the unlock screen.
pnpm --filter @zeropass/importer build
zpass-import --help
zpass-import 1pux ~/Downloads/export.1pux \
--email me@example.com \
--api http://localhost:8200/api/v1The importer decrypts client-side, re-encrypts under your ZeroPass vault key, and POSTs the resulting blob - the server never sees the legacy export.
- Master password is everything. Lose it and the vault is unrecoverable; there is no escrow.
- HTTPS is your job. The default
network_security_config.xmlon Android allows cleartext for LAN testing - tighten it for production. - The API does not currently support TOTP / hardware keys. Master password
- device-bound refresh token is the only factor today.
- No public security audit yet. The crypto primitives are standard
(Argon2id, AES-256-GCM, HKDF-SHA256 via
@noble/hashesandhash-wasm), but this codebase has not been independently reviewed. Don't bet your life on it.
If you find a vulnerability, please open a private GitHub security advisory rather than a public issue.
zeropass/
├── apps/
│ ├── api/ # Fastify backend + Prisma
│ ├── web/ # React SPA
│ ├── extension/ # Chrome MV3 extension
│ ├── android/ # Kotlin / Compose
│ └── importer/ # CLI: 1Password .1pux, Bitwarden CSV
├── packages/
│ └── shared-types/ # Zod schemas + TS types
├── scripts/
│ ├── deploy-web.sh
│ └── backup-postgres.sh
├── docker-compose.yml
└── .env.example
PRs welcome. Useful first contributions:
- A TOTP / WebAuthn second factor on top of the auth key.
- A Safari / Firefox port of the extension.
- iOS client (re-using the KDF and crypto contracts).
- End-to-end browser tests covering the autofill flow.
When opening an issue, include the component (api, web, extension,
android, importer) and steps to reproduce.