{"id":51495747,"url":"https://github.com/jeannesbryan/deaddrop","last_synced_at":"2026-07-07T15:01:33.007Z","repository":{"id":366856480,"uuid":"1271442752","full_name":"jeannesbryan/deaddrop","owner":"jeannesbryan","description":"An extreme, static-first, zero-JS, and post-quantum social syndication protocol designed for Tor networks and low-end hardware.","archived":false,"fork":false,"pushed_at":"2026-06-23T14:50:37.000Z","size":239,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-23T16:30:40.163Z","etag":null,"topics":["asynchronous","cryptography","cypherpunk","darknet","decentralized","onion-service","opsec","p2p","php","post-quantum","privacy","proof-of-work","self-hosted","social-network","sqlite","tor","zero-js"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jeannesbryan.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-16T17:04:01.000Z","updated_at":"2026-06-23T14:50:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jeannesbryan/deaddrop","commit_stats":null,"previous_names":["jeannesbryan/deaddrop"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/jeannesbryan/deaddrop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeannesbryan%2Fdeaddrop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeannesbryan%2Fdeaddrop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeannesbryan%2Fdeaddrop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeannesbryan%2Fdeaddrop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jeannesbryan","download_url":"https://codeload.github.com/jeannesbryan/deaddrop/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeannesbryan%2Fdeaddrop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35232326,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-07T02:00:07.222Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asynchronous","cryptography","cypherpunk","darknet","decentralized","onion-service","opsec","p2p","php","post-quantum","privacy","proof-of-work","self-hosted","social-network","sqlite","tor","zero-js"],"created_at":"2026-07-07T15:01:32.236Z","updated_at":"2026-07-07T15:01:32.987Z","avatar_url":"https://github.com/jeannesbryan.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# \u003e DEADDROP_ \u003cimg src=\"assets/favicon-32x32.png\" width=\"32\" align=\"center\"\u003e\n**Tor-native asynchronous micro-publishing for low-power nodes.**\n\n![Status](https://img.shields.io/badge/Status-Experimental-00ff66?style=for-the-badge\u0026logo=tor\u0026logoColor=7D4698\u0026color=110818)\n![PHP](https://img.shields.io/badge/PHP-8.2+-777BB4?style=for-the-badge\u0026logo=php)\n![SQLite](https://img.shields.io/badge/SQLite-Local_First-003B57?style=for-the-badge\u0026logo=sqlite)\n![Tor](https://img.shields.io/badge/Tor-v3_onion_required-7D4698?style=for-the-badge\u0026logo=tor)\n\nDeadDrop is an experimental **Tor-native Nano-Pub node** built with PHP, SQLite, and static `outbox.json` syndication. It is designed for small VPS instances, Armbian boxes, and other low-resource machines where heavy federated stacks are impractical.\n\nThe project focuses on a simple idea: publish locally, expose a static feed, and let a background courier pull updates from trusted peers over Tor. The web UI is intentionally minimal and designed to remain usable in restrictive browser settings.\n\n---\n\n## ⚠️ Security Status\n\nDeadDrop is a hobby/learning project and **has not been professionally audited**. Treat it as experimental software. Do not rely on it for life-critical, journalist-source-protection, dissident-safety, or high-risk operational security without an independent security review.\n\nCurrent security posture:\n\n- Uses Libsodium primitives for private-drop encryption experiments.\n- Uses Tor hidden services for network reachability.\n- Uses SQLite with local storage and optional off-webroot deployment.\n- Uses server-side authentication, short-lived unlock sessions, and CSRF-guarded protected actions.\n- Applies defensive limits against oversized remote `outbox.json` responses.\n- Requires strict Tor v3 `.onion` peer validation by default.\n- Exports a schema-versioned `outbox.json` for safer protocol evolution.\n- Pins first-seen peer keys and pauses sync when peer keys change.\n- Signs public outbox posts and verifies remote post signatures before insert.\n- Supports peer moderation states, ping review, and per-peer remote-media drop policy.\n- Encrypts private DM media into ciphertext-only `.ddm` blobs.\n- Defaults outgoing private DMs to ciphertext-only local storage unless the operator opts into a plaintext sender copy.\n- Provides a CLI-only health check for deployment verification.\n- Supports age-encrypted backup export and CLI restore.\n\nImportant limitations:\n\n- The “post-quantum” layer is currently a **placeholder/mockup**, not real ML-KEM/Kyber security.\n- The project is **not zero-knowledge** in the formal cryptographic sense.\n- Outgoing private drops do not keep a local sender-side plaintext copy by default; the operator can opt into that convenience per send.\n- Metadata reduction and deletion features are best-effort, not a guarantee against forensic recovery on all storage media.\n- Private media blobs are encrypted, but filenames, timing, sizes, and access patterns may still reveal metadata.\n- Encrypted backups reduce accidental archive disclosure only when the age private key is kept off the live web server.\n- Telegram bridge integration, if enabled, contacts Telegram’s infrastructure through Tor but still creates third-party metadata exposure.\n\nSecurity notes, limitations, and safe wording guidance are included in this README.\n\n---\n\n## Architecture\n\nDeadDrop uses a pull-based Nano-Pub model:\n\n1. **Local publishing** writes posts into SQLite.\n2. **Static export** rebuilds `outbox.json` for public syndication.\n3. **Worker sync** periodically pulls peer `outbox.json` files over Tor.\n4. **Radar** tracks peers, aliases, pinned keys, moderation status, and mutual status.\n5. **Inbox** isolates private encrypted drops from the public timeline.\n\nThis avoids always-on push fanout. Visitors can fetch `outbox.json` or the public profile without forcing expensive real-time federation behavior.\n\n---\n\n## Features\n\n### Nano-Pub Static Outbox\nDeadDrop exports posts into a static `outbox.json`. Public reads are cheap and suitable for low-power hosts.\n\n### Tor v3 Peer Policy\nProduction mode accepts only valid Tor v3 `.onion` peers by default. Localhost peers are disabled unless explicitly allowed for lab testing.\n\n### Background Courier\n`worker.php` pulls peer feeds through Tor SOCKS5 using concurrent cURL requests. Remote responses are capped to reduce memory-exhaustion risk.\n\n### Private Drops\nPrivate drops are encrypted as a Libsodium-based envelope using XChaCha20-Poly1305 for payload encryption and X25519 sealed boxes for key wrapping.\n\n### PQ Placeholder Field\nThe `pq_public` / `pq_private` fields are reserved for future post-quantum work. Current “PQ” behavior is a structural placeholder and must not be described as real ML-KEM security.\n\n### Encrypted Private Media\nPrivate DM media is encrypted into `.ddm` blobs. The media key and nonce are carried inside the encrypted private-drop payload, and media is decrypted only while the vault is unlocked.\n\n### Paranoid Inbox\nIncoming private drops remain ciphertext-at-rest. Outgoing private drops default to ciphertext-only local storage unless the operator explicitly chooses to save a sender-side plaintext copy.\n\n### Atomic Outbox Writes\n`outbox.json` is written through a temporary file and `rename()` to reduce the chance of partial/corrupted feed exports.\n\n### Off-Webroot Storage\nRecommended deployments keep SQLite data, backups, and secrets outside `/var/www/html`.\n\n### Short-Lived Unlock Sessions\nAdmin unlock state is stored server-side for a short period instead of carrying the master password in hidden form fields. Protected actions use the active unlock session and CSRF tokens instead of hidden password fields.\n\n### Schema-Versioned Outbox\n`outbox.json` exposes `schema_version`, `protocol_version`, node metadata, and capability flags. DeadDrop v11+ emits schema v2 and skips schema-less legacy feeds instead of trying to parse them.\n\n### Peer Trust And Key Pinning\nThe worker pins a peer's first observed encryption key and signing key. If a peer later advertises changed keys, sync for that peer pauses and Radar displays `[ KEY CHANGED ]` until the operator approves or rejects the pending key.\n\n### Signed Public Posts\nPublic posts exported through `outbox.json` are signed with a local Ed25519 signing key. The worker verifies remote public post signatures before processing tombstones or inserting posts.\n\n### Peer Moderation\nRadar can mark peers as `active`, `quarantined`, or `blocked`. Unknown pings are stored for review instead of being fed directly into worker sync. Blocked peers are rejected by `ping.php`.\n\n### Remote Media Policy\nRadar can set a per-peer remote media policy. When set to `drop`, the worker discards remote `media_url` values during insert while still allowing signed text posts from active peers.\n\n### CLI Health Check\n`health.php` is a terminal-only diagnostic script for checking PHP extensions, Tor SOCKS reachability, SQLite integrity, session storage, outbox schema, signed-post capability, encrypted private-media paths, paranoid inbox status, peer-trust columns, moderation state, ping review state, and Nginx exposure rules.\n\n### Age-Encrypted Backups\n`offload.php` can export `.tar.gz.age` backup archives, and `restore-backup.php` provides a CLI-only restore path. Keep the age private identity offline or outside the webroot.\n\n### Hashcash Knock Gate\n`ping.php` can require a SHA-256 proof-of-work puzzle before accepting peer knocks. This is a throttling tool, not a full DDoS solution.\n\n---\n\n## Recommended Deployment Layout\n\n```text\n/var/www/html/deaddrop/      # public PHP app and static assets\n/var/www/html/deaddrop/media # public media for public posts\n/var/www/html/deaddrop/media/private # public ciphertext-only .ddm blobs for private media\n/var/lib/deaddrop/           # private SQLite database\n/var/lib/deaddrop/private-media # local encrypted private-media cache\n/var/backups/deaddrop/       # private rotating backups, preferably .tar.gz.age\n/etc/deaddrop/config.php     # private node config/secrets\n/run/deaddrop-sessions/      # tmpfs-backed PHP sessions, if available\n```\n\nThe public webroot should not contain SQLite databases, backup archives, node secrets, or helper files intended only for inclusion by other PHP scripts.\n\n---\n\n## Live Server File Policy\n\nA clean current production node should keep only runtime web files in `/var/www/html/deaddrop`. Helper scripts that are included by PHP may remain in the directory, but they must be blocked from direct browser access by Nginx.\n\n### Keep in the live webroot\n\n```text\n/var/www/html/deaddrop/\n├── assets/\n├── auth.php      # internal auth/session helper; block direct web access\n├── db.php        # bootstrap only; reads /etc/deaddrop/config.php; block direct web access\n├── delete.php\n├── dm.php\n├── index.php\n├── net.php       # internal network policy helper; block direct web access\n├── health.php    # CLI diagnostics only; block direct web access\n├── offload.php   # CLI/cron backup only; block direct web access\n├── outbox.php    # internal outbox rebuild helper; block direct web access\n├── ping.php\n├── profile.php\n├── publish.php\n├── radar.php\n├── restore-backup.php # CLI restore only; block direct web access\n└── worker.php    # CLI/cron only; block direct web access\n```\n\n`outbox.json` is generated by the application and must remain publicly readable because it is the Nano-Pub feed. `media/` may remain public for public-post attachments. `media/private/` contains encrypted `.ddm` blobs only; plaintext private media should never be written there.\n\n### Keep outside the webroot\n\n```text\n/etc/deaddrop/config.php        # actual private config/secrets\n/var/lib/deaddrop/deaddrop.sqlite\n/var/lib/deaddrop/private-media\n/var/backups/deaddrop/\n/run/deaddrop-sessions/\n```\n\nThe actual `config.php` should not live in `/var/www/html/deaddrop`. For an open-source repository, publish only `config.example.php`.\n\n### Remove from live server after setup\n\n```text\nkeygen.php\npassword-generator.php\nmigrate.sh\n```\n\n`keygen.php` and `password-generator.php` are init-only utilities. `migrate.sh` is only useful when migrating an older node; for a fresh reinstall it is not needed on the live server. If these files are kept in the repository, place them under a `scripts/` or `tools/` directory rather than deploying them to the live webroot.\n\n### Documentation files\n\n```text\nREADME.md\nCHANGELOG.md\n```\n\nThese are useful for the source repository, but optional on STB/VPS live deployments. Security notes and claims mapping are kept inside this README to keep the repository simpler.\n\n---\n\n\n## System Dependencies\n\nFor Debian 12 / Armbian Bookworm with PHP 8.2:\n\n```bash\nsudo apt update\nsudo apt install -y \\\n  nginx tor sqlite3 curl git nano ufw zram-tools util-linux age tar \\\n  php8.2-fpm php8.2-cli php8.2-sqlite3 php8.2-curl php8.2-mbstring php8.2-sodium\n```\n\nDeadDrop is documented for Debian 12 / Armbian Bookworm with PHP 8.2+. Older Debian releases are not the recommended path for current installs.\n\nNotes:\n\n- `util-linux` provides `flock`, used by cron examples to prevent overlapping worker/backup runs.\n- `age` is required for encrypted backup export and restore.\n- `tar` is used to package backup contents before encryption.\n- `exiftool` is optional and only needed if your local deployment still uses metadata-stripping paths that call it.\n\n---\n\n## Nginx Configuration: Tor-Only + Current Hardening\n\nDeadDrop is intended to be served through Tor, with Nginx bound only to localhost. If the same onion also serves a root landing page from `/var/www/html`, keep both the root site and `/deaddrop` inside one server block.\n\nOn Debian/Armbian, the active site config may have any name. This README uses the existing deployment convention:\n\n```bash\nsudo nano /etc/nginx/sites-available/deaddrop\nsudo ln -sf /etc/nginx/sites-available/deaddrop /etc/nginx/sites-enabled/deaddrop\n```\n\nUse this as the combined current server block for the root landing page plus DeadDrop:\n\n```nginx\nserver {\n    listen 127.0.0.1:80;\n    server_tokens off;\n    client_max_body_size 2M;\n\n    server_name YOUR_ONION_ADDRESS.onion;\n\n    root /var/www/html;\n    index index.php index.html;\n\n    # Disable directory listing globally.\n    autoindex off;\n\n    # Root landing page hardening.\n    # Relevant when /var/www/html serves index.php, blog.php, store.php,\n    # mail.php, radar.php, admin_blog.php, assets/, img/, and /deaddrop.\n    location ~ ^/(config|setup)\\.php$ {\n        return 403;\n    }\n\n    location ^~ /data/ {\n        return 403;\n    }\n\n    # Block include-only / CLI-only / secret-bearing PHP files.\n    # This also blocks config.php as defense-in-depth for accidental webroot copies.\n    # Keep this block before the generic PHP-FPM handler.\n    location ~ ^/deaddrop/(auth|db|net|outbox|worker|offload|restore-backup|health|keygen|password-generator|config)\\.php$ {\n        return 403;\n    }\n\n    # Block private storage paths if old deployments still contain them.\n    # New deployments should keep these outside /var/www/html entirely.\n    location ^~ /deaddrop/data/ {\n        return 403;\n    }\n\n    location ^~ /deaddrop/backup/ {\n        return 403;\n    }\n\n    location ^~ /deaddrop/keys/ {\n        return 403;\n    }\n\n    # Defense-in-depth: block accidental database, backup, env, log, and swap files.\n    location ~* \\.(sqlite|sqlite3|db|bak|backup|old|swp|env|ini|log|agekey)$ {\n        return 403;\n    }\n\n    # Block hidden files and directories such as .git and .env.\n    location ~ /\\. {\n        return 403;\n    }\n\n    # Normal public routing.\n    location / {\n        try_files $uri $uri/ =404;\n    }\n\n    # PHP-FPM handler.\n    location ~ \\.php$ {\n        include snippets/fastcgi-php.conf;\n        fastcgi_pass unix:/run/php/php8.2-fpm.sock;\n    }\n}\n```\n\nValidate and reload:\n\n```bash\nsudo nginx -t\nsudo systemctl reload nginx\n```\n\nQuick local exposure checks:\n\n```bash\ncurl -I http://127.0.0.1/config.php\ncurl -I http://127.0.0.1/setup.php\ncurl -I http://127.0.0.1/data/npc.sqlite\ncurl -I http://127.0.0.1/deaddrop/health.php\ncurl -I http://127.0.0.1/deaddrop/restore-backup.php\ncurl -I http://127.0.0.1/deaddrop/outbox.php\ncurl -I http://127.0.0.1/deaddrop/outbox.json\n```\n\nThe helper files and old private paths should return `403` or `404`. `outbox.json` should remain public once generated.\n\n`outbox.json` must remain public because it is the Nano-Pub feed. The `media/` directory may remain public for public-post attachments. `media/private/` may also be public, but it must contain only encrypted `.ddm` private-media blobs.\n\nFor production, keep SQLite, backups, `/etc/deaddrop/config.php`, and root-site SQLite state outside the webroot. The root landing page hardening above assumes root-site state lives under `/var/lib/npc`, not `/var/www/html/data`.\n\n\n---\n\n## PHP Session Storage on tmpfs\n\nDeadDrop uses short-lived server-side PHP sessions for the unlock flow. This prevents the master password from being carried through hidden form fields.\n\nFor low-power nodes and eMMC-based devices, session files should live in `/run` so they are stored on tmpfs instead of persistent storage.\n\n```bash\nsudo install -o www-data -g www-data -m 700 -d /run/deaddrop-sessions\necho 'd /run/deaddrop-sessions 0700 www-data www-data -' | sudo tee /etc/tmpfiles.d/deaddrop-sessions.conf\nsudo systemd-tmpfiles --create /etc/tmpfiles.d/deaddrop-sessions.conf\n```\n\n`auth.php` automatically uses `/run/deaddrop-sessions` when the directory exists and is writable by `www-data`. If the directory is unavailable, PHP falls back to the default session path.\n\n---\n\n## Private Media Storage\n\nDeadDrop v13 uses two private-media locations:\n\n```text\n/var/lib/deaddrop/private-media         # local encrypted cache, outside webroot\n/var/www/html/deaddrop/media/private    # public ciphertext-only .ddm blobs\n```\n\nCreate both directories before testing encrypted DM media:\n\n```bash\nsudo install -o www-data -g www-data -m 700 -d /var/lib/deaddrop/private-media\nsudo install -o www-data -g www-data -m 755 -d /var/www/html/deaddrop/media/private\n```\n\nThe files under `media/private/` are encrypted blobs. The keys required to decrypt them are carried inside encrypted private-drop payloads and are only used while the vault is unlocked.\n\n---\n\n## Configuration Notes\n\nUse a private config file outside the webroot:\n\n```php\n\u003c?php\nreturn [\n    'node_name'   =\u003e 'YOUR_NODE_NAME',\n    'node_url'    =\u003e 'http://your-v3-onion-address.onion/deaddrop',\n    'admin_hash'  =\u003e 'YOUR_BCRYPT_ADMIN_HASH',\n    'max_outbox'  =\u003e 50,\n    'outbox_schema_version' =\u003e 2,\n\n    'db_path'     =\u003e '/var/lib/deaddrop/deaddrop.sqlite',\n    'private_media_path' =\u003e '/var/lib/deaddrop/private-media',\n    'backup_path' =\u003e '/var/backups/deaddrop',\n    'backup_retention' =\u003e 7,\n    'backup_include_config' =\u003e true,\n    'backup_encryption' =\u003e true,\n    'backup_age_recipient' =\u003e 'age1REPLACE_WITH_YOUR_PUBLIC_RECIPIENT',\n\n    'session_ttl_seconds' =\u003e 900,\n    'paranoid_inbox' =\u003e true,\n    'save_private_plaintext_copy' =\u003e false,\n\n    'allow_local_peers' =\u003e false,\n\n    'tg_on'       =\u003e false,\n    'tg_token'    =\u003e '',\n    'tg_chat'     =\u003e ''\n];\n```\n\nFor production, keep:\n\n```php\n'allow_local_peers' =\u003e false,\n```\n\nOnly enable localhost peers in an isolated lab.\n\n---\n\n## Outbox Schema And Signing Policy\n\nDeadDrop v11+ uses schema v2 as the minimum public feed format. DeadDrop v12 adds signed public posts and signing-key advertisement while keeping the schema boundary at v2:\n\n```json\n{\n  \"protocol\": \"Nano-Pub\",\n  \"protocol_version\": \"12\",\n  \"schema_version\": 2,\n  \"node\": {\n    \"name\": \"YOUR_NODE_NAME\",\n    \"url\": \"http://your-v3-onion-address.onion/deaddrop\",\n    \"public_key\": \"...\",\n    \"pq_public\": null,\n    \"signing_public_key\": \"...\",\n    \"capabilities\": {\n      \"signed_posts\": true,\n      \"private_media\": true,\n      \"encrypted_media\": true,\n      \"paranoid_inbox\": true\n    }\n  },\n  \"capabilities\": {\n    \"signed_posts\": true,\n    \"private_media\": true,\n    \"encrypted_media\": true,\n    \"paranoid_inbox\": true\n  },\n  \"posts\": [\n    {\n      \"id\": \"local-post-id\",\n      \"content\": \"public text\",\n      \"post_signature_alg\": \"Ed25519\",\n      \"post_signature\": \"base64...\"\n    }\n  ]\n}\n```\n\nSchema-less outboxes are treated as legacy and skipped by the worker. This keeps the parser simple while preserving an explicit version boundary for later features such as encrypted media and paranoid inbox mode.\n\nThe worker verifies remote public post signatures before processing tombstones or inserting posts. Unsigned or invalidly signed posts are skipped.\n\n---\n\n## Peer Trust And Moderation\n\nDeadDrop v12 strengthens federation safety around peer identity and network intake.\n\n### First-Seen Key Pinning\n\nWhen the worker first sees a peer outbox, it pins the peer's encryption public key and signing public key in `following`.\n\nIf the same peer later advertises changed keys:\n\n- worker sync for that peer pauses,\n- Radar shows `[ KEY CHANGED ]`,\n- pending key fingerprints are displayed,\n- admin must approve or reject the pending key manually.\n\nThis prevents silent public-key replacement during background sync.\n\n### Moderation States\n\nRadar supports three peer states:\n\n| State | Worker behavior | Ping behavior |\n|---|---|---|\n| `active` | included in worker sync | known pings enter trusted queue |\n| `quarantined` | skipped by worker | pings stay quarantined for review |\n| `blocked` | skipped by worker | rejected by `ping.php` |\n\nUnknown pings are stored as `pending` so the operator can review them in Radar before locking, quarantining, blocking, or dismissing them.\n\n### Remote Media Policy\n\nEach peer can use one of two remote-media policies:\n\n| Policy | Behavior |\n|---|---|\n| `allow` | worker may keep valid remote public `media_url` values |\n| `drop` | worker discards remote `media_url` values during insert |\n\nThis is useful for reducing exposure to untrusted remote media while still allowing signed text posts from active peers.\n\n---\n\n## Encrypted DM Media And Paranoid Inbox\n\nDeadDrop v13 hardens private-data handling.\n\n### Encrypted DM Media\n\nPrivate DM media is not stored as a normal public image/video attachment. Instead:\n\n1. `publish.php` encrypts the uploaded file with a random XChaCha20-Poly1305 key.\n2. The encrypted blob is written as `media/private/\u003chash\u003e.ddm`.\n3. A local encrypted cache copy may be kept under `private_media_path`.\n4. The media key, nonce, MIME type, blob URL, and integrity hash are placed inside the encrypted private-drop payload.\n5. `dm.php` decrypts and streams the media only while the vault is unlocked.\n\nThe public `.ddm` file is still public in the sense that peers can fetch it, but it is ciphertext-only.\n\n### Paranoid Inbox\n\nIncoming private drops remain as HYBRID ciphertext in the local inbox and are decrypted during render after admin unlock.\n\nOutgoing private drops do not keep a local plaintext copy by default:\n\n```php\n'paranoid_inbox' =\u003e true,\n'save_private_plaintext_copy' =\u003e false,\n```\n\nThe DM form provides an explicit sender-side checkbox to save a plaintext local copy for convenience. If unchecked, the sender cannot re-read their outgoing private DM locally.\n\nHealth check warns if older outgoing split-ledger plaintext copies are still present in the local inbox.\n\n---\n\n\n## Encrypted Backup Setup\n\nDeadDrop can export encrypted backup archives using `age`. This turns the old plaintext rotating backup into an encrypted `.tar.gz.age` export handled by `offload.php`, with restore handled by `restore-backup.php`.\n\nInstall `age` on the node if it is not already installed:\n\n```bash\nsudo apt update\nsudo apt install age -y\n```\n\nGenerate the backup keypair on a trusted admin workstation, not on the public node when possible:\n\n```bash\nage-keygen -o deaddrop-backup.agekey\n```\n\nThe command prints a public recipient beginning with `age1...`. Put only that public recipient in `/etc/deaddrop/config.php`:\n\n```php\n'backup_encryption' =\u003e true,\n'backup_age_recipient' =\u003e 'age1REPLACE_WITH_YOUR_PUBLIC_RECIPIENT',\n```\n\nKeep `deaddrop-backup.agekey` private. Do not place the `AGE-SECRET-KEY-...` identity in `/var/www/html/deaddrop`, `/etc/deaddrop/config.php`, or the public repository.\n\nRun an encrypted backup manually:\n\n```bash\nsudo -u www-data php /var/www/html/deaddrop/offload.php --no-jitter\n```\n\nNormal cron can omit `--no-jitter`. Use `flock` to prevent overlapping backup jobs:\n\n```cron\n0 0 * * * flock -n /tmp/deaddrop-offload.lock /usr/bin/php /var/www/html/deaddrop/offload.php \u003e\u003e /var/log/deaddrop/offload.log 2\u003e\u00261\n```\n\nExpected output path:\n\n```text\n/var/backups/deaddrop/deaddrop_backup_YYYYmmdd_HHMMSS.tar.gz.age\n```\n\nBackups include SQLite, WAL/SHM sidecars when present, public media, encrypted `.ddm` private-media blobs, `outbox.json`, a manifest, and optionally `/etc/deaddrop/config.php` when `backup_include_config` is enabled. The temporary plaintext tarball exists only during export/restore and is deleted afterward.\n\nRestore is CLI-only. First test with `--dry-run` on a disposable machine or non-production environment:\n\n```bash\nsudo php /var/www/html/deaddrop/restore-backup.php \\\n  /var/backups/deaddrop/deaddrop_backup_YYYYmmdd_HHMMSS.tar.gz.age \\\n  /path/to/deaddrop-backup.agekey \\\n  --dry-run\n```\n\nFor a real restore, stop runtime services first:\n\n```bash\nsudo systemctl stop nginx php8.2-fpm\n```\n\nThen restore with explicit confirmation:\n\n```bash\nsudo php /var/www/html/deaddrop/restore-backup.php \\\n  /var/backups/deaddrop/deaddrop_backup_YYYYmmdd_HHMMSS.tar.gz.age \\\n  /path/to/deaddrop-backup.agekey \\\n  --yes\n```\n\nRepair permissions and restart services:\n\n```bash\nsudo chown -R www-data:www-data /var/lib/deaddrop /var/www/html/deaddrop/media\nsudo chown root:www-data /etc/deaddrop/config.php\nsudo chmod 0640 /etc/deaddrop/config.php\nsudo systemctl start php8.2-fpm nginx\n```\n\n`restore-backup.php` creates a local safety copy under `/var/backups/deaddrop/restore_safety_*` before overwriting live files. Keep the age identity/private key off the node whenever possible.\n\n---\n\n## Health Check\n\nDeadDrop includes a CLI-only health check:\n\n```bash\nsudo -u www-data php /var/www/html/deaddrop/health.php\n```\n\nJSON output is available for automation:\n\n```bash\nsudo -u www-data php /var/www/html/deaddrop/health.php --json\n```\n\nUse the health check after installation, after Nginx changes, after moving storage paths, after enabling encrypted backups, after upgrading peer-trust or moderation features, and after enabling encrypted DM media or paranoid inbox behavior.\n\n---\n\n## Private Drop Model\n\nDeadDrop’s private-drop model is experimental and intentionally simple:\n\n```text\nplaintext -\u003e optional padding -\u003e XChaCha20-Poly1305 ciphertext\nsymmetric key -\u003e X25519 sealed box\noptional second wrap -\u003e placeholder field, not real PQ security\n```\n\nIncoming private drops are stored as ciphertext and decrypted only when the vault is unlocked. Outgoing private drops are stored as ciphertext-only by default; sender-side plaintext copies are optional and explicit.\n\nPrivate DM media is encrypted separately into `.ddm` blobs. The media decryption key is carried inside the encrypted private-drop payload, not in `media_url`.\n\nBy default, outgoing private drops are stored locally as ciphertext only. Operators can opt into a sender-side plaintext copy per message, but that weakens at-rest privacy for those outgoing messages.\n\nThis is not a formal zero-knowledge system.\n\n---\n\n## Threat Model\n\nDeadDrop attempts to reduce risk from:\n\n- casual web crawling,\n- accidental clearnet exposure when deployed behind Tor correctly,\n- oversized peer feed memory abuse,\n- public feed corruption during write interruptions,\n- silent peer key replacement during background sync,\n- unsigned or invalidly signed public feed posts,\n- unreviewed unknown pings entering worker sync automatically,\n- unwanted remote media URLs from selected peers,\n- plaintext private-media leakage through public media attachments,\n- default outgoing private-DM plaintext copies,\n- accidental backup disclosure when encrypted backups are configured correctly,\n- basic unauthorized admin access,\n- low-grade ping flooding,\n- common deployment mistakes detectable by the CLI health check.\n\nDeadDrop does **not** currently protect against:\n\n- a compromised host,\n- malicious PHP extensions or OS-level malware,\n- a stolen admin password,\n- a stolen age private identity or backups decrypted on an unsafe host,\n- browser compromise,\n- traffic correlation by powerful adversaries,\n- professional forensic recovery across all storage types,\n- cryptographic attacks caused by unaudited protocol design,\n- metadata leakage from encrypted private media blob timing, size, or access patterns,\n- malicious peers that publish harmful public content,\n- social graph discovery through operational mistakes,\n- metadata exposure from optional third-party integrations.\n\n---\n\n## Development Philosophy\n\nDeadDrop prefers:\n\n- boring, local-first components,\n- small PHP scripts over heavy services,\n- static syndication over real-time push,\n- explicit Tor routing,\n- low memory use,\n- deployability on cheap hardware,\n- honest security language over theatrical certainty.\n\nThe aesthetic can stay cyberpunk. The security claims should stay precise.\n\n---\n\n## Security Notes And Claims Mapping\n\nDeadDrop is experimental software and has not been independently audited. Use accurate language when describing it.\n\nPrefer:\n\n- “experimental”\n- “best-effort hardening”\n- “Libsodium-based private drop prototype”\n- “Tor-native pull syndication”\n- “post-quantum placeholder”\n- “not audited”\n- “not formal zero-knowledge”\n- “age-encrypted backups”\n- “schema-versioned outbox”\n- “first-seen peer key pinning”\n- “signed public posts”\n- “operator-controlled peer moderation”\n- “encrypted DM media”\n- “paranoid inbox mode”\n- “CLI deployment health check”\n\nAvoid unless independently verified:\n\n- “military-grade”\n- “mathematically impervious”\n- “absolute security”\n- “zero clearnet leaks”\n- “total immunity”\n- “post-quantum secure”\n- “zero-knowledge”\n- “forensics-proof”\n\nSecurity claims should be mapped like this:\n\n| Old wording | Safer wording |\n|---|---|\n| Post-Quantum Armor | Post-quantum placeholder / reserved field |\n| ML-KEM Kyber security | ML-KEM/Kyber is not implemented yet |\n| Zero-Knowledge RAM Vault | Short-lived server-side unlock session and ciphertext-oriented inbox design |\n| Plaintext is NEVER written | Incoming private drops and default outgoing private drops are ciphertext-at-rest; explicit sender-side plaintext copies can still be enabled |\n| Total immunity against traffic analysis | Padding reduces simple size-based leakage but does not prevent traffic correlation |\n| Absolute data vaporization | Best-effort deletion using SQLite secure_delete and file shredding where supported |\n| Zero clearnet leaks | Tor routing is used for configured peer/Telegram requests, but deployment mistakes or integrations may still leak metadata |\n| Military-grade | Uses standard Libsodium primitives in an unaudited application protocol |\n| Mathematically impervious | Encrypted with modern primitives; protocol is not audited |\n| Forensics-proof | Not forensics-proof, especially on flash storage, backups, snapshots, or compromised hosts |\n| 0% JavaScript | Designed to work without JavaScript; verify no inline JS remains before claiming strict no-JS |\n| Encrypted backups make the node safe if compromised | Encrypted backups reduce archive exposure when the age private key is kept off the live host |\n| Health check proves the node is secure | Health check catches common deployment mistakes; it is not a security audit |\n| Schema versioning prevents protocol attacks | Schema versioning improves migration safety, but does not authenticate peers |\n| Short session means secrets cannot leak | Short server-side sessions reduce browser/form exposure but do not protect a compromised host |\n| Key pinning proves the peer is legitimate | First-seen key pinning detects later key changes; the first key still depends on operator trust |\n| Signed posts mean safe content | Signatures verify authorship/integrity, not whether content is safe or true |\n| Blocked peers cannot affect the node at all | Blocked peers are rejected by DeadDrop policy, but network-level traffic can still reach Tor/Nginx |\n| Remote media drop sanitizes posts | Remote media drop discards `media_url`; it does not sanitize post text or prove peer intent |\n| Encrypted DM media hides all media metadata | Encrypted DM media hides file contents; blob size, timing, and access patterns may still leak metadata |\n| Paranoid inbox is zero-knowledge | Paranoid inbox reduces plaintext-at-rest; the live server/runtime is still trusted |\n\nProduction defaults should stay conservative:\n\n```php\n'allow_local_peers' =\u003e false,\n'tg_on' =\u003e false,\n'session_ttl_seconds' =\u003e 900,\n'paranoid_inbox' =\u003e true,\n'save_private_plaintext_copy' =\u003e false,\n'private_media_path' =\u003e '/var/lib/deaddrop/private-media',\n'backup_include_config' =\u003e true,\n'backup_encryption' =\u003e true,\n'backup_age_recipient' =\u003e 'age1...'\n```\n\nKnown limitations:\n\n- The host OS, PHP runtime, web server, Tor daemon, and browser remain trusted components.\n- A stolen master password compromises admin access and may decrypt local data.\n- Encrypted backups do not help if the live host, master password, or age private identity is compromised.\n- `health.php` catches common configuration mistakes but cannot prove the node is secure.\n- Public posts and public media are intentionally public.\n- Private DM media blobs are encrypted, but their size and timing may still be observable.\n- If the operator enables sender-side plaintext copies, those outgoing DMs are no longer ciphertext-only at rest.\n- First-seen key pinning is not a replacement for out-of-band key verification.\n- Signed posts verify feed integrity only when the pinned signing key is trusted.\n\n---\n\n## Contributing\n\nIssues, hardening patches, threat-model reviews, and documentation fixes are welcome.\n\nSecurity-related contributions should clearly state:\n\n- what risk is being reduced,\n- what attack scenario is still out of scope,\n- whether the patch changes the public `outbox.json` schema,\n- whether it changes the schema v2+ compatibility boundary.\n\n---\n\n## Disclaimer\n\nDeadDrop is experimental software. Use it at your own risk. Run it only if you understand the tradeoffs of operating a Tor hidden service, storing secrets on a server, and using unaudited cryptographic application code.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeannesbryan%2Fdeaddrop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjeannesbryan%2Fdeaddrop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeannesbryan%2Fdeaddrop/lists"}