Theodicean.SharpAdb
0.0.8
See the version list below for details.
dotnet add package Theodicean.SharpAdb --version 0.0.8
NuGet\Install-Package Theodicean.SharpAdb -Version 0.0.8
<PackageReference Include="Theodicean.SharpAdb" Version="0.0.8" />
<PackageVersion Include="Theodicean.SharpAdb" Version="0.0.8" />
<PackageReference Include="Theodicean.SharpAdb" />
paket add Theodicean.SharpAdb --version 0.0.8
#r "nuget: Theodicean.SharpAdb, 0.0.8"
#:package Theodicean.SharpAdb@0.0.8
#addin nuget:?package=Theodicean.SharpAdb&version=0.0.8
#tool nuget:?package=Theodicean.SharpAdb&version=0.0.8
Theodicean.SharpAdb
Managed .NET client for the Android Debug Bridge wire protocol. Talks directly to adbd on the remote device. No adb binary, no local adb-server, no native dependencies.
Why
The published official adb binaries does not support all architectures and often has dependencies on native libraries.
Additionally, software that wants to use the ADB either needs to bundle the correct binary or rely on the end user to install it on their system.
Theodicean.SharpAdb implements the device protocol directly. Your process opens a TCP socket to the device, runs the CNXN/AUTH handshake, and you get a multiplexed stream over which you can run shell commands, push and pull files, install APKs, and so on.
Status
Minimum device versions, by feature:
- Shell + helpers (
ExecuteAsync, install, properties, input, logcat, …): Android 7 (API 24), via theshell_v2adbd feature. - File transfer (
SyncSession): Android 9 (API 28), via thesendrecv_v2adbd feature.
SyncSession.OpenAsync throws NotSupportedException if the device does not advertise sendrecv_v2; everything else works against Android 7+.
Working:
- TCP transport (port 5555 after
adb tcpip, or any IP:port the device is reachable on) - CNXN handshake, banner parsing, max-payload negotiation
- RSA-2048 authentication (signature path + RSAPUBLICKEY enrollment)
- STLS upgrade for devices that require TLS on the debug socket. Not supported on macOS if the device requires TLS 1.3 (see the note under wireless pairing below — the underlying cause is the same .NET/macOS limitation, and it's not specific to wireless connections; any STLS peer that mandates TLS 1.3 hits it). TLS 1.2 STLS peers are unaffected on any platform.
- Multiplexed
AdbStreamwith the per-write OKAY ack the protocol requires. Tolerates a small burst of unacked inbound WRTEs (queue depth 8, not 1) — realadbdwas observed occasionally splitting a single logical response across two back-to-back WRTE packets during end-to-end testing against a real emulator, even without thedelayed_ackfeature negotiated shell,v2,raw:for interactive commands with separate stdout/stderr and exit code (AdbShellResult)exec:for raw byte-stream services (e.g.screencap -p→ PNG bytes); no stdout/stderr split or exit codesync:v2 (LST2, LIS2, SND2, RCV2) for file transfer, with 64-bit sizes and full POSIX stat fields- Streaming APK install via
cmd package install -S <size> -— no/data/local/tmpstaging - Helpers: reboot, package install/uninstall/list, properties, processes, logcat (raw + parsed), screencap, key events, text input, taps/swipes, app start/stop, port forward
- Fault propagation from the read loop to open streams and to subsequent
OpenAsynccalls - Wireless pairing with the 6-digit PIN (Android 11+,
AdbPairing.PairAsync): SPAKE2 over Ed25519 (BoringSSL's construction, not the RFC 9382 P-256 variant) plus TLS 1.3 via BouncyCastle (needed for the RFC 5705 exported keying material .NET's own TLS stack doesn't expose). Validated end-to-end against a real device on Linux (an Android emulator running genuine AOSPadbd/BoringSSL) — pairing, the subsequentConnectTcpAsync, and running a shell command all succeeded repeatedly.AdbKnownHostspersists the device GUID pairing returns, for matching against_adb-tls-connect._tcpmDNS instance names (mDNS discovery itself is out of scope for this library — bring your own client).- Pairing succeeding does not mean you can then connect on macOS. Pairing uses BouncyCastle directly and works fine there, but the ongoing connection afterward goes through the same STLS/
SslStreampath noted above, which macOS can't complete against a TLS-1.3-only device. A BouncyCastle-based fix for that path was tried and correctly negotiated TLS 1.3, but hit a separate, real limitation in BouncyCastle's own async API (bcgit/bc-csharp#481: its asyncStreammethods are sync-over-async, each wrapping a blocking call in a pooled task) that deadlocked this library's long-lived background read loop against foreground writes under .NET's thread-pool starvation behavior. For now, use Linux or Windows to actually connect to a wirelessly-paired device; macOS can still be used to perform the pairing step itself.
- Pairing succeeding does not mean you can then connect on macOS. Pairing uses BouncyCastle directly and works fine there, but the ongoing connection afterward goes through the same STLS/
Not implemented:
- USB transport. The protocol layer is transport-agnostic (
IAdbTransport), so a USB transport using libusb or a platform-specific API can plug in without touching the rest. - mDNS device discovery.
- Sync v2 transparent compression (Brotli/LZ4/Zstd). Compression flag is sent as 0.
Install
Available on NuGet as Theodicean.SharpAdb.
dotnet add package Theodicean.SharpAdb
Or via PackageReference:
<PackageReference Include="Theodicean.SharpAdb" Version="*" />
Quick start
using Theodicean.SharpAdb;
using Theodicean.SharpAdb.Auth;
using Theodicean.SharpAdb.Services;
// Load the same key adb itself uses, so the device already trusts it.
var pem = File.ReadAllText(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".android", "adbkey"));
using var key = AdbAuthKey.LoadFromPem(pem);
await using var conn = await AdbConnection.ConnectTcpAsync("192.168.1.42", 5555, [key]);
Console.WriteLine($"connected to {conn.DeviceInfo.Model} ({conn.DeviceInfo.Product})");
var result = await conn.ExecuteAsync("getprop ro.build.version.release");
if (result.IsSuccess)
Console.WriteLine($"android {result.Stdout.Trim()}");
Authentication
The first time a key is presented to a device, the user has to tap "Allow USB debugging from this computer" on the device screen. After that the device persists the public key in /data/misc/adb/adb_keys and subsequent connections authenticate silently.
// Generate a new key and persist it for reuse
using var key = AdbAuthKey.Generate("alice@workstation");
File.WriteAllText("./mykey.pem", key.ExportPrivateKeyPem());
// First connect: device prompts user, AuthenticationMethod = PublicKey
// Subsequent connects: silent, AuthenticationMethod = Signature
await using var conn = await AdbConnection.ConnectTcpAsync(host, port, [key]);
If you want to fail fast instead of triggering the on-device prompt (useful in tests):
var opts = new AdbConnectOptions { SendPublicKeyOnAuthFailure = false };
await using var conn = await AdbConnection.ConnectTcpAsync(host, port, [key], opts);
// Throws AdbAuthenticationException if the device doesn't already trust the key.
For Android 11+ devices that only support wireless debugging, pair using the 6-digit code shown under Developer Options → Wireless debugging → "Pair device with pairing code" (equivalent to adb pair; validated against Android Simulator — see Status above). On macOS, pairing works but the subsequent ConnectTcpAsync will not — see the macOS note under Status; use Linux or Windows for the actual connection:
using Theodicean.SharpAdb.Pairing;
// host:port and the 6-digit code come from the device's pairing screen; the pairing port is
// NOT the regular ADB debug port.
var result = await AdbPairing.PairAsync("192.168.1.42", 37123, "493719", key);
await AdbKnownHosts.AddAsync(result); // remembers the device GUID for later mDNS matching
// Successful pairing means the device now trusts `key`. Regular connects still go through
// ConnectTcpAsync against the (separate, frequently-changing) debug port shown on-device or
// found via _adb-tls-connect._tcp mDNS. Not supported on macOS (see Status).
await using var conn = await AdbConnection.ConnectTcpAsync("192.168.1.42", 42891, [key]);
Finding the debug port via mDNS
The debug port adbd advertises changes across reboots and each time wireless debugging is toggled, so a real app should resolve it fresh before connecting rather than hardcoding a value from setup. mDNS discovery itself is not part of this library — bring your own client (e.g. Theodicean.Makaretu.Dns.Multicast on NuGet) and query for the service type below. Match the mDNS instance name directly against the GUID AdbKnownHosts stored for that device — that is how real adb itself correlates a discovery result to an already-paired device, no other lookup involved:
using Makaretu.Dns;
// _adb-tls-connect._tcp is the ongoing debug-port service; _adb-tls-pairing._tcp is only
// present while the device's pairing screen is open and isn't needed for regular reconnects.
var sd = await ServiceDiscovery.CreateInstance();
sd.ServiceInstanceDiscovered += async args =>
{
var instanceName = args.ServiceInstanceName!.Labels[0]; // == the device GUID from pairing
if (await AdbKnownHosts.ContainsAsync(instanceName))
{
// extract host/port from args.Message's SRV + address records, then ConnectTcpAsync
}
};
await sd.QueryServiceInstances(new DomainName("_adb-tls-connect._tcp"));
File transfer
await using var sync = await SyncSession.OpenAsync(conn);
await using var src = File.OpenRead("./build/app.apk");
await sync.PushAsync(src, "/data/local/tmp/app.apk");
await using var dst = File.Create("./screenshot.png");
await sync.PullAsync("/sdcard/Pictures/screenshot.png", dst);
var stat = await sync.StatAsync("/data/local/tmp");
if (stat.IsDirectory) { /* ... */ }
await foreach (var entry in sync.ListAsync("/sdcard"))
Console.WriteLine($"{entry.Name} ({entry.Size} bytes)");
Helpers
// Properties
var sdk = await conn.GetPropertyAsync("ro.build.version.sdk");
var all = await conn.GetAllPropertiesAsync();
// Packages — streams the APK through `cmd package install` rather than staging on /data/local/tmp.
// FailureReason can be null when adbd printed "Failure" without a bracketed reason; fall back to
// the raw shell output (Raw.Stdout + Raw.Stderr) so the diagnostic isn't empty.
var install = await conn.InstallAsync("./app.apk");
if (!install.IsSuccess)
throw new InvalidOperationException(
install.FailureReason ?? $"install failed: {install.Raw.Stdout}{install.Raw.Stderr}");
var packages = await conn.ListPackagesAsync();
await conn.UninstallAsync("com.example.app");
// Input
await conn.SendKeyEventAsync(KeyCode.Home);
await conn.SendTextAsync("hello world");
await conn.TapAsync(540, 1200);
await conn.SwipeAsync(100, 1500, 100, 500, durationMs: 250);
// App lifecycle
await conn.StartAppAsync("com.example.app");
await conn.StopAppAsync("com.example.app");
// Screencap (PNG bytes)
var png = await conn.CaptureScreenAsync();
// Logcat
await foreach (var entry in conn.LogcatAsync(filterSpec: "*:E"))
Console.WriteLine($"{entry.Priority} {entry.Tag}: {entry.Message}");
// Port forward (local TCP -> device port). Local port 0 = auto-assign.
await using var fwd = await conn.ForwardPortAsync(localPort: 0, remotePort: 8080);
Console.WriteLine($"http://127.0.0.1:{fwd.LocalPort}/");
// Reboot
await conn.RebootAsync(RebootMode.Recovery);
Tests
Unit tests run without a device:
dotnet test tests/Theodicean.SharpAdb.Tests
Integration tests connect to a real device. Set ADB_HOST (and optionally ADB_KEY_PATH):
ADB_HOST=192.168.1.42:5555 dotnet test tests/Theodicean.SharpAdb.IntegrationTests
If ADB_KEY_PATH is unset, the fixture defaults to ~/.android/adbkey (the same file Google's adb uses). If neither path exists, a fresh key is generated and saved; the first connect will prompt the user to tap "Allow" on the device.
For one-time on-device key authorization, set ADB_RUN_BOOTSTRAP=1 and run the BootstrapKeyOnDevice test.
Pairing integration tests need ADB_PAIR_HOST and ADB_PAIR_CODE (from the device's pairing screen; add ADB_CONNECT_HOST to also verify the paired key can connect — skipped on macOS, see Status):
ADB_PAIR_HOST=192.168.1.42:37123 ADB_PAIR_CODE=493719 dotnet test tests/Theodicean.SharpAdb.IntegrationTests
License
MIT License. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
-
net10.0
- BouncyCastle.Cryptography (>= 2.7.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
-
net11.0
- BouncyCastle.Cryptography (>= 2.7.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.9-pre03 | 7 | 10/26/2026 |
| 0.0.9-pre02 | 131 | 10/14/2026 |
| 0.0.9-pre01 | 84 | 10/14/2026 |
| 0.0.8 | 115 | 10/7/2026 |
| 0.0.7 | 198 | 9/9/2026 |
| 0.0.6 | 147 | 9/2/2026 |
| 0.0.5 | 153 | 8/31/2026 |
| 0.0.4 | 297 | 8/11/2026 |
| 0.0.4-pre02 | 157 | 8/10/2026 |
| 0.0.4-pre01 | 134 | 8/9/2026 |
| 0.0.3 | 127 | 8/8/2026 |
| 0.0.3-pre04 | 127 | 8/7/2026 |
| 0.0.3-pre03 | 377 | 6/15/2026 |
| 0.0.3-pre02 | 231 | 5/15/2026 |
| 0.0.3-pre01 | 126 | 5/15/2026 |
| 0.0.2 | 205 | 5/13/2026 |
| 0.0.2-pre01 | 143 | 5/12/2026 |
| 0.0.1 | 157 | 5/10/2026 |
| 0.0.1-pre01 | 258 | 5/3/2026 |