Client SDK Specification
VaultSandbox Client SDK Specification
Section titled “VaultSandbox Client SDK Specification”A language-agnostic specification for implementing VaultSandbox client libraries. This document provides all necessary information to build a fully-functional SDK in any programming language.
Table of Contents
Section titled “Table of Contents”- Overview
- Architecture
- Naming Conventions
- Authentication
- Cryptographic Requirements
- API Reference
- Data Structures
- Delivery Strategies
- Error Handling
- Behavioral Specifications
- Implementation Checklist
Overview
Section titled “Overview”VaultSandbox is a secure, receive-only SMTP server designed for QA/testing environments. The client SDK enables:
- Creating temporary email inboxes with quantum-safe encryption
- Receiving and decrypting emails in real-time or via polling
- Validating email authentication (SPF/DKIM/DMARC)
- Zero cryptographic knowledge required from end users
Key Features
Section titled “Key Features”- Quantum-safe encryption: ML-KEM-768 (Kyber768) + AES-256-GCM
- Signature verification: ML-DSA-65 (Dilithium3) before decryption
- Real-time delivery: Server-Sent Events (SSE) with polling fallback
- Automatic retry: Exponential backoff for transient failures
Architecture
Section titled “Architecture”Layer Overview
Section titled “Layer Overview”┌────────────────────────────────────────────────────────────┐│ User-Facing API ││ (VaultSandboxClient, Inbox, Email classes) │├────────────────────────────────────────────────────────────┤│ Delivery Strategy Layer ││ (SSE Strategy / Polling Strategy) │├────────────────────────────────────────────────────────────┤│ HTTP Layer ││ (API Client with retry logic) │├────────────────────────────────────────────────────────────┤│ Crypto Layer ││ (ML-KEM-768, ML-DSA-65, AES-256-GCM, HKDF-SHA-512) │└────────────────────────────────────────────────────────────┘Component Responsibilities
Section titled “Component Responsibilities”| Component | Responsibility |
|---|---|
| VaultSandboxClient | Main entry point; manages inboxes, strategies, lifecycle |
| Inbox | Represents a single inbox; email operations, subscriptions |
| Represents a decrypted email with content and metadata | |
| ApiClient | HTTP communication with retry logic |
| DeliveryStrategy | Abstract interface for SSE/polling implementations |
| Crypto Module | Keypair generation, decryption, signature verification |
Client Configuration Options
Section titled “Client Configuration Options”SDKs should support these configuration options when creating a client:
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | (required) | API key for authentication |
baseUrl | string | Platform-specific | VaultSandbox API base URL |
strategy | enum | auto | Delivery strategy: auto, sse, polling |
timeout | number | 30000 | HTTP request timeout (ms) |
maxRetries | number | 3 | Maximum retry attempts for failed requests |
retryDelay | number | 1000 | Base delay between retries (ms) |
retryOn | number[] | [408,429,500,502,503,504] | HTTP status codes that trigger retry |
pollingInterval | number | 2000 | Initial polling interval (ms) |
pollingMaxBackoff | number | 30000 | Maximum polling interval (ms) |
pollingBackoffMultiplier | number | 1.5 | Polling backoff multiplier |
pollingJitterFactor | number | 0.3 | Random jitter factor (0-1) |
sseReconnectInterval | number | 5000 | SSE reconnection interval (ms) |
sseMaxReconnectAttempts | number | 10 | Max SSE reconnection attempts |
sseConnectionTimeout | number | 5000 | SSE connection timeout for auto fallback (ms) |
httpClient | object | Platform default | Custom HTTP client (optional) |
onSyncError | function | null | Callback for background sync errors (optional) |
Inbox Creation Options
Section titled “Inbox Creation Options”| Option | Type | Default | Description |
|---|---|---|---|
ttl | number | 3600 | Time-to-live in seconds (60-604800) |
emailAddress | string | null | Desired email address or domain (optional) |
Naming Conventions
Section titled “Naming Conventions”This specification uses camelCase for all identifiers in the wire format (JSON payloads, API responses). SDK implementations should adapt to their language’s idiomatic conventions:
| Language | Public API Style | Example Method | Example Field |
|---|---|---|---|
| JavaScript | camelCase | createInbox() | email.authResults |
| Go | PascalCase | CreateInbox() | Email.AuthResults |
| Python | snake_case | create_inbox() | email.auth_results |
| Rust | snake_case | create_inbox() | email.auth_results |
| Java | camelCase | createInbox() | email.authResults |
Wire format remains camelCase regardless of SDK language. For example, the JSON field emailAddress becomes:
- Go:
EmailAddress(struct field) - Python:
email_address(attribute) - JavaScript:
emailAddress(property)
SDKs should handle this translation transparently during serialization/deserialization.
Authentication
Section titled “Authentication”All API requests require the X-API-Key header.
X-API-Key: your-api-keyContent-Type: application/jsonValidating API Key
Section titled “Validating API Key”Before any operations, validate the API key:
GET /api/check-keyX-API-Key: your-api-keyResponse:
{ "ok": true}Cryptographic Requirements
Section titled “Cryptographic Requirements”Algorithm Suite
Section titled “Algorithm Suite”| Purpose | Algorithm | Standard |
|---|---|---|
| Key Encapsulation | ML-KEM-768 (Kyber768) | NIST FIPS 203 |
| Signature | ML-DSA-65 (Dilithium3) | NIST FIPS 204 |
| Symmetric Encryption | AES-256-GCM | NIST SP 800-38D |
| Key Derivation | HKDF-SHA-512 | RFC 5869 |
Key Sizes
Section titled “Key Sizes”| Key Type | Size (bytes) |
|---|---|
| ML-KEM-768 Public Key | 1184 |
| ML-KEM-768 Secret Key | 2400 |
| ML-KEM-768 Ciphertext | 1088 |
| ML-KEM-768 Shared Secret | 32 |
| ML-DSA-65 Public Key | 1952 |
| ML-DSA-65 Signature | 3309 |
| AES-256 Key | 32 |
| AES-GCM Nonce | 12 |
| AES-GCM Tag | 16 |
Constants
Section titled “Constants”HKDF_CONTEXT = "vaultsandbox:email:v1"ALGS_CIPHERSUITE = "ML-KEM-768:ML-DSA-65:AES-256-GCM:HKDF-SHA-512"PUBLIC_KEY_START_OFFSET = 1152 # Byte offset where public key starts within ML-KEM-768 secret keyKeypair Generation
Section titled “Keypair Generation”Generate an ML-KEM-768 keypair for each inbox:
# Pseudocodekeypair = ml_kem768.keygen()# Returns:# - publicKey: Uint8Array (1184 bytes)# - secretKey: Uint8Array (2400 bytes)# - publicKeyB64: base64url(publicKey)Encrypted Payload Structure
Section titled “Encrypted Payload Structure”All encrypted data from the server follows this structure:
{ "v": 1, "algs": { "kem": "ML-KEM-768", "sig": "ML-DSA-65", "aead": "AES-256-GCM", "kdf": "HKDF-SHA-512" }, "ct_kem": "<base64url: KEM ciphertext>", "nonce": "<base64url: 12-byte nonce>", "aad": "<base64url: additional authenticated data>", "ciphertext": "<base64url: AES-GCM ciphertext + tag>", "sig": "<base64url: ML-DSA-65 signature>", "server_sig_pk": "<base64url: server's signing public key>"}Payload Validation
Section titled “Payload Validation”Implementations MUST validate payloads before processing:
- Version:
vMUST be1. Reject payloads with unknown versions. - Algorithms: All algorithm fields MUST match expected values. Implementations MUST reject payloads specifying different algorithms.
- Size validation: Decoded binary fields MUST have correct sizes:
| Field | Expected size |
|---|---|
ct_kem | 1088 bytes |
nonce | 12 bytes |
sig | 3309 bytes |
server_sig_pk | 1952 bytes |
Decryption Flow
Section titled “Decryption Flow”CRITICAL: Always verify signature BEFORE decryption to detect tampering.
1. VERIFY SIGNATURE (security-critical) ├── Build transcript from encrypted payload ├── Verify ML-DSA-65 signature └── ABORT if verification fails
2. KEM DECAPSULATION ├── Decode ct_kem from base64url └── sharedSecret = ml_kem768.decapsulate(ct_kem, secretKey)
3. KEY DERIVATION (HKDF-SHA-512) ├── salt = SHA-256(ct_kem) ├── info = context || aad_length(4 bytes, big-endian) || aad └── aesKey = HKDF-Expand(sharedSecret, salt, info, 32 bytes)
4. AES-256-GCM DECRYPTION ├── Decode nonce, aad, ciphertext from base64url └── plaintext = AES-GCM-Decrypt(aesKey, nonce, aad, ciphertext)Signature Verification
Section titled “Signature Verification”Build the transcript exactly as the server does:
transcript = version (1 byte) || algs_ciphersuite (string: "ML-KEM-768:ML-DSA-65:AES-256-GCM:HKDF-SHA-512") || context (string: "vaultsandbox:email:v1") || ct_kem (bytes) || nonce (bytes) || aad (bytes) || ciphertext (bytes) || server_sig_pk (bytes)
valid = ml_dsa65.verify(signature, transcript, server_sig_pk)Server Key Pinning (Security Critical)
Section titled “Server Key Pinning (Security Critical)”CRITICAL: When verifying signatures, the client MUST compare the payload’s server_sig_pk against the pinned server public key captured at inbox creation time. This prevents man-in-the-middle attacks where an attacker could inject payloads signed with their own key.
# Pseudocodedef verify_with_pinning(encrypted_payload, pinned_server_pk): payload_server_pk = from_base64url(encrypted_payload.server_sig_pk)
# Security check: reject if server keys don't match if payload_server_pk != pinned_server_pk: raise ServerKeyMismatchError("Server key in payload does not match pinned key - possible MITM attack")
# Proceed with signature verification using the pinned key return verify_signature(encrypted_payload, pinned_server_pk)Deriving Public Key from Secret Key
Section titled “Deriving Public Key from Secret Key”In ML-KEM-768, the secret key structure is:
secretKey = cpaPrivateKey || cpaPublicKey || h || zWhere:
cpaPrivateKey: 1152 bytes (12 × k × n / 8, k=3, n=256)cpaPublicKey: 1184 bytes (the public key)h: 32 bytes (hash of public key)z: 32 bytes (random seed)
The public key starts at byte offset 1152:
public_key = secret_key[1152:2336] # Bytes 1152-2335 (1184 bytes)Library-Specific Note: The offset approach (1152) follows NIST FIPS 203 and works with libraries like cloudflare/circl. Some libraries may use different internal representations. Always verify with your chosen library’s documentation and test against the server.
API Reference
Section titled “API Reference”Base URL
Section titled “Base URL”Configurable per deployment. Example: https://smtp.vaultsandbox.com
Server Information
Section titled “Server Information”GET /api/server-info
Section titled “GET /api/server-info”Returns server cryptographic configuration.
Response:
{ "serverSigPk": "<base64url: server ML-DSA-65 public key>", "algs": { "kem": "ML-KEM-768", "sig": "ML-DSA-65", "aead": "AES-256-GCM", "kdf": "HKDF-SHA-512" }, "context": "vaultsandbox:email:v1", "maxTtl": 604800, "defaultTtl": 3600, "sseConsole": false, "allowedDomains": ["vaultsandbox.test", "example.com"]}| Field | Type | Description |
|---|---|---|
serverSigPk | string | Base64URL-encoded server signing public key for ML-DSA-65 |
algs | object | Cryptographic algorithms supported by the server |
context | string | Context string for the encryption scheme |
maxTtl | number | Maximum time-to-live for inboxes in seconds |
defaultTtl | number | Default time-to-live for inboxes in seconds |
sseConsole | boolean | Whether server SSE console logging is enabled |
allowedDomains | string[] | List of domains allowed for inbox creation |
Inbox Management
Section titled “Inbox Management”POST /api/inboxes
Section titled “POST /api/inboxes”Creates a new inbox.
Request:
{ "clientKemPk": "<base64url: client ML-KEM-768 public key>", "ttl": 3600,}| Field | Type | Required | Description |
|---|---|---|---|
clientKemPk | string | Yes | Base64url-encoded ML-KEM-768 public key |
ttl | number | No | Time-to-live in seconds (min: 60, max: 604800) |
emailAddress | string | No | Desired email address or domain (max 254 chars) |
Response:
{ "expiresAt": "2024-01-15T12:00:00.000Z", "inboxHash": "<base64url: SHA-256 hash of client KEM public key>", "serverSigPk": "<base64url: server signing public key>"}| Field | Type | Description |
|---|---|---|
emailAddress | string | The email address assigned to the inbox |
expiresAt | string | ISO 8601 timestamp when the inbox will expire |
inboxHash | string | Base64URL-encoded SHA-256 hash of the client KEM public key |
serverSigPk | string | Base64URL-encoded server signing public key for verification |
DELETE /api/inboxes/{emailAddress}
Section titled “DELETE /api/inboxes/{emailAddress}”Deletes a specific inbox. Idempotent.
Response: 204 No Content
DELETE /api/inboxes
Section titled “DELETE /api/inboxes”Deletes all inboxes for the API key.
Response:
{ "deleted": 5}GET /api/inboxes/{emailAddress}/sync
Section titled “GET /api/inboxes/{emailAddress}/sync”Returns inbox sync status for efficient polling.
Response:
{ "emailCount": 3, "emailsHash": "hash-of-email-ids"}| Field | Type | Description |
|---|---|---|
emailCount | number | Number of emails currently in the inbox |
emailsHash | string | Hash of email IDs; changes indicate new/deleted emails |
Usage: Compare emailsHash to detect changes without fetching all emails. This enables efficient polling by skipping unchanged inboxes.
Email Operations
Section titled “Email Operations”GET /api/inboxes/{emailAddress}/emails
Section titled “GET /api/inboxes/{emailAddress}/emails”Lists all emails in an inbox (metadata only).
Note: The server returns only metadata (sender, subject, date) for this endpoint. To retrieve the full email content (body, attachments), the client library must fetch each email individually using GET /api/inboxes/{emailAddress}/emails/{emailId}.
Response:
[ { "id": "email-uuid", "inboxId": "inbox-hash", "receivedAt": "2024-01-15T12:00:00.000Z", "isRead": false, "encryptedMetadata": { /* EncryptedPayload */ } }]GET /api/inboxes/{emailAddress}/emails/{emailId}
Section titled “GET /api/inboxes/{emailAddress}/emails/{emailId}”Retrieves a specific email with full content.
Response:
{ "id": "email-uuid", "inboxId": "inbox-hash", "receivedAt": "2024-01-15T12:00:00.000Z", "isRead": false, "encryptedMetadata": { /* EncryptedPayload - email headers */ }, "encryptedParsed": { /* EncryptedPayload - full email body, attachments, auth results */ }}Note: Unlike the list endpoint, this returns encryptedParsed which contains the complete email body, HTML content, attachments, links, and authentication results.
GET /api/inboxes/{emailAddress}/emails/{emailId}/raw
Section titled “GET /api/inboxes/{emailAddress}/emails/{emailId}/raw”Retrieves the raw email source (encrypted).
Response:
{ "id": "email-uuid", "encryptedRaw": { /* EncryptedPayload */ }}PATCH /api/inboxes/{emailAddress}/emails/{emailId}/read
Section titled “PATCH /api/inboxes/{emailAddress}/emails/{emailId}/read”Marks an email as read.
Response: 204 No Content
DELETE /api/inboxes/{emailAddress}/emails/{emailId}
Section titled “DELETE /api/inboxes/{emailAddress}/emails/{emailId}”Deletes a specific email.
Response: 204 No Content
Real-time Events
Section titled “Real-time Events”GET /api/events?inboxes={inboxHashes}
Section titled “GET /api/events?inboxes={inboxHashes}”Server-Sent Events endpoint for real-time email notifications.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
inboxes | string | Comma-separated inbox hashes |
Headers:
X-API-Key: your-api-keyAccept: text/event-streamEvent Format:
data: {"inboxId":"inbox-hash","emailId":"email-uuid","encryptedMetadata":{...}}| Field | Type | Description |
|---|---|---|
inboxId | string | The inbox hash that received the email |
emailId | string | Unique identifier for the new email |
encryptedMetadata | EncryptedPayload | Encrypted email metadata (from, to, subject) |
Note: SSE events only include metadata. To get full email content (body, attachments), fetch the email by ID after receiving the notification.
Connection Management:
- Subscribe to inboxes by adding their hashes to the query parameter
- On connection open, reset reconnection counter
- On connection error, disconnect and attempt reconnection with exponential backoff
- When adding/removing inbox subscriptions, reconnect with updated hash list
- On reconnection success, sync all inboxes to catch emails received during downtime
Data Structures
Section titled “Data Structures”Decrypted Email Metadata
Section titled “Decrypted Email Metadata”After decrypting encryptedMetadata:
{ "subject": "Welcome Email", "receivedAt": "2024-01-15T12:00:00.000Z"}Email Object Structure
Section titled “Email Object Structure”The fully decrypted Email object exposed to users:
{ "id": "email-uuid", "subject": "Welcome Email", "text": "Plain text content", "html": "<html>HTML content</html>", "receivedAt": "2024-01-15T12:00:00.000Z", "isRead": false, "headers": { "date": "Mon, 15 Jan 2024 12:00:00 +0000" }, "attachments": [ /* AttachmentData[] */ ], "links": ["https://example.com/verify?token=abc123"], "authResults": { /* AuthResults */ }, "metadata": {}}| Field | Type | Description |
|---|---|---|
id | string | Unique email identifier |
from | string | Sender email address |
to | string[] | Recipient email addresses |
subject | string | Email subject line |
text | string | null | Plain text body (null if not available) |
html | string | null | HTML body (null if not available) |
receivedAt | Date/string | When email was received |
isRead | boolean | Read status |
headers | object | Email headers as key-value pairs |
attachments | AttachmentData[] | Email attachments |
links | string[] | URLs extracted from email body |
authResults | AuthResults | SPF/DKIM/DMARC/ReverseDNS results |
metadata | object | Additional metadata (reserved for extensions) |
Decrypted Email Content
Section titled “Decrypted Email Content”After decrypting encryptedParsed:
{ "text": "Plain text content", "html": "<html>HTML content</html>", "headers": { "date": "Mon, 15 Jan 2024 12:00:00 +0000" }, "attachments": [ { "filename": "document.pdf", "contentType": "application/pdf", "size": 15234, "contentDisposition": "attachment", "content": "<base64: file content>", "checksum": "<optional: SHA-256 hash>" } ], "metadata": { /* Additional metadata associated with the email */ }, "links": ["https://example.com/verify?token=abc123"], "authResults": { "spf": { /* SPFResult */ }, "dkim": [ /* DKIMResult[] */ ], "dmarc": { /* DMARCResult */ }, "reverseDns": { /* ReverseDNSResult */ } }}Authentication Results
Section titled “Authentication Results”Validation Logic
Section titled “Validation Logic”Client libraries should provide a validation helper that verifies:
- SPF:
resultmust be"pass" - DKIM: At least one entry in the array must have
result: "pass" - DMARC:
resultmust be"pass" - Reverse DNS:
verifiedmust betrue
Note: The overall passed status only considers SPF, DKIM, and DMARC. Reverse DNS is checked separately and does not affect the overall pass/fail status.
Validation Result
Section titled “Validation Result”The validate() method returns a validation summary:
{ "passed": false, "spfPassed": true, "dkimPassed": true, "dmarcPassed": false, "reverseDnsPassed": true, "failures": ["DMARC policy: fail (policy: reject)"]}| Field | Type | Description |
|---|---|---|
passed | boolean | True if SPF, DKIM, and DMARC all passed |
spfPassed | boolean | True if SPF result is "pass" |
dkimPassed | boolean | True if at least one DKIM signature passed |
dmarcPassed | boolean | True if DMARC result is "pass" |
reverseDnsPassed | boolean | True if reverse DNS verified is true |
failures | string[] | Human-readable descriptions of failed checks |
SDKs may also provide a convenience method like isPassing() that returns the passed value directly.
SPF Result
Section titled “SPF Result”{ "result": "pass", "domain": "example.com", "ip": "192.0.2.1", "details": "SPF record validated"}| Field | Type | Description |
|---|---|---|
result | string | SPF check result (see values) |
domain | string | Domain checked (optional) |
ip | string | Sender IP address (optional) |
details | string | Human-readable details (optional) |
| Result | Meaning |
|---|---|
pass | Authorized sender |
fail | Not authorized |
softfail | Probably not authorized |
neutral | No assertion |
none | No SPF record |
temperror | Temporary error |
permerror | Permanent error |
DKIM Result
Section titled “DKIM Result”{ "result": "pass", "domain": "example.com", "selector": "selector1", "signature": "base64-encoded-sig"}| Field | Type | Description |
|---|---|---|
result | string | DKIM check result (see values) |
domain | string | Signing domain (optional) |
selector | string | DKIM selector (optional) |
signature | string | DKIM signature info (optional) |
| Result | Meaning |
|---|---|
pass | Valid signature |
fail | Invalid signature |
none | No signature |
DMARC Result
Section titled “DMARC Result”{ "result": "pass", "policy": "reject", "aligned": true, "domain": "example.com"}| Field | Type | Description |
|---|---|---|
result | string | DMARC check result (see values) |
policy | string | Domain’s DMARC policy (optional) |
aligned | boolean | Whether SPF/DKIM aligned (optional) |
domain | string | Domain checked (optional) |
| Result | Meaning |
|---|---|
pass | DMARC passed |
fail | DMARC failed |
none | No DMARC policy |
| Policy | Meaning |
|---|---|
none | Monitoring only |
quarantine | Treat as spam |
reject | Reject email |
Reverse DNS Result
Section titled “Reverse DNS Result”{ "verified": true, "ip": "192.0.2.1", "hostname": "mail.example.com"}| Field | Type | Description |
|---|---|---|
verified | boolean | Whether reverse DNS verified |
ip | string | Server IP address (optional) |
hostname | string | Resolved hostname (optional) |
Note: Unlike other auth results, ReverseDNS uses a verified boolean instead of a result string. SDKs may provide a convenience method like isPassing() that returns verified.
Exported Inbox Data
Section titled “Exported Inbox Data”For persistence/sharing:
{ "version": 1, "expiresAt": "2024-01-15T12:00:00.000Z", "inboxHash": "sha256-hash", "serverSigPk": "<base64url: server signing key>", "secretKey": "<base64url: client secret key>", "exportedAt": "2024-01-14T12:00:00.000Z"}| Field | Type | Required | Description |
|---|---|---|---|
version | integer | Yes | Export format version. MUST be 1. |
emailAddress | string | Yes | The inbox email address. MUST contain @. |
expiresAt | string | Yes | Inbox expiration timestamp (ISO 8601). |
inboxHash | string | Yes | Unique inbox identifier. Non-empty. |
serverSigPk | string | Yes | Server’s ML-DSA-65 public key (base64url, 1952 bytes decoded). |
secretKey | string | Yes | ML-KEM-768 secret key (base64url, 2400 bytes decoded). |
exportedAt | string | Yes | Export timestamp (ISO 8601). |
Note: The public key is NOT included in the export as it can be derived from the secret key (see Deriving Public Key from Secret Key).
Security Warning: Exported data contains private keys. Handle securely.
Import Validation
Section titled “Import Validation”Implementations MUST validate imported data in the following order:
-
Parse JSON: Verify the input is valid JSON.
-
Validate version:
if version != 1:return ERROR_UNSUPPORTED_VERSION -
Validate required fields: All fields from the export format MUST be present and non-null.
-
Validate emailAddress:
- MUST be a non-empty string
- MUST contain exactly one
@character
if emailAddress == "" or count(emailAddress, "@") != 1:return ERROR_INVALID_EMAIL -
Validate inboxHash:
- MUST be a non-empty string
if inboxHash == "":return ERROR_INVALID_INBOX_HASH -
Validate and decode secretKey:
secretKeyBytes = base64url_decode(secretKey)if decoding fails:return ERROR_INVALID_SECRET_KEYif len(secretKeyBytes) != 2400:return ERROR_INVALID_SECRET_KEY_SIZE -
Validate and decode serverSigPk:
serverSigPkBytes = base64url_decode(serverSigPk)if decoding fails:return ERROR_INVALID_SERVER_KEYif len(serverSigPkBytes) != 1952:return ERROR_INVALID_SERVER_KEY_SIZE -
Validate timestamps:
expiresAtMUST be a valid ISO 8601 timestampexportedAtMUST be a valid ISO 8601 timestamp
Keypair Reconstruction
Section titled “Keypair Reconstruction”After validation, reconstruct the full keypair:
secret_key = base64url_decode(export.secretKey)public_key = secret_key[1152:2336] # Derive from secret keykeypair = { secret_key, public_key }Duplicate Handling
Section titled “Duplicate Handling”Implementations SHOULD check for existing inboxes with the same email address or inbox hash before import and either:
- Reject the import with an error, or
- Prompt the user to confirm replacement
Delivery Strategies
Section titled “Delivery Strategies”Strategy Selection
Section titled “Strategy Selection”The /api/events SSE endpoint is always available on the server.
| Strategy | Use Case |
|---|---|
sse | Real-time updates, low latency |
polling | Firewall restrictions, simpler implementation |
auto | Recommended default; tries SSE first, falls back to polling if unavailable |
SSE Strategy
Section titled “SSE Strategy”Connection
Section titled “Connection”GET /api/events?inboxes=hash1,hash2,hash3X-API-Key: your-api-keyAccept: text/event-streamEvent Handling
Section titled “Event Handling”# Pseudocodefor event in sse_stream: data = json.parse(event.data) inbox = find_inbox_by_hash(data.inboxId) email = decrypt_email(data.encryptedMetadata, inbox.keypair) notify_callbacks(inbox, email)Reconnection
Section titled “Reconnection”- Initial interval: 5000ms
- Max attempts: 10
- Backoff multiplier: 2x
- Reset attempts on successful connection
Polling Strategy
Section titled “Polling Strategy”Algorithm
Section titled “Algorithm”# Pseudocodelast_hash = Nonecurrent_backoff = initial_interval # 2000ms
while not timeout: sync_status = GET /api/inboxes/{email}/sync
if last_hash != sync_status.emailsHash: last_hash = sync_status.emailsHash emails = GET /api/inboxes/{email}/emails current_backoff = initial_interval # Reset on change
for email in decrypt_emails(emails): if matches_filters(email): return email else: # Exponential backoff when no changes current_backoff = min(current_backoff * 1.5, max_backoff)
jitter = random() * 0.3 * current_backoff sleep(current_backoff + jitter)Configuration
Section titled “Configuration”| Parameter | Default | Description |
|---|---|---|
initialInterval | 2000ms | Starting poll interval |
maxBackoff | 30000ms | Maximum backoff delay |
backoffMultiplier | 1.5 | Backoff growth factor |
jitterFactor | 0.3 | Random jitter (0-30%) |
Error Handling
Section titled “Error Handling”Error Hierarchy
Section titled “Error Hierarchy”VaultSandboxError (base)├── ApiError (HTTP errors)│ └── Contains: statusCode, message, requestId (optional)├── NetworkError (connection failures)├── TimeoutError (operation timeouts)├── InboxNotFoundError (404 for inbox)├── EmailNotFoundError (404 for email)├── InboxAlreadyExistsError (import conflict)├── InvalidImportDataError (validation failure)├── DecryptionError (crypto failure)├── SignatureVerificationError (tampering detected)│ └── ServerKeyMismatchError (server key doesn't match pinned key - possible MITM)├── SSEError (SSE connection issues)├── StrategyError (strategy configuration)├── RateLimitedError (429 - too many requests)├── UnauthorizedError (401 - invalid API key)└── ClientClosedError (operation on closed client)HTTP Retry Logic
Section titled “HTTP Retry Logic”Retryable Status Codes
Section titled “Retryable Status Codes”408 - Request Timeout429 - Too Many Requests500 - Internal Server Error502 - Bad Gateway503 - Service Unavailable504 - Gateway TimeoutRetry Algorithm
Section titled “Retry Algorithm”# Pseudocoderetry_delay = 1000 # msmax_retries = 3
for attempt in range(max_retries + 1): try: return http_request() except RetryableError: if attempt < max_retries: sleep(retry_delay * (2 ** attempt)) # Exponential backoff else: raiseCritical Errors
Section titled “Critical Errors”SignatureVerificationError and DecryptionError should:
- Be logged immediately with full context
- Never be silently ignored
- Halt the operation
- Potentially trigger security alerts
Security Requirements
Section titled “Security Requirements”Timing Attack Prevention
Section titled “Timing Attack Prevention”Implementations MUST use constant-time operations for:
- Server key comparison (pinned vs payload key)
- Signature verification
- Any comparison involving secret data
Error Message Handling
Section titled “Error Message Handling”Implementations MUST NOT reveal whether a failure occurred during:
- Signature verification vs. decryption
- MAC verification vs. decryption
Use generic error messages to prevent oracle attacks. For example, use a single “decryption failed” error rather than distinguishing between signature failure and AEAD failure.
Memory Handling
Section titled “Memory Handling”Implementations SHOULD:
- Zero secret key material after use when possible
- Avoid logging or serializing secret keys except for export
- Use secure memory allocations where available
Random Number Generation
Section titled “Random Number Generation”All random values (keypairs, nonces) MUST be generated using a cryptographically secure random number generator (CSPRNG).
Behavioral Specifications
Section titled “Behavioral Specifications”Default Values
Section titled “Default Values”| Configuration | Default | Description |
|---|---|---|
| HTTP timeout | 30000ms | Maximum time for HTTP request completion |
| Wait timeout | 30000ms | Maximum time to wait for email arrival |
| Poll interval (initial) | 2000ms | Starting interval for polling strategy |
| Poll max backoff | 30000ms | Maximum polling interval after backoff |
| Poll backoff multiplier | 1.5 | Multiplier for exponential backoff |
| Poll jitter factor | 0.3 | Random jitter (0-30%) to prevent thundering herd |
| Max retries | 3 | Maximum HTTP retry attempts |
| Retry delay | 1000ms | Base delay between retries (doubles each attempt) |
| SSE reconnect interval | 5000ms | Initial SSE reconnection delay |
| SSE max reconnect attempts | 10 | Maximum SSE reconnection attempts before fallback |
| SSE backoff multiplier | 2 | SSE reconnection backoff multiplier |
| SSE connection timeout | 5000ms | Time to wait for SSE before falling back (auto) |
| Default inbox TTL | 3600s | Default inbox time-to-live (1 hour) |
| Min inbox TTL | 60s | Minimum allowed inbox TTL |
| Max inbox TTL | 604800s | Maximum allowed inbox TTL (7 days) |
Email Filtering
Section titled “Email Filtering”waitForEmail supports these filter options:
| Filter | Type | Description |
|---|---|---|
subject | string | regex | Match email subject |
from | string | regex | Match sender address |
predicate | function | Custom filter function |
timeout | number | Max wait time (ms), default 30000 |
pollInterval | number | Polling interval (ms) |
Inbox Lifecycle
Section titled “Inbox Lifecycle”- Create: Generate keypair, register with server
- Use: Receive emails, decrypt, process
- Export (optional): Save keypair + metadata
- Delete: Clean up server resources
Inbox Methods
Section titled “Inbox Methods”| Method | Description | Returns |
|---|---|---|
getEmails() | Fetch and decrypt all emails | Email[] |
getEmail(id) | Fetch and decrypt specific email | Email |
getRawEmail(id) | Get decrypted raw RFC 5322 source | string |
waitForEmail(opts) | Wait for email matching filters | Email |
waitForEmailCount(n, opts) | Wait for at least N matching emails | Email[] |
watch() | Subscribe to new emails (returns channel/observable) | Channel<Email> |
onNewEmail(cb) | Subscribe with callback | Subscription |
getSyncStatus() | Get email count and hash for change detection | SyncStatus |
markEmailAsRead(id) | Mark email as read | void |
deleteEmail(id) | Delete specific email | void |
delete() | Delete inbox and all emails | void |
export() | Export inbox data including keys | ExportedInboxData |
isExpired() | Check if inbox TTL has passed | boolean |
Email Methods
Section titled “Email Methods”Email objects may include convenience methods for common operations:
| Method | Description | Returns |
|---|---|---|
markAsRead() | Mark this email as read | void |
delete() | Delete this email | void |
getRaw() | Fetch raw RFC 5322 source | RawEmail |
Client Methods
Section titled “Client Methods”| Method | Description | Returns |
|---|---|---|
createInbox(opts) | Create new inbox with auto-generated keypair | Inbox |
importInbox(data) | Import inbox from exported data | Inbox |
importInboxFromFile(path) | Import inbox from JSON file | Inbox |
exportInboxToFile(inbox, path) | Export inbox to JSON file | void |
deleteInbox(email) | Delete specific inbox | void |
deleteAllInboxes() | Delete all inboxes for API key | number (count) |
getInbox(email) | Get tracked inbox by email address | Inbox? |
getInboxes() | Get all tracked inboxes | Inbox[] |
watchInboxes(inboxes) | Monitor multiple inboxes simultaneously | InboxMonitor |
getServerInfo() | Get server configuration | ServerInfo |
checkKey() | Validate API key | boolean |
close() | Close client and release resources | void |
Subscription Pattern
Section titled “Subscription Pattern”For real-time email notifications, implement a subscription interface:
# Pseudocodeclass Subscription: def unsubscribe(self): """Stop receiving notifications and clean up resources""" pass
# Usagesubscription = inbox.on_new_email(lambda email: print(email.subject))# ... later ...subscription.unsubscribe()InboxMonitor (Multiple Inbox Watching)
Section titled “InboxMonitor (Multiple Inbox Watching)”For monitoring multiple inboxes simultaneously:
# Pseudocodemonitor = client.watch_inboxes([inbox1, inbox2, inbox3])monitor.on('email', lambda inbox, email: print(f"New email in {inbox.email_address}: {email.subject}"))# ... later ...monitor.unsubscribe() # Stop all subscriptionsEmail Processing Flow
Section titled “Email Processing Flow”1. Receive encrypted email data2. Verify ML-DSA-65 signature (MUST be first)3. Decapsulate KEM ciphertext4. Derive AES key via HKDF5. Decrypt metadata (from, to, subject)6. Decrypt parsed content (text, html, attachments)7. Decode attachment content from base648. Build Email object with all fieldsImplementation Checklist
Section titled “Implementation Checklist”Core Requirements
Section titled “Core Requirements”- ML-KEM-768 keypair generation
- ML-KEM-768 decapsulation
- ML-DSA-65 signature verification
- Server key pinning (compare payload key vs pinned key)
- AES-256-GCM decryption
- HKDF-SHA-512 key derivation with correct salt/info construction
- Base64url encoding/decoding
- HTTP client with retry logic
- API key authentication
- Derive public key from secret key (offset 1152)
Client Features
Section titled “Client Features”- Create inbox with auto-generated keypair
- Delete inbox / delete all inboxes
- Get inbox by email address
- List all tracked inboxes
- List emails in inbox (metadata only)
- Get specific email by ID (with full content)
- Get raw email source
- Mark email as read
- Delete email
- Wait for email with filters (subject, from, predicate)
- Wait for email count
- Watch/subscribe to single inbox
- Get sync status for change detection
- Check inbox expiration
Delivery Strategies
Section titled “Delivery Strategies”- SSE strategy with reconnection and backoff
- Polling strategy with exponential backoff and jitter
- Auto strategy (SSE with polling fallback)
- Strategy-agnostic subscription interface
- Sync after SSE reconnection (catch missed emails)
Advanced Features
Section titled “Advanced Features”- Export inbox (keypair + metadata to JSON)
- Import inbox from exported data
- Import/export to file
- Monitor multiple inboxes simultaneously (InboxMonitor)
- Authentication results validation helper
- Custom HTTP client support
Error Handling
Section titled “Error Handling”- All error types from hierarchy
- HTTP retry with exponential backoff
- Timeout handling for all operations
- SSE reconnection with backoff
- Server key mismatch detection (MITM protection)
- Graceful client close/cleanup
Email Object
Section titled “Email Object”- All fields populated (id, from, to, subject, text, html, etc.)
- Convenience methods (markAsRead, delete, getRaw)
- Auth results with validation method
- Attachment content base64 decoding
Testing
Section titled “Testing”- Unit tests for crypto operations
- Unit tests for transcript construction
- Unit tests for HKDF key derivation
- Integration tests against live server
- Error scenario coverage
- Concurrent inbox handling
- Import/export round-trip tests
- Interoperability tests (see client-interop)
Interoperability Testing
Section titled “Interoperability Testing”SDK implementations SHOULD pass the official interoperability test suite at github.com/vaultsandbox/client-interop. This suite validates:
- Cryptographic operations (KEM, signatures, AEAD, HKDF)
- Transcript construction for signature verification
- Export/import format compatibility across implementations
- Base64URL encoding/decoding
Appendix: Base64url Encoding
Section titled “Appendix: Base64url Encoding”VaultSandbox uses URL-safe Base64 (RFC 4648 Section 5):
Standard Base64: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/URL-safe Base64: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_Encoding Rules
Section titled “Encoding Rules”- Use
-instead of+ - Use
_instead of/ - Do NOT include
=padding characters - Implementations MUST reject input containing
+,/, or=
Encoding
Section titled “Encoding”def to_base64url(data: bytes) -> str: return base64.urlsafe_b64encode(data).rstrip(b'=').decode('ascii')Decoding
Section titled “Decoding”def from_base64url(s: str) -> bytes: # Add padding if needed padding = 4 - (len(s) % 4) if padding != 4: s += '=' * padding return base64.urlsafe_b64decode(s)Appendix: Transcript Construction
Section titled “Appendix: Transcript Construction”For signature verification, the transcript must be constructed byte-for-byte identical to the server:
def build_transcript(encrypted_data): version_bytes = bytes([encrypted_data.v]) # 1 byte
algs = encrypted_data.algs algs_ciphersuite = f"{algs.kem}:{algs.sig}:{algs.aead}:{algs.kdf}" algs_bytes = algs_ciphersuite.encode('utf-8')
context_bytes = HKDF_CONTEXT.encode('utf-8') # "vaultsandbox:email:v1"
ct_kem = from_base64url(encrypted_data.ct_kem) nonce = from_base64url(encrypted_data.nonce) aad = from_base64url(encrypted_data.aad) ciphertext = from_base64url(encrypted_data.ciphertext) server_sig_pk = from_base64url(encrypted_data.server_sig_pk)
return ( version_bytes + algs_bytes + context_bytes + ct_kem + nonce + aad + ciphertext + server_sig_pk )Appendix: HKDF Key Derivation
Section titled “Appendix: HKDF Key Derivation”def derive_key(shared_secret, context, aad, ct_kem): # Salt is SHA-256 hash of KEM ciphertext salt = sha256(ct_kem)
# Info construction context_bytes = context.encode('utf-8') # "vaultsandbox:email:v1" aad_length = len(aad).to_bytes(4, 'big') # 4 bytes, big-endian info = context_bytes + aad_length + aad
# HKDF with SHA-512 return hkdf_sha512( ikm=shared_secret, salt=salt, info=info, length=32 # 256 bits for AES-256 )Version History
Section titled “Version History”| Version | Date | Changes |
|---|---|---|
| 0.8.0 | 2026-01-04 | Aligned with VaultSandbox cryptographic protocol spec: |
- Export format: added version field, renamed secretKeyB64 | ||
to secretKey, removed publicKeyB64 | ||
| - Added import validation section with detailed steps | ||
| - Added payload validation (version, algorithms, sizes) | ||
| - Added security requirements (timing attacks, error messages, | ||
| memory handling, CSPRNG) | ||
| - Added Base64URL rejection requirement for invalid chars | ||
| - Added interoperability testing reference | ||
| 0.7.0 | 2025-12-30 | Simplified auth results: SDKs use wire format field names |
(result, details, signature, verified) directly. | ||
| Removed JSON→SDK field mapping requirement. | ||
| 0.6.0 | 2025-12-30 | Added: key sizes, client config options, inbox/email methods, |
| server key pinning, auth results JSON mapping, error types, | ||
| subscription patterns, expanded checklist | ||
| 0.5.0 | 2025-12 | Initial specification |