Direct client↔server Noise sessions for Pubky using snow. Default build is direct-only. PKARR is optional metadata behind a feature flag.
- Direct transport first: XX for first contact, IK when the server static is pinned or delivered OOB.
- Keep Ring keys cold: device statics are derived on demand and passed directly to
snowwithout living in app buffers. - Simple integration: tiny DataLink-style adapter with
encryptanddecrypt. - App-layer binding: export a session tag to bind Paykit and Locks messages to the live channel.
- Footgun defenses: reject invalid peer statics that would yield an all-zero X25519 shared secret.
- A thin, conservative wrapper around
snowwith Pubky ergonomics. - A closure-based key feed so secrets do not leak into general app memory.
- A set of helpers for XX and IK patterns, identity binding, and a minimal adapter.
- Not a reimplementation of Noise.
- Not a messaging protocol or a full RPC layer.
- Not a PKARR transport (PKARR is optional out-of-band metadata only).
- Noise revision: 34 (as implemented by current
snow). - Suites:
Noise_XX_25519_ChaChaPoly_BLAKE2sandNoise_IK_25519_ChaChaPoly_BLAKE2s. - Hash: BLAKE2s. AEAD: ChaCha20-Poly1305. DH: X25519.
default = []: direct-only, no PKARR, no extra dependencies.pkarr: optional signed metadata fetch and verification for server static and epoch.trace: opt-intracingandhexfor non-sensitive logs.secure-mem: opt-in best-effort page pinning and DONTDUMP on supported OSes (server side).pubky-sdk: Convenience wrapper forRingKeyProviderusing Pubky SDKKeypair.storage-queue: Support for storage-backed messaging using Pubky storage as a queue (requirespubkyandasync-trait).
- Device X25519 static is derived per device and per epoch using HKDF and a seed available to Ring. The secret is created inside a closure and passed directly to
snow::Builder::local_private_keyviasecrecy::Zeroizing<[u8;32]>. - The app never stores the raw secret beyond the closure scope. No logs, no clones, no return of the secret to caller code.
- Rotation is achieved by bumping epoch. Ring can recreate the same statics for a device and epoch. Homeserver can revoke by policy.
Each session has a unique SessionId derived from the handshake state. This can be used to track sessions at the application layer.
let session_id = transport.session_id();
println!("Session ID: {}", session_id);
// SessionId is serializable for persistence
let bytes = session_id.to_bytes();
let restored = SessionId::from_bytes(bytes);The NoiseSessionManager allows managing multiple concurrent sessions.
let mut manager = NoiseSessionManager::new_client(client);
manager.add_session(session_id, link);
// For thread-safe access (important for mobile apps)
use pubky_noise::ThreadSafeSessionManager;
let safe_manager = ThreadSafeSessionManager::new_client(client);For mobile applications, use NoiseManager for full lifecycle management:
use pubky_noise::{NoiseManager, MobileConfig};
let config = MobileConfig::default(); // Auto-reconnect, mobile-friendly settings
let mut manager = NoiseManager::new_client(client, config);
// 3-step handshake
let (temp_id, first_msg) = manager.initiate_connection(&server_pk, None)?;
// ... send first_msg to server, receive response ...
let session_id = manager.complete_connection(&temp_id, &response)?;
// Save state before app suspension
let state = manager.save_state(&session_id)?;
// ... persist state ...
// Restore after app resume
manager.restore_state(state)?;See docs/MOBILE_INTEGRATION.md for complete mobile integration guide.
For messages larger than the Noise packet limit, use StreamingNoiseLink to automatically split and reassemble chunks.
let mut streaming = StreamingNoiseLink::new_with_default_chunk_size(link);
let chunks = streaming.encrypt_streaming(large_data)?;When direct connection is not possible or asynchronous messaging is required, you can use StorageBackedMessaging (requires storage-queue feature). This uses Noise for encryption but Pubky storage as a message queue.
This implementation follows the Outbox Pattern: senders write to their own repository (authenticated write), and receivers poll the sender's repository (public read).
It is critical to persist the read/write counters to avoid data loss or message replay across application restarts.
// Write to your own storage, read from peer's storage
let mut queue = StorageBackedMessaging::new(
link,
session,
public_client,
"/pub/me/outbox".to_string(),
"pubky://peer_pk/pub/peer/outbox".to_string()
).with_counters(saved_write_counter, saved_read_counter); // Resume from saved state
queue.send_message(b"hello async world").await?;
let msgs = queue.receive_messages(Some(10)).await?;
// Save new counters (critical for production!)
let write_counter = queue.write_counter();
let read_counter = queue.read_counter();
save_state(write_counter, read_counter);Configure retry logic for mobile networks:
use pubky_noise::RetryConfig;
let retry_config = RetryConfig {
max_retries: 3,
initial_backoff_ms: 100,
max_backoff_ms: 5000,
operation_timeout_ms: 30000,
};
queue = queue.with_retry_config(retry_config);- Pattern:
XX. - Client:
NoiseClient::build_initiator_xx_tofu(hint) -> (HandshakeState, first_msg). - Server:
NoiseServer::build_responder_read_xx(first_msg) -> HandshakeState. - Caller pins the server static post-handshake through an out-of-band path, then uses IK for future connections.
- Pattern:
IK. - Client:
NoiseClient::build_initiator_ik_direct(server_static_pub, hint) -> (HandshakeState, first_msg). - Server:
NoiseServer::build_responder_read_ik(first_msg) -> (HandshakeState, IdentityPayload).
cargo build
cargo test
use std::sync::Arc;
use pubky_noise::{NoiseClient, NoiseServer, DummyRing};
use pubky_noise::datalink_adapter::{
client_start_ik_direct, server_accept_ik, client_complete_ik, server_complete_ik
};
let ring_client = Arc::new(DummyRing::new([1u8;32], "kid"));
let ring_server = Arc::new(DummyRing::new([2u8;32], "kid"));
let client = NoiseClient::<_, ()>::new_direct("kid", b"dev-client", ring_client);
let server = NoiseServer::<_, ()>::new_direct("kid", b"dev-server", ring_server);
// assume you have the server static pinned OOB as `server_static_pk`
let server_static_pk: [u8;32] = [0; 32]; // mocked
// 3-step handshake
// Step 1: Client creates first message
let (c_hs, first_msg) = client_start_ik_direct(&client, &server_static_pk, None)?;
// Step 2: Server accepts and returns response
let (s_hs, client_id, response) = server_accept_ik(&server, &first_msg)?;
// Step 3: Both complete handshake
let mut c_link = client_complete_ik(c_hs, &response)?;
let mut s_link = server_complete_ik(s_hs)?;
// send data
let ct = c_link.encrypt(b"hello")?;
let pt = s_link.decrypt(&ct)?;
assert_eq!(&pt, b"hello");Structured error codes for mobile/FFI integration:
use pubky_noise::{NoiseError, NoiseErrorCode};
match result {
Err(e) => {
let code = e.code(); // NoiseErrorCode enum
let message = e.message(); // Owned String for FFI
// Map to platform-specific errors
}
Ok(data) => { /* success */ }
}Zeroizingreduces lifetime of secrets in memory but cannot guarantee full eradication across OS subsystems. For servers, enablesecure-memand run under minimal privileges.- Enforce input size caps and rate limits in your network layer to avoid trivial DoS.
- Keep
snowup to date. If suites change, bump minor version of this crate. - For mobile apps: Always persist session state and counters before suspension to avoid data loss.
This crate is designed for production mobile apps (iOS/Android) with:
- Lifecycle management:
NoiseManagerhandles session persistence and restoration - Thread safety:
ThreadSafeSessionManagerfor concurrent access - Network resilience: Automatic retry with exponential backoff
- Battery optimization: Configurable aggressive/conservative modes
- Error codes: FFI-friendly structured errors
Complete Guide: See docs/MOBILE_INTEGRATION.md for:
- State persistence patterns
- Thread safety guidelines
- Platform-specific considerations (iOS/Android)
- Network resilience best practices
- Memory management tips
This crate includes fuzz targets and concurrency tests:
# Run fuzz tests (requires nightly)
cd fuzz
cargo +nightly fuzz run fuzz_handshake -- -max_total_time=60
# Run loom concurrency tests
RUSTFLAGS="--cfg loom" cargo test --test loom_tests --release0.7.x: Mobile-optimized manager, thread-safe wrappers, simplified API, fuzz targets.0.6.x: Session management, streaming, and storage queue features.- Bump minor for API changes, patch for internal refactors and tests.