Snipto is a secure, lightweight online snippet sharing service. It allows users to quickly create and share text snippets via unique URLs, with end-to-end encryption, ephemeral storage, and QR code integration. Snipto is designed for personal use, teams, and anyone who wants a private alternative to public paste services.
- Instant Snippet Creation: No “create” page — simply visit a URL and start typing.
- End-to-End Encryption (E2EE): Content is encrypted client-side by default. The server never sees the raw snippet or the encryption key.
- Optional Password Protection: Secure your snippets with a personal password.
- Snipto ID (Asymmetric E2EE): Generate a public Snipto ID from a passphrase. Anyone can encrypt to it; only you can decrypt — your private key is re-derived from your passphrase in the browser and never transmitted.
- Customizable Expiration: Choose how long your snippet stays available (up to 1 week) when using password protection or Snipto ID.
- Ephemeral Snippets: Snippets are deleted after the first view and expire automatically after the chosen time (default 1 hour).
- QR Code Generation: Generate a QR code upon snippet creation for easy mobile sharing.
- Security Focused: Built with Laravel 13 (PHP 8.5+), Alpine.js (CSP-compliant build), and strict Content Security Policy (CSP) headers.
- Access a Snipto URL: Visit
https://snipto.net/{slug}. - Create or View:
- If the slug does not exist, you can create a new snippet.
- If it exists, Snipto identifies the protection mode. If password-protected, the user is prompted for the password before the payload is retrieved.
- Snippet Sharing Modes:
- Random Secret (Default): The snippet is encrypted in the browser with a randomly generated 256-bit key. The key is appended to the URL as
#k=. The server never receives this key. - Password Protected: The snippet is encrypted using a user-provided password. The recipient must enter the password to view the content. Customizable expiration times.
- Snipto ID (Asymmetric): The sender encrypts to the recipient's published Snipto ID using X25519 ECDH. Only the recipient — who re-derives their private key from their passphrase in the browser — can decrypt. Private keys never leave the browser; the server only sees ciphertext, the sender's ephemeral public key, and a public per-recipient salt.
- E2EE Disabled (Plaintext): Users can opt-out of encryption. In this mode, the snippet is sent to the server as-is. The UI provides a clear warning when this mode is active.
- Random Secret (Default): The snippet is encrypted in the browser with a randomly generated 256-bit key. The key is appended to the URL as
- Automatic Expiration: Snippets are single-use only — once they are successfully viewed, they are removed from the database. All snippets expire automatically 1 hour after creation (or up to 1 week if configured in password mode).
- Secure Rendering: Decrypted content is rendered inside a sandboxed iframe using Trusted Types to prevent XSS.
- Navigate to
https://snipto.net/{your-slug}. - Enter your content in the text area.
- Select your protection method using the switcher:
- Random Secret: E2EE using a link-based key (Default).
- Password: E2EE using a password you choose (min 8 characters).
- Snipto ID: Paste the recipient's published Snipto ID. Only they can decrypt.
- E2EE Disabled: No encryption (Not recommended for sensitive data).
- Click Snipto it.
- Visit
https://snipto.net/sniptoid. - Enter a passphrase (minimum 20 characters) — or click Generate for me to roll a 6-word diceware passphrase from the EFF short wordlist (~62 bits of entropy).
- If you used the generator, reveal or copy the passphrase before continuing — once you derive the ID, the passphrase is wiped from memory.
- Click Generate Snipto ID. Save both the passphrase and the resulting 64-character Snipto ID — both are required to decrypt incoming messages.
- Share your Snipto ID with anyone who wants to send you encrypted snippets.
- Navigate to the snippet URL.
- Based on the protection mode:
- Random Secret: The URL must include the
#k=fragment to decrypt. - Password: You will be prompted to enter the password.
- Snipto ID: You will be prompted for the passphrase that generated your Snipto ID.
- E2EE Disabled: The content is displayed immediately.
- Random Secret: The URL must include the
- Once viewed, the snippet is immediately removed from the database.
Snipto is designed with end-to-end encryption (E2EE) as a core principle. All encryption and decryption operations occur client-side in the browser using the Web Crypto API.
Symmetric snippets are encrypted using AES-256-GCM with a 12-byte random nonce. Snipto ID snippets layer X25519 ECDH key agreement on top, with HKDF-SHA256 splitting the shared secret into separate AES-GCM and HMAC keys.
Key derivation depends on the protection mode:
- Random Secret (URL fragment): PBKDF2-HMAC-SHA256, 100,000 iterations, salt = nonce. The 16-character random URL secret carries ~96 bits of entropy on its own, so a memory-hard KDF would just add latency without meaningful security gain.
- Password Protected: Argon2id at the OWASP strong tier (memory 64 MB, opsLimit 3, parallelism 1, output 32 bytes), via libsodium WebAssembly. Per-snippet salt is derived deterministically from a domain-separated SHA-256 over the nonce; the resulting master key is split via HKDF-SHA256 into AES-GCM and HMAC keys.
- Snipto ID: Argon2id with the same parameters, but the salt is a fresh 16-byte random value generated once per Snipto ID and published as the first 16 bytes of the ID itself (Snipto ID =
base64(salt(16) || pubkey(32))→ 64 chars). The Argon2id output becomes the X25519 private key; the public key is the second half of the published Snipto ID. Senders ship the salt back to the server in therecipient_saltcolumn, so the recipient can re-derive the key pair from the passphrase alone — no need to paste their Snipto ID to read incoming messages. The salt being public is by design: it defeats rainbow-table precomputation (every Snipto ID has its own keyspace) without leaking anything an attacker doesn't already see in the published ID.
The X25519 + Argon2id stack is feature-detected at runtime; browsers without support see an "update your browser" banner and the Snipto ID mode is hidden.
Both the Snipto ID page and the Password-protected mode offer a Generate for me button that produces a 6-word diceware passphrase from the EFF short wordlist (1,296 words, CC-BY-3.0, ~62 bits). Generation uses crypto.getRandomValues with rejection sampling to avoid modulo bias. Generated passphrases are masked by default; the submit button stays disabled until the user reveals or copies the value, preventing accidental loss of an unmemorable secret.
- Server Storage: The server stores the base64-encoded encrypted payload (ciphertext + auth tag + HMAC), the nonce, the protection type, and a SHA-256
key_hash. For Snipto ID, it also stores the sender's ephemeral X25519 public key (sender_public_key) and the recipient's per-Snipto-ID salt (recipient_salt). Thekey_hashfor Snipto ID is SHA-256 of the raw ECDH shared secret. - Access Control: The
key_hashis required to retrieve the encrypted payload from the API, compared withhash_equals()for timing-safe comparison. This prevents unauthorized access even if the slug is guessed. - Integrity: AES-GCM and an additional HMAC-SHA256 ensure that any tampering with the ciphertext or nonce is detected during decryption. HMAC verification runs before decryption is attempted.
Snipto employs a multi-layered defense-in-depth strategy to protect user data and prevent common web attacks.
-
Strict Content Security Policy (CSP): Using
Spatie\Csp, the application enforces a strict policy:default-src 'none': All resources are blocked by default.frame-ancestors 'none': Prevents clickjacking.connect-src 'self': Restricts AJAX requests only to the origin server.require-trusted-types-for 'script': Mandates the use of Trusted Types to prevent DOM-based XSS.- Alpine.js CSP: Uses the CSP-compatible build of Alpine.js to avoid
unsafe-eval.
-
Cross-Origin Isolation: Specialized middleware ensures the browser runs in a secure context:
Cross-Origin-Opener-Policy: same-origin(COOP)Cross-Origin-Embedder-Policy: require-corp(COEP)Cross-Origin-Resource-Policy: same-origin(CORP)
-
Secure Content Rendering:
- Decrypted snippets are rendered in a sandboxed
<iframe>(sandbox="allow-same-origin") withoutallow-scripts, isolating the content from the main document. - Implements a localized Meta CSP (
default-src 'none') inside the iframe'ssrcdocto prevent any resource loading or data exfiltration. - Implements a Trusted Types policy (
snipto-srcdoc) to sanitize and safely inject thesrcdoc.
- Decrypted snippets are rendered in a sandboxed
The fastest way to run Snipto is using the official image. This method does not require cloning the repository.
docker run -d \
--name snipto \
-p 8080:8080 \
-e APP_KEY=base64:$(openssl rand -base64 32) \
-e DB_CONNECTION=sqlite \
-e DB_DATABASE=/tmp/database.sqlite \
tchubaba/snipto:latestVisit http://localhost:8080 to start sharing snippets.
Note: For persistence, it is recommended to mount a volume for the SQLite database so your data survives container updates. For high-traffic environments, you should probably connect to an external MariaDB/MySQL instance.
Snipto is Dockerized for both production and development environments.
When working with a cloned repository, the default mode is Development. The deployment mode is controlled by the APP_ENV environment variable in your .env file:
- Development (Default): Set
APP_ENV=local(default in.env.example). This mode installs dev-dependencies, generates IDE helper files, and starts the Vite development server with hot-module replacement. - Production: Set
APP_ENV=production. This mode optimizes the application for performance, runscomposer install --no-dev, caches configurations, and builds production assets.
To switch modes:
- Update your
.envfile (e.g.,APP_ENV=localorAPP_ENV=production). - Rebuild and start the containers:
make build && make up.
By default, Snipto starts a MariaDB container. To use an external database instead:
- Open
config.mkand removewith-dbfrom theCOMPOSE_PROFILESvariable. - Update your
.envfile with your external database credentials (DB_HOST,DB_PORT,DB_DATABASE,DB_USERNAME,DB_PASSWORD). - Run
make up.
The application will automatically wait for your external database to be accessible before running migrations.
A Makefile is provided to simplify common tasks:
| Command | Description |
|---|---|
make up |
Start the containers in the background. |
make down |
Stop and remove the containers. |
make build |
Rebuild the Docker images. |
make restart |
Restart all services. |
make artisan cmd="..." |
Run an Artisan command (e.g., make artisan migrate). |
make composer cmd="..." |
Run a Composer command. |
make npm cmd="..." |
Run an NPM command. |
make shell |
Open a bash shell inside the app container. |
make test |
Run the PHPUnit test suite. |
make grumphp |
Run GrumPHP quality checks. |
make fix |
Automatically fix code style issues using Laravel Pint. |
make logs |
Tail the application logs. |
make fresh |
Reset the database and run migrations with seeders. |
This project is licensed under the MIT License.
Snipto — Share your snippets. End-to-end encrypted.