Keep It Secret, Keep It Safe — a self-hostable, end-to-end encrypted file-sharing service inspired by Mozilla Send.
Files are encrypted in the browser before upload using AES-256-GCM. The server only stores ciphertext and a small amount of metadata. The decryption key lives in the URL fragment (#...), which browsers never send to the server.
- Zero runtime dependencies. TypeScript is the only build dependency. The server uses only Node's built-ins (
node:http,node:sqlite,node:crypto,node:fs). The browser uses only the Web Crypto API. - End-to-end encryption. AES-256-GCM with a per-file 256-bit key. Filename and MIME type are encrypted too — the server never sees them in cleartext.
- Chunked. 1 MiB plaintext chunks with truncation-resistant AAD; per-chunk nonces.
- Optional password. Encrypts the file key with
PBKDF2-SHA256(600,000 iters). Without the password, the link alone is useless. - Per-file limits. Choose max downloads (1–100) and TTL (1 hour to 365 days). The reaper deletes ciphertext from disk once either limit is reached.
- Admin-gated uploads. Bearer tokens are provisioned by an admin (with TTL and revocation). Downloads remain public-by-link.
- Node.js 24 or newer (uses the built-in
node:sqlitemodule), or Docker.
Multi-arch images (linux/amd64, linux/arm64) are published to GHCR on every push to main and on tagged releases.
docker run -d \
--name kiskis \
-p 8787:8787 \
-e ADMIN_PASSWORD=change-me \
-v kiskis-data:/data \
ghcr.io/incanta/kiskis:latestVisit http://localhost:8787/admin to sign in and create an upload token.
Save the following as compose.yaml:
services:
kiskis:
image: ghcr.io/incanta/kiskis:latest
container_name: kiskis
restart: unless-stopped
ports:
- "8787:8787"
environment:
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?set ADMIN_PASSWORD in .env}
# SECURE_COOKIES: "true" # enable when serving over HTTPS
# MAX_FILE_BYTES: "2147483648"
volumes:
- kiskis-data:/data
volumes:
kiskis-data:Create a .env next to it (Compose auto-loads it) and start:
echo "ADMIN_PASSWORD=change-me" > .env
docker compose up -dTo update later: docker compose pull && docker compose up -d.
cp .env.example .env
# edit .env, set ADMIN_PASSWORD at minimum
yarn install
yarn build
yarn startVisit http://localhost:8787/admin to sign in and create an upload token.
| Var | Default | Purpose |
|---|---|---|
ADMIN_PASSWORD |
(required) | Bootstrap password for the admin account. Changes to this rotate the stored hash on next login. |
PORT |
8787 |
HTTP port. |
DATA_DIR |
./data |
Where the SQLite DB and ciphertext blobs live. |
SECURE_COOKIES |
false |
Set to true when serving over HTTPS. |
MAX_FILE_BYTES |
2147483648 |
Plaintext size cap. Hard-clamped at 2 GiB. |
Upload requires an admin-provisioned auth token to prevent unwanted usage:
Admin interface:
Upload interface:
Client-side password protection:
Download interface:
- The decryption key never reaches the server. It is generated in the browser, encoded into the URL fragment (
#k1.<key>or#p1.<salt>.<wrapNonce>.<wrappedKey>), and browsers do not send fragments in HTTP requests. - The server stores: ciphertext blobs, encrypted metadata, base nonces (random per-file), chunk counts, and policy flags. It does not store filenames, MIME types, the file key, the password, or any password-derived material.
- Downloading consumes one of the file's allowed downloads at the moment the user confirms — wrong password attempts do not consume downloads.
- File and chunk identifiers are random base64url strings, not enumerable.
- Admin sessions are random opaque tokens stored server-side, set as
HttpOnly; SameSite=Strictcookies. - Upload tokens are random 32-byte secrets shown to the admin once on creation; only an 8-character prefix is retained for display thereafter.
- Browser-side downloads accumulate decrypted plaintext in memory before triggering the save (no third-party stream-saver library). Files up to ~1 GiB are practical on typical desktops.
- No anti-abuse rate limiting beyond admin-token gating. Add a reverse proxy (nginx, Caddy) if exposing to the public internet.
- HTTPS is recommended —
Securecookies and protected fragments rely on a trustworthy transport.




