Server Stash Security Whitepaper
Version 1.0 · May 2026
1. Summary
Server Stash is an end-to-end encrypted credentials vault. Users store IPs, passwords, sub-account details, command snippets, and other server-related secrets, organised into per-project workspaces. All sensitive data is encrypted in the user's browser before it reaches our servers, and is decrypted in the browser at view time.
This document describes the cryptographic design, the threat model the design is meant to defend against, and (equally importantly) the threats it is not designed to defend against. Honest scope is the only basis on which a security claim is meaningful.
The design rests on three properties:
- Two passwords, separated by purpose. An account password, used only to authenticate access to ciphertext blobs on our servers, is unrelated to the master password, used only to derive the encryption key. The two are independent secrets that the user sets at signup. The server only ever sees the account password.
- Encryption happens client-side. The master password and the encryption key derived from it never leave the user's device. The server stores opaque ciphertext.
- Authenticated encryption. Every record is encrypted with AES-256-GCM, which provides both confidentiality and integrity in a single primitive. A tampered ciphertext fails to decrypt rather than producing arbitrary plaintext.
If our database is dumped, an attacker obtains ciphertext blobs and per-user salts. To recover any plaintext, they would need to brute-force a user's master password against PBKDF2-SHA256 with 310,000 iterations. The per-guess cost makes large-scale offline attacks against well-chosen master passwords economically prohibitive.
2. Threat Model
2.1 What Server Stash defends against
- Database compromise (read-only). An attacker who gains read access to our storage tier, through SQL injection, an exposed backup, a stolen disk, or a rogue insider with database credentials, obtains only ciphertext. Without each user's master password, the data is not recoverable.
- Network observation. All traffic is served over TLS. An attacker on the network path between the user and our servers sees only encrypted transport carrying already-encrypted payloads.
- Server operator curiosity. The Server Stash operations team cannot read user data in the normal course of operating the service. The decryption key does not exist on our servers, in our logs, or in any backup we hold.
- Lawful access requests. Any subpoena or court order served on Server Stash can compel us only to produce the data we hold, which is ciphertext and metadata. We have no mechanism by which we can produce decrypted user data, because we do not possess the means to decrypt it.
2.2 What Server Stash does not defend against
These are limitations that any browser-based zero-knowledge product shares. We list them explicitly because pretending they do not exist would undermine the credibility of the claims that do hold.
- Active server compromise that modifies served code. Server Stash is delivered as JavaScript from our servers. A sufficiently privileged attacker, or Server Stash itself acting in bad faith, could replace the served JavaScript with a version that exfiltrates the master password the moment the user types it. This is the canonical limitation of in-browser end-to-end encryption. Mitigations are described in §6.
- A compromised endpoint. If the user's device is infected with malware capable of reading the browser's memory, observing keystrokes, or exfiltrating clipboard contents, no server-side cryptography can protect them. Endpoint security is the user's responsibility.
- A weak master password. PBKDF2 raises the cost of brute force per guess, but it cannot protect a master password that is in a leaked credential dump or guessable from the user's other public information. The strength of the master password is the strength of the encryption.
- Loss of the master password. Because we cannot decrypt data, we cannot help recover it. There is no reset path. This is a deliberate property, not a missing feature.
- Account password compromise. An attacker who obtains the user's account password, through phishing, password reuse against another breached service, or credential stuffing, gains access to retrieve the user's ciphertext blobs from our servers. They still cannot read them without the master password. They can, however, delete data they cannot read, so account password hygiene matters.
3. Cryptographic Design
3.1 The two-password separation
At signup, the user sets two passwords:
- Account password. Sent to our server during login, where it is verified against a stored bcrypt hash. This password gates access to ciphertext blobs. The server stores its bcrypt hash; the plaintext is held in memory only for the duration of the login request.
- Master password. Used in the browser to derive the AES-GCM encryption key. Never transmitted. Never stored. If the user closes the browser tab, the master password and derived key cease to exist; the user is prompted to re-enter the master password the next time they open the app.
These two values are unrelated. Compromise of the account password does not yield the master password, and vice versa. The server has no record of the master password in any form.
3.2 Key derivation
The encryption key is derived from the master password using PBKDF2-SHA256 with the following parameters:
| Parameter | Value |
|---|---|
| Algorithm | PBKDF2-HMAC-SHA256 |
| Iterations | 310,000 |
| Salt | 16 random bytes per user |
| Output | 256 bits |
| Output use | AES-256-GCM key |
The salt is generated once per user with crypto.getRandomValues and stored alongside the user's encrypted data. A salt is not secret. Its purpose is to ensure that two users with the same master password derive different keys, and that a precomputed rainbow table cannot be reused across users.
The iteration count of 310,000 follows OWASP's 2023 recommendation for PBKDF2-SHA256 in interactive contexts. We are aware that OWASP's 2024 update raises the recommendation to 600,000, and we plan to migrate as part of the envelope versioning scheme described in §7. PBKDF2-SHA256 at 310k still provides a meaningful per-guess cost: on commodity GPU attack hardware, a single guess takes on the order of tens of milliseconds, making attacks against passphrases of reasonable entropy infeasible at scale.
We chose PBKDF2 over Argon2id for one reason only: PBKDF2 is natively supported by the Web Crypto API. Argon2id offers stronger guarantees against custom hardware attackers and is on our roadmap. Both choices are defensible; ours is the conservative one given the deployment surface (every modern browser without WebAssembly polyfills).
3.3 Encryption
Each record is encrypted with AES-256-GCM:
- Algorithm. AES-256 in Galois/Counter Mode. Provides confidentiality and authenticated integrity in a single primitive.
- IV. 12 bytes (96 bits), generated fresh per encryption with crypto.getRandomValues. Random 96-bit IVs are safe for AES-GCM up to approximately 2^32 encryptions under a single key, far beyond any per-user volume.
- Auth tag. 128 bits, appended to the ciphertext by the Web Crypto API. Decryption fails if the tag does not match.
- Plaintext. UTF-8 encoded record content.
3.4 Storage envelope
A stored record has the form:
salt : iv : ciphertext+tag
Each component is base64-encoded. The salt is included in every record envelope rather than stored separately. This is a deliberate redundancy: it means a record can be decrypted given only the master password and the envelope itself, with no dependency on a separate user-record lookup. The cost is roughly 24 bytes per record.
Versioning of the envelope is on our near-term roadmap (see §7). Until that is in place, the format is implicit v1.
4. Authentication Flow
The login flow has two distinct phases. We separate them deliberately.
4.1 Phase 1: Account authentication
- User submits username and account password to a standard HTTPS login endpoint.
- Server verifies the account password against the stored bcrypt hash.
- On success, the server issues a session cookie and returns the user's encrypted blobs.
This phase is conventional. The server sees the account password during the request, hashes it, and discards the plaintext. The account password authenticates the user to retrieve their ciphertext; it has no role in decrypting it.
4.2 Phase 2: Local decryption
- The browser receives the encrypted blobs.
- The browser prompts the user for their master password. This is a separate, client-side prompt, never submitted as a form to our server.
- The browser extracts the salt from a record envelope, runs PBKDF2 to derive the key, and decrypts the records.
- The derived key is held in memory as a non-extractable CryptoKey (extractable: false in the Web Crypto API), preventing JavaScript code, including our own, from reading the raw key bytes back out.
- On tab close or sign-out, all key material is dropped.
The two phases are conceptually and practically independent. The master password never crosses any network boundary. The encryption key is derived in, used in, and discarded from a single browser session.
5. The Served-JavaScript Question
This section addresses head-on the most legitimate critique of any browser-based zero-knowledge system.
A user's master password is typed into a form whose code is delivered by Server Stash. In principle, that code could be modified, by us, by a compromised member of our team, by an attacker with deploy access, or by court order, to log the master password before using it. If that happened, the attacker could derive the encryption key and read all of that user's data. This is a real concern, and one we cannot eliminate purely through cryptography.
We treat it as a defence-in-depth problem, not a single-bullet one.
- Restrictive browser security policy. The site declares to the browser that it will not load scripts from third-party origins, will not execute inline scripts, and will not use dynamic code evaluation. Injection of arbitrary script via XSS is therefore materially harder than it would be on a typical web app.
- No third-party JavaScript. No analytics SDKs, no error trackers, no chat widgets, no advertising tags. Every line of JavaScript on the page is code we wrote and serve from our own origin. The dependency tree is intentionally narrow.
- No third-party static assets where avoidable. Fonts and images are self-hosted to avoid leaking visitor information to external CDNs and to keep the loaded asset list under our own control.
- Reproducible-build aspiration. Producing a build that anyone can verify against the source, so that the JavaScript a user receives can be checked against an open repository, is on our roadmap. Today it is not yet possible.
For users with the most demanding threat models, the honest recommendation is a browser extension with a signed, versioned distribution channel: the extension code is reviewed once, installed once, and runs the same decryption logic without re-fetching it from our servers on every page load. We plan to ship one. Until then, the user accepts the same trust assumption that every web-based encrypted product (Proton, Bitwarden's web vault, Standard Notes' web app) requires.
We say this plainly because we would rather ship a product whose limitations are stated honestly than one whose marketing collapses on first inspection by a determined reader.
6. Implementation Notes
6.1 Web Crypto API
All cryptographic operations use window.crypto.subtle, the browser's native Web Crypto implementation, which delegates to the platform's vetted cryptographic library. We do not ship our own AES, our own PBKDF2, or our own random number generator. crypto.getRandomValues is the only randomness source.
6.2 Key handling
The derived CryptoKey is created with extractable: false. This is enforced by the browser: even malicious JavaScript injected into our page cannot call crypto.subtle.exportKey to retrieve the raw key bytes. The key can only be used through crypto.subtle.encrypt and crypto.subtle.decrypt. This narrows the window in which a successful XSS would be useful to the attacker.
6.3 Memory hygiene
JavaScript does not provide reliable memory zeroisation. Strings are immutable and garbage collection is non-deterministic. We minimise the lifetime of sensitive material: the master password is held only long enough to derive the key, then dereferenced; the key is dropped on logout or tab close. We do not promise full memory-scrubbing, because in this runtime no one honestly can.
6.4 Transport
All endpoints are served over HTTPS only. The site instructs browsers to refuse any future plaintext connections, so a user who has visited Server Stash once cannot subsequently be downgraded to an unencrypted connection by a network attacker. Legacy and weak TLS configurations are not accepted.
7. Roadmap
We are explicit about what is and is not yet implemented:
- Envelope versioning. A version prefix (e.g. v1:salt:iv:ciphertext+tag) is not yet in the on-disk format. We will add it before the first parameter migration. Adding it later requires a one-time re-encryption pass for existing records.
- PBKDF2 iteration upgrade. Migration from 310,000 to 600,000 iterations, gated on the envelope versioning above.
- Argon2id option. Offered as an opt-in for users who can afford the additional ~100ms unlock cost in exchange for better resistance to custom-hardware attackers.
- Per-vault keys. Currently, all of a user's records are encrypted under a single key derived from their master password. To support sharing in the planned team tier, we will move to a key hierarchy in which each vault has its own symmetric key, wrapped under the user's master key (and, for shared vaults, under each member's public key). This is a precondition for team features.
- Browser extension. A signed, versioned extension distribution that decouples the decryption logic from the served web JavaScript.
- Independent audit. Once the above changes are in place, we intend to commission an independent cryptographic review and publish the report.
8. Glossary
- AES-256-GCM. Advanced Encryption Standard, 256-bit key, Galois/Counter Mode. Provides authenticated encryption.
- PBKDF2. Password-Based Key Derivation Function 2. Iterated hashing to slow down password guessing.
- Salt. Random per-user value mixed into key derivation to ensure that identical passwords across users yield different keys.
- IV (initialisation vector). Per-encryption random value that ensures identical plaintexts produce different ciphertexts.
- Auth tag. Cryptographic checksum produced by GCM. Tampering with the ciphertext invalidates the tag and decryption fails.
- Zero-knowledge. Property of a system in which the service operator cannot, by construction, recover plaintext user data.
- Envelope. The stored format combining salt, IV, and ciphertext.
9. Contact
Security concerns: support@serverstash.dev.
We commit to acknowledging reports within two business days and publishing a post-mortem for any incident with user-visible impact.