Skip to content

Zero-Knowledge Architecture

This page explains what gets encrypted, where the encryption happens, and what the ingestion worker actually receives.

Every field you pass to log() is encrypted: actor, action, target, metadata, and the telemetry context object. The entire payload becomes a single AES-256-GCM ciphertext before it leaves your server.

Volidator’s ingestion worker receives the searchable blind indexes (for actor, action, and optionally target and tenant) and the ciphertext. Nothing else.

The SDK encrypts inside log() on your server, synchronously, before the HTTP request to the ingestion worker is made.

// What you write:
await volidator.log({
actor: 'user_123',
action: 'file.delete',
target: 'file_456',
tenant: 'johnsbakery'
});
// What reaches Volidator's server:
// {
// actorBlindIndex: "a3f9c...hmac",
// actionBlindIndex: "77b2e...hmac",
// targetBlindIndex: "c3b8a...hmac",
// tenantBlindIndex: "99f2e...hmac",
// encryptedPayload: "base64-aes-gcm-ciphertext"
// }

The SDK derives a 256-bit key by running your encryptionKey through SHA-256. For each log entry, it generates a random 12-byte initialization vector (IV). It uses AES-256-GCM, which provides both confidentiality and authentication.

The output format is: [IV (12 bytes)] [ciphertext] [auth tag (16 bytes)], base64-encoded. The IV is prepended to the ciphertext so the browser client can extract it for decryption without any server round-trip.

Because the ingestion worker never receives the encryption key, Volidator cannot:

  • Read any log field
  • Respond to subpoenas for log content
  • Expose log content in a data breach

Volidator can confirm that a log was stored, query by blind index, and return ciphertext. Decryption happens only in the browser of a user who has the key.