{"id":27958536,"url":"https://github.com/btreemap/deadrop","last_synced_at":"2025-07-03T15:05:08.936Z","repository":{"id":291047925,"uuid":"976406471","full_name":"BTreeMap/deadrop","owner":"BTreeMap","description":null,"archived":false,"fork":false,"pushed_at":"2025-05-06T22:27:22.000Z","size":162,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-05-07T18:51:55.571Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/BTreeMap.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2025-05-02T03:57:51.000Z","updated_at":"2025-05-06T22:27:25.000Z","dependencies_parsed_at":"2025-05-07T18:48:12.249Z","dependency_job_id":null,"html_url":"https://github.com/BTreeMap/deadrop","commit_stats":null,"previous_names":["btreemap/deadrop"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/BTreeMap/deadrop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BTreeMap%2Fdeadrop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BTreeMap%2Fdeadrop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BTreeMap%2Fdeadrop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BTreeMap%2Fdeadrop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/BTreeMap","download_url":"https://codeload.github.com/BTreeMap/deadrop/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BTreeMap%2Fdeadrop/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263348034,"owners_count":23452866,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":[],"created_at":"2025-05-07T18:25:12.861Z","updated_at":"2025-07-03T15:05:08.896Z","avatar_url":"https://github.com/BTreeMap.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# deadrop (WIP Proposal)\n\n**Status:** This document is currently a proposal/Work-in-Progress; no specific timeline for completion has been set.\n\nEncrypted \"dead‑drop\" service allowing users to anonymously submit and retrieve data using a single X25519 keypair. Data is encrypted client-side and stored server-side under your public key. Retrieval and notification APIs perform a stateless proof-of-possession via a single challenge endpoint returning an encrypted JWT, which is then used with standard `Authorization: Bearer` headers.\n\n---\n\n## Features\n\n* **Client (`deadrop.sh`)**\n\n  * `send`: encrypt \u0026 upload text or files\n  * `retrieve`: authenticate \u0026 download all ciphertexts for your key\n  * `notify`: register Telegram push notifications\n  * Fully stateless challenge using encrypted JWTs; no server-side session storage\n\n* **Server (`deadrop.joefang.org`)**\n\n  * `POST /upload`: store encrypted payload\n  * `POST /challenge`: issue encrypted JWT challenge for a given scope (retrieve/notify)\n  * `POST /retrieve`: verify JWT \u0026 return stored items\n  * `POST /notify`: verify JWT \u0026 register Telegram hook\n\n---\n\n## Prerequisites\n\n* **Client**\n\n  * `bash` (≥ 4.4)\n  * [`age`](https://github.com/FiloSottile/age) (X25519 encryption)\n  * [`curl`](https://curl.se)\n  * [`jq`](https://stedolan.github.io/jq)\n\n* **Server**\n\n  * [Rust](https://www.rust-lang.org/) (stable toolchain)\n  * [Docker](https://www.docker.com/)\n  * Database (e.g. PostgreSQL) for storing ciphertexts and notification registrations\n  * JWT library supporting HS256 (provided by Rust crate)\n\n---\n\n## Installation\n\nClone the repo and make the client script executable:\n\n```sh\ngit clone https://github.com/joefang/deadrop.git\ncd deadrop\nchmod +x deadrop.sh\n```\n\nServer code is in `/server`. See its `Dockerfile` for build instructions and its own README (to be created) for deployment details.\n\n---\n\n## Client Usage (`deadrop.sh`)\n\n### Common flags\n\n* `-k, --pubkey \u003cfile|string\u003e`\n  X25519 public key (file path or raw string)\n\n* `-i, --identity \u003cfile\u003e`\n  X25519 private key for decryption/authentication\n\n* `-e, --endpoint \u003cURL\u003e`\n  Override default server URL (env var `ENDPOINT` also respected)\n\n### `send`\n\nEncrypt and upload data:\n\n```sh\n# upload a string\ndeadrop.sh send -k id_x25519.pub -m \"hello world\"\n\n# upload a file\ndeadrop.sh send -k id_x25519.pub -f /path/to/secret.txt\n```\n\nServer endpoint: `POST /upload`\nHeaders: `X-PubKey: \u003cpubkey\u003e`\nBody: binary ciphertext\n\n### `retrieve`\n\nAuthenticate via encrypted JWT and download items:\n\n```sh\ndeadrop.sh retrieve -i id_x25519 -o ./downloads\n```\n\n1. Client calls `POST /challenge` with `{ \"pubkey\": \"\u003cpub\u003e\", \"scope\": \"retrieve\" }` → returns `{ \"ciphertext\": \"\u003cage-encrypted JWT\u003e\" }`\n2. Client decrypts ciphertext to get the JWT.\n3. Client calls `POST /retrieve` with `Authorization: Bearer \u003cjwt\u003e` header.\n4. Server verifies JWT (`sub`, `aud: \"/retrieve\"`, `exp`), then returns stored items as a paginated JSON list of item IDs and an optional opaque `next_cursor` token for pagination.\n\nEach item is saved and decrypted locally. To fetch more items, pass the `next_cursor` value as the `cursor` query parameter in the next request. The format and contents of the cursor are not specified and may change; treat it as an opaque string.\n\n### `notify`\n\nRegister a Telegram hook:\n\n```sh\ndeadrop.sh notify -i id_x25519 -t \"@alice\"  # or numeric user ID\n```\n\n1. Client calls `POST /challenge` with `{ \"pubkey\": \"\u003cpub\u003e\", \"scope\": \"notify\", \"telegram\": \"\u003ctarget\u003e\" }` → returns `{ \"ciphertext\": \"\u003cage-encrypted JWT\u003e\" }`\n2. Client decrypts ciphertext to get the JWT.\n3. Client calls `POST /notify` with `Authorization: Bearer \u003cjwt\u003e` header.\n4. Server verifies JWT (`sub`, `aud: \"/notify\"`, `exp`, `telegram`), then registers the hook.\n\n---\n\n## Server API\n\nAll endpoints expect and return JSON unless otherwise specified. Client decrypts the JWT challenge received from `/challenge`.\n\n### `POST /upload`\n\n* **Headers**: `X-PubKey: \u003cuser X25519 pub\u003e`\n* **Body**: raw ciphertext\n* **Response**: `201 Created` on success\n\nServer stores each blob with its associated public key and timestamp.\n\n### `POST /challenge`\n\n* **Body**: `{ \"pubkey\": \"\u003cuser X25519 pub\u003e\", \"scope\": \"\u003cretrieve|notify\u003e\", \"telegram\"?: \"\u003ctelegram_target_if_notify\u003e\" }`\n* **Response**: `{ \"ciphertext\": \"\u003cage-encrypted JWT\u003e\" }`\n\nServer creates a JWT with:\n\n```json\n{\n  \"sub\": \"\u003cpubkey\u003e\",\n  \"aud\": \"/\u003cscope\u003e\", // e.g., \"/retrieve\" or \"/notify\"\n  \"iat\": \u003cnow\u003e,\n  \"exp\": \u003cnow + 300\u003e,\n  \"telegram\"?: \"\u003ctelegram_target_if_notify\u003e\" // Included only for notify scope\n}\n```\n\nThen signs (HS256) and encrypts via age for the user’s `pubkey`.\n\n### `POST /retrieve`\n\n* **Headers**: `Authorization: Bearer \u003csigned JWT\u003e`\n* **Query**: `cursor` (optional, opaque server-issued token for pagination)\n* **Response**: `{ \"items\": [ \"\u003citem_id1\u003e\", ... ], \"next_cursor\": \"\u003copaque-cursor-token\u003e\" }`\n\nServer verifies JWT signature, `aud: \"/retrieve\"`, `exp`, matches `sub` to an existing user with data, then returns a paginated list of item IDs and a server-issued opaque cursor for pagination. The client must not attempt to construct or modify the cursor value.\n\n### `POST /notify`\n\n* **Headers**: `Authorization: Bearer \u003csigned JWT\u003e`\n* **Response**: `200 OK`\n\nServer verifies JWT signature, `aud: \"/notify\"`, `exp`, and the presence of the `telegram` claim. It then registers the Telegram ID from the claim for push notifications on future uploads associated with the `sub` (pubkey).\n\n---\n\n## Security Considerations\n\n* **Single keypair**: only X25519 used for both encryption and proof-of-possession.\n* **Stateless Authentication**: Encrypted JWT carries necessary claims (`iat`/`exp`/`sub`/`aud`/`telegram`); server verifies the token presented in the `Authorization` header. No server-side session state needed after issuing the challenge.\n* **Short TTL** (default 5 minutes) on JWT prevents replay attacks.\n* **TLS** protects headers (including `Authorization`) and bodies in transit.\n* **Scope Claim**: The `aud` (audience) claim in the JWT ensures a token issued for one purpose (e.g., `retrieve`) cannot be used for another (e.g., `notify`).\n\n---\n\n## License\n\nMIT © Joe Fang\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbtreemap%2Fdeadrop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbtreemap%2Fdeadrop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbtreemap%2Fdeadrop/lists"}