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_BLAKE2s,Noise_IK_25519_ChaChaPoly_BLAKE2s, andNoise_NN_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-intracingfor non-sensitive logs.secure-mem: Best-effort memory hardening using platform mlock. UseLockedBytes<N>wrapper for sensitive key material.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 an internal epoch) using HKDF and a seed available to Ring. The secret is created inside a closure and passed directly to
snow::Builder::local_private_keyviazeroize::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.
- Epoch rotation is not implemented yet (epoch is currently fixed to 0); applications should use fresh device IDs (or a new seed) to rotate until explicit epoch rotation is added.
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);When you already know the server's static key (from a previous XX handshake or out-of-band):
- 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).
For first contact when the server's static key is unknown:
- Pattern:
XX. - Client:
NoiseClient::build_initiator_xx_tofu(hint) -> (HandshakeState, first_msg, hint). - Server:
NoiseServer::build_responder_xx(first_msg) -> (HandshakeState, response, server_pk). - Client:
NoiseClient::complete_initiator_xx(hs, response, hint) -> (HandshakeState, final_msg, server_identity, server_pk). - Server:
NoiseServer::complete_responder_xx(hs, final_msg, server_pk) -> (HandshakeState, client_identity). - After handshake: Pin the learned
server_pkand use IK for future connections.
use pubky_noise::datalink_adapter::{
client_start_xx_tofu, server_accept_xx, client_complete_xx, server_complete_xx
};
// Step 1: Client initiates (no server key needed)
let init = client_start_xx_tofu(&client, Some("server.example.com"))?;
// Step 2: Server accepts and responds with identity
let (s_hs, response, server_pk) = server_accept_xx(&server, &init.first_msg)?;
// Step 3a: Client completes and learns server's key
let (result, final_msg) = client_complete_xx(&client, init.hs, &response, init.server_hint.as_deref())?;
// Step 3b: Server completes
let (s_link, client_id) = server_complete_xx(&server, s_hs, &final_msg, &server_pk)?;
// Pin server_pk for future IK connections!
save_pinned_key(result.server_static_pk);
⚠️ Security Warning: The NN pattern provides forward secrecy only with NO identity binding. An active attacker can trivially MITM this connection. Use ONLY when:
- The transport layer provides authentication (e.g., TLS with pinned certs)
- You are building a higher-level authenticated protocol on top
- You explicitly accept the MITM risk for your use case
- Pattern:
NN. - Client:
NoiseClient::build_initiator_nn() -> (HandshakeState, first_msg). - Server:
NoiseServer::build_responder_nn(first_msg) -> (HandshakeState, response). - Client:
NoiseClient::complete_initiator_nn(hs, response) -> HandshakeState.
use pubky_noise::datalink_adapter::{client_start_nn, server_accept_nn, client_complete_nn, server_complete_nn};
// WARNING: NO AUTHENTICATION!
let (c_hs, first_msg) = client_start_nn(&client)?;
let (s_hs, response) = server_accept_nn(&server, &first_msg)?;
let c_link = client_complete_nn(&client, c_hs, &response)?;
let s_link = server_complete_nn(s_hs)?;
// DANGER: You have NO cryptographic proof of who you're talking to!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. Run under minimal privileges and treat host memory as potentially observable in crash/forensics scenarios.- 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.