use std::io::Write;
use std::process::Command;
use crate::error::{Error, Result};
use crate::progress::{Event, ProgressFn};
pub const APPLE_VID: u16 = 0x05ac;
pub const DRIVER_PIDS: &[u16] = &[
0x1222, 0x1227, 0x1280, 0x1281, 0x1282, 0x1283, 0xf014, 0x1881, ];
const RESTORE_MODE_PIDS: std::ops::RangeInclusive<u16> = 0x1290..=0x12af;
pub fn is_target_pid(pid: u16) -> bool {
DRIVER_PIDS.contains(&pid)
}
#[derive(Debug, Clone)]
pub struct Target {
pub pid: u16,
pub name: String,
}
pub fn connected_targets() -> Result<Vec<Target>> {
use nusb::MaybeFuture;
let devices = nusb::list_devices()
.wait()
.map_err(|e| Error::Usb(e.to_string()))?;
Ok(devices
.filter(|d| d.vendor_id() == APPLE_VID && is_target_pid(d.product_id()))
.map(|d| {
let pid = d.product_id();
let name = d
.product_string()
.map(str::to_owned)
.unwrap_or_else(|| format!("Apple device (mode {pid:#06x})"));
Target { pid, name }
})
.collect())
}
pub fn install_winusb(progress: ProgressFn) -> Result<usize> {
let targets = connected_targets()?;
if targets.is_empty() {
return Err(Error::DriverInstall(
"no Apple device in DFU or recovery mode is connected — put the target into DFU \
(see `restorekit status`) and re-run"
.into(),
));
}
progress(Event::DriverSetupStarting);
let base = std::env::temp_dir();
let stamp = std::process::id();
let work = base.join(format!("restorekit-winusb-{stamp}"));
let script = base.join(format!("restorekit-winusb-{stamp}.ps1"));
let _ = std::fs::remove_dir_all(&work);
std::fs::create_dir_all(&work).map_err(|e| Error::DriverInstall(e.to_string()))?;
let inf_path = work.join("restorekit_winusb.inf");
write_file(&inf_path, winusb_inf(DRIVER_PIDS).as_bytes())?;
write_file(&script, WINUSB_SETUP_PS1.as_bytes())?;
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
let output = Command::new("powershell")
.creation_flags(CREATE_NO_WINDOW)
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"])
.arg(&script)
.arg("-WorkDir")
.arg(&work)
.output()
.map_err(|e| Error::DriverInstall(format!("failed to run PowerShell: {e}")));
let _ = std::fs::remove_dir_all(&work);
let _ = std::fs::remove_file(&script);
let output = output?;
if !output.status.success() {
let mut detail = String::from_utf8_lossy(&output.stderr).trim().to_string();
if detail.is_empty() {
detail = String::from_utf8_lossy(&output.stdout).trim().to_string();
}
return Err(Error::DriverInstall(format!(
"WinUSB setup did not complete: {detail}"
)));
}
for t in &targets {
progress(Event::DriverBound {
name: t.name.clone(),
});
}
Ok(targets.len())
}
fn winusb_inf(pids: &[u16]) -> String {
let mut models = String::new();
for pid in pids {
models.push_str(&format!(
"%DeviceName% = WINUSB_Install, USB\\VID_{APPLE_VID:04X}&PID_{pid:04X}\n"
));
}
for pid in RESTORE_MODE_PIDS {
models.push_str(&format!(
"%DeviceName% = WINUSB_Install, USB\\VID_{APPLE_VID:04X}&PID_{pid:04X}&RESTORE_MODE&MI_00\n"
));
}
format!(
r#"; Generated by RestoreKit - binds WinUSB to an Apple device so libusb can access it.
[Version]
Signature = "$Windows NT$"
Class = USBDevice
ClassGuid = {{88BAE032-5A81-49f0-BC3D-A4FF138216D6}}
Provider = %ProviderName%
CatalogFile = restorekit_winusb.cat
DriverVer = 01/01/2024,1.0.0.0
[Manufacturer]
%ProviderName% = Standard,NTamd64
[Standard.NTamd64]
{models}
[WINUSB_Install.NT]
Include = winusb.inf
Needs = WINUSB.NT
[WINUSB_Install.NT.Services]
Include = winusb.inf
Needs = WINUSB.NT.Services
[Strings]
ProviderName = "RestoreKit"
DeviceName = "Apple Mobile Device (RestoreKit WinUSB)"
"#
)
}
const WINUSB_SETUP_PS1: &str = r#"param([Parameter(Mandatory=$true)][string]$WorkDir)
$ErrorActionPreference = 'Stop'
$cat = Join-Path $WorkDir 'restorekit_winusb.cat'
$subject = 'CN=RestoreKit WinUSB'
$cert = Get-ChildItem Cert:\CurrentUser\My -ErrorAction SilentlyContinue |
Where-Object { $_.Subject -eq $subject } | Select-Object -First 1
if (-not $cert) {
$cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject $subject `
-CertStoreLocation Cert:\CurrentUser\My -KeyUsage DigitalSignature `
-TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3')
}
New-FileCatalog -Path $WorkDir -CatalogFilePath $cat -CatalogVersion 2 | Out-Null
Set-AuthenticodeSignature -FilePath $cat -Certificate $cert | Out-Null
$cer = Join-Path $WorkDir 'restorekit.cer'
Export-Certificate -Cert $cert -FilePath $cer | Out-Null
Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\Root | Out-Null
Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\TrustedPublisher | Out-Null
Get-ChildItem $WorkDir -Filter '*.inf' | ForEach-Object {
pnputil /add-driver $_.FullName /install | Out-Host
}
$svcs = Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue |
Where-Object { $_.InstanceId -match 'VID_05AC' } |
ForEach-Object { (Get-PnpDeviceProperty -InstanceId $_.InstanceId -KeyName 'DEVPKEY_Device_Service' -ErrorAction SilentlyContinue).Data }
if ($svcs -contains 'WINUSB' -or $svcs -contains 'winusb') { exit 0 }
Write-Error 'WinUSB was not bound to any connected Apple device'
exit 1
"#;
fn write_file(path: &std::path::Path, bytes: &[u8]) -> Result<()> {
let mut f = std::fs::File::create(path).map_err(|e| Error::DriverInstall(e.to_string()))?;
f.write_all(bytes)
.map_err(|e| Error::DriverInstall(e.to_string()))?;
Ok(())
}
pub const RESTORE_WATCH_ARG: &str = "--rk-bind-restore-mode";
pub struct RestoreWatcherGuard {
liveness: std::path::PathBuf,
}
impl Drop for RestoreWatcherGuard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.liveness);
}
}
pub fn spawn_restore_mode_watcher() -> Option<RestoreWatcherGuard> {
let liveness = std::env::temp_dir().join(format!(
"restorekit-restore-live-{}.tmp",
std::process::id()
));
if std::fs::write(&liveness, b"1").is_err() {
return None;
}
if is_elevated() {
let live = liveness.clone();
std::thread::spawn(move || run_restore_mode_watch_worker(&live));
return Some(RestoreWatcherGuard { liveness });
}
let exe = match std::env::current_exe() {
Ok(p) => p,
Err(_) => {
let _ = std::fs::remove_file(&liveness);
return None;
}
};
let params = format!("{RESTORE_WATCH_ARG} \"{}\"", liveness.display());
if launch_elevated_hidden(&exe.to_string_lossy(), ¶ms).is_err() {
let _ = std::fs::remove_file(&liveness);
return None;
}
Some(RestoreWatcherGuard { liveness })
}
fn is_elevated() -> bool {
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
use windows_sys::Win32::Security::{
GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
unsafe {
let mut token: HANDLE = std::ptr::null_mut();
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
return false;
}
let mut elevation = TOKEN_ELEVATION { TokenIsElevated: 0 };
let mut ret_len = 0u32;
let ok = GetTokenInformation(
token,
TokenElevation,
&mut elevation as *mut _ as *mut core::ffi::c_void,
std::mem::size_of::<TOKEN_ELEVATION>() as u32,
&mut ret_len,
);
CloseHandle(token);
ok != 0 && elevation.TokenIsElevated != 0
}
}
pub fn run_restore_mode_watch_worker(liveness: &std::path::Path) {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
let script = std::env::temp_dir().join(format!(
"restorekit-restore-watch-{}.ps1",
std::process::id()
));
if write_file(&script, RESTORE_WATCH_PS1.as_bytes()).is_err() {
return;
}
let _ = Command::new("powershell")
.creation_flags(CREATE_NO_WINDOW)
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"])
.arg(&script)
.arg("-LivenessFile")
.arg(liveness)
.status();
let _ = std::fs::remove_file(&script);
}
fn launch_elevated_hidden(exe: &str, params: &str) -> Result<()> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::UI::Shell::{ShellExecuteExW, SHELLEXECUTEINFOW};
use windows_sys::Win32::UI::WindowsAndMessaging::SW_HIDE;
fn wide(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
let verb = wide("runas");
let file = wide(exe);
let par = wide(params);
unsafe {
let mut sei: SHELLEXECUTEINFOW = std::mem::zeroed();
sei.cbSize = std::mem::size_of::<SHELLEXECUTEINFOW>() as u32;
sei.lpVerb = verb.as_ptr();
sei.lpFile = file.as_ptr();
sei.lpParameters = par.as_ptr();
sei.nShow = SW_HIDE;
if ShellExecuteExW(&mut sei) == 0 {
return Err(Error::DriverInstall(
"could not start the restore-mode driver watcher (UAC declined?)".into(),
));
}
}
Ok(())
}
const RESTORE_WATCH_PS1: &str = r#"param([string]$LivenessFile)
$ErrorActionPreference = 'SilentlyContinue'
Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class RkNewDev {
[DllImport("newdev.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern bool UpdateDriverForPlugAndPlayDevices(
IntPtr hwnd, string HardwareId, string FullInfPath, uint InstallFlags, out bool bReboot);
}
"@
# INSTALLFLAG_FORCE (0x1): install even though Apple's WHQL driver outranks ours.
# INSTALLFLAG_NONINTERACTIVE (0x4): never prompt.
$FLAGS = 0x1 -bor 0x4
# Our staged WinUSB package (has the RESTORE_MODE interface entries).
$inf = (Get-ChildItem C:\Windows\INF\oem*.inf -ErrorAction SilentlyContinue | Where-Object {
$c = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue
$c -match 'RestoreKit' -and $c -match 'RESTORE_MODE'
} | Select-Object -First 1).FullName
if (-not $inf) { exit 1 }
$leaf = Split-Path $inf -Leaf
$deadline = (Get-Date).AddMinutes(45)
$bound = $false
while ((Get-Date) -lt $deadline) {
if ($LivenessFile -and -not (Test-Path $LivenessFile)) { break }
$dev = Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue |
Where-Object { $_.InstanceId -match 'VID_05AC&PID_12[0-9A-Fa-f]{2}&RESTORE_MODE&MI_00' } |
Select-Object -First 1
if ($dev) {
$infNow = (Get-PnpDeviceProperty -InstanceId $dev.InstanceId -KeyName DEVPKEY_Device_DriverInfPath -ErrorAction SilentlyContinue).Data
if ($infNow -ne $leaf) {
$hwid = ($dev.InstanceId -replace '\\[^\\]+$', '')
$reboot = $false
[RkNewDev]::UpdateDriverForPlugAndPlayDevices([IntPtr]::Zero, $hwid, $inf, $FLAGS, [ref]$reboot) | Out-Null
}
$bound = $true
} elseif ($bound) {
break
}
Start-Sleep -Milliseconds 350
}
"#;