Ownware
Home›Secreta›Manual
Secreta · Manual

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_create decides 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.

Permissionviewermemberadmin
log.viewyesyesyes
secrets.create—yesyes
secrets.burn—yesyes
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.

← Back to Secreta · Quickstart · API · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →