Security model

The relay handles one thing per request: a base64 blob it cannot read. The key needed to read that blob is generated on your machine, delivered in a part of the URL that browsers never transmit, and used once.

XSalsa20-Poly1305 key in URL fragment only one-time no account, no escrow

The one-sentence version

What actually happens to your secret

The value is encrypted in the browser with a key generated on your machine. The key travels in the URL fragment, which browsers never send to a server. The server holds ciphertext until the CLI decrypts it locally and writes it to .env. The request is then dead.

What each party can see

Who observes what during a tunnelled request.
Party Can see Cannot see
The human's browser The plaintext they typed, and the key from the fragment. Nothing relevant — this is the trusted endpoint.
Your local CLI and server The plaintext, after decrypting it locally, and the random port the server bound to. —
The bore.pub relay The ciphertext blob and its size, the timing of the request, the ephemeral remote port, and the request path. The value, and the key. The relay never receives the fragment, so it has nothing to decrypt with.
A network observer That a plain HTTP request occurred, and its rough size. The value or the key. The payload is already encrypted before it leaves the browser.

The path is not a secret

PinchPass derives the request path from the secret's name: it lowercases the name, strips anything outside a–z, 0–9 and _, and joins multiple names with underscores. Asking for GEMINI_API_KEY produces /claim/gemini_api_key.

That path is guessable, and it is not a bearer token. Anyone who knows the name can find the endpoint while the request is live. This is a deliberate trade-off, and it is safe because the path is not what protects the value:

  • Submitting something requires producing ciphertext under a key you do not have.
  • A hostile or mistaken submission simply fails to decrypt, and the CLI reports decryption failed (tampered payload?).
  • The secret never travels in the path, the query string, or a header.

Treat the name as public — pick names that say what the credential is, because the human sees them on the form — and rely on the key to protect the value.

The crypto

Exact parameters, as implemented.
ElementValue
Cipher XSalsa20-Poly1305 (authenticated encryption)
Browser implementation TweetNaCl.js, embedded inline in the served page. No CDN, no external script, no WebCrypto requirement.
Go implementation golang.org/x/crypto/nacl/secretbox — the project's only dependency.
Key 32 random bytes from crypto/rand, base64url-encoded (43 characters), delivered in the URL fragment and nowhere else.
Nonce 24 bytes, from nacl.randomBytes(24) in the browser, fresh for every submission.
Payload The nonce concatenated with the secretbox output, base64-encoded into the POST body as value=….
Minimum accepted blob 41 bytes — 24-byte nonce, 16-byte MAC, at least 1 byte of ciphertext. Anything shorter is rejected with 400.
Multiple secrets One JSON object of name to value, encrypted as a single payload. The CLI rejects any key that was not requested.
Fragment validation The page refuses to submit unless the fragment matches ^#k=([A-Za-z0-9_-]{43})$, then disables the button and explains that the key is missing.

Why plain HTTP is acceptable

The payload is encrypted before it leaves the browser, so the wire carries ciphertext that neither the relay nor a network observer can open. That is why a plain http:// link is safe for the value itself, and why PinchPass does not require HTTPS, a certificate, or WebCrypto — it works on a LAN, on a bare bore.pub URL, and on a machine with an outdated TLS stack.

Be precise about what that buys you. HTTPS would add nothing for confidentiality of the value, but it would hide metadata from a network observer: that a request happened at all, its size, and its timing. Those are visible on plain HTTP. If that metadata matters to you, run the request over a channel you already trust.

One-time and time-limited

Confidentiality comes from the cipher. What limits the damage of a link that escapes is its lifetime:

What this does not protect against

Read this before trusting it with something irreplaceable

PinchPass moves one value, once, from a browser to a file. It is not a secrets manager, a KMS, or a vault, and it has no recovery story.

Verify it yourself

None of the above requires trusting this page. Every claim is checkable in a few minutes, against the running tool and the source:

  1. Run a request with -json and read the url field. Confirm the key appears after a #, and that the path is just your lowercased secret name:

    shell
    pinchpass request GEMINI_API_KEY -json | head -20
  2. While the request is live, fetch the form and read its source. Confirm TweetNaCl is embedded inline — there is no external script tag — and that the key is read from location.hash:

    shell
    curl -s http://127.0.0.1:<port>/claim/gemini_api_key | grep -c 'location.hash'
  3. Open the link, watch the network tab, and submit. The only request is a POST whose body is value= followed by base64. There is no header, query parameter, or field carrying your plaintext or the key.

  4. Send the same request again with curl, or reload the page. Both return 404 with the link-expired page, proving the request is spent.

  5. Start a request with -ttl 1, submit nothing, and wait. The process exits non-zero and .env is untouched.

  6. Read the code that does the work: server/handlers.go for the form and the submit handler, server/server.go for the path derivation and lifecycle, cmd/request.go for the key generation and local decryption, and tunnel/bore.go for the relay client. It is a small codebase, and the crypto is all in the standard library and one x/crypto package.

The full surface is listed in the CLI reference, and the source is at github.com/rubybear-lgtm/PinchPass.