Secreta Manual, as shipped in the download
Secreta — User Manual
One-time, burn-after-reading links, on your own server. Version 3.1.4 [src: app/controllers/api.php:30].
About this manual
Every statement here was written by reading Secreta's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder this docs directory sits in. If a sentence disagrees with the application, the application is right and this manual has a bug — the markers exist so you can check rather than take our word for it.
Nothing is described that the code does not do. Where something is deliberately limited, that is said plainly rather than left out — which matters more here than in most products, because the subject is confidentiality.
The zero-knowledge contract
This is the whole product, and it is worth reading carefully.
The decryption key is generated in the browser and travels only in the URL fragment — the part after #, which browsers never send to the server. The server therefore cannot decrypt a secret; it only ever handles ciphertext [src: app/src/Secret.php:7].
That is where the confidentiality comes from. Not from the hosting, not from the database, not from trusting the operator.
The second layer, and what it is not
The server also encrypts the stored ciphertext at rest, under a key local to the installation [src: app/src/Secret.php:12].
The source is careful to say what that is for and what it is not: it protects a stolen database file, but it is not the source of confidentiality — the browser key the server never sees is [src: app/src/Secret.php:13].
Being clear about that distinction matters. An installation with at-rest encryption but a compromised server would still be safe, because the key was never there to steal.
The same primitive on both sides
The server's AES-GCM helpers use exactly the primitive the browser does — AES-256-GCM with the 16-byte tag appended to the ciphertext — so a blob produced by either side decrypts on the other [src: app/src/Secret.php:17].
Links
A share token is 18 random bytes rendered URL-safe, about 144 bits of entropy [src: app/src/Secret.php:26]. It is meant to be unguessable rather than memorable.
A new link can be shown as a QR code for handing a secret across a desk. The code is drawn in the creator's browser by the bundled qr.js from the link itself, key included, and nothing about it is sent to the server [src: app/assets/secreta.js:213]. Anyone who can photograph the screen can read it, exactly as with the link.
Ciphertext larger than roughly 192 KB of plaintext is refused [src: app/src/Secret.php:28]. The nonce must be exactly 12 bytes — anything else did not come from this product's own client [src: app/src/Secret.php:30] — and a blob shorter than the GCM tag plus one byte cannot be real [src: app/src/Secret.php:32].
Expiry and view limits
Expiry is chosen from a fixed set of presets: never, 1 hour, 6 hours, 1 day, 3 days, 1 week, 30 days [src: app/src/Secret.php:75]. Anything else falls back to a day [src: app/src/Secret.php:97].
A secret burns once its view count reaches its limit, at which point the ciphertext must be destroyed [src: app/src/Secret.php:146].
When ciphertext is deleted it is first overwritten with random noise of the same length [src: app/src/Secret.php:259].
The install's policy, and the trap inside it
An administrator sets ceilings — a maximum lifetime, a maximum number of views, and a view count above which a passphrase becomes mandatory. Every creator is clamped to them [src: app/src/Secret.php:93].
⚠️ Zero means opposite things on the two sides
The source flags this as "the zero trap", and it is worth understanding because it is the kind of bug that would be invisible [src: app/src/Secret.php:85]:
In a request, 0 means "never expires". In a policy, 0 means "no ceiling". Opposite ends of the same scale, spelled the same way.
So an unlimited request under a finite policy has to become the policy's ceiling rather than sailing through as the smaller number. Getting it backwards would make "never" the one lifetime that escaped the policy entirely [src: app/src/Secret.php:88].
The implementation says so explicitly: never (0) is unlimited, so it always exceeds a finite ceiling [src: app/src/Secret.php:102]. A clamped expiry becomes the largest offered preset within the ceiling, and that function never returns "never" [src: app/src/Secret.php:130].
Clamping versus refusing
A lifetime or a view count over the ceiling is quietly clamped, and you are told which fields were adjusted [src: app/src/Secret.php:93].
A missing mandatory passphrase is refused, not downgraded [src: app/src/Secret.php:116], and the reasoning is good: quietly turning a five-view link into a one-view link would strand four recipients with a dead link [src: app/src/Secret.php:117].
What is logged
Metadata only. The audit entry holds a secret id, an action, a hashed address and a timestamp — and the source states that no field in the array or the table could ever hold secret content [src: app/src/Secret.php:247].
Actions are limited to a fixed set, and an unknown one throws rather than being recorded [src: app/src/Secret.php:34].
The raw IP address is never stored. It is hashed one-way with a per-installation salt [src: app/src/Secret.php:211].
The stored row is likewise ciphertext and metadata only — never plaintext, never the client key, never the passphrase [src: app/src/Secret.php:221].
Who can do what
Creating a secret is a public action by design, so roles do not govern it [src: app/src/Perms.php:5]. What roles govern is the administrative surface, and the source names the asset as the installation's own exposure [src: app/src/Perms.php:6]:
require_login_to_createdecides whether anyone on the internet may store blobs on your server. Turning it off turns a team tool into an open relay [src: app/src/Perms.php:8].- The sharing policy is the ceiling every creator is clamped to; raising it silently lengthens the life of every secret made afterwards [src: app/src/Perms.php:10].
- "Reset all data" destroys every outstanding secret, colleagues mid-delivery included [src: app/src/Perms.php:12].
When the setting is on and nobody is signed in, creation is locked [src: app/controllers/app.php:34].
This table was produced by executing the product's own permission function against every permission the routes ask for.
| Permission | viewer | member | admin |
|---|---|---|---|
log.view | yes | yes | yes |
secrets.create | — | yes | yes |
secrets.burn | — | yes | yes |
settings.view / settings.manage | — | — | yes |
team.view / team.manage | — | — | yes |
audit.view | — | — | yes |
Note that log.view — the metadata log — is available to a viewer, while audit.view is not. Grants below admin are enumerated rather than wildcarded [src: app/src/Perms.php:18].
⚠️ Backups follow the model here
Worth stating because several products in this family do not: Secreta's database backups are permission-gated, requiring the settings permission rather than a bare sign-in [src: app/controllers/api.php:177]. A viewer cannot download the database.
The API and agent access
Only two tools are exposed to an agent [src: app/controllers/v3.php:78]: creating a secret, and statistics.
There is deliberately no tool that reads a secret, and the reason given is exactly right: reading burns it, and an agent consuming the recipient's view would destroy the delivery [src: app/controllers/v3.php:75].
An agent creating a secret must do the encryption itself. The endpoint accepts ciphertext only — AES-256-GCM, base64 of ciphertext with the 16-byte tag, plus the base64 12-byte nonce — and the agent appends # and its own base64url key to the returned share URL [src: app/controllers/v3.php:73].
That is the zero-knowledge contract extended to automation rather than quietly suspended for it.
What Secreta does not do
It cannot recover a secret for you [src: app/src/Secret.php:7]. The server never had the key. If the link is lost, the secret is gone, and no administrator can help.
It does not read secrets on an agent's behalf [src: app/controllers/v3.php:75], because reading is destructive.
It does not store your IP address [src: app/src/Secret.php:211], your plaintext, your key or your passphrase [src: app/src/Secret.php:221].
It does not protect you from a compromised recipient. Once somebody has opened the link, what they do with the contents is outside the product entirely.
At-rest encryption is not the guarantee [src: app/src/Secret.php:13]. It protects a stolen database file. The browser key the server never sees is what protects the secret.