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

Vaulta working model · no real cryptography

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.

Discipline
Web
Year
2025
Stack
  • TypeScript
  • React
  • Vue
  • Express
  • WebCrypto

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

Vaulta working model · no real cryptography

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.

A diagram of how the keys are arranged, and the states a request moves through when the app asks for a second check.

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.

  1. Upload, browse, share and revoke, with a React and Vue front end on Express

  2. Threshold-based trust scoring decides what each request must prove

  3. All key material derived and held in the browser

  4. WebCrypto throughout: PBKDF2 at 200,000 iterations, RSA-OAEP, RSA-PSS

  5. Separate encryption and signing keypairs, so the failure modes stay independent

  6. 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

  1. 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.

  2. 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.

  3. 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.