This document explains where keys are stored, how signing and verification work, and what the server and client are responsible for.
Public keys are generated by the client during registration and sent to the server.
They are stored server-side at:
storage/users/<canonical_username>/public.pem
Server code reference:
await fs.promises.writeFile(path.join(dir, 'public.pem'), publicKeyPem);The server exposes public keys via:
GET /api/users/:username/public-key
The server never modifies or generates public keys.
Private keys are generated locally by the client and never leave the client machine.
They are stored locally at:
<project_root>/crypto/keys/_private.pem
Client code reference:
def key_paths(username):
u = canon(username)
return (
CRYPTO_KEYS_DIR / f"{u}_private.pem",
CRYPTO_KEYS_DIR / f"{u}_public.pem",
)The server has no access to private keys at any point.
The fileId is NOT a cryptographic signature.
- fileId is a UUID generated by the server
- it is only used to locate files on disk
Server code reference:
const id = crypto.randomUUID();
res.json({ fileId: id });The actual digital signature is stored separately.
The digital signature is stored inside meta.json as:
signatureB64
Client meta creation:
"signatureB64": base64.b64encode(sig).decode(),The signature is never displayed unless the client explicitly shows it.
The client signs the encrypted file bytes (ciphertext), not the plaintext.
This ensures integrity of the stored and transmitted data.
Client code reference:
sig = sign_data(cipher, sender_priv)Verification occurs entirely on the client during download.
Steps:
- Download meta.json
- Download blob.bin (ciphertext)
- Fetch sender public key from server
- Verify signature against ciphertext
- Abort if invalid
- Decrypt only if valid
Verification code reference:
sig_ok = verify_signature(ciphertext, signature, sender_pub)
if not sig_ok:
raise RuntimeError("Signature verification FAILED")Signing:
- sender private key
Verification:
- sender public key (fetched from server)
Crypto implementation reference:
signature = s_private_key.sign(...)
s_public_key.verify(signature, data, ...)The server DOES:
- authenticate users with JWT
- store public keys
- store encrypted blobs and metadata
- authorize access
The server DOES NOT:
- decrypt files
- decrypt AES keys
- verify signatures
- access plaintext
All cryptographic trust decisions are enforced client-side.
JWT is used only for authentication with the server.
If the token expires:
- client must log in again
- a new token replaces the old one
JWT is unrelated to encryption or signing.
All file types are supported.
The server treats files as opaque encrypted bytes and does not inspect contents.
storage/
└── users/
├── _index.json
├── alice/
│ ├── public.pem
│ └── files/
│ └── <fileId>/
│ ├── blob.bin
│ └── meta.json
crypto/keys/
├── alice_private.pem
├── alice_public.pem
~/.zt_file_client/
└── session.json
POST /api/auth/register
POST /api/auth/login
GET /api/users/:username/public-key
POST /api/files/upload
GET /api/files/:id/meta
GET /api/files/:id/blob
GET /api/files/inbox
GET /api/files/outbox
