Skip to content

thatSFguy/reticulum-lora-webclient

Repository files navigation

reticulum-lora-webclient

A browser-based Reticulum messaging client. Connects either directly to an RNode LoRa modem over Web Bluetooth or Web Serial, or to any running Reticulum daemon (rnsd) over a WebSocket bridge, and exchanges encrypted LXMF messages — including file and image attachments — with Sideband, NomadNet, MeshChat, and other Reticulum nodes anywhere on the network. It also browses NomadNet pages (micron markup, interactive forms, tables, file downloads).

Live app: https://thatsfguy.github.io/reticulum-lora-webclient/

No build step, no framework, no bundler. Plain ES modules, loaded directly in the browser. The LoRa path runs entirely in the browser with no server. The TCP-via-WebSocket path needs a small bridge process to sit between the browser and an existing rnsd — pick either the prebuilt Go binary (no runtime to install; shows a live connection-status screen) or the Python script (tools/ws_bridge.py).

What it does

  • Connects over any of three transports:
    • Web Bluetooth to an RNode (primary, Chrome/Edge/Brave on desktop and Android).
    • Web Serial to an RNode (desktop fallback).
    • WebSocket to a local or remote rnsd via a small bridge script (any modern browser, including Safari and Firefox).
  • Configures the radio (frequency, bandwidth, spreading factor, coding rate, TX power) and turns it on — when talking to an RNode. When talking to rnsd over WebSocket there is no radio to configure; the network config lives on the daemon side.
  • Generates and persists an Ed25519 / X25519 Reticulum identity in IndexedDB. Export, import, or regenerate it from the Settings panel.
  • Sends and receives Reticulum announces, with ratchet emission and rotation — a fresh ratchet keypair is advertised and rotated on every announce (auto-announce fires once at connect and every five minutes thereafter, so relay identity caches stay warm).
  • Encrypts and decrypts LXMF messages using the standard Reticulum ECDH + HKDF + AES-256-CBC + HMAC-SHA256 scheme. Short text goes out as an opportunistic single packet, retried until a delivery PROOF returns; a message too large for one packet is automatically upgraded to a Link (single link packet, or a Resource if larger). The conversation view shows per-message state (sending → sent ✓ → delivered ✓✓ / failed).
  • Initiates Reticulum Links and transfers Resources — file and image attachments (with optional captions) are sent to a contact over an initiator Link as a Resource.
  • Acts as link responder too: validates LINKREQUESTs, emits LRPROOFs signed with our long-term Ed25519 key, handles LRRTT, decrypts inbound link traffic, and sends per-packet PROOF receipts back so senders do not retry forever. Sideband and MeshChat round-trip cleanly both ways.
  • Browses NomadNet pages over initiator Links + the REQUEST/RESPONSE protocol: renders micron markup (headings, colors, links, tables), interactive form fields (text, checkbox, radio) with submit, file downloads, plus a node sidebar with bookmarks and history.
  • Node aliases and contact-card exchange (paste, QR scan, or manual hash).
  • Filters the contact list by LXMF name_hash so announces from telemetry beacons, heartbeats, or other non-LXMF destinations do not pollute it. Contacts get an unread-count badge and a small delete button in the sidebar.
  • Stores identity, contacts, and message history locally in IndexedDB. Messages are sorted in the conversation view by their IndexedDB insertion order, which keeps the timeline correct even when a clockless LoRa sender reports a nonsense timestamp. Nothing leaves your browser except over the radio link.

What it does not do (yet)

  • Propagation node / store-and-forward. No offline delivery; both parties must be on the air at the same time.
  • Multi-hop transport routing. We are a leaf node, not a transport/router — no routing tables.
  • NomadNet partials / server-side includes (rendered as a placeholder) and identify-on-connect for ALLOW_LIST (auth-gated) pages.
  • No IFAC, no LXMF stamp emission (inbound stamps are handled for signature verification), no GROUP destinations.

See CLAUDE.md for the scope rules and implementation plan, and docs/PROTOCOL_NOTES.md for the detailed Reticulum / LXMF interop findings accumulated while building this client.

Platform support

Platform Web Bluetooth Web Serial WebSocket (TCP via bridge) Works?
Chrome Android Yes No Yes Primary target
Chrome/Edge desktop Yes Yes Yes Dev and daily use
Brave desktop Yes Yes Yes Works
Safari (iOS/macOS) No No Yes WebSocket only
Firefox No No Yes WebSocket only

WebSocket works everywhere, which is the practical way to use the client from Safari, Firefox, or iOS. The LoRa-over-RNode paths require a browser that implements Web Bluetooth or Web Serial.

Web Bluetooth requires HTTPS (or http://localhost). GitHub Pages and any other HTTPS host are fine. WebSocket from an HTTPS page must be wss:// (see the TCP section below for the mixed-content caveat).

Running it

Because it is all static files with ES module imports, any HTTPS static host works. Locally:

# from the project root
python -m http.server 8000

Then open http://localhost:8000/ in Chrome, Edge, or Brave. localhost is treated as a secure origin, so Web Bluetooth and Web Serial are both available without a certificate.

For a public deploy, push to gh-pages (or any static bucket) and visit the HTTPS URL directly. No build step.

Using it

  1. Connect. Click Connect (BLE) and pick your RNode from the Bluetooth chooser, or click Connect (Serial) and select the USB serial port, or click Connect (WebSocket) with a bridge URL to reach a remote rnsd (see the TCP section below). The webapp will detect the modem, read firmware version and battery, and auto-start the radio with the values in the collapsible Radio Configuration panel — or, on the WebSocket path, skip all radio config and go straight to the messaging UI.
  2. Set your display name and click Send Announce. This broadcasts your identity and destination to the network so other Reticulum nodes can learn how to reach you. Your LXMF address is shown under Your Identity.
  3. Wait for announces. When another node announces, it shows up in the contact list on the left.
  4. Open a conversation. Click a contact to open the conversation view, type a message, and hit Enter. Incoming messages from that contact land in the same view.

Identity persists across reloads. Export Identity writes a JSON file containing your private keys; Import Identity loads such a file back in (replacing the current identity, then reloading the app — export the current one first if you want to keep it); New Identity generates a fresh keypair (and will change your LXMF address).

To browse NomadNet pages, open the NomadNet view, pick a discovered node (or paste a node hash), and navigate its pages — links, forms, tables, and /file/ downloads all work over the same Reticulum Link machinery.

TCP (WebSocket) connection

The "Connect (WebSocket)" option lets the web client join a Reticulum network through an existing rnsd instead of talking to a local LoRa radio. This is how you use the client from Safari, Firefox, or iOS (none of which have Web Bluetooth or Web Serial), how you use it from a machine that has no RNode attached, and how you reach a wider Reticulum mesh that spans TCP, I2P, or another backbone configured on the daemon side.

Architecture

Browsers cannot open raw TCP sockets — the security model only exposes HTTP, WebSocket, and WebTransport. So the web client's "TCP" option really speaks WebSocket to a small bridge script which sits in front of your rnsd's TCP interface and copies bytes in both directions:

┌──────────────┐   WebSocket    ┌──────────────┐   TCP    ┌─────────┐   LoRa / I2P / TCP
│  Browser     │ ◄────────────► │ ws_bridge.py │ ◄──────► │  rnsd   │ ◄─────────────────►  Reticulum network
│  web client  │  (HDLC frames) │              │          │         │
└──────────────┘                └──────────────┘          └─────────┘
  • The web client builds raw Reticulum packets the same way it does for the LoRa path, but frames them in HDLC (0x7E flag, 0x7D escape) instead of KISS before handing them to the transport.
  • The bridge process — either the Go binary (ws_bridge.exe on Windows, ws_bridge on Linux/macOS, prebuilt and attached to each bridge-v* GitHub release) or the Python script (tools/ws_bridge.py) — accepts WebSocket connections, opens a TCP connection to an rnsd running a TCPServerInterface, and forwards raw bytes in both directions without parsing any frames.
  • rnsd receives HDLC frames from the bridge exactly the way it does from any other TCP peer — the bridge is indistinguishable on the wire from a local TCP client.

The Go binary is the default suggestion: ~3-4 MB, no runtime dependency, no pip install, instant start. The Python script is there as a no-build fallback if you already have Python and prefer not to download a binary.

Identity and all protocol work stays in the browser. rnsd is only acting as a transport — it does not own your Reticulum identity, does not see your private keys, and does not decrypt your messages. From rnsd's point of view, the browser is a peer node on its TCP interface.

Step-by-step setup

1. Install and configure rnsd on the machine that will run the bridge (can be the same machine as the browser, or a server on your network).

pip install rns

Edit ~/.reticulum/config (create it if it does not exist) and add a TCP server interface:

[[RNS TCP Server Interface]]
    type = TCPServerInterface
    interface_enabled = True
    listen_ip = 0.0.0.0
    listen_port = 4242

Along with whatever other interfaces you want to use as your network backbone — another TCPClientInterface pointing at a public RNS node, an I2PInterface, an AutoInterface for LAN discovery, a RNodeInterface if you have an RNode plugged in directly, etc. See upstream Reticulum documentation for options.

Start rnsd:

rnsd

Leave it running. You should see a line like Listening for TCP connections on 0.0.0.0:4242.

2. Get the bridge. Pick one of the two paths.

2a. Prebuilt Go binary (recommended). Easiest is the in-app Connect via TCP → Download the bridge button, which links to a fixed, version-independent URL on this site. You can also grab those directly:

  • https://thatsfguy.github.io/reticulum-lora-webclient/bridge/ws_bridge-windows-amd64.exe — Windows 10/11 64-bit
  • https://thatsfguy.github.io/reticulum-lora-webclient/bridge/ws_bridge-linux-amd64 — Linux 64-bit
  • https://thatsfguy.github.io/reticulum-lora-webclient/bridge/ws_bridge-darwin-arm64 — macOS Apple Silicon

These are mirrored on every deploy from the bridge releases page (which also has the per-version filenames and SHA256SUMS.txt). The stable Pages URLs exist specifically so the download link doesn't change across versions — GitHub's own release-asset URLs rotate a signed token on every request, which breaks Windows SmartScreen "report as safe" (it can't attach to a URL that never recurs). If you report the Windows binary to https://www.microsoft.com/wdsi/filesubmission, use the fixed …/bridge/ws_bridge-windows-amd64.exe URL above, not the release-assets.githubusercontent.com/…?sig=… URL the browser redirects to. (The durable fix for the unsigned-binary warning is code signing; the stable URL just makes the report and per-URL reputation actually stick.)

Then verify the download against the published SHA256SUMS.txt:

sha256sum -c SHA256SUMS.txt          # Linux / macOS / Git Bash
certutil -hashfile ws_bridge-*.exe SHA256   # PowerShell on Windows

On Linux / macOS, chmod +x ws_bridge-* once after downloading.

2b. Python script (alternative). If you'd rather not download a binary:

pip install websockets

The Python bridge depends only on websockets (stdlib asyncio does the rest). rns is already installed from step 1.

3. Start the bridge. It listens on ws://localhost:7878 by default. The Reticulum daemon target (host:port) is supplied by the webapp at connect time — the bridge itself takes no rnsd flags (Go bridge) or uses defaults (localhost:4242, Python bridge).

# Go binary (Windows)
ws_bridge.exe                          # listen on localhost:7878
ws_bridge.exe -bind 0.0.0.0 -port 7878 # LAN-visible, custom port

# Go binary (Linux / macOS)
./ws_bridge-*-linux-amd64              # same defaults

# Python script
python tools/ws_bridge.py
python tools/ws_bridge.py --ws-host 0.0.0.0 --ws-port 7878 --rnsd-host 10.0.0.5 --rnsd-port 4242

The Go bridge clears the terminal and shows a live status screen — version, listen address, each connected client (browser, rnsd target, bytes up/down), total throughput, and links to the webapp/source. Run it with -plain to keep the old scrolling log instead (for running as a service or when output is redirected). The Python bridge prints a two-line banner. Either way, a running bridge is the signal you can connect.

Per-connection rnsd target — the practical difference between the two bridges: the Go bridge accepts the rnsd host:port from the webapp via query parameters on every connection, so one running bridge can serve any number of webapp instances pointed at any number of different rnsds without restart. The Python bridge ignores those query parameters and always uses its own --rnsd-host/--rnsd-port flags from startup; the same webapp UI works against either bridge.

4. Open the web client — either the live GitHub Pages URL or a local python -m http.server 8000 copy — and hit Connect (WebSocket). Two fields in the connect card:

  • WebSocket bridge URL — defaults to ws://localhost:7878. Change only if your bridge runs elsewhere.
  • Reticulum daemon (host:port) — the rnsd you want to reach. On a fresh install this is prefilled with a public Reticulum hub picked at random (the ↻ button rerolls to another, spreading new-user load across hubs instead of concentrating it on one), so you can just click Connect. Override it with your own daemon (e.g. localhost:4242) any time — a custom value sticks. Required by the Go bridge; ignored by the Python bridge but harmless to fill in.

Both fields persist across reloads (localStorage). The log panel will print WebSocket connected and Connected to Reticulum network via WebSocket; the messaging panel appears without any radio-config step.

5. Announce yourself. Enter a display name and click Send Announce. Within a second or two your announce should show up in any other Reticulum client connected to the same network — including Sideband and MeshChat if they are on the same backbone.

Mixed-content caveat

If you load the web client from https://thatsfguy.github.io/reticulum-lora-webclient/ and try to connect to ws://localhost:7878, the browser will refuse. Modern browsers block plain ws:// connections from HTTPS pages as a mixed-content policy. Three ways around it:

  1. Load the web client locally, not from GitHub Pages. python -m http.server 8000 from the repo root and open http://localhost:8000/. Now ws://localhost:7878 is same-origin in terms of scheme compatibility and the browser allows it. This is the fastest way to try the TCP path.

  2. Serve the bridge as wss:// with a certificate the browser trusts. With the Python bridge, edit tools/ws_bridge.py to wrap the websockets.serve call in an ssl_context. The Go binary doesn't currently have a built-in TLS flag — option 3 below is the right path for that. Either way, any cert works as long as the browser trusts it — letsencrypt, a self-signed cert you imported into the OS trust store, or a development cert from mkcert. Then update the URL field in the web client to wss://your.domain:7878.

  3. Use a reverse proxy. Run nginx or caddy in front of the bridge with a TLS cert, terminating TLS and forwarding wss:// to the plain bridge. This is the production story for anything exposed to the internet, and the recommended way to put TLS in front of the Go binary.

Option 1 is fine for one-machine testing. Option 3 is the right answer for anything you want to keep running.

Security

The browser owns your Reticulum identity. Your Ed25519 and X25519 private keys live in IndexedDB in the browser where you are running the web client. The bridge and the rnsd never see them. If you expose the WebSocket bridge to the open internet without TLS, an attacker between you and the bridge can observe every encrypted Reticulum packet you send and receive, but cannot impersonate you or read your LXMF messages (both ends of the ECDH are protected inside the Reticulum protocol). That said, running plaintext WebSocket to a bridge is still a bad idea for general use; use wss:// for anything beyond localhost.

Public-facing rnsd instances that accept TCP connections should probably require IFAC (interface access codes) or be tunneled through something with authentication. The bridge is a dumb forwarder — it will happily connect any WebSocket client to the rnsd it is configured to talk to. If you expose the bridge publicly without locking down the rnsd, anyone who can reach the WebSocket port can inject packets into your Reticulum network.

Troubleshooting

  • "WebSocket error before open" immediately after clicking Connect. The bridge is not running, or is listening on a different port, or the URL in the field is wrong. Verify with curl -v http://localhost:7878/ — a running bridge will respond with an HTTP 400 (WebSocket Upgrade Required), which is good.
  • Connection opens then immediately closes, bridge logs cannot reach rnsd. rnsd is not running, or its TCP interface is on a different port, or is bound to a different address than the bridge is trying to connect to. Check the rnsd logs for Listening for TCP connections on ….
  • Connected but no announces appear. rnsd has no upstream network interface configured (only the TCP server interface, which is how the bridge reached it). Edit ~/.reticulum/config to add a backbone interface that actually touches other nodes.
  • Announces appear but nobody can reach you. Check that you have clicked Send Announce at least once, and that the log is showing Periodic announce skipped every 5 minutes without error. Relay identity caches do expire; that is why the periodic re-announce is mandatory.
  • Works on Chrome but not Safari. You are probably loading the live GitHub Pages URL and running into the mixed-content block. Serve the static files locally (python -m http.server 8000) and try again.

Architecture

All Reticulum protocol logic runs in the browser — identity, announce, encrypt/decrypt, LXMF, link handshake, retry queue, packet receipts. What changes between transports is only how the finished raw Reticulum packet gets from our browser out onto the network.

                                 ┌──► KISS ──► RNode fw ──► SX126x ──► LoRa RF   (BLE / Serial path)
                                 │
Browser (all protocol logic) ────┤
                                 │
                                 └──► HDLC ──► WebSocket ──► ws_bridge ──► rnsd ──► network  (WebSocket path)

The BLE / Serial path needs an RNode and gives you direct-to-LoRa messaging with no server. The WebSocket path needs rnsd and a small bridge script, but runs everywhere (including Safari, Firefox, iOS) and can reach any Reticulum network rnsd is connected to — LoRa via a local RNode, TCP backbones to public nodes, I2P, AutoInterface LAN discovery, whatever you configure on the daemon side.

Module layout

reticulum-lora-webclient/
  index.html              Single-page app shell
  css/style.css           Dark theme

  js/
    ble-transport.js       Web Bluetooth NUS byte stream
    serial-transport.js    Web Serial byte stream
    websocket-transport.js WebSocket byte stream (for the TCP-via-bridge path)
    kiss.js                KISS frame encode/decode for the RNode path
    hdlc.js                HDLC frame encode/decode for the rnsd path
    rnode.js               RNode command layer (detect, configure, send/recv over KISS)
    rnsd-interface.js      Reticulum-direct interface over HDLC+WebSocket
                           (exposes the same shape as rnode.js so app.js doesn't branch)
    reticulum.js           Reticulum packet header encode/decode + constants
    identity.js            Ed25519 + X25519 keypair, identity hash, destination hash, ratchet
    crypto.js              ECDH + HKDF + Token (AES-256-CBC + HMAC-SHA256)
    announce.js            Build, parse, and validate Reticulum announces (incl. ratchet)
    link.js                Reticulum Link: responder validation, initiator handshake,
                           LRPROOF build/verify, link_id derivation, signalling encoding,
                           Token encrypt/decrypt over the derived link key
    resource.js            Resource transfer (multi-packet): send + receive, proofs
                           (attachments, large messages, NomadNet pages/files)
    lxmf.js                LXMF message pack/unpack + signature
    nomadnet.js            NomadNet REQUEST/RESPONSE protocol + link-target parsing
    micron.js              Micron markup → HTML (headings, styling, links, fields, tables)
    known-destinations.js  Well-known destination hash labels
    store.js               IndexedDB for identity, contacts, messages, nodes, bookmarks
    app.js                 UI controller and state management
    (transport-config.js, transport-flasher-app.js, dfu.js power the RNode flasher page)

  hubs.json                Curated public RNS hubs prefilled into the TCP connect field
  tools/                   Go + Python WS bridges, Python RNS-based offline verifiers
  test/, tests/            In-browser self-test page + round-trip harness vs RNS reference
  docs/PROTOCOL_NOTES.md   Reticulum / LXMF interop findings reference

Libraries (@noble/curves for Ed25519/X25519 and @msgpack/msgpack for LXMF payload serialization) are loaded from a CDN via an import map in index.html. Web Crypto handles AES-CBC, HMAC, HKDF, and SHA-256 natively.

Diagnostic tools and bridge

The tools/ directory contains Python scripts that validate the web client's wire output against the Python RNS reference, plus the WebSocket bridge used by the TCP connection option.

  • tools/ws_bridge.go — the WebSocket↔TCP forwarder for the "Connect (WebSocket)" option, shipped as prebuilt binaries on each bridge-v* release. Single CGO-free binary; shows a live status screen (-plain for plain logging). Per-connection rnsd target via query params, so one bridge serves many webapp instances. See the TCP (WebSocket) connection section above.
  • tools/ws_bridge.py — the same forwarder as a no-binary Python fallback. Requires pip install websockets; rnsd target via --rnsd-host/--rnsd-port flags.
  • tools/identity_info.py — dumps every derivable public piece of an exported identity (enc/sig/ratchet private and public bytes, identity hash, LXMF destination hash). Read-only, never touches network.
  • tools/verify_lrproof.py — runs a self-test of RNS's Ed25519, X25519, and HKDF primitives, then verifies a real LRPROOF hex string (lifted from the web client log) against Identity.validate to prove our link-proof signatures are byte-compatible with upstream.
  • tools/verify_announce.py — builds an lxmf.delivery announce with RNS using the web client's identity and runs it through Identity.validate_announce, proving our announce format is acceptable to the upstream reference.
  • tools/rns_responder.py — runs Python RNS as a link responder against a supplied LINKREQUEST data field, captures the LRPROOF bytes RNS would emit, and prints them field by field for a byte-for-byte diff against the web client's own output.

The Python verifiers and the Python bridge depend only on rns, umsgpack, and websockets from pip. The Go bridge has no runtime dependencies.

Development notes

  • Open the browser DevTools console to see stack traces. The in-page log shows a terse one-line error, but the full trace only lives in the console.
  • The webapp listens for error and unhandledrejection on window and mirrors the message into the log, so uncaught errors from async handlers still show up.
  • store.js uses a single IndexedDB database named reticulum-webclient with object stores for identity, contacts, messages, nodes, bookmarks, and history. To wipe local state, open DevTools then Application then Storage then Clear site data.
  • The KISS parser accumulates bytes across BLE notifications and emits complete frames on FEND boundaries. BLE splits frames at arbitrary points, so any per-notification framing assumption will break.
  • Reticulum destination hashes are computed with the identity hexhash outside the name hash input, matching upstream Destination.hash(identity, app_name, *aspects). The hexhash appears only in the human-readable Destination.name, never in on-wire hashes.
  • LRPROOF packets have a special framing exception in upstream Packet::pack: the 16-byte destination slot of the header carries the link_id instead of the SINGLE destination's hash, and the flag byte's destination-type bits are hardcoded to LINK regardless of the destination the packet was constructed with. Our buildPacket matches this by accepting destType and destHash as explicit parameters rather than deriving them from a destination object.
  • Every accepted CONTEXT_NONE data packet on an established link gets an immediate PROOF packet sent back, carrying the 32-byte SHA-256 of the received packet's hashable part plus an Ed25519 signature of that hash. Without this packet receipt, the sender's delivery-receipt timeout fires and it retries on a fresh link, producing a "same message keeps arriving" loop.
  • Periodic re-announcement is mandatory for inbound link delivery, not cosmetic. Relays validate inbound LRPROOFs by looking up the responder's identity in their own Identity.known_destinations cache, and that cache gets GC'd — without a periodic refresh the LRPROOF is dropped at the relay before ever reaching the initiator. See docs/PROTOCOL_NOTES.md §14 for detail.
  • See docs/PROTOCOL_NOTES.md for the full set of protocol-layer findings, including the destination hash formula, Web Crypto AES-CBC auto-padding gotcha, LXMF wire format differences between opportunistic and link delivery, stamp handling for signature verification, and the clockless-sender timestamp workaround.

Security and trust model

All Reticulum protocol work — identity generation, ECDH key exchange, AES-256-CBC encryption, HMAC authentication, Ed25519 signing, LXMF message packing — runs inside your browser (or the Android WebView). The radio or daemon on the other end of the transport only sees fully encrypted packets.

What is protected:

  • Message content is end-to-end encrypted. Each LXMF message uses a fresh ephemeral X25519 key exchange, HKDF-SHA256 key derivation, and AES-256-CBC + HMAC-SHA256 (Reticulum's Token / modified Fernet construction). Neither the transport layer, relay nodes, nor the rnsd daemon can read your messages.
  • Delivery receipts on link-delivered messages include an Ed25519 signature that is computationally unforgeable without the responder's private key.
  • Announce signatures are Ed25519-signed over the full announce body including the destination hash, public key, name hash, random hash, ratchet (if present), and app data. Forging an announce for a destination you do not own the private key for is infeasible.

What is NOT protected (known limitations):

  • Private keys at rest are stored unencrypted in the browser's IndexedDB. Anyone with access to your browser profile — browser extensions with matching host permissions, device backup tools, physical access to an unlocked device, or root access on Android — can extract them. The Export Identity file is likewise unencrypted JSON containing the complete signing and encryption private keys. Treat it like a password.
  • Forward secrecy is partial. The ratchet keypair now rotates on every announce (~5 min) and the new public key is advertised; retired ratchet private keys are not persisted, so traffic a peer encrypted to an expired ratchet can't be recovered from the stored identity. Caveats: peers that don't yet know a ratchet fall back to your long-term X25519 key (no FS for that traffic), and the current ratchet plus long-term key still live unencrypted in IndexedDB — anyone who extracts those can read messages encrypted to them.
  • BLE transport is cleartext at L2. Web Bluetooth does not request BLE bonding, so the NUS link between your device and the RNode modem is not encrypted at the Bluetooth radio layer. An observer within Bluetooth range (~10 m) can see the encrypted Reticulum packets and their headers (destination hashes, packet types, sizes, timing) but cannot decrypt message content.
  • WebSocket ws:// to remote hosts exposes packet headers. When using the WebSocket transport to a non-localhost destination over ws:// (not wss://), Reticulum packet headers are visible to network observers on the path. Message content remains end-to-end encrypted, but destination hashes, packet types, and timing metadata leak. The app shows a visible warning banner when a non-localhost ws:// connection is active. Use wss:// for remote connections if your bridge supports TLS.
  • Metadata. Reticulum packet headers contain 16-byte destination hashes in cleartext by design. Any observer on the radio channel or transport path can correlate who is communicating with whom by watching destination hashes, even though they cannot read message content. Periodic announces broadcast your full 64-byte public key, display name, and destination hash to the mesh every five minutes. This is inherent to the Reticulum protocol, not specific to this client.
  • Map tiles. The Nodes view loads map tiles from OpenStreetMap (tile.openstreetmap.org). The tile server sees your IP address and the geographic region you are viewing, though it does not see your Reticulum identity or messages.

Recommendations for alpha testers:

  1. Do not run this on a device where untrusted browser extensions have access to your browsing data.
  2. Use wss:// (not ws://) for any WebSocket connection to a remote host.
  3. Keep your Export Identity JSON file in a secure location (password manager, encrypted drive). Anyone who obtains it can impersonate you and decrypt your messages.
  4. Understand that your display name and destination hash are broadcast to the mesh every five minutes. Do not use a display name that reveals information you want to keep private.

Related projects

About

Browser-based Reticulum messaging client — encrypted LXMF chat over LoRa via an RNode (Web Bluetooth / Web Serial) or to rnsd over WebSocket. No build step, runs on GitHub Pages.

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors