Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is a pure Rust implementation of the Android Debug Bridge (ADB) client protocol. The project consists of three main components:

- **adb_client**: Core library implementing ADB protocols for both server and direct device communication
- **adb_cli**: Command-line interface using the adb_client library
- **pyadb_client**: Python bindings for the adb_client library

## Development Commands

### Build Commands
```bash
# Build all workspace members
cargo build --release --all-features

# Build specific package
cargo build -p adb_client --release --all-features
cargo build -p adb_cli --release --all-features
cargo build -p pyadb_client --release --all-features
```

### Testing and Quality
```bash
# Run all tests
cargo test --verbose --all-features

# Run linting
cargo clippy --all-features

# Format code
cargo fmt --all --check

# Generate documentation
cargo doc --all-features --no-deps

# Run benchmarks
cargo bench
```

### Local Development
When working on a new release locally, the workspace uses path dependencies via `[patch.crates-io]` to reference the local `adb_client` crate.

## Architecture Overview

### Core Device Types
The library provides multiple ways to connect to Android devices:

1. **ADBServerDevice** (`server_device/`): Connects through ADB server (standard adb behavior)
2. **ADBUSBDevice** (`device/adb_usb_device.rs`): Direct USB connection to device
3. **ADBTcpDevice** (`device/adb_tcp_device.rs`): Direct TCP/IP connection to device
4. **ADBEmulatorDevice** (`emulator_device/`): Specialized emulator communication

### Transport Layer
The transport layer (`transports/`) abstracts different connection types:
- **TCPServerTransport**: Communication with ADB server
- **USBTransport**: Direct USB device communication
- **TcpTransport**: Direct TCP device communication
- **TCPEmulatorTransport**: Emulator-specific transport

### Message Protocol
Device communication uses the ADB message protocol (`device/adb_transport_message.rs`):
- **ADBTransportMessage**: Core message structure
- **MessageCommand**: Command types (AUTH, CNXN, OPEN, etc.)
- Authentication handled via RSA keys (`device/models/adb_rsa_key.rs`)

### Command Implementation
Commands are organized by device type:
- **device/commands/**: Direct device commands (shell, push, pull, install, etc.)
- **server/commands/**: ADB server commands (devices, connect, disconnect, etc.)
- **server_device/commands/**: Server-mediated device commands
- **emulator_device/commands/**: Emulator-specific commands (SMS, rotate, etc.)

### Key Features
- **Pure Rust**: No shell command execution - implements ADB protocol directly
- **Multiple connection modes**: Server proxy, direct USB, direct TCP/IP
- **Advanced features**: Framebuffer capture, mDNS device discovery
- **Authentication**: RSA key-based device authentication
- **Cross-platform**: Windows, macOS, Linux support

### Error Handling
Centralized error handling via `error.rs` with `RustADBError` enum covering all failure modes.

### Constants and Models
- **constants.rs**: Protocol constants and magic numbers
- **models/**: Data structures for requests/responses across all components

## Testing Notes
- Tests require physical devices or emulators to be connected
- Use `cargo test --verbose --all-features` for comprehensive testing
- Benchmarks available for performance-critical operations like file push
92 changes: 78 additions & 14 deletions adb_client/src/device/adb_usb_device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use rusb::Device;
use rusb::DeviceDescriptor;
use rusb::UsbContext;
use rusb::constants::LIBUSB_CLASS_VENDOR_SPEC;
use std::fs::read_to_string;
use std::fs::{create_dir_all, read_to_string, write};
use std::io::Read;
use std::io::Write;
use std::path::Path;
Expand All @@ -19,15 +19,46 @@ use crate::device::adb_transport_message::{AUTH_RSAPUBLICKEY, AUTH_SIGNATURE, AU
use crate::{Result, RustADBError, USBTransport};

pub fn read_adb_private_key<P: AsRef<Path>>(private_key_path: P) -> Result<Option<ADBRsaKey>> {
Ok(read_to_string(private_key_path.as_ref()).map(|pk| {
match ADBRsaKey::new_from_pkcs8(&pk) {
Ok(pk) => Some(pk),
Err(e) => {
log::error!("Error while create RSA private key: {e}");
None
let path = private_key_path.as_ref();
log::debug!("Attempting to read ADB private key from: {}", path.display());

match read_to_string(path) {
Ok(pk) => {
log::debug!("Successfully read private key file");
match ADBRsaKey::new_from_pkcs8(&pk) {
Ok(pk) => {
log::debug!("Successfully parsed RSA private key");
Ok(Some(pk))
},
Err(e) => {
log::error!("Error while parsing RSA private key: {e}");
Ok(None)
}
}
},
Err(e) => {
log::debug!("Could not read private key file '{}': {}", path.display(), e);
log::debug!("Will generate a random RSA key instead");
Ok(None)
}
})?)
}
}

pub fn save_adb_private_key<P: AsRef<Path>>(private_key: &ADBRsaKey, private_key_path: P) -> Result<()> {
let path = private_key_path.as_ref();
log::debug!("Saving ADB private key to: {}", path.display());

// Create parent directory if it doesn't exist
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
log::debug!("Created directory: {}", parent.display());
}

let pem_content = private_key.to_pkcs8_pem()?;
write(path, pem_content)?;
log::info!("Successfully saved ADB private key to: {}", path.display());

Ok(())
}

/// Search for adb devices with known interface class and subclass values
Expand Down Expand Up @@ -88,11 +119,15 @@ fn is_adb_device<T: UsbContext>(device: &Device<T>, des: &DeviceDescriptor) -> b
}

pub fn get_default_adb_key_path() -> Result<PathBuf> {
homedir::my_home()
let home = homedir::my_home()
.ok()
.flatten()
.map(|home| home.join(".android").join("adbkey"))
.ok_or(RustADBError::NoHomeDirectory)
.ok_or(RustADBError::NoHomeDirectory)?;

let adb_key_path = home.join(".android").join("adbkey");
log::debug!("Default ADB key path: {}", adb_key_path.display());

Ok(adb_key_path)
}

/// Represent a device reached and available over USB.
Expand All @@ -105,6 +140,7 @@ pub struct ADBUSBDevice {
impl ADBUSBDevice {
/// Instantiate a new [`ADBUSBDevice`]
pub fn new(vendor_id: u16, product_id: u16) -> Result<Self> {
log::debug!("Creating ADBUSBDevice with vendor_id: {:#06x}, product_id: {:#06x}", vendor_id, product_id);
Self::new_with_custom_private_key(vendor_id, product_id, get_default_adb_key_path()?)
}

Expand Down Expand Up @@ -134,9 +170,18 @@ impl ADBUSBDevice {
transport: USBTransport,
private_key_path: PathBuf,
) -> Result<Self> {
let private_key = match read_adb_private_key(private_key_path)? {
let private_key = match read_adb_private_key(&private_key_path)? {
Some(pk) => pk,
None => ADBRsaKey::new_random()?,
None => {
log::info!("Generating new random ADB private key");
let new_key = ADBRsaKey::new_random()?;
if let Err(e) = save_adb_private_key(&new_key, &private_key_path) {
log::warn!("Failed to save generated ADB private key: {}", e);
} else {
log::info!("Successfully saved new ADB private key to: {}", private_key_path.display());
}
new_key
},
};

let mut s = Self {
Expand Down Expand Up @@ -168,8 +213,10 @@ impl ADBUSBDevice {

/// Send initial connect
pub fn connect(&mut self) -> Result<()> {
log::debug!("Connecting to USB transport...");
self.get_transport_mut().connect()?;

log::debug!("Sending initial CNXN message...");
let message = ADBTransportMessage::new(
MessageCommand::Cnxn,
0x01000000,
Expand All @@ -179,14 +226,17 @@ impl ADBUSBDevice {

self.get_transport_mut().write_message(message)?;

log::debug!("Waiting for device response...");
let message = self.get_transport_mut().read_message()?;
// If the device returned CNXN instead of AUTH it does not require authentication,
// so we can skip the auth steps.
if message.header().command() == MessageCommand::Cnxn {
log::debug!("Device accepted connection without authentication");
return Ok(());
}
message.assert_command(MessageCommand::Auth)?;

log::debug!("Device requires authentication, received AUTH message");
// At this point, we should have receive an AUTH message with arg0 == 1
let auth_message = match message.header().arg0() {
AUTH_TOKEN => message,
Expand All @@ -197,12 +247,15 @@ impl ADBUSBDevice {
}
};

log::debug!("Signing auth token...");
let sign = self.private_key.sign(auth_message.into_payload())?;

log::debug!("Sending AUTH signature...");
let message = ADBTransportMessage::new(MessageCommand::Auth, AUTH_SIGNATURE, 0, &sign);

self.get_transport_mut().write_message(message)?;

log::debug!("Waiting for AUTH response...");
let received_response = self.get_transport_mut().read_message()?;

if received_response.header().command() == MessageCommand::Cnxn {
Expand All @@ -213,19 +266,30 @@ impl ADBUSBDevice {
return Ok(());
}

log::debug!("Signature authentication failed, sending public key...");
let mut pubkey = self.private_key.android_pubkey_encode()?.into_bytes();
pubkey.push(b'\0');

let message = ADBTransportMessage::new(MessageCommand::Auth, AUTH_RSAPUBLICKEY, 0, &pubkey);

self.get_transport_mut().write_message(message)?;

log::info!("Waiting for authorization from Android device...");
log::info!("Please check your device screen and accept the USB debugging authorization if prompted.");
log::debug!("Waiting for final AUTH response with 30 second timeout...");
let response = self
.get_transport_mut()
.read_message_with_timeout(Duration::from_secs(10))
.read_message_with_timeout(Duration::from_secs(30))
.and_then(|message| {
message.assert_command(MessageCommand::Cnxn)?;
Ok(message)
})
.map_err(|e| {
log::error!("Authentication timeout - please check that:");
log::error!("1. USB debugging is enabled on your Android device");
log::error!("2. You've accepted the USB debugging authorization dialog");
log::error!("3. Your device is unlocked and accessible");
e
})?;

log::info!(
Expand Down
6 changes: 5 additions & 1 deletion adb_client/src/device/models/adb_rsa_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use base64::{Engine, engine::general_purpose::STANDARD};
use num_bigint::{BigUint, ModInverse};
use num_traits::FromPrimitive;
use num_traits::cast::ToPrimitive;
use rsa::pkcs8::DecodePrivateKey;
use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey};
use rsa::traits::PublicKeyParts;
use rsa::{Pkcs1v15Sign, RsaPrivateKey};

Expand Down Expand Up @@ -108,6 +108,10 @@ impl ADBRsaKey {
.private_key
.sign(Pkcs1v15Sign::new::<sha1::Sha1>(), msg.as_ref())?)
}

pub fn to_pkcs8_pem(&self) -> Result<String> {
Ok(self.private_key.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)?.to_string())
}
}

fn set_bit(n: usize) -> Result<BigUint> {
Expand Down
13 changes: 11 additions & 2 deletions adb_client/src/transports/usb_transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,25 @@ impl USBTransport {
/// Instantiate a new [`USBTransport`].
/// Only the first device with given vendor_id and product_id is returned.
pub fn new(vendor_id: u16, product_id: u16) -> Result<Self> {
for device in rusb::devices()?.iter() {
log::debug!("Searching for USB device with vendor_id: {:#06x}, product_id: {:#06x}", vendor_id, product_id);

let devices = rusb::devices()?;
log::debug!("Found {} USB devices to scan", devices.len());

for device in devices.iter() {
if let Ok(descriptor) = device.device_descriptor() {
log::debug!("Checking device - vendor_id: {:#06x}, product_id: {:#06x}",
descriptor.vendor_id(), descriptor.product_id());

if descriptor.vendor_id() == vendor_id && descriptor.product_id() == product_id {
log::debug!("Found matching USB device!");
return Ok(Self::new_from_device(device));
}
}
}

Err(RustADBError::DeviceNotFound(format!(
"cannot find USB device with vendor_id={} and product_id={}",
"cannot find USB device with vendor_id={:#06x} and product_id={:#06x}",
vendor_id, product_id
)))
}
Expand Down