Direct LAN file & content sharing between macOS and Android — like AirDrop, but cross-platform.
- Discovery: mDNS / Bonjour (
_skiff._tcp) - Transport: TLS 1.3 with mutual TLS authentication
- Pairing: 6-digit PIN with HMAC verifier + cert fingerprint pinning
- Payloads: files (streamed, no size cap), images, video, screenshots, URLs, text, drag-and-drop, share-sheet target
.
├── PROTOCOL.md Wire spec
├── assets/icon.svg Source artwork
├── macos/Drop SwiftUI app (Swift 6, macOS 14+)
│ └── tools/build-app.sh Build Skiff.app for distribution
└── android/drop Jetpack Compose app (Kotlin 2.1, min SDK 29)
Need Xcode 16+ (provides the Swift 6 toolchain).
cd macos/Drop
swift run Skiff # development run
# or build a distributable bundle
bash tools/build-app.sh # produces macos/Drop/build/Skiff.app
# Icon is bundled automatically (from macos/Drop/Resources/Skiff.icns).
# To refresh after editing assets/icon.svg:
bash tools/generate-icon.sh
open build/Skiff.appOn first launch macOS asks for Local Network access — allow it.
cd macos/Drop
bash tools/build-app.sh
bash create_dmg.sh # optional — produces build/Skiff.dmgHost Skiff.dmg (or zip Skiff.app) on your site. macOS Gatekeeper will warn
on first launch because the app is not notarized — users right-click → Open
once, then it runs normally. That's expected for unsigned distribution.
Optional: Developer ID + notarization (no Gatekeeper warning)
Requires a $99/year Apple Developer account. See tools/release-app.sh or:
bash tools/build-app.sh
codesign --force --options runtime --timestamp \
--sign "Developer ID Application: YOUR NAME (TEAMID)" \
build/Skiff.app
ditto -c -k --sequesterRsrc --keepParent build/Skiff.app build/Skiff.zip
xcrun notarytool submit build/Skiff.zip --apple-id you@example.com \
--team-id TEAMID --password "@keychain:AC_PASSWORD" --wait
xcrun stapler staple build/Skiff.app
hdiutil create -volname Skiff -srcfolder build/Skiff.app -ov \
-format UDZO build/Skiff.dmgcd android/drop
gradle wrapper --gradle-version 8.10.2 # one-time
./gradlew :app:installDebug # USB or emulatorTargets Android 10+ (API 29).
For a signed release build to distribute via your site or Play Store:
cp keystore.properties.example keystore.properties # edit with your keystore
./gradlew :app:assembleRelease
# Output: app/build/outputs/apk/release/app-release.apk (signed when keystore.properties exists)- Mutual TLS with both certs self-signed and pinned by SHA-256 of the cert DER. A MITM presenting a different cert fails the handshake.
- Pairing PIN uses HMAC-SHA256 with the PIN as the key and the two cert fingerprints as the message. The receiver rate-limits to 3 failures before regenerating the PIN.
- Trust binding persists across restarts: each side records the other's
(id, fingerprint)intrust_store.json. Any future TLS connection presenting a mismatched fingerprint is rejected.
| Item | State | Plan |
|---|---|---|
| Private keys in Keychain (Mac) / AndroidKeyStore (new installs) | ✅ | Legacy file keys migrate on load |
| HMAC-PIN handshake is not a true PAKE | Offline brute-forcing the PIN is possible if the wire is captured, but rate-limiting + ephemeral PIN narrows the window | SPAKE2 (RFC 9382) — bigger refactor |
| Mac → Android mDNS sometimes blocked by corporate VPNs | Worked around by typing IP:port directly via the + icon on Android | QR-code pairing |
| Foreground service can be killed by aggressive OEM battery-savers | First-launch dialog points at the battery whitelist | n/a |
| Milestone | Status |
|---|---|
| 1. Protocol spec | ✅ |
| 2. Bonjour / NSD discovery | ✅ |
| 3. PIN pairing | ✅ |
| 4. Cert fingerprint pinning | ✅ |
| 5. Text / URL / file transfer | ✅ |
| 6. Mutual TLS | ✅ |
| 7. Streaming file transfer (no cap) | ✅ |
| 8. Foreground service (Android) | ✅ |
| 9. Drag & drop (Mac) + share sheet (Android) | ✅ |
| 10. Manual IP fallback (Mac + Android) | ✅ |
| 11. Inbox polish (image previews, swipe-to-delete, smart timestamps) | ✅ |
| 12. .app bundle script | ✅ |
| 13. Crash logging | ✅ |
# from the Mac terminal, both devices should appear:
dns-sd -B _skiff._tcp local.
# probe the local server:
curl -k --cert /tmp/yourcert.pem --key /tmp/yourkey.pem \
https://<mac-ip>:<port>/v1/infoInside the app:
- Run Skiff on both devices.
- On one device tap the lock / "Show PIN" button — a 6-digit PIN appears.
- On the other device tap that peer → enter the PIN → Pair.
- Once a green shield appears, tap the peer again to send a message, file, or use drag-and-drop (Mac) / share sheet (Android).
If devices don't see each other, tap + on either platform and type the
other device's IP:port from its footer (e.g. 192.168.0.9:54321).
- Android signing keystore for release APKs.
keytool -genkey -v .... - Privacy policy to host on your site — draft in
docs/privacy-policy.md(add your contact email). - Crash reporting backend (Sentry / Bugsnag / Crashlytics) if you want
remote reports. Skiff already writes local crash logs to disk;
CrashLogger.install()is the hook. - Beta with real users. Plan for at least 5–10 testers across different Wi-Fi networks; OEM Android skins kill foreground services in unpredictable ways.