Termiyo

Security

How the vault works

No hand-waving: here are the actual constructions, the parameters, and why each one was chosen.

What we are defending against

Three situations shape every decision below. A sync server that stores your data and should never be able to read it. A stolen laptop, where the database file is in someone else’s hands. And an attacker with that file offline, running guesses against it as fast as hardware allows.

The third is the one that decides the key derivation, because it is the only one where the attacker’s budget, rather than your password, sets the pace.

A second factor, and what it is for

Two-factor sign-in guards the account, not the vault, and the difference matters more than it sounds. A stolen or guessed password is no longer enough to sign a new device in and pull down your sealed keys — the encrypted bundle and, because the same password derives the key that opens it, everything inside. That is the attack a code actually stops.

It is still enough to open a vault on a machine somebody already has. The vault is encrypted here, on your own disk, with no server in the loop to ask anything — so there is nothing there for a code to guard. Two-factor also does not sign out devices that are already signed in, and it does not make a forgotten password recoverable.

Turning it on shows you ten recovery codes, once. Those, and any machine still signed in — which can turn the factor off with your password — are the two ways back if your authenticator is gone. There is no third: nobody here can wave you past a factor, because there is nothing on our side that could.

Argon2id, not a fast hash

Your master password is stretched into a 32-byte key with Argon2id at 64 MiB of memory and three passes — libsodium’s `crypto_pwhash`, which runs Argon2id in a single lane. Memory is the point: a GPU can run enormous numbers of hash operations in parallel, but it cannot give each of them 64 MiB of fast memory. That turns a wide parallel attack back into a narrow one.

For comparison, a PBKDF2-SHA256 derivation at a thousand iterations — still a common default — costs an attacker under a millisecond per guess and parallelises almost perfectly. Argon2id at these parameters takes roughly 130 ms on a laptop and resists exactly that parallelism.

The parameters are recorded alongside the vault, so raising them later affects new derivations while existing vaults still open.

Every record sealed on its own

Each vault has its own random 32-byte key, wrapped under the master key. Records are encrypted with XChaCha20-Poly1305 using a fresh 24-byte nonce, and the record’s own identity — its type, its vault and its id — is bound in as associated data.

That last part matters more than it sounds. Without it, anyone holding the ciphertext could move a valid encrypted blob to a different record and the client would accept it. With it, a relabelled record simply fails to open.

Sharing without handing over a key

Each account has an X25519 keypair. Sharing a vault seals its key to the recipient’s public key, so a server can broker who has access to what while never holding a key that opens anything.

Revoking access deletes the sealed copy and stops the sync. It cannot reach what they already have: anyone who has already read the data has read it, and no system can undo that. Nor does revoking change the vault’s key — Termiyo cannot — so somebody who synced while they had Can edit still holds a key that opens whatever is saved in that vault later, wherever those records reach them; sharing the vault with them again, even as Read only, sends them at least its hosts. Changing the passwords and keys on the servers themselves is what takes a secret back.

Host keys, pinned

The first time Termiyo sees a server it shows you the key and its SHA-256 fingerprint, and pins what you accept. Every reconnect is checked against that pin.

When a key changes, Termiyo says so plainly rather than burying it: a rebuilt server and an intercepted connection look identical to software, and only you know which one you were expecting.

The connection itself

Termiyo’s SSH is pure JavaScript — the ssh2 library, a from-scratch implementation that drives Node’s own crypto, not a binding to the C libssh2. A buffer overflow in parsing a packet — the memory-safety class of flaw that visits SSH libraries written in C — is not a bug this code can have, because there is no such library in it to have it.

What the two ends agree to speak is a deliberate allow-list, narrower than the library’s own defaults. Key exchange is Curve25519, or an ECDH or finite-field group with a SHA-2 hash; the host key is Ed25519, ECDSA, or RSA with SHA-256 or SHA-512; the cipher is ChaCha20-Poly1305 or AES-GCM, with AES-CTR under an encrypt-then-MAC HMAC as the floor. No SHA-1 MAC, no CBC, no 3DES, no SHA-1 key exchange — a host that still needs one of those has to be allowed it for that host, which makes the weakening something you chose and can see rather than a silent fallback.

What the terminal will accept

The window that draws your terminal takes its instructions from the host, and two of those can reach past the screen. A program can set your clipboard (OSC 52): Termiyo writes what it is handed but never reads it back — the query form, which would copy your local clipboard to the host, is recognised only so that it can be refused — and a single write is capped at 100,000 bytes, so a host cannot stream megabytes at the terminal to make it allocate without bound. A malformed or empty payload is ignored, not treated as “clear the clipboard”.

A program can also print a clickable link (OSC 8). Every link the terminal offers — whether the host marked it as one or it is a bare URL on the line — leaves by a single gate that opens only http: and https:. A file: link, which could launch a local program, a javascript: URL, or an app’s own scheme that hands off to another program, is turned away before the request to open it is ever made.

Where the secrets actually live

Connections run in a separate process from the interface. The window that draws your terminal has no network access, no filesystem access, and never receives a password or a private key — it asks to connect to a saved host by name, and a different process decides what that means.

Anything shown in the interface is redacted first. Saving a record back with a masked field leaves the stored secret untouched.

Verifying what you downloaded

Two different checks, and they answer different questions. The SHA-256 beside each download on the download page tells you the bytes you have are the bytes we built: `shasum -a 256 <file>` on macOS and Linux, `certutil -hashfile <file> SHA256` on Windows.

The signature tells you who built them, and your operating system checks it for you before it will run the app. You can check it yourself first. On macOS: `spctl -a -t open --context context:primary-signature -vv <file>.dmg` should answer "accepted" with "source=Notarized Developer ID".

The Windows installer is unsigned, so Properties has no Digital Signatures tab. The missing tab does not tell you whether the file was altered. Save the installer without running it, then run `certutil -hashfile "<file>" SHA256`, replacing `<file>` with its saved path. Compare the result with the SHA-256 beside that exact download on the download page before opening the installer. If they differ, do not run it. A matching hash confirms that the bytes match our published file; it is not a publisher signature. Unlike the Linux case below, that is a gap of ours rather than a property of the platform — Windows checks signatures perfectly well, we have not yet given it one to check — and this page will go on saying so until the installer you download carries a signature you can check for yourself.

On Linux there is no equivalent — a .deb or an AppImage carries no signature your system checks, so the hash is the whole of what you can verify. That is a property of the platform rather than a choice of ours, and it is worth knowing rather than assuming otherwise.

What leaves your machine

Two things, and both are listed. Termiyo sends a short report about itself at most once every 24 hours — version, platform, processor, how it was installed, interface language, whether update checks are on, and a count of the crash dumps in its own folder — with no identifier attached and nothing from your sessions in it; it is on by default and one click turns it off, on the first run or in Settings → Help. A crash writes a dump into a folder on your own disk, and sending one of those to us is a separate switch that stays off until you turn it on; Settings → Help lists it, says what a dump can contain, and opens the folder.

Sending one to us is a switch on that screen. It is off on a fresh install, off after an update, and while it is off no dump leaves the machine — attaching one to a bug report by hand is still the only way one travels. Turn it on and, if “Check for updates when Termiyo starts” is on, the next crash uploads its dump over HTTPS, along with the name your own machine gave that file, the version, the platform, the processor architecture, and an id for that installation which is created at that first upload — and nothing else; the same screen lists what went and has a button that asks our server to delete it. This paragraph used to say crash reporting was absent rather than switched off, which was true of every build before the one that added the address. The privacy page has the detail. With update checking switched off, a dump you agreed to send waits on this machine and goes at the first launch after checking is back on.

With update checking switched off, the updater never loads: the library is imported on first use, so with checking off the module is never imported and has nothing to make a request with. That much is structural rather than a promise about a branch.

What we had wrong here, and are correcting: this said that with checking off a launch makes no outbound request at all, and offered tcpdump as the way to check it. A launch nobody touches is now silent with checking off — measured on the wire, on Linux — but opening the vault starts traffic that has nothing to do with the update setting, and this page used to name only part of it. The first is the terminals you left open: SSH tabs that were still open when Termiyo last closed are opened again as soon as the vault is, each one a full login to its host — that is what puts your work back where you left it, and a tab you close before quitting does not come back. Signed out, until you have answered the offer of an account, the first-run screen asks whether termiyo.com answers, so it knows whether offering you an account is worth doing — once per launch, at the first unlock; while termiyo.com cannot be reached the offer is not made, so the next launch asks again. Signed in, Termiyo asks whether your address has been confirmed — once per launch, and once more while it has not been, because the panel that then asks for your code reads the answer again — syncs when the vault opens and every five minutes after that, asks which of your vaults a host, a group or a proxy can go into when you open the form for one, and, if you have no AI key of your own, asks how much of your account’s AI allowance is left when you open the AI panel or start typing in a terminal. While the host list is open, every saved SSH or telnet host Termiyo can reach directly is checked every 15 seconds — a connection to its port, closed without a byte sent — so the dot beside it is current; those connections go to your servers, not to us, and show up in their logs. A host saved by name rather than by address also costs a DNS question each time, and that goes to whichever resolver your machine uses. Hiding the host list stops those checks (unless you have turned on “Alert when a host stops responding” in Settings → Terminal, which keeps them going for as long as the vault is open), but not one more of the same kind: while the tab in front is a terminal connected to one of those hosts, the status bar checks that host every 10 seconds for the latency it shows, list or no list, for as long as the bar reads “Connected” — and, for a host saved by name, asks your resolver about it each time too. And what you set up does what you set it up to do: a scheduled backup copies itself to the mirror you gave it, webhooks call their addresses, and scheduled tasks and forwards connect to their hosts — a scheduled task once per run, logging out again when its snippet is done. So the claim was wrong in the one form we said was worth making, the checkable one.

A terminal you share is a second place the session exists, and the second place is a browser tab. The bytes go from your machine to theirs and through nothing else: no relay carries them, which is a choice — a TURN server would see the traffic, so Termiyo does not use one, and our own server only introduces the two of you. What we do use is public STUN, Google’s and Cloudflare’s, to ask what your address looks like from outside; those two learn that address, and nothing else. The guest’s browser is given no STUN server at all. Then the cost: a guest is watching in a browser, so extensions can read that page and the browser may keep a picture of it for a tab preview. A guest starts read-only and cannot type until you say so, and the share stops when you stop it or after four hours. What it is not is the same protection the app itself gives, and the guest page says so on the guest’s own screen rather than only here.

What we cannot claim: the SQLite file itself is not encrypted, only the records inside it. Someone with the file learns how many hosts you have, when you last touched each one, and the labels you gave them. Their addresses, usernames, passwords and keys are encrypted. We would rather say that here than have you discover it.

Found something?

Security reports go to info@termiyo.com. We will confirm receipt within two working days.