Encrypted-key storage playground. RSA key pairs are generated in the browser, the private key is wrapped with a passphrase-derived AES-GCM key, and only ciphertext plus KDF metadata ever reach the server. A JWT-protected API lets you sync the encrypted blob to MongoDB while keeping the passphrase local.
- Backend: Express + TypeScript + MongoDB (Mongoose).
/api/authhandles signup/login and issues JWTs;/api/accountsstores and fetches encrypted key material. Account writes are guarded byAuthorization: Bearer <token>. - Frontend: React + Vite + TypeScript. Generates RSA-4096 pairs, derives AES-GCM keys via PBKDF2 (310k iterations) and can be extended to scrypt/Argon2. Includes a tabbed UI for managing keys, copying the public key, and handling future encrypt/decrypt UX.
- Data shape:
{ username, publicKey, encryptedPrivateKey: { cipherText, iv, authTag? }, kdf: { algorithm, salt, keyLength, iterations?, memoryCost?, parallelism?, hash? }, notes, timestamps }. Passphrases and plaintext keys never leave the browser.
Prereqs: Node 18+, pnpm/npm/yarn, and MongoDB running locally (default URI mongodb://localhost:27017/yas-web).
Backend
cd backendcp .env.example .envand setMONGODB_URI,JWT_SECRET,CORS_ORIGINnpm installnpm run dev(listens onhttp://localhost:4000)
Frontend
cd frontendcp .env.example .env(configureVITE_API_BASEif you change the backend origin)npm installnpm run dev(Vite onhttp://localhost:5173)
backend/.envMONGODB_URI– Mongo connection stringPORT– API port (default 4000)JWT_SECRET– signing key for auth tokens (required in prod)CORS_ORIGIN– frontend origin allowed by CORS (default*)
frontend/.envVITE_API_BASE– base URL for API requests (defaulthttp://localhost:4000/api)
-
POST /api/auth/signupCreates the user after strength validation (>=10 chars, upper/lower/number/symbol).
-
POST /api/auth/login{ "username": "alice", "password": "StrongP@ssw0rd" }Returns
{ "token": "<jwt>", "user": { "id", "username" } }. Use the token as a Bearer auth header for account writes.
-
POST /api/accounts(requiresAuthorization: Bearer <jwt>){ "username": "alice", "publicKey": "-----BEGIN PUBLIC KEY-----...", "encryptedPrivateKey": { "cipherText": "base64", "iv": "base64", "authTag": "base64" // optional, AES-GCM tag (browser crypto stacks it onto cipherText) }, "kdf": { "algorithm": "PBKDF2", "salt": "base64", "iterations": 310000, "keyLength": 32, "hash": "SHA-256" }, "notes": "optional text" }Upserts by
username. Returns{ id, createdAt, updatedAt, upserted }with201 Createdfor first write and200 OKfor updates. -
GET /api/accounts/username/:username -
GET /api/accounts/:id
Both read endpoints return the sanitized record (public key + encrypted blob + metadata). Private material stays encrypted.
- Passphrases stay in the browser; only ciphertext, IV, optional authTag, KDF metadata, username, public key, notes, and timestamps are persisted.
- JWT secret falls back to
dev-secretonly when unset; set a strongJWT_SECRETbefore deploying. - Consider rate limiting, Argon2/scrypt KDF options, and hardware-backed key storage for production setups.
- Ship the encrypt/decrypt tabs to support hybrid file sharing (AES payload + RSA-OAEP wrapped key).
- Offer user-selectable KDFs (PBKDF2, scrypt, Argon2) with UI guidance on memory/CPU tradeoffs.
{ "username": "alice", "password": "StrongP@ssw0rd" }