Skip to content

MedabisAmina/Nodex

Repository files navigation

Nodex Logo

Nodex

Offline today. Connected always.


What is Nodex?

Nodex is an offline-first, peer-to-peer mesh communication app built for the hackathon challenge:

"How do we keep people connected when all networks go down? Build an offline mesh communication app where nearby phones relay messages directly between each other using Bluetooth or Wi-Fi Direct."

Every phone running Nodex is simultaneously a sender, a receiver, and a relay node. No internet. No cell towers. No servers. Just phones talking to phones — across a building, a disaster zone, or anywhere infrastructure fails.


Demo scenario

Phone A  ──BLE──►  Phone B  ──BLE──►  Phone C
(sender)            (relay)            (receiver — out of A's range)

A sends a message. B relays it automatically. C receives it — with zero internet, zero cell signal, zero servers involved. That is the entire value proposition, and it works today.


Feature overview

Feature Status Notes
BLE peer discovery ✅ Done Google Nearby Connections, P2P_CLUSTER strategy
Multi-hop message relay ✅ Done hopCount gate (max 5), excludeId flood prevention
Smart unicast routing ✅ Done Routing table populated via routing_adv packets
Message deduplication ✅ Done Hive seen_ids_box — survives app restart
Message persistence ✅ Done Hive messages_box with timestamp sort
Message sync on connect ✅ Done sync_req / sync_res protocol
Direct message encryption ✅ Done X25519 ECDH + AES-256-CTR
Cryptographic identity ✅ Done X25519 keypair generated once, stored in Hive
Human-readable alias ✅ Done Deterministic from public key — Silent-Blue-Fox-92
Signal / latency tracking ✅ Done Ping/pong with exponential moving average
Background keep-alive ✅ Done flutter_background_service foreground notification
SOS broadcast ✅ Done MessageType.sos — floods mesh with TTL=5
Global chamber ✅ Done recipientId = "global_chamber" — unencrypted broadcast
End-to-end encryption ✅ Done DM only — global chamber is plaintext by design

Architecture

Layer model

┌─────────────────────────────────┐
│           UI Layer              │  Flutter widgets, screens, navigation
├─────────────────────────────────┤
│       Routing Layer             │  NearbyServiceController — dedup, TTL,
│                                 │  smart routing, ping, sync
├─────────────────────────────────┤
│      Transport Layer            │  Google Nearby Connections API
│                                 │  Strategy.P2P_CLUSTER (BLE + Wi-Fi Direct)
├─────────────────────────────────┤
│      Storage Layer              │  Hive — messages, seen IDs, keypair
└─────────────────────────────────┘

The routing layer is transport-agnostic. It does not know or care whether the underlying link is BLE or Wi-Fi Direct — swapping the transport requires changing only NearbyServiceController, not the rest of the app.

Packet protocol

Every byte sent on the wire is a MeshPacket serialised as JSON:

{ "type": "chat", "data": { ...Message fields... } }
Packet type Direction Purpose
chat broadcast / unicast Carries a Message payload
sync_req → new peer "Give me everything newer than timestamp T"
sync_res → requester List of missing Message objects
routing_adv broadcast "I am reachable via this endpoint, hops=N"
ping_req broadcast Latency probe
ping_res unicast reply Latency response

Message model

Message {
  id          : UUIDv4          // deduplication key
  senderId    : String          // sender's X25519 public key (base64)
  senderName  : String          // same as senderId (alias resolved in UI)
  recipientId : String          // target pubkey, or "global_chamber"
  content     : String          // plaintext or AES-256-CTR ciphertext
  timestamp   : int             // Unix ms — used for sync window
  hopCount    : int             // incremented per relay, gate at 5
  isEncrypted : bool
  iv          : String          // base64 nonce for AES-CTR (DM only)
}

Security model

What is implemented

Threat Mechanism Status
Message tampering X25519 ECDH shared secret + AES-256-CTR for DMs
Relay loop / infinite flood seen_ids_box dedup + hopCount ≤ 5 gate
Replay attack seen_ids_box persisted across sessions
Identity spoofing Identity = X25519 public key, not a username
Global chamber eavesdropping By design — global chamber is plaintext

Scoped to future work

Threat Future mechanism
Global chamber privacy Symmetric group key exchanged on join
Sybil attack (fake nodes) Web-of-trust key signing, rate limiting per pubkey
Traffic analysis Message padding to fixed size
MAC-less AES-CTR Upgrade to AES-256-GCM for authenticated encryption

Note on MacAlgorithm.empty: The current AES-CTR implementation uses no MAC, meaning ciphertext integrity is not verified before decryption. This is a known, conscious hackathon trade-off. Upgrading to AES-GCM in CryptoService is a one-line change.


Project structure

lib/
├── core/
│   ├── alias_generator.dart        # Deterministic human alias from pubkey
│   ├── app_state.dart              # (placeholder — global state future)
│   ├── background_service.dart     # Foreground notification keep-alive
│   ├── crypto_service.dart         # X25519 keygen + AES-256-CTR encrypt/decrypt
│   ├── database_service.dart       # Hive wrapper — messages + seen IDs
│   ├── mesh_message.dart           # Legacy wire format (MeshMessage — unused in v2)
│   ├── nearby_service_controller.dart  # THE brain — all mesh logic lives here
│   └── permissions.dart            # Android runtime permission orchestration
├── models/
│   ├── mesh_packet.dart            # Wire envelope: { type, data }
│   └── message.dart                # Domain model for a single message
├── features/
│   ├── home/                       # Home screen — activity feed, live rooms
│   ├── nearby/                     # Peer discovery and connection UI
│   ├── chat/                       # DM chat screen with encryption
│   ├── messages/                   # Conversation list screen
│   ├── sos/                        # SOS broadcast screen
│   └── profile/                    # Identity and settings
├── shared/
│   ├── app_shell.dart              # Navigation shell + custom bottom nav
│   └── theme.dart                  # Design tokens
└── main.dart                       # Entry point — permissions → init → shell

Getting started

Prerequisites

  • Flutter 3.x (stable channel)
  • Android device or emulator with API 21+
  • Two or more physical Android devices for mesh testing (emulators cannot do BLE)

Install & run

git clone https://github.com/MedabisAmina/Nodex.git
cd Nodex
flutter pub get
flutter run

Required pubspec.yaml dependencies

dependencies:
  nearby_connections: ^3.0.0
  cryptography: ^2.7.0
  hive_flutter: ^1.1.0
  uuid: ^4.0.0
  crypto: ^3.0.0                    # for AliasGenerator SHA-256
  flutter_background_service: ^5.0.0
  flutter_local_notifications: ^17.0.0
  permission_handler: ^11.0.0
  location: ^6.0.0

Permissions (AndroidManifest.xml)

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />

License

MIT © 2026 Nodex Team

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors