encrypted proximity exchange over bluetooth.
Drift is a library for exchanging encrypted data between nearby devices over Bluetooth Low Energy. no internet. no server. no accounts. two devices walk past each other and trade sealed messages that only they can open.
the cryptographic primitives are the same ones XMTP uses in their MLS ciphersuite: X25519 for the key exchange, ChaCha20-Poly1305 for authenticated encryption, Ed25519 for signatures. we use them directly over BLE rather than through the full MLS protocol, because an ephemeral two-device exchange does not need group state management.
built for KidBlocksOS as a StreetPass system for kids' tablets. but the protocol is general purpose. anything that needs encrypted local exchange between devices.
each device runs a BLE peripheral (advertising a GATT service) and a BLE central (scanning for other devices) simultaneously. when two Drift devices detect each other:
Device A (scanner) Device B (advertiser)
| |
|--- BLE discovery ------------>|
| |
|--- read B's X25519 pubkey --->|
|<-- B's ephemeral public key --|
| |
|--- write A's X25519 pubkey -->|
| both derive shared secret |
| (X25519 ECDH) |
| |
|--- read B's encrypted card -->|
| (ChaCha20-Poly1305) |
| |
|--- write A's encrypted card ->|
| (ChaCha20-Poly1305) |
| |
|--- disconnect ----------------| done. ~2 seconds.
the shared secret is ephemeral. derived from single-use X25519 keypairs generated fresh each boot. after the exchange, the key material is discarded. the encounter gets logged locally with the decrypted profile card.
| component | algorithm | what it does |
|---|---|---|
| key exchange | X25519 (ECDH) | two strangers agree on a shared secret |
| encryption | ChaCha20-Poly1305 | authenticated encryption, seals the card |
| key derivation | ECDH shared secret | direct use of the X25519 output |
| transport | BLE GATT | two characteristics, read/write |
the ciphersuite primitives match XMTP's MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519. we chose them because the XMTP team did the hard work of evaluating which algorithms belong together. we just run them over a different wire.
MLS (RFC 9420) is designed for persistent groups with evolving membership. Drift exchanges are ephemeral: two devices, one encounter, done. the full MLS handshake (KeyPackage, Welcome, group state) adds ~800 bytes of overhead that does not fit in a single BLE GATT read (512 byte MTU limit). the raw X25519 exchange fits in 44 bytes.
MLS gives you forward secrecy through ratcheting. we get forward secrecy through ephemerality: the keypairs are regenerated every boot and the shared secret is never stored.
the MLS proof of concept is still in the repo (src/mls-session.mjs, 16/16 tests passing) for anyone who wants the full protocol on a transport with larger payloads.
XMTP's transport is gRPC to centralized nodes. it requires internet. for a kid at a park with no WiFi, that is a non-starter. Drift takes the crypto primitives that XMTP chose (because they chose well) and runs them over a transport that works with zero infrastructure.
in KidBlocksOS, the payload is a profile card:
{
"v": 2,
"name": "CosmicFox",
"buddy": "dinosaur",
"age": 7,
"stats": {
"encounters": 42,
"appsBought": 7,
"appsSold": 2,
"appsListed": 3,
"appsOwned": 5,
"projectsBuilt": 12,
"totalVolume": 1.50
},
"apps": [
{ "id": "cookie-catcher", "name": "Cookie Catcher", "studio": "games" },
{ "id": "space-dino", "name": "Space Dinosaur", "studio": "games", "mine": true }
]
}stats are pulled live from the device: encounter count from the drift log, apps bought/sold/listed from the marketplace data, projects built from the project directory, volume from the activity log. the card refreshes every 5 minutes.
but Drift does not care what the payload is. it encrypts and delivers bytes. the application decides what those bytes mean.
two GATT characteristics under service UUID ff01:
| char | UUID | properties | purpose |
|---|---|---|---|
| key | ff02 |
read, write | ephemeral X25519 public key + display name |
| card | ff03 |
read, write | nonce + ChaCha20-Poly1305 encrypted card |
the key characteristic payload is msgpack-encoded: { k: <32-byte pubkey>, n: <name string> }. ~44 bytes.
the card characteristic payload is msgpack-encoded: { n: <12-byte nonce>, c: <ciphertext> }. typically under 200 bytes.
both fit comfortably in a single BLE GATT read/write (512 byte MTU).
- scanner reads
ff02from advertiser (gets advertiser's X25519 pubkey) - scanner writes own X25519 pubkey to
ff02 - both sides derive shared secret via X25519 ECDH
- advertiser encrypts its card with ChaCha20-Poly1305 using the shared secret
- scanner reads
ff03(gets advertiser's encrypted card, decrypts it) - scanner encrypts its own card and writes to
ff03 - advertiser decrypts the received card
- disconnect
total: 2 reads, 2 writes. under 500 bytes transferred. ~2 seconds.
cd drift
npm install
npm testthe test creates two MLS identities, runs the full DM handshake (group creation, Welcome, join), encrypts profile cards in both directions, and decrypts them. all on one machine, no BLE hardware needed.
16/16 passed
Total: 97ms
- Node.js 22+
- for BLE: Linux with BlueZ 5.x (Raspberry Pi works out of the box)
- for dual-role (advertise + scan simultaneously): BlueZ 5.72+ with experimental flag
- for tests only: no hardware needed, pure crypto
drift/
src/
mls-session.mjs MLS DM sessions (full RFC 9420 proof of concept)
test/
mls-dm.test.mjs MLS handshake + encrypted exchange test
package.json
README.md
LICENSE
the BLE transport and encounter storage run as part of KidBlocksOS. the MLS crypto layer is the standalone library.
working on two Raspberry Pi 5 devices over BLE 5.0.
- X25519 + ChaCha20-Poly1305 encrypted card exchange
- BLE GATT service (advertising + scanning)
- dual-role operation (advertise and scan simultaneously via BlueZ D-Bus)
- encounter logging with 8-hour per-device cooldown
- live device stats in profile cards (encounters, apps, volume)
- msgpack serialization (compact BLE payloads)
- systemd service for background operation
- KidBlocksOS UI integration (encounter plaza, home screen tile)
- MLS proof of concept (16/16 tests, RFC 9420 ciphersuite)
- Ed25519 wallet signatures on cards
- SQLite encounter database (currently JSON file)
- OTA card schema upgrades
the cryptographic design follows XMTP's lead. they picked the right primitives for encrypted messaging between wallet-identified devices. Drift uses those primitives for local transport.
ts-mls by Luka Jacobowitz provides the MLS implementation used in the proof of concept.
@noble/curves and @noble/ciphers by Paul Miller provide the X25519 and ChaCha20-Poly1305 implementations.
part of KidBlocksOS