In short
ResponderHTTP is a local desktop app. It has no account, no server and no telemetry, so there is nothing of yours held anywhere but your own machine. The security work goes into three places: what is written to disk, what goes over the network, and what the app’s own window is allowed to do.
-
Secrets are encrypted at rest with AES-256-GCM, under a key kept in the operating system’s credential store, never in the database beside them
-
TLS certificates are verified by default against the operating system’s trusted roots. Turning verification off is a visible setting on one request, never a global switch
-
History, saved responses, logs and exports keep no secrets. The rules are enforced in the Rust core, not only in the interface
-
Only the requests you send leave the machine. The app makes no update checks, no analytics calls and no crash reports
-
The engine is memory-safe Rust with libcurl, TLS and SQLite compiled into the binary, so there is no system curl to call and no system SSL library to go stale
Secrets at rest
Everything you create lives in one SQLite database in your user profile. Most of it is not sensitive: URLs, header names, collection trees. A few fields are, and those are encrypted before they are written.
What counts as a secret
-
The credential in a request’s Auth tab, for HTTP and gRPC requests: the Basic password, the Bearer token, the API key value, or a custom scheme’s value. The username and the key’s name are not secrets: they say how to authenticate, not with what
-
Environment variables you mark as secret. They are masked in the editor as well as encrypted
Header values, cookies in the cookie jar and ordinary variables are stored as plain text. Put credentials in the Auth tab or in a secret variable, and refer to it as {{name}} wherever it is needed.
How they are encrypted
-
One data key: 32 random bytes from the operating system’s random source (through aws-lc), created the first time the app starts. It is held in the credential store, never in the database, and the app wipes its copy from memory when it is no longer needed
-
AES-256-GCM through aws-lc’s randomized-nonce key, so a fresh 12-byte nonce is drawn for every value and no code in the app ever picks one
-
Each value is bound to where it lives. The table, the row id and the field name are the cipher’s associated data, so a value copied into another row or field fails to decrypt instead of quietly becoming that row’s secret
-
Each value names its key. A 16-byte key id is stored with every value, so a value sealed under a key that has since gone is recognised as such rather than tried and misread
-
Deleted data is overwritten. SQLite’s secure_delete is on, so a secret that is blanked or re-encrypted does not linger in the file’s free pages
The stored form of one value:
version (1 byte) | key id (16) | nonce (12) | ciphertext | tag (16)
A copied, synced or backed-up database therefore carries no readable secrets. On another machine, or after the key is gone, everything else opens normally and each secret field loads empty, marked as needing to be entered again. A missing secret never stops a request, a collection or the app from loading.
Where the key lives
-
Windows: one generic credential in Windows Credential Manager, named data-key@io.github.zoran-php.responderhttp, owned by your Windows account
-
Linux: one item in your desktop keyring through the Secret Service, which is GNOME Keyring on Fedora Workstation or KWallet on KDE. It is labelled “ResponderHTTP data key”, and secret-tool lookup service io.github.zoran-php.responderhttp username data-key finds it. If the keyring is locked, the desktop asks you to unlock it
-
Linux, Flatpak : the key is kept in an encrypted keyring file inside the app’s sandbox, whose own key comes from the desktop through the Secret portal
There is no plain-text fallback. If the credential store cannot be reached, for example on a Linux session with no keyring running, the app runs normally but refuses to save secrets for that session and says so. It never writes one unencrypted instead.
A key is only created when the store says there is none. Any other failure, such as access denied or a store that is not responding, leaves the session without a key rather than replacing it. A replacement would make every secret sealed under the old key unreadable, and the old key may be back after a restart.
What never keeps a secret
-
History and saved responses. A literal secret is blanked before the entry is stored. A value that is only a {{placeholder}} is kept, because it names a secret rather than containing one, and it lets a re-run work without typing it again. Saved response snapshots substitute non-secret variables only
-
The log. The app never logs request or response bodies, auth headers, tokens, cookies, WebSocket or gRPC messages, gRPC metadata or stream events. Every log line from Rust and from the interface also passes one redactor, which blanks credential-like query parameters (token, api_key, password and the like), Bearer, Basic and Digest credentials, and cookie headers. The redactor is a safety net: the rule is not to log them in the first place
-
The WebSocket and gRPC message logs. A message you typed with a secret variable is shown with the placeholder, not the resolved value
-
OpenAPI export. Credential-like query parameter values and Authorization, Cookie and Proxy-Authorization headers are left out of the exported file, at most a security scheme naming them is written, never the value, and the export lists what it left out, so a spec can be committed or shared without carrying a token
-
Copy as cURL produces text for you to paste; the app never runs it
TLS and the network
-
Verification is on by default for HTTP, WebSocket and gRPC alike: the certificate chain and the host name are both checked. Turning it off is a per-request setting you can see, meant for a local test server, and it applies to that request only
-
Trusted roots come from the operating system: the Windows certificate store, or /etc/pki on Fedora. A root your organisation installs is trusted, and a root the OS distrusts is not, without the app shipping its own bundle to keep up to date
-
TLS is rustls, a TLS implementation in Rust, compiled into the binary. It offers TLS 1.2 and 1.3 only; a request can raise its minimum to 1.3
-
Credentials stay on their host. When a redirect crosses to another host, the Authorization header is dropped unless you explicitly allow it for that request, which is how a token would otherwise reach a server that was never meant to see it
-
Cookies follow RFC 6265: each is sent only to the domain, path and scheme it was set for, and a Secure cookie never goes over plain HTTP
-
Nothing else goes out. The only network traffic is the requests you send. There is no update check, no analytics and no crash reporting
The app itself
The window
The interface is a web page drawn by the system’s webview: WebView2 on Windows, WebKitGTK on Linux. The page itself is bundled into the app and never loaded from the internet. Because a script running in that page can call the app’s commands, it is treated as a privileged place, and content from outside is never rendered in it as HTML.
-
Response bodies are text. An HTML, SVG or JavaScript response is shown in the code editor with syntax highlighting, never rendered or run
-
Markdown documentation is sanitised against an explicit allowlist, because it may come from an imported OpenAPI file written by someone else. Scripts, styles, frames, embeds and forms are removed, links become inert text you can copy, and images do not load, so a remote image cannot act as a tracking beacon
-
The window can reach only the app’s own commands and the logger. No shell, file-system, HTTP or opener plugin is exposed to it. The engine reads only files you chose, such as a multipart attachment or a file you import
The code
-
Rust and memory safety. unsafe code is forbidden everywhere except one file of hand-written libcurl WebSocket bindings. Each block there carries a written safety argument, and the compiler rejects unsafe in any other file
-
No SQL injection. Every query is parameterised, and SQL lives only in the persistence layer
-
No shelling out. libcurl runs inside the process. The app never starts a curl program or any other process to make a request
-
Checked builds. Every release build is inspected for the libraries it links. The check fails if libcurl, OpenSSL or SQLite appears as a system library rather than compiled in, and on Linux it allows only the desktop’s own libraries: WebKitGTK, GTK, glib, D-Bus, zlib and the C runtime
-
Pinned dependencies. Every Rust and JavaScript dependency is locked to an exact version in the repository’s lock files
-
Bounded input. Streams, message logs and imported schema files have size limits, so a hostile or runaway server cannot fill memory without bound
What it does not protect against
Honest limits, so you can judge the rest:
-
Software running as you. Anything running under your own account can ask the credential store for the key, as the app does. This is true of every app that uses the OS credential store, including browsers’ saved passwords
-
Secrets in memory. Encryption is for data at rest. While the app runs, a secret you are editing or sending is decrypted in memory and passes between the interface and the engine
-
Plain-text fields. Header values, cookies and variables not marked secret are stored unencrypted. So are URLs, which is why an API key belongs in the Auth tab rather than typed into the query string
-
A missing Content Security Policy. The webview does not yet set one. Its defence today is that it renders no outside content as HTML, as described above. Adding one is an open item
-
Unsigned direct downloads. The Windows NSIS and MSI installers and the Linux RPM, AppImage and Flatpak bundle are not signed yet, so Windows SmartScreen warns about the installers and dnf cannot check the RPM’s signature. Get them only from the project’s own release page. Packages from the Microsoft Store, and from Flathub once listed there, are signed by the store
Removing everything
-
Windows, installer: uninstalling with “Delete the application data” ticked removes the database, the logs and the Credential Manager entry together. An update never removes the key
-
Windows, Microsoft Store: uninstalling removes the database and logs, but a Store uninstall runs no clean-up of its own, so the Credential Manager entry stays. Remove it with cmdkey /delete:data-key@io.github.zoran-php.responderhttp
-
Linux, RPM: sudo dnf remove responder-http removes the program. Your data stays in your home folder: delete ~/.local/share/io.github.zoran-php.responderhttp/ and remove the key with secret-tool clear service io.github.zoran-php.responderhttp username data-key
Deleting only the database is also safe: the key alone decrypts nothing, and a fresh database starts empty.
Reporting a vulnerability
If you find a security problem, please email it rather than opening a public issue, with the version, the operating system and the steps to reproduce it, and allow time for a fix to be released before describing the problem in public.