diff --git a/Cargo.lock b/Cargo.lock index 8dcd347..1ef72d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1502,7 +1502,8 @@ dependencies = [ "env_logger 0.11.8", "hyper", "hyper-util", - "md5", + "md5 0.7.0", + "md5crypt", "nusb", "reqwest", "serde", @@ -1744,6 +1745,21 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + +[[package]] +name = "md5crypt" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8db1a978f99f584f2e72601860f68bb2cea9c09f4408f5e83f805db3434fb0" +dependencies = [ + "md5 0.8.0", +] + [[package]] name = "mdns-sd" version = "0.13.9" diff --git a/daemon/src/display/mod.rs b/daemon/src/display/mod.rs index 3e9c19d..6f19ca5 100644 --- a/daemon/src/display/mod.rs +++ b/daemon/src/display/mod.rs @@ -1,7 +1,7 @@ mod generic_framebuffer; -pub mod orbic; pub mod headless; +pub mod orbic; pub mod tmobile; pub mod tplink; pub mod tplink_framebuffer; diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 2c48b2f..e3449a2 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -241,6 +241,7 @@ async fn run_with_config( Device::Tplink => display::tplink::update_ui, Device::Tmobile => display::tmobile::update_ui, Device::Wingtech => display::wingtech::update_ui, + Device::Pinephone => display::headless::update_ui, }; update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx); diff --git a/installer/Cargo.toml b/installer/Cargo.toml index a286494..b1dc248 100644 --- a/installer/Cargo.toml +++ b/installer/Cargo.toml @@ -15,6 +15,7 @@ env_logger = "0.11.8" hyper = "1.6.0" hyper-util = "0.1.11" md5 = "0.7.0" +md5crypt = "1.0.0" nusb = "0.1.13" reqwest = { version = "0.12.15", features = ["json"], default-features = false } serde = { version = "1.0.219", features = ["derive"] } diff --git a/installer/src/main.rs b/installer/src/main.rs index df9997b..00b24cd 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand}; use env_logger::Env; mod orbic; +mod pinephone; mod tmobile; mod tplink; mod util; @@ -18,12 +19,16 @@ struct Args { command: Command, } +// A note on stylisation of device names: strip special characters and spell like This regardless +// of the manufacturer's capitalisation. #[derive(Subcommand, Debug)] enum Command { /// Install rayhunter on the Orbic Orbic RC400L. Orbic(InstallOrbic), /// Install rayhunter on the TMobile TMOHS1. Tmobile(TmobileArgs), + /// Install rayhunter on a PinePhone's Quectel modem. + Pinephone(InstallPinephone), /// Install rayhunter on the TP-Link M7350. Tplink(InstallTpLink), /// Install rayhunter on the Wingtech CT2MHS01. @@ -58,6 +63,9 @@ struct InstallTpLink { #[derive(Parser, Debug)] struct InstallOrbic {} +#[derive(Parser, Debug)] +struct InstallPinephone {} + #[derive(Parser, Debug)] struct Util { #[command(subcommand)] @@ -69,7 +77,7 @@ enum UtilSubCommand { /// Send a serial command to the Orbic. Serial(Serial), /// Start an ADB shell - Shell(Shell), + Shell(NoArgs), /// Root the Tmobile and launch adb. TmobileStartAdb(TmobileArgs), /// Root the Tmobile and launch telnetd. @@ -80,6 +88,10 @@ enum UtilSubCommand { WingtechStartTelnet(WingtechArgs), /// Root the Wingtech and launch adb. WingtechStartAdb(WingtechArgs), + /// Unlock the Pinephone's modem and start adb. + PinephoneStartAdb(NoArgs), + /// Lock the Pinephone's modem and stop adb. + PinephoneStopAdb(NoArgs), /// Send a file to the TP-Link device over telnet. /// /// Before running this utility, you need to make telnet accessible with `installer util @@ -151,7 +163,7 @@ struct Serial { } #[derive(Parser, Debug)] -struct Shell {} +struct NoArgs {} async fn run() -> Result<(), Error> { env_logger::Builder::from_env(Env::default().default_filter_or("off")).init(); @@ -160,6 +172,8 @@ async fn run() -> Result<(), Error> { match command { Command::Tmobile(args) => tmobile::install(args).await.context("Failed to install rayhunter on the Tmobile TMOHS1. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?, Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?, + Command::Pinephone(_) => pinephone::install().await + .context("Failed to install rayhunter on the Pinephone's Quectel modem")?, Command::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?, Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?, Command::Util(subcommand) => match subcommand.command { @@ -195,6 +209,8 @@ async fn run() -> Result<(), Error> { } UtilSubCommand::WingtechStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Wingtech CT2MHS01")?, UtilSubCommand::WingtechStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Wingtech CT2MHS01")?, + UtilSubCommand::PinephoneStartAdb(_) => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?, + UtilSubCommand::PinephoneStopAdb(_) => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?, } } diff --git a/installer/src/orbic.rs b/installer/src/orbic.rs index e8d6af5..61b67a4 100644 --- a/installer/src/orbic.rs +++ b/installer/src/orbic.rs @@ -4,12 +4,12 @@ use std::time::Duration; use adb_client::{ADBDeviceExt, ADBUSBDevice, RustADBError}; use anyhow::{Context, Result, anyhow, bail}; +use nusb::Interface; use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer}; -use nusb::{Device, Interface}; use sha2::{Digest, Sha256}; use tokio::time::sleep; -use crate::util::echo; +use crate::util::{echo, open_usb_device}; use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT}; pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found. @@ -138,7 +138,8 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result { get_adb().await } -async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> { +/// Test rayhunter on the device over adb without forwarding. +pub async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> { const MAX_FAILURES: u32 = 10; let mut failures = 0; while failures < MAX_FAILURES { @@ -474,22 +475,3 @@ pub fn open_orbic() -> Result> { Ok(None) } - -/// General function to open a USB device -fn open_usb_device(vid: u16, pid: u16) -> Result> { - let devices = match nusb::list_devices() { - Ok(d) => d, - Err(_) => return Ok(None), - }; - - for device in devices { - if device.vendor_id() == vid && device.product_id() == pid { - match device.open() { - Ok(d) => return Ok(Some(d)), - Err(e) => bail!("device found but failed to open: {}", e), - } - } - } - - Ok(None) -} diff --git a/installer/src/pinephone.rs b/installer/src/pinephone.rs new file mode 100644 index 0000000..c988b36 --- /dev/null +++ b/installer/src/pinephone.rs @@ -0,0 +1,276 @@ +use std::io::Write; +use std::path::Path; +use std::time::Duration; + +use adb_client::{ADBDeviceExt, ADBUSBDevice}; +use anyhow::{Context, Result, anyhow, bail}; +use md5::compute as md5_compute; +use md5crypt::md5crypt; +use nusb::Interface; +use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer}; +use tokio::time::sleep; + +use crate::orbic::test_rayhunter; +use crate::util::{echo, open_usb_device}; +use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT}; + +const USB_VENDOR_ID: u16 = 0x2C7C; +const USB_PRODUCT_ID: u16 = 0x125; +const USB_INTERFACE_NUMBER: u8 = 2; + +pub async fn install() -> Result<()> { + echo!("Unlocking modem ... "); + start_adb().await?; + sleep(Duration::from_secs(3)).await; + let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap(); + println!("ok"); + + adb.run_command(&["mount", "-o", "remount,rw", "/"], "exit code 0")?; + adb.run_command(&["mkdir", "-p", "/data/rayhunter"], "exit code 0")?; + + let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); + adb.install_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)?; + adb.install_file( + "/data/rayhunter/config.toml", + CONFIG_TOML + .replace("#device = \"orbic\"", "device = \"pinephone\"") + .as_bytes(), + )?; + adb.install_file( + "/etc/init.d/rayhunter_daemon", + RAYHUNTER_DAEMON_INIT.as_bytes(), + )?; + adb.install_file( + "/etc/init.d/misc-daemon", + include_bytes!("../../dist/scripts/misc-daemon"), + )?; + adb.run_command( + &["chmod", "755", "/etc/init.d/rayhunter_daemon"], + "exit code 0", + )?; + adb.run_command(&["chmod", "755", "/etc/init.d/misc-daemon"], "exit code 0")?; + + println!("Rebooting device and waiting 30 seconds for it to start up."); + adb.run_command(&["shutdown -r -t 1 now"], "exit code 0")?; + sleep(Duration::from_secs(30)).await; + + echo!("Unlocking modem ... "); + start_adb().await?; + sleep(Duration::from_secs(3)).await; + let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap(); + println!("ok"); + + echo!("Testing rayhunter ... "); + test_rayhunter(&mut adb).await?; + println!("ok"); + println!("rayhunter is running on the modem. Use adb to access the web interface."); + + Ok(()) +} + +struct Qusbcfg { + vendor_id: u16, + product_id: u16, + diag: u8, + nmea: u8, + at: u8, + modem: u8, + net: u8, + adb: u8, + audio: u8, +} + +impl Default for Qusbcfg { + fn default() -> Self { + Qusbcfg { + vendor_id: USB_VENDOR_ID, + product_id: USB_PRODUCT_ID, + diag: 1, + nmea: 1, + at: 1, + modem: 1, + net: 1, + adb: 0, + audio: 0, + } + } +} + +impl std::fmt::Display for Qusbcfg { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fmt.write_str(&format!( + "AT+QCFG=\"usbcfg\",{:#X},{:#X},{},{},{},{},{},{},{}", + self.vendor_id, + self.product_id, + self.diag, + self.nmea, + self.at, + self.modem, + self.net, + self.adb, + self.audio + ))?; + Ok(()) + } +} + +/// Start the adb daemon on the Quectel modem. +/// A reimplementation of qadbkey-unlock.c by "igem, 2019 ;)" +pub async fn start_adb() -> Result<()> { + let tty = serial_interface()?.unwrap(); + + let get_qadbkey = tty + .send_at_command("AT+QADBKEY?") + .await + .context("Failed to request QADBKEY")?; + let resp = String::from_utf8_lossy(&get_qadbkey); + if !resp.contains("\r\nOK\r\n") { + bail!("Received unexpected response: {0}", resp); + } + let salt = match resp.find("+QADBKEY: ") { + Some(i) => &resp[i + 10..i + 18], + None => bail!("Received unexpected response: {0}", resp), + }; + + let hashed = &md5crypt(b"SH_adb_quectel", salt.as_bytes())[12..28]; + let hashed = String::from_utf8_lossy(hashed); + + let unlock = tty + .send_at_command(&format!("AT+QADBKEY=\"{hashed}\"")) + .await + .context("Failed to send AT+QADBKEY")?; + let resp = String::from_utf8_lossy(&unlock); + if !resp.contains("\r\nOK\r\n") { + bail!("Received unexpected response: {0}", resp); + } + + let adb_enable = Qusbcfg { + adb: 1, + ..Default::default() + }; + let start_adb = tty + .send_at_command(&adb_enable.to_string()) + .await + .context("Failed to send enable adb command.")?; + let resp = String::from_utf8_lossy(&start_adb); + if !resp.contains("\r\nOK\r\n") { + bail!("Received unexpected response: {0}", resp); + } + + Ok(()) +} + +/// Stop the adb daemon on the Quectel modem. +pub async fn stop_adb() -> Result<()> { + let tty = serial_interface()?.unwrap(); + let adb_disable = Qusbcfg::default(); + let stop_adb = tty + .send_at_command(&adb_disable.to_string()) + .await + .context("Failed to disable adb.")?; + let resp = String::from_utf8_lossy(&stop_adb); + if !resp.contains("\r\nOK\r\n") { + bail!("Received unexpected response: {0}", resp); + } + Ok(()) +} + +trait Install { + fn run_command(&mut self, command: &[&str], expected_output: &str) -> Result<()>; + fn install_file(&mut self, dest: &str, payload: &[u8]) -> Result<()>; +} + +impl Install for ADBUSBDevice { + /// Run an adb shell command, append '; echo exit code $?' to the command and verify its output. + fn run_command(&mut self, command: &[&str], expected_output: &str) -> Result<()> { + let mut buf = Vec::::new(); + let mut cmd = Vec::<&str>::new(); + cmd.extend_from_slice(command); + cmd.extend_from_slice(&[";", "echo", "exit code $?"]); + self.shell_command(&cmd, &mut buf)?; + let output = String::from_utf8_lossy(&buf); + if !output.contains(expected_output) { + bail!("{expected_output:?} not found in: {output}"); + } + Ok(()) + } + + /// Transfer a file to the modem's filesystem with adb push. + /// Validates the file sends successfully to /tmp before overwriting the destination. + fn install_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> { + echo!("Sending file {dest} ... "); + let file_name = Path::new(dest) + .file_name() + .ok_or_else(|| anyhow!("{dest} does not have a file name"))? + .to_str() + .ok_or_else(|| anyhow!("{dest}'s file name is not UTF8"))? + .to_owned(); + let push_tmp_path = format!("/tmp/{file_name}"); + let file_hash = md5_compute(payload); + self.push(&mut payload, &push_tmp_path)?; + self.run_command(&["md5sum", &push_tmp_path], &format!("{file_hash:x}"))?; + self.run_command(&["mv", &push_tmp_path, dest], "exit code 0")?; + println!("ok"); + Ok(()) + } +} + +/// Claim the modem's USB interface for sending AT commands. +fn serial_interface() -> Result> { + if let Some(device) = open_usb_device(USB_VENDOR_ID, USB_PRODUCT_ID)? { + let interface = device + .detach_and_claim_interface(USB_INTERFACE_NUMBER) + .context("detach_and_claim_interface({USB_INTERFACE_NUMBER}) failed")?; + return Ok(Some(interface)); + } + Ok(None) +} + +trait AT { + async fn send_at_command(&self, command: &str) -> Result>; +} + +impl AT for Interface { + /// Send an AT command to the Quectel modem. + async fn send_at_command(&self, command: &str) -> Result> { + let mut data = String::new(); + data.push_str("\r\n"); + data.push_str(command); + data.push_str("\r\n"); + + let timeout = Duration::from_secs(1); + + let enable_serial_port = Control { + control_type: ControlType::Class, + recipient: Recipient::Interface, + request: 0x22, + value: 3, + index: USB_INTERFACE_NUMBER as u16, + }; + + self.control_out_blocking(enable_serial_port, &[], timeout) + .context("Failed to send control request")?; + + tokio::time::timeout(timeout, self.bulk_out(0x3, data.as_bytes().to_vec())) + .await + .context("Timed out writing command")? + .into_result() + .context("Failed to write command")?; + + let response = tokio::time::timeout(timeout, self.bulk_in(0x84, RequestBuffer::new(256))) + .await + .context("Timed out reading response")? + .into_result() + .context("Failed to read response")?; + + Ok(response) + } +} + +#[test] +fn test_qadbcfg_fmt() { + assert_eq!( + Qusbcfg::default().to_string(), + "AT+QCFG=\"usbcfg\",0x2C7C,0x125,1,1,1,1,1,0,0" + ); +} diff --git a/installer/src/util.rs b/installer/src/util.rs index 68304d9..b368c72 100644 --- a/installer/src/util.rs +++ b/installer/src/util.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use std::time::Duration; use anyhow::{Context, Result, bail}; +use nusb::Device; use reqwest::Client; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; @@ -132,3 +133,20 @@ pub async fn http_ok_every( } Ok(()) } + +/// General function to open a USB device +pub fn open_usb_device(vid: u16, pid: u16) -> Result> { + let devices = match nusb::list_devices() { + Ok(d) => d, + Err(_) => return Ok(None), + }; + for device in devices { + if device.vendor_id() == vid && device.product_id() == pid { + match device.open() { + Ok(d) => return Ok(Some(d)), + Err(e) => bail!("device found but failed to open: {}", e), + } + } + } + Ok(None) +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index e231b31..c53f741 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -24,4 +24,5 @@ pub enum Device { Tplink, Tmobile, Wingtech, + Pinephone, }