Security

Zero-knowledge is a tested property of the architecture, not a claim painted on after the fact.

What the server never gets

Master passwords and vault plaintext stay on clients. The AU API stores opaque encrypted blobs, sync metadata, and organisation membership — enough to move ciphertext between devices, not enough to open a vault.

Server assemblies are forbidden from referencing the client crypto library or decrypting AES-GCM payloads. That boundary is enforced by dedicated zero-knowledge tests.

The key hierarchy

A master password becomes four purpose-built keys, each with one job. Only one of them ever leaves your device — and that one can't unlock anything.

MK

Master key

Argon2id over your password and a per-account salt, run entirely on your device. Held in memory for the session, never written to disk or sent anywhere.

SMK

Stretched key

An HKDF branch of the master key whose only job is unwrapping your vault key. Domain-separated so it can never be reused for anything else.

UK

User key

32 random bytes generated once at signup, never re-derived. It's what your vault is actually encrypted under, so changing your password just re-wraps this key — your vault itself is never touched.

AUTH

Auth value

The only password-derived value that leaves your device — and it can't unwrap anything. The server peppers and re-hashes it before storage, so a stolen database dump still can't be brute-forced offline.

The envelope format

Every encrypted value — a vault item, a wrapped key, an attachment chunk — uses the same small binary envelope, so there's one code path to audit for how ciphertext is built and read back.

AES-256-GCM: [0x01 version][0x01 alg][12-byte nonce][ciphertext ‖ 16-byte tag] RSA-OAEP wrap: [0x01 version][0x02 alg][384-byte RSA-3072-OAEP-SHA256 ciphertext]
  • The nonce is generated freshly random inside the encrypt call itself — callers can't supply one, so nonce reuse isn't a mistake an application developer can make.
  • Every envelope carries a version byte and an algorithm tag, so the format can evolve without ambiguity about how to read old data.
  • Associated data — the record's purpose, owner, and key version — is bound into the GCM tag, so ciphertext can't be silently moved to a different record or replayed under an old key.
Worth being precise about

AES-GCM isn't key-committing, so a failed decrypt is usually — not certifiably — attributable to the wrong password rather than tampering. We only lean on that inference where it's low-stakes (telling a user their password is wrong); every other envelope treats a decrypt failure as an integrity error, full stop.

Sharing, without trusting the server as a middleman

Organisations need shared vault access. The naive way — the server hands out a shared key — makes the server a party to your secrets. PassStash wraps it per-recipient instead.

Wrapped per member

An admin wraps the organisation's shared key under one member's RSA-OAEP public key at a time — never a key the server could unwrap on its own.

Signed, not just encrypted

The admin signs the wrap (RSA-PSS) over the org, member, member's public key, and key version. The server stores every public key in plaintext — without this signature, a compromised server could substitute its own key into the wrap. With it, a member only accepts a wrap that verifies against a pinned signer.

Recovery, opted into deliberately

Zero-knowledge cuts both ways: lose your master password with no recovery set up, and that vault is gone — nobody, including us, holds a spare key. Organisations can opt into a recovery path that keeps that guarantee intact for everyone who doesn't use it.

  • Recovery is per-member opt-in — a member's user key is wrapped to the org's recovery public key only if they choose to enable it.
  • The recovery private key is held only by current Owners, wrapped to each Owner's own key — not to the shared org key every member holds, which would let any member recover any consenting member's vault.
  • An Owner recovering a member's account unwraps their original vault key and re-wraps that same, unchanged key under a new password — the vault itself is never re-encrypted, and no plaintext ever passes through the server.

Client KDF & envelope

Argon2id key derivation and AES-GCM envelopes are implemented in both C# and TypeScript against shared vector suites.

Transport & ops

TLS terminates on Ripple’s shared ingress. PassStash customer names stay DNS-only on Cloudflare so client IPs remain visible for rate limits.

Hardening in flight

TOTP 2FA, JWT rotation / JWKS, and HIBP breach checks are implemented. Encrypted export/import and pepper-rotation runbooks continue to land.

What the server can — and can't — see

Data Server visibility
Vault item contents Never — ciphertext only
Your master password Never transmitted in any form
Keys that could decrypt your vault Never — no such key exists server-side
Full password-breach hash Never — only a 5-character prefix
Peppered, hashed authentication value Yes — used only to verify login
Sync metadata (timestamps, item IDs, key versions) Yes — required to sync ciphertext between devices
Account info (email, organisation membership) Yes — needed to run the account and org system
Threat model honesty

A compromised client device can still expose unlocked vault material. PassStash’s promise is that a breached sync region cannot decrypt customer vaults from stored data alone.