The encrypted identity file uses a fixed-format envelope:
Key derivation: MasterKey + Salt via HKDF-SHA512 produces a per-file key. Encryption uses ChaCha20-Poly1305.
End-to-end encrypted, peer-to-peer file sharing with post-quantum cryptography.
18 min read
Hierarchical key management with platform-native secure storage. Master keys are 32 bytes sourced from the most secure available backend, with automatic fallback through four tiers.
The encrypted identity file uses a fixed-format envelope:
Key derivation: MasterKey + Salt via HKDF-SHA512 produces a per-file key. Encryption uses ChaCha20-Poly1305.
Hybrid post-quantum key exchange combining X25519 (classical ECDH) with ML-KEM-768 (lattice-based KEM). Both must be broken to compromise the session key.
Hybrid KEM Key Exchange
========================
Seed1 = X25519Encapsulate(ownPriv, peerPub) --> encSeed1
Seed2 = MLKEMEncapsulate(peerMLKEMPub) --> encSeed2
SessionKey = HKDF-SHA512(seed1 || seed2, "keibidrop-session-v1")
= 32 bytes (AES-256 / ChaCha20 key)
[4B big-endian length] [12B nonce] [ciphertext + 16B AEAD tag]
|
+-- 4B prefix + 8B counter
OUTBOUND: 0x4F555442 ("OUTB")
INBOUND: 0x494E4244 ("INBD")
Nonce prefixes prevent nonce reuse on bidirectional streams sharing the same key.
| Cipher | Selection Criteria | Performance |
|---|---|---|
| ChaCha20-Poly1305 | Universal (all platforms) | Fast on ARM / no AES-NI |
| AES-256-GCM | Hardware AES detected | Fast on x86 with AES-NI |
Zero-trust authenticated key exchange. Both peers verify each other's fingerprint (exchanged out-of-band) and derive a shared session key using the hybrid KEM.
Full duplex: TWO TCP connections, each with its own handshake. Both peers verify each other's fingerprints. CONNECTION 1: Alice outbound -> Bob inbound Alice Bob ----- --- TCP SYN -----------------------------------------> Accept | Send handshake: | [4B len | JSON] ---------------------------------> | {fingerprint: Bob's expected FP, | public_keys: {x25519: Alice_pub, mlkem: Alice_pub}, enc_seeds: {x25519: encSeed1, mlkem: encSeed2}, | outboundPort, supportedCiphers[], persistent} | v Verify Alice's fingerprint (constant-time compare vs registered) X25519Decapsulate(encSeed1) -> seed1 MLKEMDecapsulate(encSeed2) -> seed2 DeriveKey(seed1+seed2) -> SEK_inbound Negotiate cipher (AES-GCM or ChaCha20) SecureConn wraps Connection 1 (Alice writes, Bob reads) CONNECTION 2: Bob outbound -> Alice inbound Alice Bob ----- --- Accept <----------------------------------------- TCP SYN | | Send handshake: | <--------------------------------------------- [4B len | JSON] | {fingerprint: Alice's expected FP, | public_keys: {x25519: Bob_pub, mlkem: Bob_pub}, | enc_seeds: {x25519: encSeed3, mlkem: encSeed4}, v outboundPort, supportedCiphers[], persistent} Verify Bob's fingerprint (constant-time compare vs registered) X25519Decapsulate(encSeed3) -> seed3 MLKEMDecapsulate(encSeed4) -> seed4 DeriveKey(seed3+seed4) -> SEK_outbound Negotiate cipher SecureConn wraps Connection 2 (Bob writes, Alice reads) RESULT: Two independent encrypted channels Alice -> Bob: SecureConn(conn1, SEK_inbound, nonce_prefix=OUTB) Bob -> Alice: SecureConn(conn2, SEK_outbound, nonce_prefix=INBD) Each direction has: - Independent symmetric key (from different KEM seeds) - Independent nonce counter (prefix prevents collision) - Independent rekey schedule (1GB or 1M messages) gRPC multiplexes on top of these two SecureConn streams.
{
"fingerprint": "base64(86 chars)",
"public_keys": {
"x25519": "base64(32B)",
"mlkem": "base64(1184B)"
},
"enc_seeds": {
"x25519": "base64(32B)",
"mlkem": "base64(1088B)"
},
"outboundPort": 26100,
"supportedCiphers": ["chacha20-poly1305", "aes-256-gcm"],
"persistent": true
}
Forward secrecy via periodic key rotation. Triggered at 1GB transferred or 1M messages (whichever first). New KEM seeds are exchanged mid-session without connection teardown.
Privacy-preserving signaling server. The relay stores opaque encrypted blobs keyed by irreversible tokens. It cannot correlate users, reverse tokens, or decrypt connection metadata.
Fingerprint (86 chars)
|
+-- ExtractRoomPassword (first 32 bytes)
| |
| +-- HKDF "keibidrop-relay-lookup-v1" --> Lookup Token
| |
| +-- HKDF "keibidrop-relay-encrypt-v1" --> Encryption Key
|
What the relay sees:
POST /register { token: "abc...", blob: "encrypted..." }
GET /fetch { token: "abc..." } --> "encrypted..."
What the relay CANNOT do:
- Reverse token back to fingerprint
- Decrypt the blob (contains IP + port + pubkeys)
- Correlate which users are communicating
1. Alice registers:
POST /register { Authorization: Bearer <lookup_token> }
Body: Encrypt(IP + port + pubkeys, encryption_key)
2. Bob fetches (using Alice's fingerprint):
GET /fetch { Authorization: Bearer <lookup_token> }
Response: encrypted blob
3. Bob decrypts: IP + port + pubkeys
Bob connects directly to Alice via TCP
TTL: 10 minutes, in-memory only (no persistence)
TCP relay for peers behind symmetric NATs or firewalls. The bridge pairs connections by token and performs blind byte forwarding without access to the encrypted stream content.
Creator Bridge Joiner
======= ====== ======
dial(token+"pair1") ------> queue["pair1"]
|
<----+---- dial(token+"pair1")
PAIRED: io.Copy(creator, joiner)
dial(token+"pair2") ------> queue["pair2"]
|
<----+---- dial(token+"pair2")
PAIRED: io.Copy(creator, joiner)
Token = SHA256(sorted(fp1, fp2) + direction)
Direction tagging prevents self-pairing.
Bridge tokens are deterministic: both peers independently compute the same token from their sorted fingerprints and a direction tag. This enables rendezvous without prior coordination.
token = SHA256(sort(fingerprint_a, fingerprint_b) + "pair1")
// "pair1" = inbound connection direction
// "pair2" = outbound connection direction
Virtual filesystem that presents peer files as local directories. Applications interact via standard POSIX syscalls; KEIBIDROP transparently fetches file data over gRPC on demand.
Application (e.g., git clone, vim, Finder)
| syscall: open(), read(), write(), rename()
+----v---------+
| Kernel |
| VFS/FUSE |
+----+---------+
| FUSE protocol (via /dev/fuse)
+----v--------------+
| KeibiDrop FUSE |
| fuse_directory.go |
| Getattr/Read/ |
| Write/Rename |
+----+--------------+
| gRPC: StreamFile, Notify
+----v---------+
| Peer |
+--------------+
| Structure | Type | Purpose |
|---|---|---|
| AllDirMap | map[string]*Dir | Path to directory node |
| AllFileMap | map[string]*File | Path to file metadata + data |
| RemoteFiles | map[string]*File | Files available from peer |
| OpenFileHandlers | map[uint64]*HandleEntry | Open file handle state |
Path convention: FUSE uses /filename (leading slash), no-FUSE uses filename (bare).
1. git writes: .git/objects/pack/tmp_pack_XXXX.lock
2. FUSE Write: data buffered, Notify ADD_FILE to peer
3. git renames: tmp_pack_XXXX.lock -> pack-XXXX.idx
4. FUSE Rename: update maps, Notify RENAME_FILE to peer
5. Peer: os.Rename on disk, update RemoteFiles map
6. Size check: if final size != cached size, re-download
| Platform | FUSE Backend | Mount Options |
|---|---|---|
| macOS | macFUSE (osxfuse) | noappledouble, defer_permissions, iosize=524288 |
| Linux | FUSE3 (libfuse3) | allow_other, default_permissions |
| Windows | WinFSP | --VolumePrefix=\\keibidrop |
| iOS/Android | N/A (no-FUSE only) | - |
LEVEL 1: Minimal FUSE (works but breaks everything) Getattr -> return hardcoded size/mode Read -> fetch entire file from peer, return bytes Write -> write to local, notify peer Problems: mmap crashes (git), stale cache (sync), no resume, OOM on large files LEVEL 2: Per-file direct_io + on-demand reads OpenEx -> set direct_io=true (bypass kernel page cache for freshness) EXCEPT .git/ paths (git needs mmap for pack files) Read -> on-demand: fetch only requested byte range via gRPC Problems: slow random reads, no caching, 612 concurrent prefetches crash LEVEL 3: Bitmap-tracked cache + prefetch semaphore Read -> check ChunkBitmap: cached? -> local read not cached? -> gRPC StreamFile -> write to local -> mark bitmap Prefetch semaphore: max 8 concurrent StreamFile streams Problems: rename races with git, fcopyfile on macOS, ENOENT caching LEVEL 4: Production (current) + Debounced notifications (200ms per-path, batched to 64) + Rename retargets pending notifications + ENOENT cache disabled (negative_vncache removed) + Post-release writes buffered (macOS fcopyfile) + Fsync after Release handled (git's fsync race) + Mode bits preserved cross-peer + Monotonic handle IDs (fd reuse prevention) + Panic recovery -> EIO (never crash the mount)
NOTIFICATION DEBOUNCE (200ms per path) Problem: git clone creates 600+ files in seconds. Each file triggers: ADD_FILE notification -> gRPC -> peer. 600 concurrent gRPC calls -> OOM, socket exhaustion. Solution: per-path debounce timer (200ms) Write file.txt t=0ms -> start 200ms timer for "file.txt" Write file.txt t=50ms -> reset timer (file still changing) Write file.txt t=100ms -> reset timer [200ms passes] -> send ONE ADD_FILE notification with final size If RENAME arrives for a pending file: Cancel pending ADD_FILE for old path Retarget to new path Restart 200ms timer BATCH NOTIFICATIONS (up to 64 per RPC) Problem: even after debounce, hundreds of individual RPCs are slow. Solution: BatchNotify RPC packs up to 64 notifications per call. Sequence number ensures ordering. REMOVE DEBOUNCE (1000ms) Problem: git's atomic write pattern: write tmp -> rename tmp -> target. The rename triggers REMOVE_FILE for the old path. But the REMOVE arrives before the ADD for the new path. Peer deletes the file, then re-adds it -> flicker, data loss. Solution: buffer REMOVE for 1000ms. If ADD arrives for same path within that window, cancel the REMOVE. PREFETCH SEMAPHORE (max 8 concurrent) Problem: git clone resolving deltas opens 600 pack files simultaneously. Each open triggers a prefetch (full file download). 600 concurrent StreamFile streams -> server OOM. Solution: buffered channel semaphore (cap 8). 9th prefetch blocks until one finishes.
FROM LAUNCH TO FIRST FILE READ (Desktop FUSE mode): 1. App Launch KD_Initialize -> NewKeibiDrop -> InitSession (ephemeral keys) EnablePersistentIdentity -> LoadOrCreate identity UI shows fingerprint 2. Fingerprint Exchange User copies fingerprint -> sends via Signal/Telegram Peer pastes -> AddPeerFingerprint (stored in session) 3. Connect Connect() -> fingerprint tiebreak -> creator/joiner Creator: registerRoomToRelay -> listen for peer (or bridge) Joiner: getRoomFromRelay -> dial peer (or bridge) 4. Handshake Outbound: send PeerHandshakeMessage (pubkeys, enc_seeds, cipher list) Inbound: receive, verify fingerprint, decapsulate seeds, derive SEK Wrap both TCP connections in SecureConn 5. gRPC Setup Start gRPC server on inbound SecureConn Connect gRPC client to outbound SecureConn No TLS (encryption handled by SecureConn layer below) 6. FUSE Mount FS.Mount(mountPoint) -> cgofuse FileSystemHost Root Dir created (AllDirMap, AllFileMap = empty) FS.Root set -> KDSvc.FS = FS (gRPC service can access filesystem) Mount blocks (FUSE event loop) until Unmount 7. Peer Sends File List Peer's AddFile/batch -> Notify(ADD_FILE) RPC arrives service.go processes: creates File in RemoteFiles map Sets: Name, RelativePath, Size, Mode bits 8. User Opens Finder / ls Finder calls: readdir("/mnt/keibidrop/") FUSE Readdir handler: iterate AllFileMap + AllDirMap Returns directory listing with file names and sizes 9. User Opens a File open("/mnt/keibidrop/report.pdf") FUSE Open -> allocate HandleEntry (monotonic ID) -> check shouldUseDirectIo (true for non-.git files) -> set handle.DirectIo = true 10. First Read read(fd, buf, 65536, offset=0) FUSE Read -> check bitmap: chunk 0 cached? NO -> OpenStreamProvider() -> gRPC StreamFile(path) -> peer reads file from disk -> sends 512KB chunks -> we write to local cache file -> mark bitmap -> copy requested bytes to FUSE read buffer -> return to application 11. Subsequent Reads bitmap says chunk cached -> local disk read -> no network Sub-millisecond response time
macOS (macFUSE via cgofuse) Mount options: noappledouble, defer_permissions, iosize=524288 Quirks: - fcopyfile: writes can arrive AFTER Release (post-close writes) - com.apple.quarantine xattr: return ENODATA or apps won't open files - negative_vncache: REMOVED (caches ENOENT, breaks async file arrival) - .DS_Store: filtered from file list - /var -> /private/var symlink: check both in waitForUnmount Stale mount cleanup: diskutil unmount force Linux (FUSE3 via cgofuse) Mount options: allow_other, default_permissions Quirks: - user_allow_other required in /etc/fuse.conf - FUSE3 vs FUSE2: different header paths - inotify works normally (unlike macOS FSEvents) Stale mount cleanup: fusermount -u Windows (WinFSP via cgofuse) Mount options: --VolumePrefix=\\keibidrop Quirks: - Drive letter mount: "K:." normalized to "K:" - No /dev/fuse: uses minifilter driver - Different permission model (no Unix mode bits) - Antivirus may scan every file read (performance impact) Stale mount cleanup: N/A (WinFSP handles it) Mobile (no FUSE) iOS: no kernel module access, uses no-FUSE SyncTracker Android: no FUSE (Android's FUSE is for MediaStore, not user apps) Both use: AddFile/PullFile API via gomobile bindings
| Syscall | macOS behavior | Linux behavior | Windows behavior |
|---|---|---|---|
| Rename | Atomic within volume | Atomic within volume | May fail across volumes |
| mmap | Works if direct_io off | Works if direct_io off | WinFSP emulates |
| xattr | quarantine, FinderInfo | user.*, security.* | N/A (NTFS streams) |
| fsync | May race with Release | Ordered per POSIX | FlushFileBuffers |
Linux kernel rename syscall -> FUSE -> gRPC -> peer 1. Application os.Rename("/mnt/kd/.git/objects/pack/tmp.lock", "/mnt/kd/.git/objects/pack/pack-abc.idx") 2. Kernel VFS sys_renameat2(AT_FDCWD, old_path, AT_FDCWD, new_path, 0) -> VFS resolves paths through FUSE superblock -> FUSE_RENAME request queued to /dev/fuse 3. cgofuse dispatch Go runtime receives FUSE_RENAME from kernel Dispatches to Dir.Rename(oldpath, newpath) handler This runs on a FUSE worker goroutine (pool of ~12) 4. KeibiDrop Rename handler (fuse_directory.go) func (d *Dir) Rename(oldpath string, newpath string) int { // Validate: no dot-dot, no crossing mount boundary checkPath(oldpath) -> ENAMETOOLONG check checkPath(newpath) -> ENAMETOOLONG check // Lock maps (prevent concurrent Getattr from reading stale data) d.AfmLock.Lock() // Rename on local disk FIRST (before map updates) os.Rename(d.LocalDownloadFolder+oldpath, d.LocalDownloadFolder+newpath) // Update in-memory maps if file, ok := d.AllFileMap[oldpath]; ok { delete(d.AllFileMap, oldpath) file.Name = newName file.RelativePath = newpath d.AllFileMap[newpath] = file } // Update RemoteFiles map (for incoming notifications) d.RemoteFilesLock.Lock() if rf, ok := d.RemoteFiles[oldpath]; ok { delete(d.RemoteFiles, oldpath) d.RemoteFiles[newpath] = rf } d.RemoteFilesLock.Unlock() d.AfmLock.Unlock() // Notify peer via gRPC (debounced) d.OnLocalChange(types.FileEvent{ Type: RENAME_FILE, OldPath: oldpath, NewPath: newpath, Attr: stat, // includes final size for size-mismatch detection }) refreshDirStat(parentDir) // update ctime/mtime under lock return 0 // success } 5. Notification pipeline OnLocalChange -> debounce timer (200ms) -> if RENAME, retarget any pending ADD_FILE for oldpath -> after debounce: enqueue to notifyCh (bounded channel, cap 64) -> notification worker drains channel: BatchNotify RPC -> peer's gRPC server 6. Peer receives (service.go) BatchNotify handler processes RENAME_FILE: -> os.Rename(localCache+oldpath, localCache+newpath) on disk -> update SyncTracker/RemoteFiles maps -> if Attr.Size != cached file size: re-download (EditRemoteFile) 7. Peer's FUSE reflects change Next Getattr/Readdir on peer returns updated name Application sees renamed file instantly
macOS-specific differences in rename path: 1. Kernel uses renamex_np() (macOS extension) instead of renameat2 -> cgofuse translates to the same Rename callback 2. Finder rename triggers ADDITIONAL operations: -> Getattr on new path (check if exists) -> exchangedata() syscall (macOS atomic swap, optional) -> setxattr com.apple.FinderInfo (Finder metadata) -> We return ENODATA for FinderInfo -> Finder continues normally 3. fcopyfile race (macOS-only): cp uses fcopyfile() internally which can WRITE after close() -> Our Write handler checks: if handle == nil (already Released) -> buffer the post-release write -> flush on next Open of same path 4. .DS_Store creation: Finder creates .DS_Store in every directory it visits -> Filtered from notifications (isInternalFile check) -> Never synced to peer 5. Same notification pipeline as Linux after step 4
Explicit file sharing via AddFile/PullFile API. Used on mobile platforms (iOS/Android) and as fallback when FUSE is unavailable. Supports resumable downloads via chunk bitmaps.
SENDER RECEIVER
====== ========
User picks file
|
AddFile(path)
|
SyncTracker.LocalFiles[name] = File
|
Notify(ADD_FILE) --------------------------> peer sees file in list
|
PullFile(name)
|
StreamFile RPC <-- server pushes chunks
|
bitmap tracks 512KB chunks
|
.kdbitmap file for resumption
|
complete -> SyncTracker.LocalFiles
Files are divided into 512KB chunks. A ChunkBitmap tracks which chunks have been received. On interruption, the .kdbitmap file persists progress, enabling resumable transfers.
ChunkBitmap {
TotalChunks int
Received []bool // per-chunk completion
File string // .kdbitmap path on disk
}
Chunk size: 512KB (524288 bytes)
StreamFile: server pushes all chunks sequentially (no per-chunk RTT)
Application-layer protocol running over SecureConn. All RPCs are encrypted end-to-end. The gRPC server runs inside the peer process, serving file operations and health signals.
| RPC | Direction | Purpose |
|---|---|---|
| Notify | unary | File change event (ADD/REMOVE/RENAME/EDIT) |
| BatchNotify | unary | Batched notifications (up to 64 per call) |
| StreamFile | server stream | Push-based file download (all chunks) |
| Read | bidi stream | Pull-based parallel reads (random access) |
| Heartbeat | unary | Health monitoring (5s interval) |
| Rekey | unary | Forward secrecy key rotation |
| Type | Value | Trigger |
|---|---|---|
| ADD_FILE | 0 | New file shared |
| EDIT_FILE | 1 | File content modified |
| REMOVE_FILE | 2 | File deleted/unshared |
| RENAME_FILE | 3 | File renamed (carries old + new path) |
| ADD_DIR | 4 | Directory created |
| EDIT_DIR | 5 | Directory metadata changed |
| REMOVE_DIR | 6 | Directory removed |
| DISCONNECT | 7 | Graceful disconnect signal |
How a raw TCP connection becomes an encrypted gRPC channel, and how data flows through it.
APPLICATION LAYER +-------------------------------------------------------------------+ | gRPC Service (keibidrop.proto) | | | | Phase 1: Metadata Sync Phase 2: File Transfer | | +-------------------------+ +------------------------+| | | Notify(ADD_FILE) | | StreamFile(path) || | | Notify(RENAME_FILE) | | -> server pushes || | | Notify(REMOVE_FILE) | | all chunks || | | BatchNotify(64 max) | | -> bitmap tracks || | | Heartbeat(seq, ts) | | progress || | | Rekey(new seeds) | | || | +-------------------------+ | Read(bidi stream) || | Unary RPCs, small payloads | -> client requests || | Peer learns file list instantly | specific chunks || | | -> parallel workers || | +------------------------+| | Streaming RPCs, large | | payloads (512KB chunks) | +------------------------------+------------------------------------+ | protobuf serialization ENCRYPTION LAYER +------------------------------+------------------------------------+ | SecureConn (pkg/session/secureconn.go) | | | | Write path: plaintext -> AEAD.Seal(nonce, pt, nil) | | -> [4B length | 12B nonce | ciphertext + 16B tag] | | | | Read path: [4B length] -> read(len) -> AEAD.Open -> plaintext | | | | Nonce: [4B direction prefix | 8B monotonic counter] | | OUTB = 0x4F555442 INBD = 0x494E4244 | | | | Cipher: AES-256-GCM (if hardware) or ChaCha20-Poly1305 | | Key: SEK derived from hybrid KEM (X25519 + ML-KEM-1024) | | Rekey: every 1GB or 1M messages (forward secrecy) | +------------------------------+------------------------------------+ | encrypted bytes TRANSPORT LAYER +------------------------------+------------------------------------+ | Raw TCP Connection | | | | Direct P2P: tcp6 [local_ipv6]:26001 -> [peer_ipv6]:26003 | | Bridge: tcp [local] -> bridge.keibisoft.com:26600 | | (bridge does io.Copy, sees only encrypted bytes) | | LAN: tcp 192.168.x.x:26531 -> 192.168.x.x:26001 | | | | Two connections per session: | | Conn 1: outbound direction (Alice -> Bob) | | Conn 2: inbound direction (Bob -> Alice) | | Each wrapped independently in SecureConn with separate keys | +-------------------------------------------------------------------+
User opens video file in VLC: VLC calls open("/mnt/keibidrop/video.mp4") | +----v------------------+ | Kernel VFS -> FUSE | syscall dispatch +----+------------------+ | +----v--------------------------------------------------------+ | KeibiDrop FUSE handler: Open() | | -> allocate handle (monotonic ID, prevents fd reuse) | | -> check if file is in AllFileMap | | -> if remote: set direct_io (unless .git/ path) | | -> return handle ID to kernel | +----+--------------------------------------------------------+ | VLC calls read(fd, buf, 65536, offset=0) | +----v--------------------------------------------------------+ | KeibiDrop FUSE handler: Read() | | -> check local cache (bitmap: chunk downloaded?) | | -> if cached: read from local disk, return | | -> if not cached: | | | | | +----v------------------------------------------+ | | | gRPC: StreamFile(path, offset, size) | | | | -> peer opens file on their disk | | | | -> pushes 512KB chunks via gRPC stream | | | | -> each chunk: SecureConn encrypts | | | | -> TCP sends to us | | | | -> we decrypt, write to local cache | | | | -> update bitmap (mark chunks done) | | | | -> return data to FUSE read buffer | | | +-----------------------------------------------+ | +--------------------------------------------------------------+ | VLC receives data, decodes video frame, displays Subsequent reads: bitmap says cached -> local disk read -> no network
User taps "Save All" on iPhone: SaveAllFiles() | +----v--------------------------------------------------------+ | For each file (sequential): | | 1. Mark file as downloading in UI | | 2. SaveFile(name) -> ExportFile -> PullFile | | | | | +----v------------------------------------------+ | | | PullFile(remoteName, localPath) | | | | -> Truncate local file to expected size | | | | -> Create ChunkBitmap (512KB granularity) | | | | -> HealthMonitor.TransferStarted() | | | | -> pullParallelRead(): | | | | 4 worker goroutines | | | | each: gRPC StreamFile -> decrypt | | | | -> pwrite at chunk offset | | | | -> bitmap.Set(chunkIndex) | | | | -> HealthMonitor.TransferEnded() | | | | -> delete .kdbitmap (download complete) | | | +-----------------------------------------------+ | | 3. Update UI: downloading=false, saved=true | +--------------------------------------------------------------+ If interrupted (app killed, network drop): -> .kdbitmap preserved on disk -> next PullFile detects partial file + bitmap -> resumes from last completed chunk
Dual-stack local network discovery combining custom UDP multicast beacons with standard mDNS/Bonjour service advertisement. Peers are discovered within seconds on the same subnet.
+---------------------------+ +------------------------+
| UDP Multicast Beacon | | mDNS / Bonjour |
| 224.0.0.167:26999 | | 224.0.0.251:5353 |
| JSON: {name, port} | | _keibidrop._tcp |
| Every 3 seconds | | PTR + SRV + A |
+---------------------------+ +------------------------+
| |
| all platforms | macOS: CGo DNS-SD (native)
| | others: pure Go multicast
| | iOS: Swift NetService
| | (advertise only)
+----------------+-------------------+
|
Peer discovered:
name + IP + port
The mDNS implementation handles both browsing (finding peers) and responding (advertising self). On macOS, browsing uses native DNS-SD via CGo for reliability with .local resolution. Other platforms use pure-Go multicast DNS.
Service name: _keibidrop._tcp.local.
Response records:
PTR: _keibidrop._tcp.local. -> instance._keibidrop._tcp.local.
SRV: instance._keibidrop._tcp.local. -> hostname:port
A: hostname.local. -> IPv4 address
TXT: version, fingerprint-prefix (for UI display)
Continuous connection monitoring with automatic recovery. The health monitor detects degradation via RTT measurement, and the reconnect manager handles exponential backoff recovery.
[Healthy] ---RTT > 500ms---> [Degraded] ---5 failures---> [Disconnected]
^ | |
| | v
+------RTT < 500ms------------+ [Reconnecting]
exponential backoff:
1s, 2s, 4s, 8s, 16s, 30s
|
success
|
v
[Connected]
| Parameter | Value | Description |
|---|---|---|
| HeartbeatInterval | 5s | Time between heartbeat pings |
| HeartbeatTimeout | 5s | Max wait for heartbeat response |
| MaxFailures | 5 | Consecutive failures before disconnect |
| DegradedThreshold | 500ms | RTT above this = degraded |
| Backoff sequence | 1, 2, 4, 8, 16, 30s | Exponential with 30s cap |
KEIBIDROP runs on five platforms with platform-native integrations. The Go engine is shared; platform-specific code handles secure storage, filesystem access, discovery, and UI rendering.
| Component | macOS | Linux | Windows | iOS | Android |
|---|---|---|---|---|---|
| Keychain | Keychain Services | libsecret (D-Bus) | DPAPI Credential Manager | iOS Keychain (SecItem) | EncryptedSharedPreferences |
| FUSE | macFUSE | FUSE3 | WinFSP | N/A (no-FUSE) | N/A (no-FUSE) |
| mDNS Browse | CGo DNS-SD | Pure Go multicast | Pure Go multicast | Pure Go multicast | Pure Go multicast |
| mDNS Advertise | Go mDNS responder | Go mDNS responder | Go mDNS responder | Swift NetService | Go mDNS responder |
| UI | Rust / Slint | Rust / Slint | Rust / Slint | SwiftUI | Jetpack Compose |
| Build | make build-rust | make build-rust | make build-rust | make build-ios | make build-android |
| Binary | keibidrop | keibidrop | keibidrop.exe | KeibiDrop.xcframework | keibidrop.aar |
Three separate UI frontends share one Go engine. Desktop uses Rust/Slint with C FFI. Mobile uses gomobile to generate platform-native bindings (xcframework for iOS, AAR for Android).
+----------------------------------------------------+
| UI Frontends |
+-------------+----------------+---------------------+
| Rust/Slint | SwiftUI | Jetpack Compose |
| (desktop) | (iOS) | (Android) |
+------+------+-------+--------+-----------+---------+
| | |
+------+------+ +-----+------+ +----------+--------+
| rustbridge | | gomobile | | gomobile |
| (C FFI) | | (xcframe) | | (AAR) |
+------+------+ +-----+------+ +----------+--------+
| | |
+------+--------------+---------------------+--------+
| Go Engine |
| pkg/logic/common/ |
+----------------------------------------------------+
The rustbridge package exports 80+ functions via CGo for the Rust/Slint desktop UI. Key exports include:
// Identity
KD_LoadOrCreateIdentity() -> error
KD_GetFingerprint() -> string
KD_AddContact(name, fingerprint) -> error
// Connection
KD_Connect(fingerprint) -> error
KD_Disconnect() -> error
KD_GetConnectionStatus() -> string
// Files
KD_AddFile(path) -> error
KD_PullFile(name) -> error
KD_ListFiles() -> JSON
// Events
KD_PollEvent() -> string // non-blocking
KD_SetupEventCallbacks() -> error
KD_GetLastError() -> string
The kd command-line interface communicates with a background daemon over a Unix socket. Designed for scripting and AI agent integration with JSON input/output.
User terminal
|
+----v------+ +---------------+
| kd CLI |--JSON-->| kd daemon |
| (client) |<--JSON--| (Go engine) |
+-----------+ Unix +---------------+
socket
/tmp/kd.sock
| Command | Description |
|---|---|
| kd start | Start daemon (background engine process) |
| kd stop | Stop daemon gracefully |
| kd show fingerprint | Print own fingerprint |
| kd show status | Connection state + peer info |
| kd show config | Current configuration |
| kd register | Register on relay for discovery |
| kd connect <fp> | Connect to peer by fingerprint |
| kd add-file <path> | Share a file with connected peer |
| kd pull-file <name> | Download a file from peer |
| kd list | List shared files (local + remote) |
18 min read | KEIBIDROP Series | Technical Deep Dive | Post-Quantum gRPC | Relay Privacy | Forward Secrecy | Git Clone Between Peers | Agent CLI