Search 36 exports & guides... /
npm GitHub ↗
● Built-In Native Swift & Kotlin Crypto + TCP Engine

Peer-to-Peer Encrypted TCP Messaging for React Native

@harbouli/fprot uses WebRTC exclusively as a temporary handshake channel to discover peers and exchange ephemeral X25519 keys. Once authenticated over a direct raw TCP socket, WebRTC is closed and 100% of application traffic travels over end-to-end encrypted TCP.

$npm install @harbouli/fprot react-native-webrtc
Ed25519 + X25519
Pinned Identity & Ephemeral PFS Session Keys
0 Bytes
Chat Payload Exposed to Signaling Server
Stop-and-Wait ARQ
Write-Ahead Journal & Idempotent ACKs
36 Exports
100% of Internal Utilities Exported
Interactive Wire Protocol Inspector

Stage 1 — Out-of-Band Signed Signal Envelope (SignalProtocol.ts):

{
  "body": "{\"v\":1,\"conversationId\":\"room-1\",\"from\":\"Ed25519_Pub_A\",\"to\":\"Ed25519_Pub_B\",\"type\":\"offer\",\"challenge\":\"Ch_16CharBase64\",\"attempt\":\"At_16CharBase64\",\"sdp\":\"...\"}",
  "signature": "Ed25519_Detached_Signature_Over_fprot.signaling.v1:body"
}

Complete Root Package Exports

Every class, cryptographic primitive, storage journal, signaling codec, stream framer, constant, and TypeScript type is exported directly from @harbouli/fprot:

import {
  // Core Classes & Transports
  ReliableConversation, P2PTcpPeer, WebSocketSignaling,
  MessageStore, LineDecoder, EventBus, TcpSocket,

  // Ed25519 Identity, Signatures & Encrypted At-Rest Storage
  loadOrCreateIdentity, validatePublicKey, validateIdentity,
  sign, verify, conversationStorageKey, createEncryptedStorage, validatePayload,

  // Ephemeral X25519 Session Keys & JSON Frame Encryption
  createSessionKeys, exportPublicKey, importPublicKey,
  encryptJson, decryptJson, randomId,

  // Cross-Platform Native Crypto Primitives & Base64URL
  toBase64Url, fromBase64Url, randomBytes, sha256,
  boxKeypair, boxSeal, boxOpen,
  signKeypair, signDetached, verifyDetached,
  secretboxKeygen, secretboxSeal, secretboxOpen,

  // Signaling Protocol Codecs & Constants
  encodeSignal, decodeSignal, encodeReliableSignal, decodeReliableSignal,
  encodePeerSignal, decodePeerSignal, encodeSdpSignal, decodeSdpSignal,
  MAX_MESSAGE_BYTES, MAX_MESSAGES, SIGNAL_VERSION, NONCE_BYTES,
} from '@harbouli/fprot';

Core Classes & Transports

ReliableConversation Class • src/reliable/ReliableConversation.ts

Immortal conversation supervisor that spawns disposable P2PTcpPeer instances across generations, persists outgoing messages in MessageStore before wire transmission, enforces Stop-and-Wait ARQ delivery receipts (inFlight), runs encrypted 10s heartbeats, and auto-reconnects with jittered exponential backoff.

new ReliableConversation(options: ReliableConversationOptions)
MemberSignatureBehavior
stateConversationState'stopped' | 'offline' | 'reconnecting' | 'connecting' | 'connected'
messagesChatMessage[]Deep-cloned array of all stored incoming and outgoing messages.
start()Promise<void>Validates Ed25519 keys, loads MessageStore from disk, starts signaling, and triggers peer discovery.
sendMessage(payload)Promise<string>Validates JSON purity (≤ 2,730 chars), commits to encrypted disk as 'pending', updates UI listeners, and flushes over TCP.
setAvailable(bool)voidConnects OS foreground/background and network state. Passing true resets retry count to 0 and discovers immediately.
reconnect()voidDrops current TCP peer and re-runs discovery with fresh getHostOptions() (use on Wi-Fi IP changes).
stop()voidStops signaling, destroys active peer, clears timers, and enters 'stopped'.
on(event, cb)UnsubscribeSubscribes to 'state', 'messages', or 'error' events.
P2PTcpPeer Class • src/P2PTcpPeer.ts

Single-session peer-to-peer transport. Gathers non-trickle WebRTC ICE candidates, exchanges ephemeral 32-byte X25519 public keys and the Host's TCP endpoint over an ordered WebRTC data channel (p2p-tcp-control), verifies a mutual encrypted TCP ready frame (seq: 1), and immediately closes WebRTC and the TCP listener.

new P2PTcpPeer(options?: P2PTcpPeerOptions)
MemberSignatureBehavior
createOffer(host)Promise<string>Starts TCP listener on host.listenHost/port, creates X25519 keys, gathers ICE candidates, and returns encoded offer.
acceptOffer(offer)Promise<string>Applies remote offer SDP, creates X25519 keys, gathers ICE candidates, and returns encoded answer.
acceptAnswer(answer)Promise<void>Applies Guest answer SDP on the Host; triggers control channel handshake and TCP connection.
sendMessage(payload)stringEncrypts payload with next monotonic txSequence and writes newline-delimited frame to TCP socket.
close()voidCloses TCP socket, server, and WebRTC, and zeroes ephemeral private key in memory (privateKey.fill(0)).
WebSocketSignaling Class • src/reliable/WebSocketSignaling.ts

Auto-reconnecting WebSocket transport for server/signaling.mjs performing 2-factor authentication (shared 32+ char token + Ed25519 signature over fprot.broker.v1:${nonce}).

new WebSocketSignaling(options: WebSocketSignalingOptions)
MessageStore Class • src/reliable/MessageStore.ts

Single-writer write-ahead persistent journal. Commits snapshots to KeyValueStorage before mutating in-memory rows. Provides load(), snapshot(), pending(), idempotent add(message), and acknowledge(id).

new MessageStore(storage: KeyValueStorage, key: string)
LineDecoder Class • src/internal/LineDecoder.ts

Newline-delimited (\n) TCP stream framer that enforces maxBytes before splitting to prevent memory exhaustion attacks.

new LineDecoder(maxBytes: number) // decoder.push(chunk: string): string[]
EventBus<Events> Class • src/internal/EventBus.ts

Generic type-safe event emitter with on(event, listener): Unsubscribe, emit(event, value): void, and clear(): void.

TcpSocket Object • src/internal/tcp.ts

Cross-platform TCP Server & Client bridge routing through built-in Swift/Kotlin FprotNative sockets on iOS/Android and node:net in Node/Jest tests.

TcpSocket.createServer(onConnect): TcpSocketServer TcpSocket.createConnection(options, onConnect?): TcpSocketConnection

Identity, Signature & Storage Utilities

loadOrCreateIdentityFunction
loadOrCreateIdentity(secureStorage: KeyValueStorage): Promise<PeerIdentity>

Loads existing Ed25519 keypair from secureStorage.getItem('fprot.identity.v1'), verifies it with a live signature self-check, or creates and saves a new 32B/64B Ed25519 identity keypair.

validatePublicKey & validateIdentityFunction
validatePublicKey(key: string): void validateIdentity(identity: PeerIdentity): void

Verifies canonical 32-byte base64url Ed25519 public keys and 64-byte private keys with a live sign/verify round-trip check over 'fprot.identity.check'.

sign & verifyFunction
sign(body: string, identity: PeerIdentity): string verify(body: string, signature: string, publicKey: string): boolean

Detached Ed25519 signing and boolean signature verification over UTF-8 strings.

conversationStorageKeyFunction
conversationStorageKey(id: string, local: string, remote: string): string

Computes fprot.chat.<base64url_sha256> over JSON.stringify([id, local, remote]) to isolate storage records per conversation pair.

createEncryptedStorage & validatePayloadFunction
createEncryptedStorage(storage: KeyValueStorage, secureStorage: KeyValueStorage): Promise<KeyValueStorage> validatePayload(payload: unknown): asserts payload is JsonValue

createEncryptedStorage wraps any storage backend with AEAD encryption (storing the 256-bit master key in secureStorage under fprot.storage-key.v1) and binds the record key name inside the authenticated plaintext. validatePayload enforces strict JSON purity (≤ 2,730 chars, rejecting undefined, NaN, Infinity, functions, and symbols).

Session & Native Cryptographic Utilities

createSessionKeys, exportPublicKey & importPublicKeyFunction
createSessionKeys(): Promise<SessionKeys> exportPublicKey(key: Uint8Array): string importPublicKey(key: string): Uint8Array

Generates ephemeral 32-byte X25519 keypairs and converts binary Uint8Array buffers to/from unpadded Base64URL.

encryptJson, decryptJson & randomIdFunction
encryptJson(value: unknown, remotePublicKey: Uint8Array, localPrivateKey: Uint8Array): EncryptedFrame decryptJson<T>(frame: EncryptedFrame, remotePublicKey: Uint8Array, localPrivateKey: Uint8Array): T randomId(): string

Encrypts and authenticates JSON values into { v: 1, nonce, ciphertext } frames and generates 16-character (96-bit entropy) Base64URL IDs.

toBase64Url, fromBase64Url, randomBytes & sha256Function
toBase64Url(bytes: Uint8Array): string fromBase64Url(str: string): Uint8Array randomBytes(size: number): Uint8Array sha256(utf8Input: string): Uint8Array

Low-level RFC 4648 §5 Base64URL codec, CSPRNG byte generator, and SHA-256 hash primitive.

boxKeypair / boxSeal / boxOpen / signKeypair / secretboxKeygenFunction
boxKeypair(): { publicKey: Uint8Array; privateKey: Uint8Array } boxSeal(plaintextUtf8: string, nonce12: Uint8Array, remotePub32: Uint8Array, localPriv32: Uint8Array): Uint8Array boxOpen(ciphertextWithTag: Uint8Array, nonce12: Uint8Array, remotePub32: Uint8Array, localPriv32: Uint8Array): string signKeypair() / signDetached(msg, secretKey64) / verifyDetached(msg, sig64, pub32) secretboxKeygen() / secretboxSeal(plain, nonce12, key32) / secretboxOpen(cipher, nonce12, key32)

Direct access to the cross-platform X25519 ECDH + AES-256-GCM / ChaCha20-Poly1305 and Ed25519 native cryptographic primitives.

encodeSignal, decodeSignal, encodePeerSignal & decodePeerSignalFunction
encodeSignal(signal: Signal, identity: PeerIdentity): string decodeSignal(raw: string, localKey: string, remoteKey: string, conversationId: string): Signal encodePeerSignal(type: 'offer' | 'answer', sdp: string): string decodePeerSignal(input: string, expectedType: 'offer' | 'answer'): SignalEnvelope

Reliable Ed25519-signed signaling codec (fprot.signaling.v1:) and low-level WebRTC SDP offer/answer codec.

Exported ConstantsConstants
ConstantValueDescription
MAX_MESSAGE_BYTES16384Base size constant (MAX_MESSAGE_BYTES / 6 = 2730 max serialized chars per chat message).
MAX_MESSAGES2000Maximum messages stored per conversation in MessageStore.
SIGNAL_VERSION1Version number for SignalEnvelope payloads.
NONCE_BYTES12Nonce byte length for boxSeal and secretboxSeal.
Part 2 • Architecture Deep Dive

What is Signaling & How Connection Setup Works

How two mobile devices solve the P2P Bootstrap Paradox using Ed25519-signed envelopes, anti-replay challenge/attempt nonces, and an ephemeral WebRTC control channel that self-destructs once TCP connects.

1. The P2P Bootstrap Paradox

Two mobile phones cannot open a direct TCP socket without prior coordination because:

  • Dynamic IP Addresses & Ephemeral Ports: Mobile IPs change on every Wi-Fi/Cellular switch, and the Host binds a temporary free OS port (port: 0) per attempt.
  • Asynchronous Availability: Device A and Device B may launch the app at different times and need a lightweight rendezvous signal (wake / request).
  • Pre-TCP Session Key Exchange: Both devices must exchange ephemeral 32-byte X25519 public keys before opening the TCP socket so the very first TCP frame (seq: 1) is already encrypted.

2. Two-Stage Handshake & Challenge-Attempt State Machine

sequenceDiagram autonumber participant H as Host (ReliableConversation) participant S as Signaling Broker participant G as Guest (ReliableConversation) H->>S: Signal wake (signed by Host Ed25519) S->>G: Forward wake Note over G: Generates 16-char challenge ID (reused if wake repeats) G->>S: Signal request (challenge, signed by Guest Ed25519) S->>H: Forward request Note over H: Saves challenge, generates 16-char attempt ID, opens TCP listener H->>S: Signal offer (challenge, attempt, sdp, signed by Host Ed25519) S->>G: Forward offer Note over G: Verifies signal.challenge == this.challenge, accepts Offer SDP G->>S: Signal answer (challenge, attempt, sdp, signed by Guest Ed25519) S->>H: Forward answer Note over H: Verifies challenge and attempt match, calls peer.acceptAnswer

3. Why Repeated wake Signals Never Invalidate In-Flight Offers

In ReliableConversation.ts (line 237), when the Guest receives a wake signal from the Host, it checks if (!this.challenge) this.challenge = randomId(). By reusing an outstanding challenge rather than overwriting it, a duplicate wake packet never invalidates an offer that the Host is currently gathering ICE candidates for.

4. Reference Broker Hardening (server/signaling.mjs)

ProtectionMechanismLimit
2-Factor Authcrypto.timingSafeEqual shared token + Ed25519 signature over fprot.broker.v1:${nonce}10s auth timeout
Rate LimitingPer-socket fixed-window counter disconnects flooding clients with code 1008120 msgs / 60s
Frame Size CapmaxPayload and perMessageDeflate: false prevent memory/zlib bombs132 KB max
Sender PinningEnforces JSON.parse(envelope.body).from === socket.publicKeyStrict match
Part 3 • Resilience Engineering

Reliability, Persistence & Fault Tolerance

How ReliableConversation and MessageStore guarantee zero message loss, strict FIFO ordering, and idempotent deduplication across network drops and app restarts.

1. Immortal Supervisor vs. Disposable Peers

Raw OS TCP sockets break whenever a mobile user switches between Wi-Fi and Cellular or backgrounds the app. fprot solves this by making P2PTcpPeer strictly single-use and disposable, while ReliableConversation acts as a persistent supervisor that spawns a fresh P2PTcpPeer with a new ++this.generation counter on every reconnect.

Every async callback checks this.current(peer, generation) so late events from a dying socket can never corrupt the new connection.

2. Write-Before-Memory Commit Pipeline (MessageStore)

sequenceDiagram autonumber participant UI as React Native UI participant RC as ReliableConversation participant MS as MessageStore Journal participant Disk as Encrypted AsyncStorage UI->>RC: await chat.sendMessage(payload) RC->>MS: await store.add(message with status pending) MS->>Disk: setItem(encrypted snapshot v1) Disk-->>MS: Write committed on disk MS->>MS: Update in-memory rows array MS-->>RC: Resolved RC->>UI: Emit messages event (shows pending) RC->>RC: flush() sets inFlight and transmits over TCP

3. Stop-and-Wait ARQ & Lost-ACK Idempotent Deduplication

  • Single In-Flight Lock (this.inFlight): Only the oldest pending outgoing message is transmitted at a time, guarded by a 10-second ackTimer.
  • Lost-ACK Recovery: If Device B receives message m1, saves it to disk, and sends { kind: "ack", id: m1 }, but the Wi-Fi drops before Device A receives the ACK, Device A will re-transmit m1 on the next generation's TCP connection.
  • Idempotent Receiver: Device B's MessageStore.add() detects that incoming:m1 already exists with identical sentAt and payload, returns false (preventing duplicate UI entries), and re-transmits the ack so Device A marks m1 as delivered!

4. Glare Immunity & Exponential Backoff with Jitter

While state === 'connected', ReliableConversation ignores all incoming signaling packets (if (this.state === 'connected') return;), relying solely on its encrypted TCP heartbeat (10s ping/pong, 35s idleTimeoutMs) to detect if the partner restarted.

Part 4 • Security & Wire Specification

Cryptographic Primitives & Wire Framing

Multi-layered defense combining pinned Ed25519 identities, ephemeral X25519 Diffie-Hellman session keys, 1-based monotonic sequence counters, and record-bound local storage encryption.

1. Three-Layer TCP Packet Encapsulation

// Layer 1: Raw newline-delimited TCP line (EncryptedFrame)
{"v":1,"nonce":"","ciphertext":""}\n

// Layer 2: Decrypted Session Payload (WirePayload in P2PTcpPeer.ts)
{
  "v": 2,
  "sender": "host",       // Verified against expected opposite role to prevent reflection attacks
  "seq": 2,               // Must equal this.rxSequence + 1 (strict anti-replay & anti-drop)
  "kind": "message",
  "message": {
    "id": "xY9zA2bC5dE8fG1h",
    "sentAt": 1780000000000,
    // Layer 3: ReliableConversation Packet (fprot.chat.v1)
    "payload": {
      "protocol": "fprot.chat.v1",
      "conversationId": "main-chat-room",
      "kind": "message",  // "message" | "ack" | "ping" | "pong"
      "id": "18c21a4f9b0d2e3c",
      "payload": "Hello over direct encrypted TCP!",
      "sentAt": 1780000000000
    }
  }
}

2. Security Threat Defense Matrix

Attack VectorDefense in @harbouli/fprot
Malicious Signaling Server (MITM)All SDP offers/answers are signed with Ed25519 (fprot.signaling.v1:) and verified against the pinned remotePublicKey.
Cross-Protocol Signature ReplayStrict domain separation prefixes: fprot.broker.v1: vs fprot.signaling.v1: vs fprot.identity.check.
TCP Ciphertext ReflectionWirePayload.sender is authenticated inside the ciphertext and checked against this.role === 'host' ? 'guest' : 'host'.
Unauthorized Port ScanningHost TCP server immediately destroys any socket connecting before WebRTC delivers remotePublicKey, and closes the listening port once seq: 1 is verified.
Storage Record SwappingcreateEncryptedStorage encrypts { name, value } together and verifies plain.name === name on read.
Part 5 • Backend Agnostic

Custom Signaling Backend Integration

Integrate ReliableConversation with your existing Socket.io, Node.js, Go, FastAPI, or Supabase Realtime infrastructure by implementing the 3-method SignalTransport interface.

1. The SignalTransport Contract

import type { SignalTransport } from '@harbouli/fprot';

export class CustomSocketSignaling implements SignalTransport {
  start(handlers: { onMessage: (msg: string) => void; onOnline: (online: boolean) => void }): void {
    // Subscribe to incoming signals for myPublicKey and report connection status
  }
  send(message: string): boolean {
    // Forward the signed JSON envelope string to the recipient specified in JSON.parse(JSON.parse(message).body).to
    return true;
  }
  stop(): void {
    // Close sockets and clean up listeners
  }
}