Skip to content

Repository files navigation

ohttp_dart

Pure Dart implementation of Oblivious HTTP (RFC 9458) — no native dependencies.

Features

  • OHTTP encapsulation / decapsulation (RFC 9458)
  • HPKE Base Mode Sender (RFC 9180) — hand-written from crypto primitives
  • Binary HTTP (RFC 9292) — serialize/parse HTTP messages
  • Transport-agnostic core — plug in any HTTP client via the OhttpTransport interface
  • package:http adapter — drop-in http.BaseClient replacement
  • TTL-based key config cache with single-flight deduplication
  • Configurable size limits for encrypted and decrypted responses

Cipher suite: DHKEM(X25519, HKDF-SHA256) + HKDF-SHA256 + AES-128-GCM

Architecture

package:ohttp_dart/ohttp_dart.dart   ← core (transport-agnostic)
package:ohttp_dart/http.dart         ← package:http adapter

The core library defines abstractions (OhttpTransport, OhttpSession, KeyConfigCache) that any HTTP client can plug into. The http.dart adapter provides HttpClientTransport and OhttpHttpClient for consumers using package:http.

OhttpSession orchestrates the full pipeline: cache lookup → BHTTP serialization → OHTTP encapsulation → transport call → decapsulation → BHTTP parsing. Cache invalidation happens automatically on gateway errors. When retryOnGatewayError is enabled (the default), a single automatic retry is performed with a freshly fetched key config.

Observer Pattern

Sessions accept an optional OhttpObserver to receive lifecycle event notifications:

class MyObserver extends OhttpObserver {
  @override
  void onKeyConfigFetched() => print('Key config fetched');

  @override
  void onKeyConfigCacheHit() => print('Using cached key config');

  @override
  void onPostToGateway() => print('Posting to gateway');

  @override
  void onDecapsulationError(Type errorType) => print('Decapsulation failed: $errorType');

  @override
  void onGatewayError(int statusCode) => print('Gateway error: $statusCode');

  @override
  void onCacheInvalidated() => print('Cache invalidated');

  @override
  void onEncapsulationError(Type errorType) => print('Encapsulation failed: $errorType');

  @override
  void onGatewayRetry() => print('Retrying after gateway error');

  @override
  void onRoundTripCompleted(Duration elapsed) => print('Round trip completed in $elapsed');

  @override
  void onRequestAborted(OhttpRequestStage stage) => print('Request aborted during $stage');
}

final session = OhttpSession.withTransport(
  transport: transport,
  observer: MyObserver(),
);

Observer methods have no-op defaults, so you only override the events you care about. Observer errors are suppressed via notifySafe() — they never affect the OHTTP pipeline. Callbacks only ever receive safe metadata (the error Type, an HTTP status code) — never keys, nonces, shared secrets, or plaintext request/response bodies.

Project Structure

lib/
├── ohttp_dart.dart                     # Core library entry point (transport-agnostic)
├── http.dart                           # Optional package:http adapter entry point
└── src/
    ├── bhttp.dart                      # Binary HTTP (RFC 9292)
    ├── bhttp_response_limits.dart      # Response size limits configuration
    ├── cipher_suite.dart               # Cipher suite constants
    ├── exceptions.dart                 # Sealed exception hierarchy
    ├── hpke.dart                       # HPKE Base Mode Sender (RFC 9180)
    ├── key_config_cache.dart           # TTL cache with single-flight for key configs
    ├── ohttp.dart                      # OHTTP encap/decap + KeyConfig (RFC 9458)
    ├── ohttp_data.dart                 # Request / response data types
    ├── ohttp_observer.dart             # Lifecycle event observer interface
    ├── ohttp_session.dart              # OHTTP session orchestrator
    ├── ohttp_transport.dart            # Transport abstraction interface
    ├── erasable_byte_array.dart        # Byte buffer that zeroes on erase(), guards post-erase reads
    ├── ohttp_constants.dart            # Centralized default values
    ├── data/
    │   └── key_config_fetch_result.dart # Result type for key config fetch (bytes + TTL)
    └── adapters/
        └── http/
            ├── http_client_transport.dart   # HttpClientTransport implementation
            └── ohttp_http_client.dart       # OhttpHttpClient drop-in replacement

Error Handling

All exceptions thrown by the library extend OhttpException (sealed class), so consumers can catch every library error with a single handler:

try {
  final response = await session.send(request);
} on OhttpException catch (e) {
  // All library errors land here
  print('OHTTP error: $e');
}

Specific exception types:

Type When
OhttpConfigException Invalid request/URL config (non-HTTPS scheme, bad authority, negative limits)
OhttpUnsupportedSuiteException KeyConfig advertises only unsupported KEM/KDF/AEAD
OhttpKeyConfigException Structurally malformed KeyConfig binary data (too short, wrong lengths, trailing data)
OhttpFormatException Malformed BHTTP data (wrong framing indicator, truncated fields, invalid varint)
OhttpGatewayException Gateway returned non-2xx response (includes statusCode; triggers cache invalidation)
OhttpDecapsulationException OHTTP response decapsulation failure (response too short, ciphertext too short for GCM tag)
OhttpCryptoException AES-GCM / HPKE crypto failure (includes optional cause)
OhttpSizeLimitException Response exceeds configured size limits (includes limit and actualSize)
OhttpNetworkException Network-level error — DNS, connection refused, etc. (includes optional cause)
OhttpTimeoutException HTTP request exceeded configured timeout (includes timeout duration and optional url)
OhttpRequestAbortedException Intentional client-side request cancellation (e.g. http.RequestAbortedException); a sibling of OhttpNetworkException, not a subtype, so an aborted request stays distinguishable from a real network error (includes optional cause)

Usage

Quick-start: drop-in http.BaseClient

import 'package:http/http.dart' as http;
import 'package:ohttp_dart/http.dart';
import 'package:ohttp_dart/ohttp_dart.dart';

final raw = http.Client();
final transport = HttpClientTransport(
  client: raw,
  keysUrl: Uri.parse('https://gateway.example.com/ohttp/config'),
  gatewayUrl: Uri.parse('https://gateway.example.com/ohttp/gateway'),
);
final session = OhttpSession.withTransport(transport: transport);
final client = OhttpHttpClient(session: session, closeWith: raw);

final response = await client.get(Uri.https('target.example.com', '/api/data'));
// Use response as a standard http.StreamedResponse
client.close();

Low-level: OhttpSession.send

final session = OhttpSession.withTransport(transport: transport);
final request = OhttpRequestData(
  method: 'GET',
  scheme: 'https',
  authority: 'target.example.com',
  path: '/api/data',
  headers: [('accept', 'application/json')],
);
final response = await session.send(request);

Session configuration

OhttpSession supports configurable limits for response sizes and a TTL override for the key config cache:

final session = OhttpSession.withTransport(
  transport: transport,
  maxEncryptedResponseBytes: 32 * 1024 * 1024, // 32 MiB (default: 16 MiB)
  keyConfigCacheTtl: Duration(minutes: 30),    // optional TTL override
  decryptedResponseLimits: BhttpResponseLimits(
    maxHeaderBytes: 32 * 1024,  // 32 KiB (default: 16 KiB)
    maxBodyBytes: 20 * 1024 * 1024, // 20 MiB (default: 10 MiB)
  ),
);

Key Config Cache TTL resolution (highest to lowest priority):

  1. Explicit keyConfigCacheTtl passed to withTransport (or ttl on KeyConfigCache)
  2. Server max-age from the Cache-Control header
  3. OhttpConstants.fallbackKeyConfigCacheTtl (1 hour)

no-cache / no-store in the server response disables caching (TTL = 0).

For full control over the cache, use the primary constructor:

final cache = KeyConfigCache(
  transport: transport,
  ttl: Duration(hours: 2),
);
final session = OhttpSession(
  transport: transport,
  cache: cache,
  maxEncryptedResponseBytes: 32 * 1024 * 1024,
);

Transport configuration

HttpClientTransport enforces HTTPS per RFC 9458 §1 and supports configurable timeouts:

final transport = HttpClientTransport(
  client: httpClient,
  keysUrl: Uri.parse('https://gateway.example.com/ohttp/config'),
  gatewayUrl: Uri.parse('https://gateway.example.com/ohttp/gateway'),
  fetchKeyConfigTimeout: Duration(seconds: 10),  // default: 30s
  postToGatewayTimeout: Duration(seconds: 15),   // default: 30s
);

For testing with non-HTTPS endpoints (e.g., MockClient with http://localhost):

final transport = HttpClientTransport.insecureForTesting(
  client: mockClient,
  keysUrl: Uri.parse('http://localhost:8080/keys'),
  gatewayUrl: Uri.parse('http://localhost:8080/gateway'),
);

Custom transport

Implement OhttpTransport to integrate with any HTTP client (Dio, etc.):

class DioTransport implements OhttpTransport {
  @override
  Future<KeyConfigFetchResult> fetchKeyConfig() async {
    // GET the key config URL, throw OhttpGatewayException on non-2xx
    // Return KeyConfigFetchResult(bytes: body, maxAge: parsedMaxAge)
  }

  @override
  Future<Uint8List> postToGateway(Uint8List body) async {
    // POST to gateway with Content-Type: message/ohttp-req
    // throw OhttpGatewayException on non-2xx
  }
}

Dependencies

Package Version Purpose
cryptography 2.9.0 Pure Dart crypto primitives (X25519, HMAC, AES-GCM)
http 1.6.0 HTTP client for the package:http adapter
meta 1.17.0 Annotations (@visibleForTesting)

Dev dependencies: test 1.25.6, kiri_check 1.3.1 (property-based testing), lints 3.0.0.

Testing

dart test

Tests cover:

  • RFC test vectors for HPKE (RFC 9180 Appendix A.1), HKDF (RFC 5869)
  • OHTTP encapsulation/decapsulation (RFC 9458)
  • BHTTP encoding/decoding (RFC 9292)
  • Key config TTL cache with single-flight deduplication
  • Observer lifecycle events and error suppression
  • Session orchestration and pipeline integration
  • package:http adapter integration
  • ErasableByteArray zeroing and post-erase guard

HPKE and BHTTP also have property-based tests (via kiri_check) alongside the fixed RFC vectors, and an integration fuzz suite (test/integration/fuzz_test.dart, also kiri_check) checks that varint decoding, BHTTP response parsing, and OHTTP KeyConfig parsing raise only typed OhttpException subtypes on arbitrary bytes.

License

See LICENSE.

About

Oblivious HTTP client for Dart

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages