A cross-platform clipboard synchronization application that lets you seamlessly share text and images between your Android phone and Windows PC in real time — securely, privately, and without any cloud service.
- What This Project Does
- Architecture Overview
- Protocols Used
- Security & Cryptography
- Android App — Logic & Language
- Windows App — Logic & Language
- Step-by-Step: Build the Android APK
- Step-by-Step: Run the Tauri Windows App
- Pairing Flow
- Project Structure
LinkSync keeps your clipboard in sync between your Android phone and Windows PC over your local network. When you copy something on your phone, it instantly appears on your PC, and vice versa — with no cloud, no third-party server, and full end-to-end encryption.
Key Features:
- Real-time bidirectional clipboard sync (text and images)
- Secure QR-code-based device pairing
- Works over WiFi (TCP/mDNS) with Bluetooth Low Energy (BLE) as fallback
- End-to-end encrypted with XChaCha20-Poly1305
- Background sync on Android (survives doze mode via VPN service)
- System tray app on Windows
- Auto-start on boot (both platforms)
- Fully local — no internet required
┌─────────────────────────────────────────────────────────────┐
│ LinkSync │
│ │
│ Android (Kotlin) Windows (Tauri/Rust/React) │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ ClipboardWatcher│◄─────────►│ ClipboardManager │ │
│ │ VpnService │ │ TCP Listener :12716 │ │
│ │ TcpClient │◄─WiFi────►│ mDNS Broadcaster │ │
│ │ BleClient │◄─BLE─────►│ BLE Scanner │ │
│ │ mDNS Discovery │ │ System Tray UI │ │
│ │ IdentityManager │ │ IdentityManager │ │
│ │ SecureSession │◄─E2E─────►│ SecureSession │ │
│ └─────────────────┘ └─────────────────────┘ │
│ │
│ Transport: TCP (primary) / BLE GATT (fallback) │
│ Discovery: mDNS (_linksync._tcp.local) / BLE Advertising │
│ Encryption: XChaCha20-Poly1305 AEAD │
│ Key Exchange: X25519 Diffie-Hellman │
│ Identity: Ed25519 signing keypairs │
└─────────────────────────────────────────────────────────────┘
All messages between Android and Windows use JSON-RPC 2.0 format, carried over encrypted TCP connections.
Message Format:
// Request
{
"jsonrpc": "2.0",
"method": "linksync.clipboard.update",
"params": { ... },
"id": "uuid-string"
}
// Response
{
"jsonrpc": "2.0",
"result": { ... },
"error": null,
"id": "uuid-string"
}Methods:
| Method | Direction | Purpose |
|---|---|---|
linksync.ping |
Both | Initial handshake / device discovery |
linksync.pair_init |
Android → PC | Send Ed25519 + X25519 public keys with pairing token |
linksync.clipboard.update |
Both | Send clipboard content (text/image, MIME type, SHA-256 hash) |
linksync.heartbeat |
Both | Keep-alive ping to detect disconnection |
- Port:
12716(default, configurable) - Frame format: 4-byte big-endian length prefix followed by the encrypted payload
- All data is encrypted before sending using XChaCha20-Poly1305
- Supports multiple simultaneous connections (async via Tokio on Windows, Coroutines on Android)
┌──────────────────────────────────────────────────────────┐
│ [4 bytes: payload length (big-endian)] [encrypted data] │
└──────────────────────────────────────────────────────────┘
- Service type:
_linksync._tcp.local - Windows PC broadcasts itself on the local network using mDNS
- Android discovers the PC automatically — no manual IP entry needed
- Uses the
mdns-sdRust crate on Windows and Android's NsdManager API - Refreshes on network changes (network watcher detects WiFi switches)
- Used when WiFi is unavailable or as an initial discovery mechanism
- Android: Advertises via BLE GATT; scans for Windows devices
- Windows: Uses
btleplugcrate + Windows BLE APIs (windows::Devices::Bluetooth) - BLE GATT characteristics carry device identity info for pairing bootstrap
- Data transport falls back to BLE when TCP is not available (SyncManager priority queue)
- Each device generates a permanent X25519 keypair on first launch
- During pairing, both sides exchange X25519 public keys
- A shared secret is derived via DH:
shared = X25519(my_private, their_public) - This shared secret is used as the symmetric key for XChaCha20-Poly1305 encryption
- Never transmitted in plaintext — only public keys are exchanged
- Each device has a permanent Ed25519 keypair stored in encrypted storage
- Public key is embedded in the QR code pairing payload
- Used to verify device identity and authenticate pairing requests
- On Android: stored in
EncryptedSharedPreferences(AES256-GCM) - On Windows: stored in local identity file (DPAPI encryption planned)
All clipboard data is encrypted end-to-end using XChaCha20-Poly1305.
Wire format:
┌──────────────────────────────────────────────────────────────┐
│ [24 bytes: random nonce] [ciphertext] [16 bytes: auth tag] │
└──────────────────────────────────────────────────────────────┘
- 24-byte random nonce generated fresh for every message
- 16-byte Poly1305 authentication tag appended automatically
- Implementations are cross-compatible:
- Android:
lazysodium-android(libsodium wrapper) - Windows:
chacha20poly1305Rust crate
- Android:
- Windows generates a QR code containing a JSON pairing payload
- Scanned by Android using CameraX + ML Kit Barcode Scanning
- QR payload contains:
{
"device_id": "uuid",
"device_name": "DESKTOP-XYZ",
"public_key": "hex-encoded-ed25519-public-key",
"x_public_key": "hex-encoded-x25519-public-key",
"pairing_token": "time-limited-secret-token",
"expires_at": 1700000000,
"ip": "192.168.1.100",
"port": 12716,
"bluetooth_mac": "AA:BB:CC:DD:EE:FF"
}- Pairing tokens are time-limited and single-use
- Android creates a local VPN tunnel (
ClipSyncVpnService) that routes no real traffic - Purpose: keeps the app alive indefinitely without being killed by Android's battery optimizer (Doze mode)
- The tunnel is a fake local interface — all data stays on the device
| Layer | Algorithm | Library |
|---|---|---|
| Key Exchange | X25519 Diffie-Hellman | lazysodium (Android), x25519-dalek (Rust) |
| Signing / Identity | Ed25519 | lazysodium (Android), ed25519-dalek (Rust) |
| Symmetric Encryption | XChaCha20-Poly1305 AEAD | lazysodium (Android), chacha20poly1305 (Rust) |
| Key Storage (Android) | AES256-GCM | androidx.security.crypto EncryptedSharedPreferences |
| Clipboard Deduplication | SHA-256 Hash | Standard library |
- Language: Kotlin
- Min SDK: 26 (Android 8.0), Target SDK: 34, Compile SDK: 35
- Java Version: 17
- UI: Jetpack Compose + Material3
- Build: Gradle with KSP (Kotlin Symbol Processing)
| Component | File | Role |
|---|---|---|
| IdentityManager | core/security/IdentityManager.kt |
Generates/stores Ed25519 + X25519 keypairs, device UUID |
| SecureSession | core/security/SecureSession.kt |
XChaCha20-Poly1305 encrypt/decrypt |
| SyncManager | core/sync/SyncManager.kt |
Clipboard queue with TCP-first, BLE-fallback retry logic |
| TcpClient | network/TcpClient.kt |
JSON-RPC 2.0 over encrypted TCP, length-prefixed frames |
| BluetoothClient | network/BluetoothClient.kt |
BLE GATT communication |
| Protocol | network/Protocol.kt |
Data classes for all JSON-RPC payloads |
| ClipSyncVpnService | features/vpn/ClipSyncVpnService.kt |
Fake VPN tunnel for background persistence |
| ClipboardWatcher | features/vpn/ClipboardWatcher.kt |
Monitors clipboard changes |
| BleAdvertiserService | features/connectivity/discovery/ |
Advertises phone over BLE |
| MdnsDiscoveryService | features/connectivity/discovery/ |
Finds Windows PC via mDNS |
| BootReceiver | receivers/BootReceiver.kt |
Auto-start on device reboot |
// Cryptography
"com.goterl:lazysodium-android" // libsodium: Ed25519, X25519, XChaCha20-Poly1305
// Database
"androidx.room:room-runtime" // Local SQLite ORM
"androidx.room:room-ktx"
// Security
"androidx.security:security-crypto" // EncryptedSharedPreferences (AES256)
// UI
"androidx.compose.ui:ui" // Jetpack Compose
"androidx.compose.material3:material3"
"androidx.activity:activity-compose"
"androidx.lifecycle:lifecycle-viewmodel-compose"
// Camera / QR
"androidx.camera:camera-camera2" // CameraX
"com.google.mlkit:barcode-scanning" // QR code scanning
// Async
"org.jetbrains.kotlinx:kotlinx-coroutines-android"
"org.jetbrains.kotlinx:kotlinx-serialization-json"- Backend: Rust 2021 edition (via Tauri 2.0)
- Frontend: React 19.1 + TypeScript 5.8
- Styling: Tailwind CSS 4.2
- Animations: Framer Motion
- Build: Vite 7.0
- Runtime: Tokio async runtime
| Component | File | Role |
|---|---|---|
| IdentityManager | src-tauri/src/auth.rs |
Keypair generation, QR payload creation |
| SecureSession | src-tauri/src/crypto.rs |
XChaCha20-Poly1305 AEAD |
| ClipboardManager | src-tauri/src/clipboard.rs |
Monitor + inject clipboard, history |
| TCP Listener | src-tauri/src/network.rs |
Async TCP server, session management, JSON-RPC |
| BLE Client | src-tauri/src/network_bt.rs |
Bluetooth LE via btleplug |
| DiscoveryManager | src-tauri/src/discovery/mod.rs |
Coordinates mDNS + BLE discovery |
| mDNS Broadcaster | src-tauri/src/discovery/mdns.rs |
Advertises PC on local network |
| Network Watcher | src-tauri/src/discovery/network_watcher.rs |
Detects WiFi changes |
| DbManager | src-tauri/src/db.rs |
SQLite: trusted devices, clipboard history |
| Dashboard | src/App.tsx |
Device status, connection info |
| Pairing Screen | src/SetupScreen.tsx |
QR code display, pairing flow |
| Settings | src/SettingsScreen.tsx |
Manage trusted devices |
tauri = "2" # Desktop app framework + system tray
tokio = { version = "1", features = ["full"] } # Async runtime
serde = "1" # Serialization
serde_json = "1" # JSON handling
mdns-sd = "0.13" # mDNS service discovery
x25519-dalek = "2.0" # X25519 key exchange
ed25519-dalek = "2.1" # Ed25519 signing
chacha20poly1305 = "0.10" # XChaCha20-Poly1305 AEAD
sqlx = { version = "0.8", features = ["sqlite"] } # Async SQLite
btleplug = "0.11.8" # Bluetooth LE
windows = "0.58" # Windows-specific Bluetooth APIs
uuid = "1" # UUID generation
rand = "0.8" # Cryptographic randomness
tauri-plugin-notification = "2"
tauri-plugin-autostart = "2"| Tool | Version | Download |
|---|---|---|
| Android Studio | Hedgehog or later | https://developer.android.com/studio |
| JDK | 17 | Bundled with Android Studio |
| Android SDK | API 34 | Via Android Studio SDK Manager |
| Android NDK | Latest stable | Via Android Studio SDK Manager (needed for lazysodium) |
git clone https://github.com/keshorerode/universal_clipboard.git
cd universal_clipboard/universal_clipboard_working/linksync-android- Launch Android Studio
- Click Open (not "New Project")
- Navigate to
universal_clipboard_working/linksync-android - Click OK and wait for Gradle sync to complete
In Android Studio:
- Go to Tools → SDK Manager
- Under SDK Platforms: check Android 14 (API 34)
- Under SDK Tools: check NDK (Side by side) and CMake
- Click Apply and let it download
In the linksync-android folder, open or create local.properties:
sdk.dir=C\:\\Users\\YourName\\AppData\\Local\\Android\\SdkAndroid Studio usually creates this automatically. Check the path matches your actual SDK location.
Option A — Android Studio (GUI):
- Wait for the Gradle sync to finish (bottom status bar)
- Go to Build → Make Project (or press
Ctrl+F9) - Once built, go to Build → Build Bundle(s)/APK(s) → Build APK(s)
- APK will be at:
app/build/outputs/apk/debug/app-debug.apk
Option B — Command Line:
# On Windows (PowerShell)
cd universal_clipboard_working/linksync-android
# Build debug APK
.\gradlew assembleDebug
# APK output location:
# app\build\outputs\apk\debug\app-debug.apk# Build release APK (requires signing config)
.\gradlew assembleReleaseVia USB (Recommended):
- Enable Developer Options on your phone: Settings → About Phone → tap Build Number 7 times
- Enable USB Debugging in Developer Options
- Connect phone via USB
- In Android Studio click Run ▶ or run:
adb install app\build\outputs\apk\debug\app-debug.apkVia File Transfer:
- Copy the APK to your phone
- On the phone: Settings → Install unknown apps → allow your file manager
- Tap the APK to install
When the app opens, grant all requested permissions:
- Location (required for BLE scanning on Android)
- Bluetooth (scan, connect, advertise)
- Camera (QR code scanning)
- Notifications
- Accessibility Service (for clipboard detection): Settings → Accessibility → LinkSync
- VPN (for background persistence): accept the VPN dialog
| Tool | Version | Install |
|---|---|---|
| Rust | Latest stable | https://rustup.rs |
| Node.js | 18 or later | https://nodejs.org |
| npm | Bundled with Node.js | — |
| Visual Studio Build Tools | 2022 | https://visualstudio.microsoft.com/visual-cpp-build-tools/ |
| WebView2 Runtime | Latest | Pre-installed on Windows 11; download for Windows 10 |
# Download and run rustup installer from https://rustup.rs
# Then verify:
rustc --version
cargo --version- Download Visual Studio Build Tools 2022
- In the installer, select: Desktop development with C++
- This installs the MSVC compiler required by Tauri
cargo install tauri-cli --version "^2.0"
# Verify
cargo tauri --versiongit clone https://github.com/keshorerode/universal_clipboard.git
cd universal_clipboard/universal_clipboard_working/linksync-windowsnpm installThis installs React, Vite, Tailwind CSS, Framer Motion, qrcode.react, and all other frontend dependencies.
cargo tauri devThis command:
- Starts the Vite dev server at
http://localhost:1420(React frontend) - Compiles the Rust backend (
src-tauri/) - Opens the Tauri window with hot-reload enabled
First run will take several minutes as Cargo downloads and compiles all Rust crates.
cargo tauri buildOutput installer will be at:
src-tauri/target/release/bundle/
├── msi/linksync-windows_0.1.0_x64_en-US.msi ← Windows Installer
└── nsis/linksync-windows_0.1.0_x64-setup.exe ← NSIS Installer
When the Windows app launches:
- IdentityManager loads or creates Ed25519 + X25519 keypairs from local storage
- TCP Listener starts on port
12716and listens for Android connections - mDNS Broadcaster advertises
_linksync._tcp.localon the local network - Clipboard Monitor begins watching for clipboard changes
- System Tray icon appears — right-click to show/hide window or quit
| Problem | Fix |
|---|---|
cargo: command not found |
Install Rust from https://rustup.rs and restart terminal |
| Rust compile error: MSVC not found | Install Visual Studio Build Tools with C++ workload |
| WebView2 error on startup | Download WebView2 Runtime from Microsoft |
| Port 12716 already in use | Check for other instances of the app running |
| Android not discovering PC | Ensure both devices are on the same WiFi network |
| BLE not working on Windows | Enable Bluetooth in Windows Settings |
Android Windows PC
│ │
│ 1. Open app → tap "Pair New Device" │
│ │
│ 2. PC shows QR code ────────│
│ (contains IP, port, │
│ Ed25519 PK, X25519 PK, │
│ pairing token) │
│ │
│ 3. Scan QR code ─────────────────────────────│
│ │
│ 4. Send pair_init ──────────────────────────►│
│ (Ed25519 PK, X25519 PK, token) │
│ │
│ 5. PC verifies token ───────│
│ saves trusted device │
│ │
│◄───────────────── 6. Respond with PC keys ────│
│ │
│ 7. Both derive shared secret via X25519 DH │
│ │
│◄══════════════ Encrypted sync starts ════════►│
universal_clipboard_working/
├── linksync-android/ ← Android (Kotlin)
│ └── app/src/main/java/com/kesho/linksync/
│ ├── core/
│ │ ├── db/ ← Room database (devices, history)
│ │ ├── model/ ← Data models
│ │ ├── security/ ← Crypto (identity, sessions)
│ │ └── sync/ ← SyncManager (queue + retry)
│ ├── features/
│ │ ├── connectivity/ ← BLE + mDNS discovery, QR scanner
│ │ └── vpn/ ← VPN persistence, clipboard watcher
│ ├── network/ ← TCP + BLE clients, protocol types
│ ├── services/ ← Foreground service, accessibility
│ └── receivers/ ← Boot receiver
│
└── linksync-windows/ ← Windows (Tauri + Rust + React)
├── src/ ← React frontend
│ ├── App.tsx ← Dashboard
│ ├── SetupScreen.tsx ← QR pairing
│ └── SettingsScreen.tsx ← Settings
└── src-tauri/src/ ← Rust backend
├── main.rs ← Entry point
├── lib.rs ← Module wiring, Tauri commands
├── auth.rs ← Identity + QR payload
├── clipboard.rs ← Clipboard monitor + inject
├── crypto.rs ← XChaCha20-Poly1305
├── db.rs ← SQLite (trusted devices)
├── network.rs ← TCP server + JSON-RPC
├── network_bt.rs ← BLE client
└── discovery/ ← mDNS broadcaster + BLE scanner
MIT