Exhibit 02 / 03
Zero Trust Architecture
Keys are derived in the browser, so the server never receives one.
- Discipline
- Web
- Built with
- TypeScript, React, Vue
- Year
- 2025
Scroll
1 · Type a passphrase
Anything at all. It is not stored or sent anywhere. Watch what the browser builds from it, and watch the other column stay empty.
In this browser
- Saltnot derived yet
- PBKDF2not derived yet
- Derived keynot derived yet
- RSA-OAEP keypairnot derived yet
- RSA-PSS keypairnot derived yet
Sent to the server
Nothing. Not the passphrase, not a hash of it, not the key.
Which is also why a forgotten passphrase is unrecoverable here. There is no one else to ask.
2 · Move the trust score
Trust is a number, not a yes or no. Every request is scored against a threshold of its own, so the same action flips as this number moves.
40/ 100
3 · Attempt something
Derive the keys first. Type a passphrase above.
- needs 20 · low riskallowed now
- needs 40 · medium riskallowed now
- needs 60 · high riskneeds step-up
- needs 85 · severe riskneeds step-up
- no threshold grants this · impossible risknever
No request attempted yet.
4 · What the server holds, right now
- Ciphertext blobs3
- Saltpublic
- Encryption public keynone yet
- Signing public keynone yet
- Encryption private keynever uploaded
- Signing private keynever uploaded
- Derived keynever transmitted
- Passphrasenever transmitted
- Plaintext bytes seen0
Requests it saw
Nothing yet.
Every row above is either ciphertext or a public key. Own this server completely and that is still all you get.
Overview
Keys are derived in the browser, so the server never receives one.
A web app for storing and sharing files. Upload, browse, share and revoke, with a React and Vue front end on an Express server.
An encrypted file store where the encryption happens in the browser rather than on the server. The usual approach encrypts at rest with a key the server also holds, which protects against a stolen disk and little else. I wanted the server to be unable to read the files even if it were fully compromised.
The interface
1 · Type a passphrase
Anything at all. It is not stored or sent anywhere. Watch what the browser builds from it, and watch the other column stay empty.
In this browser
- Saltnot derived yet
- PBKDF2not derived yet
- Derived keynot derived yet
- RSA-OAEP keypairnot derived yet
- RSA-PSS keypairnot derived yet
Sent to the server
Nothing. Not the passphrase, not a hash of it, not the key.
Which is also why a forgotten passphrase is unrecoverable here. There is no one else to ask.
2 · Move the trust score
Trust is a number, not a yes or no. Every request is scored against a threshold of its own, so the same action flips as this number moves.
40/ 100
3 · Attempt something
Derive the keys first. Type a passphrase above.
- needs 20 · low riskallowed now
- needs 40 · medium riskallowed now
- needs 60 · high riskneeds step-up
- needs 85 · severe riskneeds step-up
- no threshold grants this · impossible risknever
No request attempted yet.
4 · What the server holds, right now
- Ciphertext blobs3
- Saltpublic
- Encryption public keynone yet
- Signing public keynone yet
- Encryption private keynever uploaded
- Signing private keynever uploaded
- Derived keynever transmitted
- Passphrasenever transmitted
- Plaintext bytes seen0
Requests it saw
Nothing yet.
Every row above is either ciphertext or a public key. Own this server completely and that is still all you get.
How it works
That pushes all key management into the browser, and it means a forgotten password is unrecoverable. It is a real trade-off, chosen deliberately rather than engineered around, and the login screen says so in plain words instead of offering a reset link that cannot work.
Upload, browse, share and revoke, with a React and Vue front end on Express
Threshold-based trust scoring decides what each request must prove
All key material derived and held in the browser
WebCrypto throughout: PBKDF2 at 200,000 iterations, RSA-OAEP, RSA-PSS
Separate encryption and signing keypairs, so the failure modes stay independent
Sensitive requests are refused while a step-up challenge is pending, never queued
When things go wrong
- Below the thresholdproceed
The trust score covers what this request asks for, so it runs. Most reading and listing sits here.
- Above the thresholdrefused
A sensitive request while a step-up is pending is refused outright. It is not queued to run later under a state the person never confirmed.
- Password forgottenunrecoverable
The key was derived in the browser and never left it, so there is nothing on the server to recover from. Said plainly rather than hidden behind a reset link that cannot work.
Decisions
Why generate keys client-side and accept unrecoverable accounts?
The alternative is the server holding something that can decrypt your files, which is the thing I was trying to remove. An unrecoverable account is an honest cost of that, and saying so is better than a recovery path that quietly reintroduces the server as a key holder.
Why refuse rather than queue during a pending step-up?
A queued request executes later, under an authentication state the user never explicitly confirmed. Refusing keeps the decision and the action in the same moment.
What I would change now
RSA-OAEP with 2048-bit keys is reasonable but not what I would choose today. ECDH on X25519 with AES-GCM is faster, smaller, and has fewer sharp edges.