API: Secure API Gateway
Third-party integrators need a secure, authenticated way to use XorIDA threshold sharing without managing cryptography. The API package provides cryptographic key management, multi-window rate limiting, and a split-channel service with organization isolation and permission-based access control.
Executive Summary
The API package wraps PRIVATE.ME's core XorIDA threshold sharing in an authenticated, rate-limited service layer for third-party integrators.
Every API key is SHA-256 hashed before storage. Keys are validated on every request, checked for expiration, and verified against permission scopes. Three-tier rate limiting enforces per-minute, per-hour, and per-day request quotas. The split-channel service automatically handles share creation, storage, and reconstruction with HMAC integrity verification.
Organizations are isolated: keys can only access share sets belonging to their own org. Maximum 10 shares per split operation. Threshold must be at least 2. All operations use the Result<T, E> pattern — no thrown exceptions.
This package exists to give integrators a clean, authenticated API surface without requiring them to manage cryptographic primitives directly. The underlying XorIDA operations are handled by @private.me/crypto.
Developer Experience
API package provides structured error codes and clear validation feedback to help integrators build reliable services.
Structured Error Handling
Every operation returns a Result<T, E> with detailed error structures. Errors include machine-readable codes, human-readable messages, and actionable hints.
const result = await service.split(apiKey, { content: encoder.encode('Sensitive data'), threshold: 2, totalShares: 3, contentType: 'text/plain', }); if (!result.ok) { switch (result.error.code) { case 'INVALID_API_KEY': // The API key format is invalid or not registered console.error('Authentication failed', result.error.message); break; case 'RATE_LIMITED': // One of the rate limit windows is exhausted console.error('Rate limit exceeded', result.error.message); break; case 'INSUFFICIENT_PERMISSIONS': // Key lacks required permission scope console.error('Permission denied', result.error.message); break; } return; } // Success: use result.value const { uuid, shareIds } = result.value;
Error Categories
API package organizes errors into clear categories for systematic handling:
| Code | Category | When |
|---|---|---|
| INVALID_API_KEY | Authentication | Key format invalid or not registered |
| KEY_EXPIRED | Authentication | Key has exceeded its TTL |
| RATE_LIMITED | Rate Limiting | Per-minute, per-hour, or per-day quota exhausted |
| INSUFFICIENT_PERMISSIONS | Authorization | Key lacks required permission scope |
| INVALID_REQUEST | Validation | Request parameters invalid (threshold, totalShares) |
| RETRIEVE_FAILED | Reconstruction | Share reconstruction or HMAC verification failed |
The Problem
Third-party integrators need threshold sharing but should not manage cryptographic primitives directly. Every organization that builds on XorIDA reinvents authentication, rate limiting, and permission management.
API Key Sprawl
Storing plaintext API keys in databases creates catastrophic breach risk. The traditional model of plaintext storage with database-level encryption still exposes keys to anyone with database access. Keys stored in logs, error messages, or accidentally committed to version control become permanent liabilities.
Rate Limiting Gaps
Without rate limiting, a compromised key can exhaust computational resources or create denial-of-service conditions. Single-window rate limiters (per-minute only) allow burst abuse patterns that evade detection. Organizations need multi-window protection: per-minute for burst prevention, per-hour for sustained abuse, per-day for long-term quota enforcement.
Permission Complexity
Integrators need granular permission scopes: some keys should only create shares, others should only retrieve, others should manage keys. Without permission enforcement at the service layer, every integration must implement its own authorization logic. This creates inconsistency and security gaps.
Organizational Isolation
A multi-tenant API must guarantee that Organization A cannot access Organization B's share sets. Without enforced isolation, a single compromised key exposes all stored data across all tenants. Traditional row-level security in databases is fragile and easy to bypass.
Use Cases
The API package serves as the foundation for third-party integrations that need authenticated threshold sharing.
share:create permission generate shares on a high-side network, keys with share:retrieve reconstruct on the low side after manual courier transfer.
Architecture
The API package consists of three primary components: cryptographic key management, multi-window rate limiting, and an authenticated split-channel service.
API Key Management
API keys are generated with 32 bytes of cryptographic randomness via crypto.getRandomValues() and formatted with the xail_ prefix (64 hex characters). Only the SHA-256 hash of each key is stored. The plaintext key is shown once at creation and never persisted.
import { ApiKeyManager, DEFAULT_RATE_LIMIT } from '@private.me/api'; const keyManager = new ApiKeyManager(); const result = await keyManager.createKey( 'org-bank-123', 'Production Key', ['share:create', 'share:retrieve'], DEFAULT_RATE_LIMIT, 365 * 24 * 60 * 60 * 1000 // 1 year TTL ); if (!result.ok) { console.error(result.error.message); return; } // Show the key once and never again const { keyString, key } = result.value; console.log('API Key:', keyString); console.log('Key ID:', key.id); console.log('Store this key securely. It will not be shown again.');
Keys have configurable TTL (default: 1 year). Expired keys are rejected at validation time. Keys can be revoked immediately via revokeKey(keyId).
Multi-Window Rate Limiting
The rate limiter enforces three independent token bucket windows: per-minute, per-hour, and per-day. A request is rejected if any of the three windows is exhausted. Buckets auto-refill when their time window elapses.
import { RateLimiter, DEFAULT_RATE_LIMIT } from '@private.me/api'; const rateLimiter = new RateLimiter(); // Register limits for a key (called after key creation) rateLimiter.register(key.id, DEFAULT_RATE_LIMIT); // Check and consume a token on each request const result = rateLimiter.consume(key.id); if (!result.ok) { console.error('Rate limited:', result.error.message); return; } // Get remaining tokens per window const remaining = rateLimiter.getRemaining(key.id); console.log('Remaining:', remaining); // { minute: 59, hour: 999, day: 9999 }
Default limits: 60 requests/minute, 1000 requests/hour, 10000 requests/day. Custom limits can be configured per key at creation time.
Split-Channel Service
The split-channel service wraps @private.me/crypto's XorIDA operations with authentication, rate limiting, and HMAC integrity verification. Every request validates the API key, checks expiration, verifies permissions, and consumes a rate limit token before processing.
import { SplitChannelService } from '@private.me/api'; const service = new SplitChannelService(keyManager, rateLimiter); // Split content into threshold shares const encoder = new TextEncoder(); const content = encoder.encode('Sensitive document content'); const splitResult = await service.split(apiKey, { content, threshold: 2, totalShares: 3, contentType: 'text/plain', }); if (!splitResult.ok) { console.error(splitResult.error.message); return; } const { uuid, shareIds } = splitResult.value; console.log('Share set UUID:', uuid); console.log('Share IDs:', shareIds); // Later: retrieve content from shares const retrieveResult = await service.retrieve(apiKey, { uuid, shareIndices: [1, 2], // Any 2 of 3 shares }); if (!retrieveResult.ok) { console.error(retrieveResult.error.message); return; } const { content: reconstructed, contentType } = retrieveResult.value; const decoder = new TextDecoder(); console.log('Reconstructed:', decoder.decode(reconstructed));
Share sets are stored in memory indexed by UUID and organization ID. Only keys belonging to the same organization can access a share set. HMAC verification runs before reconstruction — if any share is tampered with, the operation fails closed.
Integration
The API package is designed for use in backend services. All state is held in memory. For production deployment, implement persistent storage adapters.
Installation
pnpm add @private.me/api
Dependencies: @private.me/shared (types), @private.me/crypto (XorIDA operations).
Complete Example
import { ApiKeyManager, RateLimiter, SplitChannelService, DEFAULT_RATE_LIMIT, } from '@private.me/api'; // Initialize components const keyManager = new ApiKeyManager(); const rateLimiter = new RateLimiter(); const service = new SplitChannelService(keyManager, rateLimiter); // Create an API key for an organization const keyResult = await keyManager.createKey( 'org-acme-corp', 'Production API Key', ['share:create', 'share:retrieve'], DEFAULT_RATE_LIMIT ); if (!keyResult.ok) throw new Error(keyResult.error.message); const { keyString, key } = keyResult.value; rateLimiter.register(key.id, DEFAULT_RATE_LIMIT); // Use the key to split content const encoder = new TextEncoder(); const content = encoder.encode('Confidential report'); const splitResult = await service.split(keyString, { content, threshold: 2, totalShares: 3, contentType: 'text/plain', }); if (!splitResult.ok) throw new Error(splitResult.error.message); console.log('Share set created:', splitResult.value.uuid); // Later: retrieve using the same key const retrieveResult = await service.retrieve(keyString, { uuid: splitResult.value.uuid, shareIndices: [1, 2], }); if (!retrieveResult.ok) throw new Error(retrieveResult.error.message); const decoder = new TextDecoder(); console.log('Reconstructed:', decoder.decode(retrieveResult.value.content));
Persistent Storage Adapter Pattern
For production deployments, wrap the API components with persistent storage adapters. The in-memory implementation is designed to be easily replaceable.
// Example: PostgreSQL adapter for ApiKeyManager interface ApiKeyStore { save(key: ApiKey): Promise<void>; findByHash(keyHash: string): Promise<ApiKey | null>; listByOrg(orgId: string): Promise<ApiKey[]>; revoke(keyId: string): Promise<boolean>; } // Example: Redis adapter for share sets interface ShareSetStore { save(orgId: string, uuid: string, shareSet: SplitResponse): Promise<void>; findByUuid(orgId: string, uuid: string): Promise<SplitResponse | null>; listByOrg(orgId: string): Promise<SplitResponse[]>; }
Security
The API package enforces defense-in-depth: cryptographic key hashing, multi-window rate limiting, permission-based authorization, and organizational isolation.
API Key Security
Keys are generated with crypto.getRandomValues() (32 bytes = 256 bits of entropy). Only the SHA-256 hash is stored. The plaintext key is returned once at creation and never persisted anywhere in the system.
Rate Limiting Defense
Three-tier rate limiting prevents both burst abuse and sustained attacks:
- Per-minute: Prevents burst abuse (default: 60 req/min)
- Per-hour: Prevents sustained attack patterns (default: 1000 req/hr)
- Per-day: Enforces long-term quotas (default: 10000 req/day)
A request is rejected if any of the three windows is exhausted. This prevents attackers from evading detection by spacing out requests just enough to pass a single-window limiter.
Permission-Based Authorization
Every API key has a list of permission scopes. The service validates permissions before processing each request:
| Permission | Operations Allowed |
|---|---|
| share:create | Create new share sets via split() |
| share:retrieve | Reconstruct content via retrieve() |
| share:list | List all share sets via listShareSets() |
| key:manage | Create, revoke, and list API keys (admin only) |
Organizational Isolation
Every API key is tagged with an organization ID. Every share set is tagged with the org ID of the key that created it. Retrieval requests validate that the requesting key belongs to the same org as the share set.
This guarantees multi-tenant isolation: Organization A cannot access Organization B's shares, even if A guesses B's share set UUID.
HMAC Integrity Verification
Every share includes an HMAC-SHA256 tag. Reconstruction verifies HMAC integrity before returning data. If any share is tampered with, the operation fails closed — no partial data leakage.
RETRIEVE_FAILED and logs the failure. The system never returns potentially corrupted data.
Performance
The API package introduces minimal overhead beyond the underlying XorIDA operations from @private.me/crypto.
Operation Overhead
Total authentication and authorization overhead: <2ms per request. The majority of request time is spent in XorIDA operations (splitting or reconstruction), not in the API layer.
Memory Footprint
In-memory storage is lightweight:
- API keys: ~200 bytes per key (hash, metadata, permissions)
- Rate limit buckets: ~80 bytes per key (3 windows × timestamps + counters)
- Share sets: Variable (depends on content size + number of shares)
For 10,000 API keys with active rate limit tracking: ~2.8 MB memory footprint (excluding share sets).
Throughput
Single-threaded throughput on a modern CPU (Apple M1):
- Key validation: ~50,000 ops/sec
- Rate limit checks: ~120,000 ops/sec
- Permission checks: ~200,000 ops/sec
The API layer is not the bottleneck. XorIDA splitting and reconstruction dominate request latency.
Limitations
The API package is designed for backend services with single-process deployments. Multi-process and distributed deployments require persistent storage adapters.
In-Memory State
All state (API keys, rate limit buckets, share sets) is held in memory. If the process restarts, all state is lost. For production deployments:
- Implement persistent storage adapters for API keys (e.g., PostgreSQL)
- Use distributed cache for rate limit state (e.g., Redis)
- Store share sets in durable storage (e.g., S3, database)
Single-Process Rate Limiting
Rate limiting state is not shared across processes. In a multi-process deployment (e.g., load-balanced instances), each process maintains independent rate limit buckets. A key with a 60 req/min limit could make 60 req/min to each process.
Solution: Use a distributed rate limiting backend (Redis, Memcached) for multi-process deployments.
No Auto-Expiration
The ApiKeyManager checks TTL per-operation but does not auto-purge expired keys. The RateLimiter refills buckets automatically but does not remove entries for revoked keys. The SplitChannelService stores share sets indefinitely.
For long-running servers, implement periodic cleanup:
// Periodically purge expired keys setInterval(() => { const allKeys = keyManager.listKeys('org-id'); for (const key of allKeys) { if (Date.now() > key.expiresAt) { keyManager.revokeKey(key.id); rateLimiter.remove(key.id); } } }, 60 * 60 * 1000); // Every hour
Maximum Share Limits
The service enforces a maximum of 10 shares per split operation. Threshold must be at least 2. These limits are enforced at the service layer and cannot be overridden.
Implementation Details
Deep dive into memory management, permission scopes, API surface, and error handling.
Memory Management
The API package uses in-memory Maps for all state. Understand the lifecycle and cleanup requirements.
State Storage
Three independent Map instances hold state:
- ApiKeyManager:
Map<string, ApiKey>(key hash → key record) - RateLimiter:
Map<string, BucketState>(key ID → bucket state) - SplitChannelService:
Map<string, SplitResponse>(UUID → share set)
Lifecycle Management
API keys are created via createKey() and revoked via revokeKey(). Revoked keys remain in the Map but are marked inactive. They are rejected at validation time.
Rate limit buckets are created via register() and removed via remove(). When a key is revoked, call remove(keyId) to free memory.
Share sets are created via split() and stored indefinitely. There is no built-in expiration. Implement application-level TTL if shares should expire.
Cleanup Strategy
// Run cleanup every hour setInterval(() => { // 1. Find and revoke expired keys const orgs = ['org-1', 'org-2']; // All orgs in your system for (const orgId of orgs) { const keys = keyManager.listKeys(orgId); for (const key of keys) { if (Date.now() > key.expiresAt) { keyManager.revokeKey(key.id); rateLimiter.remove(key.id); } } } // 2. Remove old share sets (example: 30-day TTL) const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; for (const orgId of orgs) { const shareSets = await service.listShareSets(adminKey); for (const shareSet of shareSets.value) { if (shareSet.createdAt < cutoff) { // Remove from internal Map (requires exposing a delete method) } } } }, 60 * 60 * 1000);
Permission Scopes
The API package enforces least-privilege access control via permission scopes.
Scope Definitions
| Scope | Operations | Typical Use |
|---|---|---|
| share:create | split() |
Application servers that create share sets |
| share:retrieve | retrieve() |
Application servers that reconstruct content |
| share:list | listShareSets() |
Admin dashboards, audit tools |
| key:manage | createKey(), revokeKey(), listKeys() |
Admin users, key rotation tools |
Permission Patterns
Full ACI Surface
Complete API reference for all exported functions and classes.
API Key Management
xail_ prefix + 64 hex chars (32 bytes entropy).xail_ prefix + 64 hex chars.Result<{keyString, key}, ApiError>. Default TTL: 1 year.true if revoked, false if not found.Rate Limiting
RATE_LIMITED error if any window exhausted.null if key not registered.Split-Channel Service
share:list permission.Error Reference
Complete error code taxonomy with descriptions and recovery hints.
| Code | Category | Description |
|---|---|---|
| INVALID_API_KEY | Authentication | The API key format is invalid or the key is not registered. Verify the key string matches the format xail_[64 hex chars]. |
| KEY_EXPIRED | Authentication | The API key has exceeded its TTL. Create a new key via createKey(). |
| RATE_LIMITED | Rate Limiting | One of the three rate limit windows (per-minute, per-hour, per-day) is exhausted. Wait for the window to reset. Use getRemaining() to check quotas. |
| INSUFFICIENT_PERMISSIONS | Authorization | The API key does not have the required permission scope for this operation. Check the key's permissions array. |
| INVALID_REQUEST | Validation | Request parameters are invalid. Common causes: threshold < 2, totalShares > 10, threshold > totalShares, or invalid share indices. |
| RETRIEVE_FAILED | Reconstruction | Share reconstruction failed. Shares may be corrupted or the HMAC integrity check did not pass. Verify shares were not tampered with. |
Error Handling Best Practices
const result = await service.split(apiKey, request); if (!result.ok) { const { code, message } = result.error; switch (code) { case 'INVALID_API_KEY': // Log security event, reject request logger.security('Invalid API key attempt', { keyPrefix }); return { status: 401, error: 'Unauthorized' }; case 'KEY_EXPIRED': // Notify user, trigger key rotation flow notifyAdmin('API key expired, rotation required'); return { status: 401, error: 'Key expired' }; case 'RATE_LIMITED': // Return 429 with Retry-After header const remaining = rateLimiter.getRemaining(keyId); return { status: 429, error: 'Rate limit exceeded', retryAfter: remaining?.minute === 0 ? 60 : 3600 }; case 'INSUFFICIENT_PERMISSIONS': // Log authorization failure logger.warn('Permission denied', { keyId, operation: 'split' }); return { status: 403, error: 'Forbidden' }; case 'INVALID_REQUEST': // Return 400 with validation details return { status: 400, error: message }; case 'RETRIEVE_FAILED': // Log integrity failure, potential tampering alert logger.security('HMAC verification failed', { uuid }); return { status: 500, error: 'Reconstruction failed' }; default: // Unexpected error, log and return 500 logger.error('Unexpected API error', { code, message }); return { status: 500, error: 'Internal server error' }; } }
Deployment
The API package is available upon request for enterprise integrators requiring an authenticated API layer over XorIDA threshold sharing.
Integration Requirements
# Add to package.json dependencies { "dependencies": { "@private.me/api": "^0.1.0" } } # Install via private registry pnpm install
Environment Configuration
The API package requires minimal configuration. All cryptographic operations are handled internally by @private.me/crypto.
# API service configuration API_PORT=8080 API_LOG_LEVEL="info" # Rate limiting configuration (requests per window) RATE_LIMIT_MINUTE=60 RATE_LIMIT_HOUR=1000 RATE_LIMIT_DAY=10000 # API key TTL (in milliseconds) API_KEY_TTL=86400000 # 24 hours
Production Deployment
The API package is designed for server-side integration. It is not a standalone CLI or HTTP service — it is a TypeScript library that your application imports and uses.
import { createSplitChannelService, createKeyManager } from '@private.me/api'; // Initialize services const keyManager = createKeyManager(); const splitService = createSplitChannelService(); // Create API key for a new org const keyResult = await keyManager.createKey({ orgId: 'org-12345', permissions: ['split', 'retrieve'], ttl: 86400000 // 24 hours }); if (!keyResult.ok) { throw new Error(keyResult.error.message); } // Use in HTTP handler app.post('/api/split', async (req, res) => { const apiKey = req.headers['x-api-key']; const result = await splitService.split(apiKey, req.body); if (!result.ok) { return res.status(400).json({ error: result.error }); } res.json(result.value); });
Security Best Practices
- API Key Rotation: Rotate keys regularly using the
ttlparameter. Monitor expiration and notify clients before keys expire. - Rate Limit Tuning: Adjust rate limits based on your integration's traffic patterns. Default limits are conservative.
- Scope Isolation: Use permission scopes to limit key capabilities. Grant only the minimum required permissions.
- Organization Boundaries: Every API key is bound to an
orgId. Keys cannot access shares from other organizations. - HMAC Verification: All shares include HMAC integrity checks. Failed verification indicates tampering — log and alert immediately.
- Transport Security: Always use HTTPS/TLS when transmitting API keys and shares. Never expose keys in URLs or logs.