Encrypted, no-internet file transfer between Android, iOS, Linux, macOS, and Windows, over either an ad-hoc Wi-Fi hotspot (one device hosts) or a shared network both devices are already on. Two devices, Wi-Fi (or ethernet), optionally Bluetooth. Port 3290 throughout.
All five platforms live in this repo, in three independent implementations of the same wire protocol. A change to the on-the-wire format in one is a breaking change unless mirrored in the other two:
- Rust (
core/) — the reference implementation the other two are tested against. Ships the Tauri desktop app (Flying Carpet/) for Windows + Linux. - Kotlin (
Android/FlyingCarpet/) — Android. - Swift (
Apple/) — iOS + macOS. A full port, not a binding over the Rust core. Build only on a Mac; seeApple/CLAUDE.md.
When touching the protocol (discovery bytes, version/mode preamble, Noise handshake, framing), update all three implementations and their cross-platform known-answer tests together.
- The Rust code builds for Windows and Linux only.
core/src/has justwindows/andlinux/;lib.rscfg-selectsnetwork/bluetoothon those twotarget_osvalues, and there is notarget_os = "macos"anywhere incore/. A Tauri/wry/webkit2gtkchange therefore affects two desktop platforms, not three — macOS is served by the Swift app inApple/, which shares no code withcore/. - The macOS binary is released from this repo's Releases page as a
.zipof the.app(macOS_FlyingCarpet_<version>.zip) — not a.dmg. The.dmgformat ended at v8.0.1; v9.0.0 switched to.zip. Don't infer from a macOS binary existing that the Rust code targets macOS — it's built fromApple/macOS/. There is also a Homebrew cask (brew install flying-carpet), which is maintained outside this repo. tauri.conf.jsonstill listsicons/icon.icns— genuinely stale, intentionally left alone. Not evidence of macOS support either.- The Swift code was developed in a separate
FlyingCarpetApplerepo and imported without history at the v10 release, sogit logonApple/starts at the import commit. Commit hashes cited for Swift changes in the older docs (e.g.4c59af6indocs/bluetooth-field-guide.md,4a6b889/b7e9b59inARCHITECTURE.md) belong to that repo and won't resolve here.
core/— Rust core crateflying-carpet-core(v10). Platform-split:core/src/{windows,linux}/for network/bluetooth/peripheral/central; thebluetoothmodule iscfg-selected per-OS inlib.rs. Key files:lib.rs(start_transferentry point),discovery.rs,noise.rs,sending.rs/receiving.rs.Flying Carpet/— Tauri desktop app. Rust backend inFlying Carpet/src-tauri/(workspace member), JS/HTML frontend inFlying Carpet/src/(main.js,index.html). Note the space in the directory name — quote it in shell commands.Android/FlyingCarpet/— Android app (Kotlin). Noise/discovery ports inapp/src/main/java/dev/spiegl/flyingcarpet/.Apple/— iOS + macOS apps (Swift). Protocol code shared by both inApple/shared/, per-platform UI inApple/iOS/andApple/macOS/. ReadApple/CLAUDE.mdbefore changing anything under it — the Apple platforms have constraints the others don't (no programmatic hotspot, so Apple-to-Apple requires shared network mode; BLE can't pair iPhone↔Mac).docs/— design docs (see below).ARCHITECTURE.md— connection role model.
Rust is a Cargo workspace (core + Flying Carpet/src-tauri):
cargo test— run all Rust tests (includes the Noise/discovery known-answer vectors incore/src/noise.rsandcore/src/discovery.rs).cargo buildto compile.- Desktop app:
cargo tauri dev(run) /cargo tauri build(release). Needs the Tauri CLI and the Linux deps listed inREADME.md. - Android: from
Android/FlyingCarpet/,./gradlew assembleDebug/./gradlew test. SetJAVA_HOMEto the Android Studio JBR (the bundled JDK) or Gradle fails. - Apple: macOS only, and outside the Cargo workspace —
cargo buildnever touches it. FromApple/,DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -project <iOS|macOS>/FlyingCarpet.xcodeproj -scheme "FlyingCarpet" build.DEVELOPMENT_TEAMis blank in both projects, so signing fails until you set your team in Xcode or passDEVELOPMENT_TEAM=YOURTEAMID. Details inApple/CLAUDE.md.
ARCHITECTURE.md— the four independent role axes (transfer direction / BLE peripheral-central / hotspot host-joiner / TCP server-client) and how they map per platform pair. Read before touching connection setup, hosting logic, or BLE.docs/shared-network-crypto.md— the full v10 cryptographic design (the normative reference for the handshake bytes). Read before touching anything crypto, discovery-auth, or the record/framing layer.docs/bluetooth-field-guide.md— read before touching any BLE code on any platform. The four independent axes (advertising / scanning / bonding / GATT services), seven hard-won laws, a per-platform matrix, and a symptom→cause playbook. Bluetooth bugs here are subtle, intermittent, and platform-asymmetric; several have been re-derived from scratch more than once. Chronological investigation logs:docs/windows-ble-gatt-0x8000ffff.md,docs/ble-bond-asymmetries.md.
- v10 = Noise. Every transfer (both modes) runs a
Noise_NNpsk0_25519_ChaChaPoly_SHA256handshake; the PSK isPBKDF2-HMAC-SHA256(password, salt="Flying Carpet v10 shared network PSK", 600_000). Noise is the sole cipher — the old inner per-chunk AES is gone. v10 is a clean break; v9 peers are rejected. If v10 ships before a later Noise wire change, that change must bump to v11. - Preamble → prologue binding. Version/mode are negotiated in a plaintext preamble, then every preamble byte is bound into the Noise prologue. Both platforms of any pair must build the prologue identically (
build_prologue/buildPrologue). Cross-platform KATs guard this — keep all three in sync (Rustcore/src/noise.rs, KotlinNoiseUnitTest, SwiftApple/macOS/FlyingCarpetTests/FlyingCarpetTests.swift; discovery vector:core/src/discovery.rstest_cross_platform_vector== AndroidDiscoveryUnitTest.kt== the Swift discovery test in the same file). Note the Swift KATs run only underxcodebuildon a Mac, socargo testand./gradlew testpassing is not evidence the Swift side still agrees. - Passwords: single-use + CSPRNG. The receiver mints a fresh random password per transfer and displays it; never reuse, never user-chosen, never "remember." The entire "offline crack is worthless" security argument depends on this (see the crypto doc §7). The discovery HMAC key is derived from the stretched PSK, so no fast hash of the password goes on the wire.
- Bluetooth is hotspot-only. Shared network mode exchanges the password manually (display + type/QR); do not re-add BLE to shared mode. Apple-to-Apple can't pair iPhone↔Mac over BLE by design, which is exactly the pair that would need it. Rationale is recorded in
ARCHITECTURE.md("Bluetooth + Shared Network Mode"). Every platform greys out its BT switch in shared network mode; on Apple that goes throughbluetoothSwitchShouldBeEnabled(), since restoring the switch on hardware capability alone re-enabled it after a transfer. - Receiver is the anchor. In both modes the receiver generates the password and is the TCP server (Noise responder); the sender is the TCP client (Noise initiator).
- Android: keep
res/layout/andres/layout-land/in sync when changing the UI. core/Cargo.tomlis pinned to LF line endings (.gitattributes); don't let an editor rewrite it to CRLF.- Header-value bounds (file count / filename length / chunk size sanity checks) and filename sanitization apply to values read from the Noise-decrypted stream, not the raw socket.