safedrop-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API.md ADDED
@@ -0,0 +1,197 @@
1
+ # SafeDrop public API contract
2
+
3
+ This document describes **only the public HTTP endpoints the CLI consumes**. It
4
+ is the contract between any SafeDrop client (browser or CLI) and the SafeDrop
5
+ backend. It deliberately documents nothing about the server's internals
6
+ (storage, Redis keys, token signing, rate-limit internals).
7
+
8
+ All request/response bodies are JSON unless noted. The `--api` base URL is
9
+ prefixed to every path below. For the hosted app that base is
10
+ `https://safedrop.ma/api`; for local development it is `http://localhost:4000`.
11
+
12
+ Throughout, **`uploadCode`** is a 16-character identifier and **`handshakeCode`**
13
+ is a 16-character identifier.
14
+
15
+ ---
16
+
17
+ ## Sender endpoints
18
+
19
+ ### `POST /initiate-upload`
20
+
21
+ Reserve an upload slot and obtain a presigned storage URL.
22
+
23
+ **Request body** (all optional):
24
+
25
+ ```json
26
+ { "customExpirationSeconds": 3600 }
27
+ ```
28
+
29
+ `customExpirationSeconds` clamps to the server's allowed range (max 24h); omit it
30
+ for the default TTL (15 minutes).
31
+
32
+ **Response `200`:**
33
+
34
+ ```json
35
+ {
36
+ "uploadCode": "abcdef0123456789",
37
+ "url": "https://.../presigned-put-url",
38
+ "sseToken": "…",
39
+ "senderToken": "…",
40
+ "maxFileSizeMB": 1024,
41
+ "uploadTTLSeconds": 900
42
+ }
43
+ ```
44
+
45
+ - `url` — presigned `PUT` URL for the encrypted bytes.
46
+ - `senderToken` — secret proving you are the sender; required to finalize,
47
+ authorize, and cancel. Never share it.
48
+
49
+ ### `PUT <presigned url>`
50
+
51
+ Upload the **encrypted** file bytes directly to storage.
52
+
53
+ - Header: `Content-Type: application/octet-stream`
54
+ - Body: the raw ciphertext payload (`IV || ciphertext || GCM tag`).
55
+ - Success: HTTP `2xx`.
56
+
57
+ ### `POST /upload/:uploadCode/finalize`
58
+
59
+ Attach the encrypted filename and commit the upload.
60
+
61
+ **Request body:**
62
+
63
+ ```json
64
+ { "encryptedFilename": "<base64>", "senderToken": "…" }
65
+ ```
66
+
67
+ **Response `200`:** `{ "message": "Upload finalized successfully." }`
68
+
69
+ Errors: `400` missing filename, `401` missing sender token, `403` bad sender
70
+ token, `404` bytes not uploaded, `413` file exceeds size limit.
71
+
72
+ ### `DELETE /upload/:uploadCode` (sender cancellation)
73
+
74
+ **Request body:** `{ "senderToken": "…" }`
75
+
76
+ Terminates the session and deletes the encrypted copy. `403` on bad token.
77
+
78
+ ---
79
+
80
+ ## Receiver endpoints
81
+
82
+ ### `POST /handshake/initiate`
83
+
84
+ Begin a receive handshake for an upload.
85
+
86
+ **Request body:** `{ "uploadCode": "abcdef0123456789" }`
87
+
88
+ **Response `200`:**
89
+
90
+ ```json
91
+ { "handshakeCode": "…", "recipientToken": "…", "sseToken": "…" }
92
+ ```
93
+
94
+ - `handshakeCode` — read this to the sender so they can authorize you.
95
+ - `recipientToken` — secret used to poll for the download token and to cancel.
96
+
97
+ Errors: `404` invalid or expired upload code.
98
+
99
+ ### `GET /handshake/token/:uploadCode`
100
+
101
+ Poll for the download token. Returns it once the sender has authorized.
102
+
103
+ - Header: `Authorization: Bearer <recipientToken>`
104
+
105
+ **Response `200`:** `{ "downloadToken": "<jwt>" }`
106
+
107
+ **Response `404`:** `{ "error": "Token not found or expired." }` — not yet
108
+ authorized; keep polling.
109
+
110
+ **Response `403`:** the recipient token is invalid or the session was cancelled.
111
+
112
+ > The download token is single-issue: once returned, the recipient token is
113
+ > consumed.
114
+
115
+ ### `GET /upload/:uploadCode` (download)
116
+
117
+ Download the encrypted file.
118
+
119
+ - Header: `Authorization: Bearer <downloadToken>`
120
+
121
+ **Response `200`:**
122
+
123
+ - Body: the raw ciphertext payload.
124
+ - Header `X-Encrypted-Filename`: the base64 encrypted filename to decrypt
125
+ locally.
126
+
127
+ The server **deletes its stored copy** as soon as the response finishes
128
+ streaming. Errors: `401`/`403` bad token, `404` metadata missing/expired.
129
+
130
+ ### `DELETE /handshake/:uploadCode` (recipient cancellation)
131
+
132
+ - Header: `Authorization: Bearer <recipientToken>` (a valid download token is
133
+ also accepted).
134
+
135
+ Terminates the session.
136
+
137
+ ---
138
+
139
+ ## Authorization (sender side)
140
+
141
+ ### `POST /handshake/authorize`
142
+
143
+ The sender calls this with the handshake code the receiver gave them.
144
+
145
+ **Request body:**
146
+
147
+ ```json
148
+ { "uploadCode": "abcdef0123456789", "handshakeCode": "…" }
149
+ ```
150
+
151
+ **Response `200`:** `{ "message": "Download authorized" }`
152
+
153
+ Errors: `403` wrong handshake code, `404` session terminated/expired, `429` too
154
+ many failed attempts (the session is terminated for safety).
155
+
156
+ ---
157
+
158
+ ## Real-time events (optional)
159
+
160
+ The server exposes Server-Sent Events at `GET /session-events/:sseToken`
161
+ carrying `status-authorized` and `session-terminated` messages. The CLI does
162
+ **not** require SSE — it polls `GET /handshake/token/:uploadCode` instead, which
163
+ the browser also does as a fallback. SSE is documented here only for
164
+ completeness.
165
+
166
+ ---
167
+
168
+ ## Client-side payload formats
169
+
170
+ These are computed entirely on the client; the server treats them as opaque.
171
+
172
+ ### Encryption payload
173
+
174
+ ```
175
+ payload = IV (12 bytes) || ciphertext || GCM auth tag (16 bytes)
176
+ ```
177
+
178
+ - Algorithm: **AES-256-GCM**.
179
+ - Key: 256-bit, shared as 64 lowercase hex characters.
180
+ - The filename is encrypted the same way, but its plaintext is encoded as
181
+ **UTF-16LE** before encryption, then base64-encoded for the
182
+ `X-Encrypted-Filename` header / `encryptedFilename` field.
183
+
184
+ ### Combined share code
185
+
186
+ ```
187
+ combinedCode = base64( JSON.stringify({ uploadCode, key, fullSecurity }) )
188
+ ```
189
+
190
+ ### Share link
191
+
192
+ ```
193
+ https://<host>/#code=<combinedCode>
194
+ ```
195
+
196
+ The code lives in the **URL fragment** (`#`), which browsers never transmit to a
197
+ server.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SafeDrop
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # safedrop-cli
2
+
3
+ Zero-knowledge, end-to-end encrypted file transfer from your terminal — fully
4
+ compatible with the [SafeDrop](https://safedrop.ma) web app.
5
+
6
+ Files are **encrypted on your machine before upload** and **decrypted on the
7
+ receiver's machine after download**. The SafeDrop server only ever sees
8
+ ciphertext: it never receives your plaintext files, your real filenames, or
9
+ your encryption keys.
10
+
11
+ A file sent from the CLI can be received in the browser, and a file sent from
12
+ the browser can be received by the CLI — they use the identical encryption and
13
+ code format.
14
+
15
+ ---
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install -g safedrop-cli
21
+ ```
22
+
23
+ Or run without installing:
24
+
25
+ ```bash
26
+ npx safedrop-cli send ./report.pdf
27
+ ```
28
+
29
+ Requires **Node.js 18+** (uses the built-in `fetch` and `crypto`). The package
30
+ has **zero runtime dependencies**, so it is small and easy to audit.
31
+
32
+ ---
33
+
34
+ ## Quick start
35
+
36
+ ### Send a file
37
+
38
+ ```bash
39
+ safedrop send ./report.pdf
40
+ ```
41
+
42
+ The CLI encrypts the file locally, uploads the ciphertext, and prints a **share
43
+ code** (and a share link if you point it at a hosted SafeDrop). Give that code
44
+ to your receiver. Then the receiver runs `receive`, reads you back a short
45
+ **handshake code**, you paste it in, and the transfer is authorized.
46
+
47
+ ### Receive a file
48
+
49
+ ```bash
50
+ safedrop receive eyJ1cGxvYWRDb2RlIjoi...
51
+ ```
52
+
53
+ The CLI parses the code, shows you a handshake code to read to the sender, waits
54
+ for them to authorize, then downloads and decrypts the file to your current
55
+ directory.
56
+
57
+ ---
58
+
59
+ ## Commands
60
+
61
+ ```
62
+ safedrop send <file> [options]
63
+ safedrop receive <code-or-link> [options]
64
+ ```
65
+
66
+ ### `send` options
67
+
68
+ | Option | Description |
69
+ |---|---|
70
+ | `--ttl <minutes>` | How long the transfer stays available before it expires. Default `15`, max `1440` (24h). |
71
+ | `--secure` | Enable full-security mode: both sides compare an out-of-band 3-word safety code to detect interception. |
72
+ | `--api <base-url>` | SafeDrop API base URL. Defaults to `http://localhost:4000`, or the `SAFEDROP_API` env var. |
73
+
74
+ ### `receive` options
75
+
76
+ | Option | Description |
77
+ |---|---|
78
+ | `--output`, `-o <path>` | Where to save the file. A directory (saves under the sender's filename) or a full file path. |
79
+ | `--api <base-url>` | SafeDrop API base URL. Defaults to `http://localhost:4000`, or the `SAFEDROP_API` env var. |
80
+
81
+ ### Pointing at a hosted SafeDrop
82
+
83
+ The production web app serves its API under `/api`:
84
+
85
+ ```bash
86
+ safedrop send ./report.pdf --api https://safedrop.ma/api
87
+ # or set it once:
88
+ export SAFEDROP_API=https://safedrop.ma/api
89
+ safedrop send ./report.pdf
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Examples
95
+
96
+ ```bash
97
+ # Send with a 1-hour expiry and full-security verification
98
+ safedrop send ./contract.pdf --ttl 60 --secure
99
+
100
+ # Receive from a full browser link into your Downloads folder
101
+ safedrop receive "https://safedrop.ma/#code=eyJ1cGxv..." -o ~/Downloads/
102
+
103
+ # Receive and save under a specific name
104
+ safedrop receive eyJ1cGxv... -o ./received-contract.pdf
105
+ ```
106
+
107
+ See [`examples/transcript-sender.txt`](examples/transcript-sender.txt) and
108
+ [`examples/transcript-receiver.txt`](examples/transcript-receiver.txt) for full
109
+ end-to-end terminal walkthroughs.
110
+
111
+ ---
112
+
113
+ ## How a transfer works
114
+
115
+ ```
116
+ SENDER SAFEDROP SERVER RECEIVER
117
+ ------ --------------- --------
118
+ encrypt file + name locally
119
+ generate AES-256 key
120
+ │ POST /initiate-upload ───────────▶ uploadCode, presigned URL,
121
+ │ senderToken
122
+ │ PUT ciphertext ──────────────────▶ (stores ciphertext only)
123
+ │ POST /upload/:code/finalize ─────▶ (stores encrypted filename)
124
+ print share code ◀──────────────────────────────────────────── paste code
125
+ POST /handshake/initiate
126
+ handshakeCode ◀────────────── (gets handshake code)
127
+ read handshake code ◀────────────────────────────────────────── read it aloud
128
+ │ POST /handshake/authorize ───────▶ issues download token
129
+ poll /handshake/token/:code ◀──
130
+ downloadToken ─────────────────────────────▶
131
+ GET /upload/:code ◀───────────
132
+ ciphertext ────────────────────────────────▶
133
+ (server deletes its copy) decrypt locally,
134
+ save to disk
135
+ ```
136
+
137
+ The **key never leaves the client**. The server stores only the encrypted bytes
138
+ and the encrypted filename, and deletes both the moment the download completes.
139
+
140
+ ---
141
+
142
+ ## Using it as a library
143
+
144
+ The encryption and code helpers are exported so you can build your own tools:
145
+
146
+ ```js
147
+ import {
148
+ encryptBuffer, decryptBuffer,
149
+ encodeCombinedCode, decodeCombinedCode,
150
+ SafeDropApi,
151
+ } from 'safedrop-cli';
152
+ ```
153
+
154
+ These are the same building blocks the CLI uses. See [`API.md`](API.md) for the
155
+ HTTP contract and [`SECURITY.md`](SECURITY.md) for the threat model.
156
+
157
+ ---
158
+
159
+ ## Security at a glance
160
+
161
+ - **AES-256-GCM** authenticated encryption, done entirely client-side.
162
+ - The server is **zero-knowledge**: no keys, no plaintext, no real filenames.
163
+ - Share **codes/links carry the key in the URL fragment** (`#code=...`), which
164
+ browsers never send to servers.
165
+ - The CLI **never logs** file contents or keys; the share code is the only
166
+ secret it prints, and only because sharing it is the entire purpose.
167
+ - Downloaded filenames are **sanitized** — path traversal is refused and local
168
+ files are **never overwritten without confirmation**.
169
+
170
+ Full details in [`SECURITY.md`](SECURITY.md).
171
+
172
+ ---
173
+
174
+ ## Development
175
+
176
+ ```bash
177
+ npm test # runs the cross-compatibility, code-parsing, and path tests
178
+ ```
179
+
180
+ The crypto tests encrypt with the CLI and decrypt using the **actual Web Crypto
181
+ API** (and vice versa) to guarantee browser ⇄ CLI compatibility.
182
+
183
+ ## License
184
+
185
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,144 @@
1
+ # SafeDrop CLI security model
2
+
3
+ SafeDrop is **zero-knowledge**: the server stores and relays data it cannot
4
+ read. This document explains the guarantees the CLI provides, how it provides
5
+ them, and what is explicitly out of scope.
6
+
7
+ ---
8
+
9
+ ## The core guarantee
10
+
11
+ > The SafeDrop server never receives plaintext file contents, plaintext
12
+ > filenames, or encryption keys.
13
+
14
+ Everything sensitive is encrypted and decrypted **on the client**. The CLI is
15
+ just another zero-knowledge client, identical in behavior to the browser app.
16
+
17
+ | Data | Where it is in plaintext | What the server sees |
18
+ |---|---|---|
19
+ | File contents | Sender's & receiver's machines only | AES-256-GCM ciphertext |
20
+ | Real filename | Sender's & receiver's machines only | Encrypted, base64 blob |
21
+ | Encryption key | Sender's machine, then the share code | Never |
22
+
23
+ ---
24
+
25
+ ## Cryptography
26
+
27
+ - **AES-256-GCM** authenticated encryption (confidentiality + integrity). A
28
+ tampered or truncated payload fails decryption rather than producing garbage.
29
+ - Keys are **256-bit**, generated with the OS CSPRNG (`crypto.randomBytes`).
30
+ - A fresh **96-bit IV** is generated per encryption (`crypto.randomBytes(12)`)
31
+ and prepended to the payload.
32
+ - Wire format: `IV (12B) || ciphertext || GCM tag (16B)` — byte-for-byte
33
+ identical to the browser's Web Crypto output, so files cross between the two
34
+ clients transparently. This is enforced by tests that decrypt CLI output with
35
+ the real Web Crypto API and vice versa.
36
+ - Filenames are encrypted with the same scheme; their plaintext is UTF-16LE so
37
+ the bytes match the browser exactly.
38
+
39
+ The CLI does **not** invent its own protocol — it reuses the established
40
+ SafeDrop client format. No backend logic is reimplemented; the CLI only calls
41
+ the documented HTTP API ([`API.md`](API.md)).
42
+
43
+ ---
44
+
45
+ ## Key distribution and links
46
+
47
+ - The encryption key travels **only inside the share code**, which the sender
48
+ hands to the receiver out-of-band (chat, in person, etc.).
49
+ - The combined code is `base64(JSON{ uploadCode, key, fullSecurity })`.
50
+ - The browser share link places the code in the **URL fragment**
51
+ (`https://host/#code=...`). Fragments are never sent to the server in an HTTP
52
+ request, so navigating to a link does not leak the key to the host.
53
+ - The CLI prints the share code to **stdout** and all status/log output to
54
+ **stderr**, so you can capture the code cleanly (e.g. `safedrop send f >
55
+ code.txt`) without log noise, and logs never carry the secret by accident.
56
+
57
+ ---
58
+
59
+ ## Optional MITM protection: full-security mode (`--secure`)
60
+
61
+ When enabled, both sides derive a **3-word Short Authentication String (SAS)**
62
+ from `SHA-256(key)` and compare it over a trusted channel (e.g. read it aloud on
63
+ a call). If the words differ, the key was tampered with in transit and the
64
+ transfer is aborted. The wordlist and derivation match the browser exactly, so a
65
+ browser sender and CLI receiver (or vice versa) compute the same words.
66
+
67
+ ---
68
+
69
+ ## What the CLI does and does not log
70
+
71
+ - **Never logged:** file contents, encryption keys, sender/recipient/download
72
+ tokens.
73
+ - **Printed once, intentionally:** the share code (sender) and the handshake
74
+ code (receiver). These are the secrets you are meant to share — that is the
75
+ entire purpose of the command. The share code is written to stdout; with
76
+ `--secure` the safety code is shown so you can verify it.
77
+ - Secrets are not written to any cache, history, or temp file by the CLI.
78
+
79
+ ---
80
+
81
+ ## Safe handling of downloaded files
82
+
83
+ The decrypted filename comes from the sender and is therefore **untrusted
84
+ input** after decryption. The CLI:
85
+
86
+ - **Strips directory components** from the sender's filename (`../`, absolute
87
+ paths, drive letters, Windows separators) — a malicious name can only ever
88
+ resolve to a basename inside the chosen output directory.
89
+ - **Refuses path traversal**: the resolved path is verified to stay within its
90
+ parent directory, otherwise the write is rejected.
91
+ - **Neutralizes dangerous names**: control characters and Windows-illegal
92
+ characters are removed; reserved device names (`CON`, `NUL`, `COM1`, …) are
93
+ prefixed.
94
+ - **Never overwrites without confirmation**: if the target exists you are asked;
95
+ declining writes to `name (1).ext`, `name (2).ext`, … instead.
96
+
97
+ See [`test/paths.test.js`](test/paths.test.js) for the enforced behaviors.
98
+
99
+ ---
100
+
101
+ ## Availability, expiry, and cancellation
102
+
103
+ - Transfers **expire** server-side after their TTL; the CLI reports expiry
104
+ cleanly on both sides rather than hanging.
105
+ - **Either side can cancel.** The sender's `Ctrl-C` (or empty handshake input)
106
+ calls `DELETE /upload/:code`; the receiver's `Ctrl-C` calls
107
+ `DELETE /handshake/:code`. Both delete the encrypted server copy.
108
+ - The server deletes the stored ciphertext the instant the download completes,
109
+ so each code is **single-use**.
110
+
111
+ ---
112
+
113
+ ## Validation and limits
114
+
115
+ - File size is checked against the server limit (1024 MB) **before** upload, and
116
+ the server re-checks on finalize.
117
+ - Network errors are translated into actionable messages (connection refused,
118
+ DNS failure, TLS problems, timeouts) instead of raw stack traces.
119
+
120
+ ---
121
+
122
+ ## Threat model — out of scope
123
+
124
+ SafeDrop does **not** defend against:
125
+
126
+ - A **compromised endpoint** (malware on the sender's or receiver's machine).
127
+ Plaintext exists there by necessity.
128
+ - The sender **sending the code to the wrong person**, or an attacker who
129
+ obtains the code before the legitimate receiver (use `--secure` to detect a
130
+ key swap; protect the channel you share the code over).
131
+ - **Traffic analysis** (the server learns file size and timing).
132
+ - **Denial of service** against the server.
133
+
134
+ Because the share code contains the key, **anyone who obtains the code can
135
+ decrypt the file** until the transfer is downloaded, cancelled, or expires.
136
+ Treat the code like a password and prefer ephemeral, trusted channels.
137
+
138
+ ---
139
+
140
+ ## Reporting
141
+
142
+ This CLI is open source and dependency-free for auditability. Please report
143
+ suspected vulnerabilities privately to the SafeDrop maintainers rather than
144
+ opening a public issue.
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "safedrop-cli",
3
+ "version": "1.0.0",
4
+ "description": "Zero-knowledge, end-to-end encrypted file transfer from your terminal. Fully compatible with the SafeDrop web app.",
5
+ "type": "module",
6
+ "bin": {
7
+ "safedrop": "src/cli.js"
8
+ },
9
+ "main": "src/index.js",
10
+ "exports": {
11
+ ".": "./src/index.js",
12
+ "./crypto": "./src/crypto.js",
13
+ "./code": "./src/code.js"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "README.md",
18
+ "API.md",
19
+ "SECURITY.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "test": "node --test test/",
24
+ "start": "node src/cli.js"
25
+ },
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "keywords": [
30
+ "safedrop",
31
+ "encryption",
32
+ "aes-256-gcm",
33
+ "zero-knowledge",
34
+ "file-transfer",
35
+ "e2ee",
36
+ "cli",
37
+ "secure"
38
+ ],
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/safedrop/safedrop-cli.git"
43
+ },
44
+ "dependencies": {}
45
+ }