Skip to content

christianpasinrey/tools-sync-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tools Sync API

Node.js Express MongoDB

License PRs Welcome

Zero-knowledge backend for syncing encrypted data across devices.
The server only stores encrypted blobs — it never sees plaintext data.


Zero-Knowledge Architecture

Browser (tools SPA)                    This server
┌─────────────────────┐               ┌─────────────────────┐
│                     │               │                     │
│  Password           │               │  authKey (PBKDF2)   │
│    ├─ PBKDF2 ──────────── sends ────│    └─ bcrypt hash   │
│    └─ AES-256-GCM   │               │                     │
│         └─ encrypts data │           │  Encrypted blobs    │
│              └────────────── PUT ────│    └─ {salt,iv,data}│
│                     │               │                     │
└─────────────────────┘               └─────────────────────┘

Key principle: The user's password is used for two independent derivations:

  1. Auth key (PBKDF2 with fixed salt) — sent to the server for authentication
  2. Encryption key (PBKDF2 with random salt per item) — encrypts data locally

The server only receives the derived auth key (impossible to reverse to the original password) and already-encrypted blobs. It cannot decrypt anything.


Features

Authentication

  • Registration and login with email + derived password (PBKDF2)
  • JWT access token (15 min, memory-only on client)
  • JWT refresh token (7 days, HttpOnly cookie with rotation)
  • Password change (requires current password)

Account Reset (Forgot Password)

In a zero-knowledge system, if you lose your password, your encrypted data is unrecoverable. Account reset allows you to regain access by permanently deleting all previous data.

Complete flow:

  1. User requests reset from the frontend (POST /auth/forgot-password)
  2. Server generates a cryptographic token (crypto.randomBytes(32)) and stores its bcrypt hash
  3. An email is sent via SMTP with a link to the frontend containing the token (valid for 1 hour)
  4. User sets a new password from the link
  5. Server verifies the token, deletes all VaultItems and DeletionLogs for the user, generates a new vaultSalt, and saves the new password hash
  6. User can log in with the new password (empty vault)

Reset security measures:

  • Token hashed with bcrypt in the database (never stored as plaintext)
  • Strict 1-hour expiration
  • Single-use: cleared after successful reset
  • Aggressive rate limit: 3 requests per 15 minutes
  • Generic response that does not reveal whether the email exists

Vault Sync

  • CRUD for encrypted items with Last-Write-Wins (LWW)
  • Batch push/pull (max 50 items per request)
  • DeletionLog for propagating deletions across devices
  • Incremental sync via timestamps

Quick Start

# Clone
git clone <repo-url>
cd tools-sync-api

# Install dependencies
npm install

# Configure environment variables
cp .env.example .env
# Edit .env with your values

# Development
npm run dev

# Production
npm start

The server starts on http://localhost:3001 by default.


Environment Variables

Server

Variable Description Default
PORT Server port 3001
MONGODB_URI MongoDB connection URI mongodb://127.0.0.1:27017/tools-sync
NODE_ENV Environment (development / production) development
MAX_PAYLOAD_SIZE Maximum body size 50mb
CORS_ORIGIN Allowed origin (frontend URL) http://localhost:5173
FRONTEND_URL Frontend URL (for email links) http://localhost:5173

Authentication

Variable Description Default
JWT_SECRET Secret for access tokens -
JWT_REFRESH_SECRET Secret for refresh tokens -
JWT_EXPIRES_IN Access token expiration 15m
JWT_REFRESH_EXPIRES_IN Refresh token expiration 7d
COOKIE_DOMAIN Domain for cross-subdomain cookies (empty)

Email (SMTP)

Variable Description Default
SMTP_HOST SMTP server host -
SMTP_PORT SMTP port 465
SMTP_SECURE SSL/TLS (true for 465, false for 587) true
SMTP_USER SMTP user (authentication account) -
SMTP_PASS SMTP password -
SMTP_FROM Sender email (can be an alias) -

Endpoints

Auth

Method Route Auth Rate Limit Description
POST /auth/register No 10/15min Register with email + derived authKey
POST /auth/login No 10/15min Login, returns access token + refresh cookie
POST /auth/refresh Cookie 10/15min Rotates refresh token, returns new access token
POST /auth/change-password Bearer - Change password (requires current password)
POST /auth/logout Bearer - Invalidates refresh token and clears cookie
POST /auth/forgot-password No 3/15min Sends email with reset token
POST /auth/verify-reset-token No 10/15min Verifies token without consuming it
POST /auth/reset-account No 10/15min New password + deletes all vault data

Vault

All endpoints require Authorization: Bearer <token>.

Method Route Description
GET /vault/sync-status?since= Item timestamps + deletions since timestamp
GET /vault/:storeName List items in a store (without payload)
GET /vault/:storeName/:itemId Full item with encrypted payload
PUT /vault/:storeName/:itemId Upsert (rejects if updatedAt < existing)
DELETE /vault/:storeName/:itemId Delete item + create DeletionLog entry
POST /vault/batch-push Batch upsert (max 50 items)
POST /vault/batch-pull Batch fetch by IDs (max 50)

Models

User

Field Type Description
email String Unique, lowercase, validated
passwordHash String bcrypt(authKey, 12 rounds)
vaultSalt [Number] Salt for client-side key derivation
refreshTokenHash String | null Hash of active refresh token
resetTokenHash String | null bcrypt hash of reset token
resetTokenExpiry Date | null Reset token expiry (1h from creation)
createdAt Date Registration date

VaultItem

Field Type Description
userId ObjectId Reference to User
storeName String Store name (enum)
itemId String Unique item ID
itemName String Display name
encryptedPayload Object { salt, iv, data } in Base64
payloadSize Number Bytes of decoded data field
updatedAt Number Client timestamp (ms)

Compound index: { userId, storeName, itemId } unique.

DeletionLog

Field Type Description
userId ObjectId Reference to User
storeName String Store of deleted item
itemId String Deleted item ID
deletedAt Number Timestamp (ms)

Security

Mechanism Detail
JWT Access token 15 min (memory-only) + refresh token 7 days (HttpOnly cookie, rotation)
HttpOnly cookies Refresh token in secure cookie (Secure, SameSite=Strict, HttpOnly)
bcrypt 12 rounds for passwordHash, 10 rounds for tokens
Reset tokens crypto.randomBytes(32), hashed, expire in 1h, single-use
Rate limiting Auth: 10/15min, Forgot-password: 3/15min, API: 300/15min, Batch: 10/min
Helmet HTTP security headers
CORS Configured origin only, credentials: true
Payload limits 10 MB per item, 50 MB total body
Validation express-validator on all routes
No data leaks Forgot-password always responds 200 (does not reveal account existence)

Sync Protocol

Conflict resolution: Last-Write-Wins (LWW)

  • The updatedAt field (client timestamp in ms) determines which version wins
  • Server rejects PUT if incoming.updatedAt < existing.updatedAt
  • DeletionLog enables propagating deletions across devices

Sync flow

1. GET /vault/sync-status?since=lastSync
2. Compare local vs remote timestamps
3. Pull newer remote items
4. Push newer local items
5. Process deletions

Project Structure

tools-sync-api/
├── src/
│   ├── server.js              # Entry point + middleware setup
│   ├── config/
│   │   └── db.js              # MongoDB connection
│   ├── middleware/
│   │   ├── auth.js            # JWT verify middleware
│   │   ├── rateLimiter.js     # Rate limiters (auth, api, batch, forgot)
│   │   └── errorHandler.js    # Global error handler
│   ├── models/
│   │   ├── User.js            # User schema + reset tokens
│   │   ├── VaultItem.js       # Encrypted vault items
│   │   └── DeletionLog.js     # Deletion records
│   ├── routes/
│   │   ├── auth.js            # Authentication + reset routes
│   │   └── vault.js           # Vault CRUD routes
│   ├── controllers/
│   │   ├── authController.js  # Auth + forgot/reset logic
│   │   └── vaultController.js # Vault CRUD + sync logic
│   └── utils/
│       └── email.js           # Nodemailer transporter + templates
├── .env
├── .env.example
└── package.json

Stack

Package Version Description
Express 4.21 Minimalist web framework
Mongoose 8.9 MongoDB ODM
bcrypt 5.1 Password and token hashing
jsonwebtoken 9.0 JWT tokens (access + refresh)
nodemailer 7.0 Email sending via SMTP
helmet 8.0 HTTP security headers
cors 2.8 Cross-Origin Resource Sharing
cookie-parser 1.4 Parse cookies in requests
express-rate-limit 7.5 IP-based rate limiting
express-validator 7.2 Input validation and sanitization
dotenv 16.4 Environment variables from .env

Deployment

Production requirements

  • Node.js 22+
  • MongoDB 6+ (local or Atlas)
  • SMTP server for reset emails
  • SSL certificate (HTTPS required for Secure cookies)

Critical production variables

NODE_ENV=production
JWT_SECRET=<random-64-chars>
JWT_REFRESH_SECRET=<random-64-chars>
COOKIE_DOMAIN=.your-domain.com
CORS_ORIGIN=https://your-frontend.com
FRONTEND_URL=https://your-frontend.com
SMTP_HOST=mail.your-domain.com
[email protected]
SMTP_PASS=<password>
[email protected]

HttpOnly cookies with SameSite=Strict and Secure=true require frontend and backend to share the same registrable domain (e.g., tools.example.com and sync.example.com).


Contributing

  1. Fork the repository
  2. Create a branch (git checkout -b feature/improvement)
  3. Commit your changes (git commit -m 'feat: description')
  4. Push to the branch (git push origin feature/improvement)
  5. Open a Pull Request

License

This project is licensed under the MIT License. See the LICENSE file for details.


Part of the Web Tools project — Zero-knowledge encrypted tools suite

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors