A zero-trust password manager for your browser. Never remember more than your master password. All account data is encrypted in the browser using AES-GCM with a PBKDF2-derived key and stored as encrypted files on any PHP server. No database required. No trust in the server's provider required.
No dependencies. No frameworks. No tracking. Just 42KB of vanilla JavaScript.
Works in Firefox, Chrome, Safari and Edge on desktop and mobile.
- One password to open them all: master password opens a complete password list at once
- Unlimited password lists: each master password opens its own independent account set
- Encrypted table view: searchable, sortable, one-click copy to clipboard
- Category / subcategory tree: click to filter, fully configurable with custom emojis
- New accounts: password generator with selectable complexity (7 character pools including emojis)
- TOTP support: RFC 6238 / RFC 4226 implementation, live token display (30-second countdown)
- Change master password: re-encrypts all accounts, keeps old data safe
- Edit configuration: categories, subcategories, password defaults, character sources
- Mobile responsive: collapsible detail rows on screens under 600px
- Inactivity logout: configurable countdown timer
- automated dark mode: well, we have that too
- Master Password: The only secret you need to remember. Never leaves the browser in plain text.
- Server Pepper: A cryptographically random 32-byte pepper stored in
pepper.jsonon the server. Appended to passwords before hashing, preventing rainbow table attacks without exposing the pepper. - Per-User Realm: Each master password gets a unique 32-byte random value prepended to the password before encryption, ensuring identical passwords produce different ciphertext.
- PBKDF2 Key Derivation: Master password and per-user realm derive AES-256-GCM keys via 250,000 iterations of PBKDF2 with SHA-256.
- Random Salt + IV: Every encryption operation uses a fresh 16-byte salt and 12-byte IV. These are prepended to the ciphertext (base64-encoded) - the server never sees unencrypted data.
- Session Key: The master password is encrypted with a random session password on page load. Cleared on inactivity logout.
- AES-GCM: Authenticated encryption. Tampered ciphertext decrypts to empty string automatically.
- Content Security Policy: Inline
metaCSP restricts script and style sources to self only. - Anti-Spoofing: Old encrypted data files are never deleted. When the browser uploads new data, the server first checks if there was a
.pwdfile uploaded within the last 5 minutes. If so, it overwrites that file; otherwise it creates a new timestamped file. This protects against a few things:- If an attacker injects a request, they can only overwrite the single most recent
.pwdfile - the previous version is safe. - The 5-minute window is not about limiting how often you can save. It is about preventing the server disk from filling up if a user clicks "Save" seven times in a row on a bad connection.
- If an attacker injects a request, they can only overwrite the single most recent
- Local Time check: TOTP relies on the device clock. A built-in check informes you if your device time is off.
The user enters their master password. The browser hashes it (with pepper) and sends only the hash to the server. The server looks up the matching data directory and returns both the config and the encrypted data. Everything is decrypted in the browser.
sequenceDiagram
participant Browser
participant Server
Browser->>Server: GET ajax.php?do=getPepper
Server-->>Browser: {pepper: "..."}
note right of Browser: Pepper is public, prevents rainbow tables
Browser->>Browser: hash = sha256(masterPassword + pepper)
Browser->>Server: POST ajax.php?do=login&hash=<hash>
note left of Server: Server sees only the hash.<br/>It cannot derive the master password.
Server->>Server: Find /private/<hash>/
Server->>Server: Read newest config*.json
Server->>Server: Read newest *.pwd
Server-->>Browser: {config: {...}, data: "encrypted..."}
Browser->>Browser: decrypt(data, masterPassword)
Browser->>Browser: Parse JSON, render table & tree
The user clicks Save. The browser encrypts the data and uploads it. The server checks if a .pwd file was uploaded within the last 5 minutes - if so, it overwrites that file; otherwise it creates a new one. This is the anti-spoofing mechanism described above.
sequenceDiagram
participant Browser
participant Server
Browser->>Browser: encrypt(JSON.stringify(data), masterPassword)
note right of Browser: AES-256-GCM with random<br/>salt (16B) + IV (12B)
Browser->>Server: POST ajax.php?do=store&hash=<hash>&data=...
note left of Server: Server stores file by timestamp.<br/>Cannot decrypt, cannot read.
Server->>Server: Check: was there a *.pwd<br/>uploaded within 5 minutes?
alt yes
Server-->>Browser: overwrite existing
else no
Server->>Server: Save as /private/<hash>/yyMMdd_HHmm.pwd
end
Server-->>Browser: ok
The user enters their current and new master password. The browser verifies the current password, then creates a new data directory under the new hash. The browser re-encrypts all accounts with the new password and uploads the data. The old directory remains intact - the old master password still works until the user deletes it from the config.
sequenceDiagram
participant Browser
participant Server
Browser->>Browser: Verify current master password
Browser->>Browser: sha256(newMasterPassword + pepper)
Browser->>Server: POST ajax.php?do=changeMasterPassword&hash=<oldHash>&newHash=<newHash>
note left of Server: Server creates new directory<br/>under the new hash
Server->>Server: mkdir /private/<newHash>/
Server->>Server: Create config.json with new masterRealm
Server-->>Browser: {ok: 1, masterRealm: "..."}
Browser->>Browser: Re-encrypt all accounts with new password
Browser->>Server: POST ajax.php?do=store&hash=<newHash>&data=...
Server->>Server: Save as /private/<newHash>/*.pwd
Note over Browser,Server: Old directory <masterHash1> remains intact.<br/>Old master password still works until user deletes it.
The gear icon (⚙️) opens the config editor. The user edits categories, subcategories, password defaults, and character sources. On save, the config is uploaded as a timestamped JSON snapshot. On login, the server returns the newest config*.json.
sequenceDiagram
participant Browser
participant Server
Browser->>Browser: Collect categories, subcategories,<br/>password defaults, character sources
Browser->>Server: POST ajax.php?do=saveConfig&hash=<hash>&config=JSON
Server->>Server: Save as /private/<hash>/config_yyMMdd_HHmm.json
Server-->>Browser: ok
Note over Browser: Next login will automatically<br/>read the newest config*.json
Config fields:
| Key | Type | Description |
|---|---|---|
masterRealm |
string | Per-user salt for encryption key derivation |
categories |
object | {name: emoji} mappings |
subCategories |
object | {name: emoji} mappings |
showSubCategorieIconsInTreeview |
boolean | Show subcategory emojis in tree |
passwordSuggestionLength |
number | Default password length (8–64) |
pageInactivityTimeout |
number | Seconds until auto-logout (30–3600) |
passwordSuggestionDefaults |
object | Default complexity checkboxes |
passwordSuggestionSources |
object | Character pools for generation |
| desktop | mobile |
|---|---|
![]() |
![]() |
| desktop | mobile |
|---|---|
![]() |
![]() |
![]() |
![]() |
| desktop | mobile |
|---|---|
![]() |
![]() |
![]() |
![]() |
- Copy everything to a server that supports PHP (version 5.6+).
- Point your server configuration to the
public/directory. Never expose the root folder of this project. - Make sure the
private/directory is writable by the web server user (e.g.chmod 755 private/). - Open your browser and log in with a new master password. Open the browser console (F12) and copy the shown hash.
- Create a directory in
private/named after this hash. - Login again. Done.
If you are upgrading from a version that still uses config.php instead of per-user JSON files:
- make a backup of the
/privatefolder - make folder
/publicempty but keepprivate - Copy everything to a server that supports PHP (version 5.6+).
- Open your browser and log in with your master password.
- The migration will automatically:
- Create a backup zip of all existing files in
private/. - Convert
config.phpinto per-user JSON files. - Create
pepper.jsonwith a random pepper. - Delete itself and the old
config.php.
After a successful login, migrate_to_json.php will have already deleted itself. If it has not, you can delete it manually.
PasswordManager/
├── private/
│ ├── pepper.json # Server-wide pepper (auto-created)
│ ├── migrate_to_json.php # Migration tool (auto-deletes after use)
│ ├── config-template.json # Template for new master password dirs
│ ├── <masterHash1>/
│ │ ├── config_261224_2356.json # Newest config for that hash, will be used when logging in
│ │ ├── config_190124_1455.json # Older config snapshot
│ │ ├── 270117_1234.pwd # Newest encrypted account data, will be used when logging in
│ │ ├── 270116_2157.pwd # Older encrypted account data
│ │ └── 261217_1548.pwd # Older encrypted account data
│ └── <masterHash2>/
│ ├── config_yyMMdd_HHmm.json # Timestamped config snapshots
│ └── yyMMdd_HHmm.pwd # Encrypted account data
├── public/
│ ├── index.html # Single-page UI
│ ├── scripts.js # All client-side logic
│ ├── styles.css # Responsive styles
│ └── ajax.php # Server-side endpoint
└── readme.md # This file
After changing the master password, a second directory is created:
├── private/
│ └── <masterHash2>/ # New master password directory
│ ├── config_yyMMdd_HHmm.json # Timestamped config snapshots
│ └── yyMMdd_HHmm.pwd # Re-encrypted account data
| Layer | Technology |
|---|---|
| Frontend | Vanilla JavaScript (ES6+), no dependencies |
| Encryption | Web Crypto API - AES-256-GCM, PBKDF2 (250k iterations), SHA-256 |
| TOTP | RFC 6238 / RFC 4226 native implementation |
| Backend | PHP 5.6+ (single ajax.php endpoint) |
| Storage | Text files with encrypted content (.pwd), JSON configs - no database |
| Transport | HTTPS recommended |
MIT











