Secure Send shipped on 21 August and nothing outside the code caught up. The roadmap still listed it under Deferred as "tabled", and gave as the reason that the tools plugin is browser-only by design with get_blueprint and get_models both empty - which stopped being true in the same day's commits that gave it a blueprint, a table, a migration chain, a permission and a setting. A reader following the roadmap would have concluded the feature does not exist. The generated API docs were fine: api-inventory.json and openapi.json both carry /api/tools/secrets, so the contract gates did their job. What was missing was every word written for a person, and that is the half where the security properties live. docs/SECURE-SEND.md is the new page. The parts worth having in prose rather than inferring from the code: that a lost secret is lost to everybody including an administrator, which is the same property as "a compromised database yields nothing" seen from the other side; that the 4096-character limit is the entire server-side enforcement, because a server that cannot read what it holds cannot have an opinion about content, so raising it turns a password tool into an unaudited file transfer; that redeeming is a POST because preview prefetch in chat and mail clients would otherwise spend a one-view secret before the recipient ever sees it; and that somebody should send a link to themselves through their own mail or chat client and confirm the key survives after the # before a real credential goes through it, because a product that rewrites links can strip the fragment and nothing on this side can help. securesend_allow_guests was in the database, on a settings page, and in no document. It is now in CONFIG.md with what it actually does - an unauthenticated write endpoint, rate limited per address, failing closed - rather than only its one-line description. Also corrected along the way: site_base_url was documented as scheme + host while the code deliberately tolerates a subpath in it, and the bundled plugin table in PLUGINS.md had no row for tools at all. Still missing after this: PLUGINS.md has no row for backups either.
156 lines
6.8 KiB
Markdown
156 lines
6.8 KiB
Markdown
# Secure Send
|
|
|
|
Hand somebody a password without leaving it in a chat window.
|
|
|
|
Secure Send makes a link that carries one credential, dies when it is opened,
|
|
and expires on its own if nobody opens it. It lives in **Tech Tools** at
|
|
`/tools/secure-send`, and the person receiving it opens `/s/<token>`.
|
|
|
|
It is the one tool in that section that is not purely browser-side, because a
|
|
link that expires and dies on first read needs somewhere to keep the ciphertext
|
|
until it is claimed.
|
|
|
|
## What the server can and cannot see
|
|
|
|
The secret is encrypted **in the sender's browser** with AES-GCM, and the key
|
|
travels in the **URL fragment** - the part after the `#`. Browsers never send a
|
|
fragment to a server, so the key reaches the recipient's browser and nowhere
|
|
else: not this server, not its logs, not a proxy in between.
|
|
|
|
What the `securesendsecrets` table holds is a random token, base64 ciphertext,
|
|
an IV, and the rules for the row's life. Reading the database yields nothing.
|
|
|
|
That cuts both ways, and it is worth being explicit about it:
|
|
|
|
- **Nobody can recover a secret that goes missing.** Not an administrator, not
|
|
the database owner. If the link is lost, the secret is lost. Create another.
|
|
- **The server cannot tell a password from a page of a drawing.** It cannot
|
|
look at what it holds, so it cannot enforce a rule about content. The only
|
|
lever it has is SIZE, which is the next section.
|
|
- **The audit trail records the fact, never the content.** Who created a
|
|
secret, when, and when it was read are all recorded. What was in it is not
|
|
recoverable by anyone.
|
|
|
|
## This is for credentials, not documents
|
|
|
|
The ciphertext limit is **4096 characters** (about 2800 characters of plaintext,
|
|
which the create page enforces first with a friendlier message). That limit is
|
|
not a tuning knob. In a design where the server cannot read what it stores,
|
|
size is the entire server-side control over what this feature is for, and
|
|
raising it quietly turns a password tool into an unaudited document transfer.
|
|
|
|
Everything else the create page does - counting lines, spotting file
|
|
signatures, refusing a pasted file - runs in the browser. It guides an honest
|
|
person and is bypassable by anyone willing to POST directly. The size limit is
|
|
the part that holds.
|
|
|
|
If somebody needs to move a file, the answer is a different tool, not a bigger
|
|
number here.
|
|
|
|
## Who may create, who may read
|
|
|
|
**Two different doors, on purpose.**
|
|
|
|
**Creating** normally needs a login plus the `tools.secrets` permission (admins
|
|
hold it by default; grant it to other roles from Settings > Users & Roles). The
|
|
login is not a formality: anonymous creation would make this an unattributable
|
|
write endpoint that stores arbitrary ciphertext, and the creator is the only
|
|
person these rows can ever name.
|
|
|
|
**Reading needs neither an account nor a permission.** The recipient usually has
|
|
no account here, and the link is normally handed over on a call or in a chat.
|
|
The unguessable token IS the authorisation, which is why it is 128 bits of
|
|
randomness and why the row is spent on the first successful read.
|
|
|
|
Making the create page reachable is not the same as allowing the action. The
|
|
page is served without a login so a guest at a site that allows guest creation
|
|
is not stopped by a sign-in wall; the server still decides.
|
|
|
|
### Guest creation
|
|
|
|
A site may open creation to people with no account with the
|
|
`securesend_allow_guests` setting (Settings > Tools > Secure Send). It is
|
|
**off by default**, because a guest secret has no creator and the audit trail
|
|
can then say when and from where but never who.
|
|
|
|
When it is on, guest creation is rate limited to 10 links per address per 10
|
|
minutes. That is not a security boundary - a determined person has more
|
|
addresses - but it stops an open endpoint being an easy place to park data or
|
|
fill a disk.
|
|
|
|
The setting is read straight from the settings row rather than the cache, and
|
|
**fails closed**: if it cannot be read at all, the answer is no. An error must
|
|
never be the thing that opens an unauthenticated write endpoint.
|
|
|
|
Receiving a secret never needs an account either way.
|
|
|
|
## Lifetime
|
|
|
|
| Control | Default | Range |
|
|
|---------|---------|-------|
|
|
| Expiry | 24 hours | 1 to 168 hours |
|
|
| Views | 1 | 1 to 100 |
|
|
|
|
Both apply, whichever comes first. A spent row is deleted outright, including
|
|
its ciphertext.
|
|
|
|
Expired secrets are swept opportunistically - whenever anybody creates a secret,
|
|
and whenever a dead link is clicked - rather than on a schedule. A secret nobody
|
|
ever opens would otherwise be removed by nothing, and an air-gapped site is not
|
|
going to have a cron job anybody remembers to add.
|
|
|
|
Missing, expired and spent all answer identically. Distinguishing them would
|
|
confirm to anyone holding a guessed token whether it ever named anything.
|
|
|
|
## Redeeming is a POST
|
|
|
|
`POST /api/tools/secrets/<token>` hands over the ciphertext and spends the view.
|
|
A GET would be wrong here: chat and mail clients prefetch links to build
|
|
previews, and a one-view secret consumed by a preview window is a secret the
|
|
recipient never gets to see.
|
|
|
|
There is a `GET /api/tools/secrets/<token>` as well, but it reports availability
|
|
only and never spends a view. The read page asks it first so it can say "this
|
|
secret is waiting, and opening it will destroy it" instead of consuming the only
|
|
view to find out whether there was anything there.
|
|
|
|
## Before anybody relies on it
|
|
|
|
**Send a link to yourself through whatever your site uses to pass it on -
|
|
Teams, Outlook, a chat client - and check the key is still there after the `#`.**
|
|
|
|
Some mail and chat security products rewrite links. If one strips or mangles the
|
|
fragment, the secret is unrecoverable and the server cannot help, which is the
|
|
whole point of the design. Find that out on a test secret rather than on a
|
|
production credential.
|
|
|
|
## Getting the link right
|
|
|
|
The link is built from the site's configured `site_base_url`, not from the
|
|
address the sender's browser happens to be using. Somebody creating a secret may
|
|
have reached the app by an IP, a short hostname, or a name that only resolves
|
|
inside one VLAN, while the recipient has to open the link from wherever they
|
|
are. Blank falls back to the browsing origin.
|
|
|
|
A site served under a subpath (`/shopdb`, `/ops`) needs that subpath reachable
|
|
in the link. Setting it as part of `site_base_url` works, and so does leaving it
|
|
out; the mount is added only when it is not there already.
|
|
|
|
## Where it lives
|
|
|
|
| Piece | Path |
|
|
|-------|------|
|
|
| Plugin | `plugins/tools/` |
|
|
| Table | `securesendsecrets` |
|
|
| API | `POST /api/tools/secrets`, `POST` and `GET /api/tools/secrets/<token>` |
|
|
| Create page | `/tools/secure-send` |
|
|
| Read page | `/s/<token>` |
|
|
| Setting | `securesend_allow_guests` (public, boolean, category `tools`) |
|
|
| Permission | `tools.secrets` |
|
|
|
|
## See also
|
|
|
|
- [CONFIG.md](CONFIG.md) - the `tools` settings section
|
|
- [PLUGINS.md](PLUGINS.md) - the bundled plugin set
|
|
- [API-REFERENCE.md](API-REFERENCE.md) - the response envelope these routes use
|