Add windows support to super-sideloader
Dadoum

Dadoum commited on 2026-09-11 02:23:11
Showing 23 changed files, with 1170 additions and 272 deletions.

... ...
@@ -232,6 +232,7 @@ dependencies = [
232 232
  "libc",
233 233
  "ouroboros",
234 234
  "rand 0.9.4",
235
+ "tempfile",
235 236
 ]
236 237
 
237 238
 [[package]]
... ...
@@ -4074,6 +4075,9 @@ version = "0.1.0"
4074 4075
 dependencies = [
4075 4076
  "adi",
4076 4077
  "dlopen2",
4078
+ "md-5",
4079
+ "tempfile",
4080
+ "windows-sys 0.61.2",
4077 4081
 ]
4078 4082
 
4079 4083
 [[package]]
... ...
@@ -7089,6 +7093,7 @@ dependencies = [
7089 7093
  "chrono",
7090 7094
  "core-foundation 0.10.0",
7091 7095
  "crc32fast",
7096
+ "der",
7092 7097
  "directories",
7093 7098
  "dlopen2",
7094 7099
  "env_logger",
... ...
@@ -7110,14 +7115,18 @@ dependencies = [
7110 7115
  "plist",
7111 7116
  "rand 0.10.2",
7112 7117
  "reqwest",
7118
+ "rsa",
7113 7119
  "serde",
7114 7120
  "sha1 0.11.0",
7121
+ "sha2 0.10.9",
7115 7122
  "tempfile",
7116 7123
  "thiserror 2.0.18",
7117 7124
  "tokio",
7118 7125
  "toml 0.8.23",
7119 7126
  "uuid",
7120 7127
  "walkdir",
7128
+ "windows-sys 0.61.2",
7129
+ "x509-cert",
7121 7130
  "xcode",
7122 7131
 ]
7123 7132
 
... ...
@@ -9,3 +9,6 @@ elf-loader.workspace = true
9 9
 ouroboros = "0.18.5"
10 10
 libc = "0.2.176"
11 11
 rand = "0.9"
12
+
13
+[dev-dependencies]
14
+tempfile = "3"
... ...
@@ -16,12 +16,12 @@ pub struct stat {
16 16
     pub st_size: libc::c_longlong,
17 17
     pub st_blksize: blksize_t,
18 18
     pub st_blocks: libc::c_ulonglong,
19
-    pub st_atime: libc::c_long,
20
-    pub st_atime_nsec: libc::c_long,
21
-    pub st_mtime: libc::c_long,
22
-    pub st_mtime_nsec: libc::c_long,
23
-    pub st_ctime: libc::c_long,
24
-    pub st_ctime_nsec: libc::c_long,
19
+    pub st_atime: isize,
20
+    pub st_atime_nsec: isize,
21
+    pub st_mtime: isize,
22
+    pub st_mtime_nsec: isize,
23
+    pub st_ctime: isize,
24
+    pub st_ctime_nsec: isize,
25 25
     pub st_ino: libc::c_ulonglong,
26 26
 }
27 27
 
... ...
@@ -35,17 +35,17 @@ pub struct stat {
35 35
     pub st_uid: uid_t,
36 36
     pub st_gid: gid_t,
37 37
     pub st_rdev: dev_t,
38
-    pub __pad1: libc::c_ulong,
38
+    pub __pad1: usize,
39 39
     pub st_size: off64_t,
40 40
     pub st_blksize: libc::c_int,
41 41
     pub __pad2: libc::c_int,
42
-    pub st_blocks: libc::c_long,
42
+    pub st_blocks: isize,
43 43
     pub st_atime: time_t,
44
-    pub st_atime_nsec: libc::c_long,
44
+    pub st_atime_nsec: isize,
45 45
     pub st_mtime: time_t,
46
-    pub st_mtime_nsec: libc::c_long,
46
+    pub st_mtime_nsec: isize,
47 47
     pub st_ctime: time_t,
48
-    pub st_ctime_nsec: libc::c_long,
48
+    pub st_ctime_nsec: isize,
49 49
     pub __unused4: libc::c_uint,
50 50
     pub __unused5: libc::c_uint,
51 51
 }
... ...
@@ -55,21 +55,21 @@ pub struct stat {
55 55
 pub struct stat {
56 56
     pub st_dev: dev_t,
57 57
     pub st_ino: ino_t,
58
-    pub st_nlink: libc::c_ulong,
58
+    pub st_nlink: usize,
59 59
     pub st_mode: libc::c_uint,
60 60
     pub st_uid: uid_t,
61 61
     pub st_gid: gid_t,
62 62
     pub st_rdev: dev_t,
63 63
     pub st_size: off64_t,
64
-    pub st_blksize: libc::c_long,
65
-    pub st_blocks: libc::c_long,
66
-    pub st_atime: libc::c_long,
67
-    pub st_atime_nsec: libc::c_long,
68
-    pub st_mtime: libc::c_long,
69
-    pub st_mtime_nsec: libc::c_long,
70
-    pub st_ctime: libc::c_long,
71
-    pub st_ctime_nsec: libc::c_long,
72
-    pub __unused: [libc::c_long; 3],
64
+    pub st_blksize: isize,
65
+    pub st_blocks: isize,
66
+    pub st_atime: isize,
67
+    pub st_atime_nsec: isize,
68
+    pub st_mtime: isize,
69
+    pub st_mtime_nsec: isize,
70
+    pub st_ctime: isize,
71
+    pub st_ctime_nsec: isize,
72
+    pub __unused: [isize; 3],
73 73
 }
74 74
 
75 75
 pub const O_RDONLY: libc::c_int = 0;
... ...
@@ -83,8 +83,8 @@ pub const O_NOFOLLOW: libc::c_int = 0x20000;
83 83
 #[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
84 84
 pub const O_NOFOLLOW: libc::c_int = 0x8000;
85 85
 
86
-pub type dev_t = libc::c_ulong;
87
-pub type ino_t = libc::c_ulong;
86
+pub type dev_t = usize;
87
+pub type ino_t = usize;
88 88
 pub type nlink_t = u32;
89 89
 pub type uid_t = u32;
90 90
 pub type gid_t = u32;
... ...
@@ -99,7 +99,7 @@ pub type mode_t = u32;
99 99
 #[cfg(target_pointer_width = "32")]
100 100
 pub type mode_t = u16;
101 101
 
102
-pub type time_t = libc::c_long;
102
+pub type time_t = isize;
103 103
 
104 104
 pub type suseconds_t = i64;
105 105
 
... ...
@@ -108,3 +108,16 @@ pub struct timeval {
108 108
     pub tv_sec: time_t,
109 109
     pub tv_usec: suseconds_t,
110 110
 }
111
+
112
+#[cfg(all(test, target_arch = "x86_64"))]
113
+mod tests {
114
+    use super::*;
115
+    #[test]
116
+    fn linux_abi_layout_is_independent_of_windows_long_width() {
117
+        assert_eq!(std::mem::size_of::<stat>(), 144);
118
+        assert_eq!(std::mem::offset_of!(stat, st_size), 48);
119
+        assert_eq!(std::mem::offset_of!(stat, st_atime), 72);
120
+        assert_eq!(std::mem::size_of::<timeval>(), 16);
121
+        assert_eq!(std::mem::offset_of!(timeval, tv_usec), 8);
122
+    }
123
+}
... ...
@@ -31,12 +31,12 @@ unsafe extern "C" {
31 31
     fn _errno() -> *mut libc::c_int;
32 32
     fn _timespec64_get(__ts: *mut _timespec64, __base: libc::c_int) -> libc::c_int;
33 33
     fn _timespec32_get(__ts: *mut _timespec32, __base: libc::c_int) -> libc::c_int;
34
-    fn _chsize(handle: i64, length: u64) -> usize;
34
+    fn _chsize_s(handle: libc::c_int, length: i64) -> libc::c_int;
35 35
 }
36 36
 
37 37
 #[linux_cc]
38 38
 pub unsafe fn chmod(path: *const libc::c_char, mode: mode_t) -> libc::c_int {
39
-    libc::chmod(linux_path_to_windows(path).as_ptr(), mem::transmute(mode))
39
+    libc::chmod(linux_path_to_windows(path).as_ptr(), mode as libc::c_int)
40 40
 }
41 41
 
42 42
 #[linux_cc]
... ...
@@ -56,12 +56,18 @@ pub unsafe fn free(p: *mut libc::c_void) {
56 56
 }
57 57
 
58 58
 #[linux_cc]
59
-pub unsafe fn ftruncate(handle: i64, length: u64) -> usize {
60
-    _chsize(handle, length)
59
+pub unsafe fn ftruncate(handle: libc::c_int, length: i64) -> libc::c_int {
60
+    let result = _chsize_s(handle, length);
61
+    if result == 0 {
62
+        0
63
+    } else {
64
+        *_errno() = result;
65
+        -1
66
+    }
61 67
 }
62 68
 
63 69
 #[linux_cc]
64
-pub unsafe fn gettimeofday(tp: *mut timeval, tz: *mut libc::c_void) -> libc::c_int {
70
+pub unsafe fn gettimeofday(tp: *mut timeval, _tz: *mut libc::c_void) -> libc::c_int {
65 71
     let mut time = MaybeUninit::uninit();
66 72
 
67 73
     #[cfg(target_pointer_width = "32")]
... ...
@@ -69,6 +75,9 @@ pub unsafe fn gettimeofday(tp: *mut timeval, tz: *mut libc::c_void) -> libc::c_i
69 75
     #[cfg(target_pointer_width = "64")]
70 76
     let result = _timespec64_get(time.as_mut_ptr(), 1);
71 77
 
78
+    if result != 1 {
79
+        return -1;
80
+    }
72 81
     let time = time.assume_init();
73 82
     *tp = timeval {
74 83
         tv_sec: time.tv_sec as _,
... ...
@@ -84,7 +93,7 @@ pub unsafe fn malloc(size: libc::size_t) -> *mut libc::c_void {
84 93
 }
85 94
 
86 95
 #[linux_cc]
87
-pub unsafe fn mkdir(path: *const libc::c_char, mode: mode_t) -> libc::c_int {
96
+pub unsafe fn mkdir(path: *const libc::c_char, _mode: mode_t) -> libc::c_int {
88 97
     libc::mkdir(linux_path_to_windows(path).as_ptr())
89 98
 }
90 99
 
... ...
@@ -119,16 +128,16 @@ unsafe fn local_stat_to_linux(buf: libc::stat) -> stat {
119 128
         st_dev: buf.st_dev as _,
120 129
         st_ino: buf.st_ino as _,
121 130
         st_nlink: buf.st_nlink as _,
122
-        st_mode: 0o777 as _,
131
+        st_mode: buf.st_mode as _,
123 132
         st_uid: buf.st_uid as _,
124 133
         st_gid: buf.st_gid as _,
125 134
         st_rdev: buf.st_rdev as _,
126 135
         st_size: buf.st_size as _,
127
-        st_atime: (buf.st_atime / 10000000) as _,
136
+        st_atime: buf.st_atime as _,
128 137
         // st_atime_nsec: buf.st_atime_nsec as _,
129
-        st_mtime: (buf.st_mtime / 10000000) as _,
138
+        st_mtime: buf.st_mtime as _,
130 139
         // st_mtime_nsec: buf.st_mtime_nsec as _,
131
-        st_ctime: (buf.st_ctime / 10000000) as _,
140
+        st_ctime: buf.st_ctime as _,
132 141
         // st_ctime_nsec: buf.st_ctime_nsec as _,
133 142
         ..mem::zeroed()
134 143
     }
... ...
@@ -170,24 +179,64 @@ pub unsafe fn fstat(fildes: libc::c_int, buf: *mut stat) -> libc::c_int {
170 179
 
171 180
 #[linux_cc]
172 181
 pub unsafe fn open(path: *const libc::c_char, oflag: libc::c_int) -> libc::c_int {
173
-    let mut local_flag = 0x8000;
174
-
175
-    if oflag & 0b11 == O_WRONLY {
176
-        local_flag = libc::O_RDWR;
177
-    }
178
-    if oflag & 0b11 == O_RDWR {
179
-        local_flag = libc::O_RDWR;
180
-    }
181
-    if oflag & 0b11 == O_RDONLY {
182
-        local_flag = libc::O_RDWR;
183
-    }
184
-
185
-    if oflag & O_CREAT != 0 {
186
-        local_flag |= libc::O_CREAT;
182
+    let access = match oflag & 3 {
183
+        O_WRONLY => libc::O_WRONLY,
184
+        O_RDWR => libc::O_RDWR,
185
+        _ => libc::O_RDONLY,
186
+    };
187
+    let mut local_flag = access | libc::O_BINARY;
188
+    for (linux, windows) in [
189
+        (O_CREAT, libc::O_CREAT),
190
+        (0o200, libc::O_EXCL),
191
+        (0o1000, libc::O_TRUNC),
192
+        (0o2000, libc::O_APPEND),
193
+    ] {
194
+        if oflag & linux != 0 {
195
+            local_flag |= windows;
196
+        }
197
+    }
198
+    // The CRT requires a mode argument when creating a file. ADI uses private
199
+    // provisioning files; Windows ACLs determine access beyond these mode bits.
200
+    libc::open(
201
+        linux_path_to_windows(path).as_ptr(),
202
+        local_flag,
203
+        libc::S_IREAD | libc::S_IWRITE,
204
+    )
205
+}
206
+
207
+#[cfg(test)]
208
+mod tests {
209
+    use super::*;
210
+    #[test]
211
+    fn provisioning_files_round_trip_binary_data_and_report_linux_stat() {
212
+        let directory = tempfile::tempdir().unwrap();
213
+        let path = directory.path().join("provisioning");
214
+        let path_c = CString::new(path.to_str().unwrap()).unwrap();
215
+        let data = [0, 10, 13, 10, 26, 255];
216
+        unsafe {
217
+            let fd = open(path_c.as_ptr(), O_WRONLY | O_CREAT);
218
+            assert!(fd >= 0);
219
+            assert_eq!(
220
+                write(fd, data.as_ptr().cast(), data.len() as _),
221
+                data.len() as _
222
+            );
223
+            assert_eq!(close(fd), 0);
224
+            assert_eq!(std::fs::read(&path).unwrap(), data);
225
+            let fd = open(path_c.as_ptr(), O_RDONLY);
226
+            assert!(fd >= 0);
227
+            let mut actual = [0; 6];
228
+            assert_eq!(read(fd, actual.as_mut_ptr().cast(), actual.len() as _), 6);
229
+            assert_eq!(actual, data);
230
+            let mut stat = std::mem::zeroed();
231
+            assert_eq!(fstat(fd, &mut stat), 0);
232
+            assert_eq!(stat.st_size, 6);
233
+            assert_eq!(stat.st_mode & 0o170000, 0o100000);
234
+            assert!(stat.st_mtime > 1_500_000_000);
235
+            assert_eq!(close(fd), 0);
236
+            let fd = open(path_c.as_ptr(), O_RDWR);
237
+            assert_eq!(ftruncate(fd, 2), 0);
238
+            assert_eq!(close(fd), 0);
239
+            assert_eq!(std::fs::metadata(&path).unwrap().len(), 2);
187 240
         }
188
-    if oflag & O_NOFOLLOW != 0 {
189
-        // local_flag |= libc::O_NOFOLLOW;
190 241
     }
191
-
192
-    libc::open(linux_path_to_windows(path).as_ptr(), local_flag)
193 242
 }
... ...
@@ -13,6 +13,13 @@ pub const AUTH_KIT_BUNDLE_INFORMATION: BundleInformation = BundleInformation {
13 13
     bundle_version: "1",
14 14
 };
15 15
 
16
+/// akd
17
+pub const AKD_BUNDLE_INFORMATION: BundleInformation = BundleInformation {
18
+    bundle_name: "akd",
19
+    bundle_identifier: "com.apple.akd",
20
+    bundle_version: "1.0",
21
+};
22
+
16 23
 /// Apple TV 1.5.6 (macOS Sequoia 15.7.3)
17 24
 pub const APPLE_TV_BUNDLE_INFORMATION: BundleInformation = BundleInformation {
18 25
     bundle_name: "TV",
... ...
@@ -17,9 +17,9 @@ impl Device {
17 17
             "<{}> <{}> <{}/{} ({}/{})>",
18 18
             self.device_model,
19 19
             self.operating_system_information,
20
-            authentication_framework.bundle_name,
20
+            authentication_framework.bundle_identifier,
21 21
             authentication_framework.bundle_version,
22
-            application_information.bundle_name,
22
+            application_information.bundle_identifier,
23 23
             application_information.bundle_version,
24 24
         )
25 25
     }
... ...
@@ -271,6 +271,7 @@ pub async fn login(
271 271
 
272 272
     let response = http_session
273 273
         .anisette_request_builder(Method::POST, gs_service_url)?
274
+        .header("Content-Type", "text/x-xml-plist")
274 275
         .body(dict_to_body(request_plist))
275 276
         .send()
276 277
         .await?
... ...
@@ -365,6 +366,7 @@ pub async fn login(
365 366
         // to be interrupted here. It is important because on some networks, the apptokens
366 367
         // request fails if the TLS session is kept alive.
367 368
         .header("Connection", "close")
369
+        .header("Content-Type", "text/x-xml-plist")
368 370
         .body(dict_to_body(request_plist))
369 371
         .send()
370 372
         .await?
... ...
@@ -254,6 +254,7 @@ impl<'a, 'b> AuthenticatedHTTPSession<'a, 'b> {
254 254
         let response = http_session
255 255
             .anisette_request_builder(Method::POST, gs_service_url)
256 256
             .map_err(AppTokenRequestError::Anisette)?
257
+            .header("Content-Type", "text/x-xml-plist")
257 258
             .body(request_body)
258 259
             .send()
259 260
             .await
... ...
@@ -59,6 +59,7 @@ impl AuthenticatedHTTPSession<'_, '_> {
59 59
 
60 60
         let response = self
61 61
             .authenticated_request_builder(Method::POST, post_data_url)?
62
+            .header("Content-Type", "text/x-xml-plist")
62 63
             .body(plist_to_body(request.into()))
63 64
             .send()
64 65
             .await?
... ...
@@ -6,3 +6,10 @@ edition = "2024"
6 6
 [dependencies]
7 7
 adi.workspace = true
8 8
 dlopen2 = "0.8.2"
9
+
10
+[target.'cfg(target_os = "windows")'.dependencies]
11
+md-5 = "0.10"
12
+windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", "Win32_Networking_WinSock", "Win32_Storage_FileSystem", "Win32_System_LibraryLoader", "Win32_System_Registry", "Win32_System_WindowsProgramming"] }
13
+
14
+[dev-dependencies]
15
+tempfile = "3"
... ...
@@ -1,3 +1,6 @@
1
+#[cfg(target_os = "windows")]
2
+pub mod windows;
3
+
1 4
 use adi::core_adi::{CoreADIADIProxy, CoreADIParameters, CoreADIProxy};
2 5
 use dlopen2::symbor::{Library, Symbol};
3 6
 
... ...
@@ -0,0 +1,441 @@
1
+// TODO move this to another crate
2
+
3
+use adi::core_adi::{CoreADIADIProxy, CoreADIParameters, CoreADIProxy};
4
+use md5::{Digest, Md5};
5
+use std::ffi::{CStr, OsStr};
6
+use std::io;
7
+use std::os::windows::ffi::OsStrExt;
8
+use std::path::{Path, PathBuf};
9
+use std::ptr::{null, null_mut};
10
+use windows_sys::Win32::Foundation::{
11
+    ERROR_BUFFER_OVERFLOW, ERROR_MORE_DATA, FreeLibrary, HMODULE,
12
+};
13
+use windows_sys::Win32::NetworkManagement::IpHelper::{
14
+    GAA_FLAG_INCLUDE_ALL_INTERFACES, GetAdaptersAddresses, IP_ADAPTER_ADDRESSES_LH,
15
+};
16
+use windows_sys::Win32::Storage::FileSystem::GetVolumeInformationW;
17
+use windows_sys::Win32::System::LibraryLoader::{
18
+    GetProcAddress, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR,
19
+    LoadLibraryExW,
20
+};
21
+use windows_sys::Win32::System::Registry::*;
22
+use windows_sys::Win32::System::WindowsProgramming::{
23
+    GetComputerNameW, GetCurrentHwProfileW, HW_PROFILE_INFOW,
24
+};
25
+
26
+fn wide(value: &OsStr) -> Vec<u16> {
27
+    value.encode_wide().chain(Some(0)).collect()
28
+}
29
+
30
+struct Key(HKEY);
31
+impl Drop for Key {
32
+    fn drop(&mut self) {
33
+        unsafe {
34
+            RegCloseKey(self.0);
35
+        }
36
+    }
37
+}
38
+
39
+fn open_key(root: HKEY, path: &CStr, view: u32) -> Option<Key> {
40
+    let mut key = null_mut();
41
+    (unsafe { RegOpenKeyExA(root, path.as_ptr().cast(), 0, KEY_READ | view, &mut key) } == 0)
42
+        .then_some(Key(key))
43
+}
44
+
45
+fn value_bytes(key: HKEY, name: &CStr) -> Option<Vec<u8>> {
46
+    let mut size = 0;
47
+    if unsafe {
48
+        RegQueryValueExA(
49
+            key,
50
+            name.as_ptr().cast(),
51
+            null(),
52
+            null_mut(),
53
+            null_mut(),
54
+            &mut size,
55
+        )
56
+    } != 0
57
+    {
58
+        return None;
59
+    }
60
+    loop {
61
+        let mut bytes = vec![0; size as usize];
62
+        let result = unsafe {
63
+            RegQueryValueExA(
64
+                key,
65
+                name.as_ptr().cast(),
66
+                null(),
67
+                null_mut(),
68
+                bytes.as_mut_ptr(),
69
+                &mut size,
70
+            )
71
+        };
72
+        if result == ERROR_MORE_DATA {
73
+            continue;
74
+        }
75
+        if result != 0 {
76
+            return None;
77
+        }
78
+        bytes.truncate(size as usize);
79
+        return Some(bytes);
80
+    }
81
+}
82
+
83
+fn registry_bytes(path: &CStr, name: &CStr) -> Option<Vec<u8>> {
84
+    let key = open_key(HKEY_LOCAL_MACHINE, path, 0)?;
85
+    value_bytes(key.0, name)
86
+}
87
+
88
+fn registry_path(key: HKEY, name: &CStr) -> Option<PathBuf> {
89
+    // Paths must use the Unicode API; identity values deliberately use ANSI.
90
+    let name: Vec<u16> = name
91
+        .to_bytes()
92
+        .iter()
93
+        .map(|b| *b as u16)
94
+        .chain(Some(0))
95
+        .collect();
96
+    let mut size = 0;
97
+    if unsafe {
98
+        RegQueryValueExW(
99
+            key,
100
+            name.as_ptr(),
101
+            null(),
102
+            null_mut(),
103
+            null_mut(),
104
+            &mut size,
105
+        )
106
+    } != 0
107
+    {
108
+        return None;
109
+    }
110
+    let mut data = vec![0u16; size as usize / 2 + 1];
111
+    if unsafe {
112
+        RegQueryValueExW(
113
+            key,
114
+            name.as_ptr(),
115
+            null(),
116
+            null_mut(),
117
+            data.as_mut_ptr().cast(),
118
+            &mut size,
119
+        )
120
+    } != 0
121
+    {
122
+        return None;
123
+    }
124
+    let len = data.iter().position(|c| *c == 0).unwrap_or(data.len());
125
+    use std::os::windows::ffi::OsStringExt;
126
+    Some(std::ffi::OsString::from_wide(&data[..len]).into())
127
+}
128
+
129
+fn first_mac() -> Option<Vec<u8>> {
130
+    let mut size = 0;
131
+    // iTunes takes the first adapter, including interfaces which are down.
132
+    // u64 storage guarantees the alignment required by IP_ADAPTER_ADDRESSES.
133
+    loop {
134
+        let mut storage = vec![0u64; (size as usize).div_ceil(8).max(1)];
135
+        let adapter = storage.as_mut_ptr().cast::<IP_ADAPTER_ADDRESSES_LH>();
136
+        let result = unsafe {
137
+            GetAdaptersAddresses(
138
+                0,
139
+                GAA_FLAG_INCLUDE_ALL_INTERFACES,
140
+                null(),
141
+                adapter,
142
+                &mut size,
143
+            )
144
+        };
145
+        if result == ERROR_BUFFER_OVERFLOW {
146
+            continue;
147
+        }
148
+        if result != 0 {
149
+            return None;
150
+        }
151
+        let adapter = unsafe { &*adapter };
152
+        let mut mac = vec![0; 6];
153
+        let length = (adapter.PhysicalAddressLength as usize).min(6);
154
+        mac[..length].copy_from_slice(&adapter.PhysicalAddress[..length]);
155
+        return Some(mac);
156
+    }
157
+}
158
+
159
+fn volume_serial() -> Option<Vec<u8>> {
160
+    let mut serial = 0u32;
161
+    let root = wide(OsStr::new("C:\\"));
162
+    (unsafe {
163
+        GetVolumeInformationW(
164
+            root.as_ptr(),
165
+            null_mut(),
166
+            0,
167
+            &mut serial,
168
+            null_mut(),
169
+            null_mut(),
170
+            null_mut(),
171
+            0,
172
+        )
173
+    } != 0)
174
+        .then(|| serial.to_le_bytes().to_vec())
175
+}
176
+
177
+fn computer_name_bytes() -> Option<Vec<u8>> {
178
+    let mut name = [0u16; 16];
179
+    let mut length = name.len() as u32;
180
+    if unsafe { GetComputerNameW(name.as_mut_ptr(), &mut length) } == 0 {
181
+        return None;
182
+    }
183
+    // Compatibility quirk: iTunes passes the WCHAR count as a BYTE count.
184
+    let mut bytes: Vec<_> = name.iter().flat_map(|c| c.to_le_bytes()).collect();
185
+    bytes.truncate(length as usize);
186
+    Some(bytes)
187
+}
188
+
189
+fn hardware_profile_bytes() -> Option<Vec<u8>> {
190
+    let mut profile: HW_PROFILE_INFOW = unsafe { std::mem::zeroed() };
191
+    if unsafe { GetCurrentHwProfileW(&mut profile) } == 0 {
192
+        return None;
193
+    }
194
+    Some(
195
+        profile
196
+            .szHwProfileGuid
197
+            .iter()
198
+            .take_while(|c| **c != 0)
199
+            .flat_map(|c| c.to_le_bytes())
200
+            .collect(),
201
+    )
202
+}
203
+
204
+fn format_identifier(parts: &[Option<Vec<u8>>; 7]) -> String {
205
+    parts
206
+        .iter()
207
+        .map(|part| match part {
208
+            Some(bytes) => Md5::digest(bytes)[..4]
209
+                .iter()
210
+                .map(|b| format!("{b:02X}"))
211
+                .collect(),
212
+            None => "00000000".to_string(),
213
+        })
214
+        .collect::<Vec<String>>()
215
+        .join(".")
216
+}
217
+
218
+/// Compute the same device identifier used by iTunes on Windows 8+.
219
+/// Unavailable hardware components have zero segments, as in iTunes.
220
+pub fn device_identifier() -> String {
221
+    format_identifier(&[
222
+        first_mac(),
223
+        volume_serial(),
224
+        registry_bytes(
225
+            c"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
226
+            c"ProductId",
227
+        ),
228
+        registry_bytes(
229
+            c"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0",
230
+            c"ProcessorNameString",
231
+        ),
232
+        registry_bytes(c"HARDWARE\\DESCRIPTION\\System", c"SystemBiosVersion"),
233
+        computer_name_bytes(),
234
+        hardware_profile_bytes(),
235
+    ])
236
+}
237
+
238
+fn add_directory(candidates: &mut Vec<PathBuf>, directory: &Path) {
239
+    let names: &[&str] = if cfg!(target_arch = "x86_64") {
240
+        &["CoreADI64.dll", "CoreADI.dll"]
241
+    } else {
242
+        &["CoreADI.dll"]
243
+    };
244
+    for name in names {
245
+        let path = directory.join(name);
246
+        if !candidates.contains(&path) {
247
+            candidates.push(path);
248
+        }
249
+    }
250
+}
251
+
252
+/// Candidate paths from registered desktop/Store installations and standard folders.
253
+pub fn library_candidates() -> Vec<PathBuf> {
254
+    let mut candidates = Vec::new();
255
+    for root in [HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER] {
256
+        for view in [KEY_WOW64_64KEY, KEY_WOW64_32KEY] {
257
+            if let Some(key) = open_key(
258
+                root,
259
+                c"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\iTunes.exe",
260
+                view,
261
+            ) {
262
+                if let Some(path) = registry_path(key.0, c"") {
263
+                    if let Some(parent) = path.parent() {
264
+                        add_directory(&mut candidates, parent);
265
+                    }
266
+                }
267
+            }
268
+        }
269
+    }
270
+    for variable in ["ProgramW6432", "ProgramFiles", "ProgramFiles(x86)"] {
271
+        if let Some(root) = std::env::var_os(variable) {
272
+            let root = PathBuf::from(root);
273
+            for suffix in [
274
+                "iTunes",
275
+                "Common Files/Apple/Apple Application Support",
276
+                "Common Files/Apple/Internet Services",
277
+                "Apple/Apple Application Support",
278
+            ] {
279
+                add_directory(&mut candidates, &root.join(suffix));
280
+            }
281
+        }
282
+    }
283
+    for variable in [
284
+        "CommonProgramW6432",
285
+        "CommonProgramFiles",
286
+        "CommonProgramFiles(x86)",
287
+    ] {
288
+        if let Some(root) = std::env::var_os(variable) {
289
+            for suffix in ["Apple/Apple Application Support", "Apple/Internet Services"] {
290
+                add_directory(&mut candidates, &PathBuf::from(&root).join(suffix));
291
+            }
292
+        }
293
+    }
294
+    if let Some(key) = open_key(HKEY_CURRENT_USER, c"Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppModel\\Repository\\Packages", KEY_WOW64_64KEY) {
295
+        for index in 0.. {
296
+            let mut name = vec![0u8; 512];
297
+            let mut len = name.len() as u32;
298
+            if unsafe { RegEnumKeyExA(key.0, index, name.as_mut_ptr(), &mut len, null(), null_mut(), null_mut(), null_mut()) } != 0 { break; }
299
+            name.truncate(len as usize);
300
+            if ![b"AppleInc.iTunes_".as_slice(), b"AppleInc.iCloud_".as_slice()].iter().any(|prefix| name.starts_with(prefix)) { continue; }
301
+            name.push(0);
302
+            if let Some(package) = open_key(key.0, CStr::from_bytes_with_nul(&name).unwrap(), 0) {
303
+                if let Some(root) = registry_path(package.0, c"PackageRootFolder") {
304
+                    for suffix in ["", "VFS/ProgramFilesX64/iTunes", "VFS/ProgramFilesCommonX64/Apple/Internet Services", "VFS/ProgramFilesCommonX64/Apple/Apple Application Support"] {
305
+                        add_directory(&mut candidates, &root.join(suffix));
306
+                    }
307
+                }
308
+            }
309
+        }
310
+    }
311
+    candidates
312
+}
313
+
314
+fn compatible_library(path: &Path) -> bool {
315
+    use std::io::{Read, Seek, SeekFrom};
316
+    let Ok(mut file) = std::fs::File::open(path) else {
317
+        return false;
318
+    };
319
+    let mut dos = [0; 64];
320
+    if file.read_exact(&mut dos).is_err() || &dos[..2] != b"MZ" {
321
+        return false;
322
+    }
323
+    let offset = u32::from_le_bytes(dos[60..64].try_into().unwrap());
324
+    if file.seek(SeekFrom::Start(offset as u64)).is_err() {
325
+        return false;
326
+    }
327
+    let mut pe = [0; 6];
328
+    file.read_exact(&mut pe).is_ok()
329
+        && &pe[..4] == b"PE\0\0"
330
+        && u16::from_le_bytes([pe[4], pe[5]])
331
+            == if cfg!(target_arch = "x86_64") {
332
+                0x8664
333
+            } else if cfg!(target_arch = "aarch64") {
334
+                0xaa64
335
+            } else {
336
+                0x14c
337
+            }
338
+}
339
+
340
+pub fn find_library() -> Option<PathBuf> {
341
+    library_candidates()
342
+        .into_iter()
343
+        .find(|path| compatible_library(path))
344
+}
345
+
346
+/// Owns the DLL and uses its directory for dependency resolution, without changing PATH.
347
+pub struct WindowsCoreADIProxy {
348
+    module: HMODULE,
349
+    dispatcher: unsafe extern "C" fn(u32, *const CoreADIParameters) -> i32,
350
+}
351
+
352
+impl WindowsCoreADIProxy {
353
+    pub fn open(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
354
+        let path = std::fs::canonicalize(path)?;
355
+        let module = unsafe {
356
+            LoadLibraryExW(
357
+                wide(path.as_os_str()).as_ptr(),
358
+                null_mut(),
359
+                LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS,
360
+            )
361
+        };
362
+        if module.is_null() {
363
+            return Err(io::Error::last_os_error().into());
364
+        }
365
+        let symbol = unsafe { GetProcAddress(module, c"vdfut768ig".as_ptr().cast()) };
366
+        let Some(symbol) = symbol else {
367
+            let error = io::Error::last_os_error();
368
+            unsafe {
369
+                FreeLibrary(module);
370
+            }
371
+            return Err(error.into());
372
+        };
373
+        let proxy = Self {
374
+            module,
375
+            dispatcher: unsafe { std::mem::transmute(symbol) },
376
+        };
377
+        proxy.initialize()?;
378
+        Ok(proxy)
379
+    }
380
+}
381
+
382
+impl CoreADIProxy for WindowsCoreADIProxy {
383
+    unsafe fn dispatch(&self, code: u32, parameters: *const CoreADIParameters) -> i32 {
384
+        unsafe { (self.dispatcher)(code, parameters) }
385
+    }
386
+}
387
+
388
+impl Drop for WindowsCoreADIProxy {
389
+    fn drop(&mut self) {
390
+        let _ = self.finalize();
391
+        unsafe {
392
+            FreeLibrary(self.module);
393
+        }
394
+    }
395
+}
396
+
397
+#[cfg(test)]
398
+mod tests {
399
+    use super::*;
400
+    #[test]
401
+    fn discovery_rejects_wrong_architecture_and_malformed_files() {
402
+        let temp = tempfile::tempdir().unwrap();
403
+        let path = temp.path().join("CoreADI.dll");
404
+        assert!(!compatible_library(&path));
405
+        std::fs::write(&path, b"not a DLL").unwrap();
406
+        assert!(!compatible_library(&path));
407
+        let mut bytes = vec![0u8; 70];
408
+        bytes[..2].copy_from_slice(b"MZ");
409
+        bytes[60..64].copy_from_slice(&64u32.to_le_bytes());
410
+        bytes[64..68].copy_from_slice(b"PE\0\0");
411
+        let native: u16 = if cfg!(target_arch = "x86_64") {
412
+            0x8664
413
+        } else if cfg!(target_arch = "aarch64") {
414
+            0xaa64
415
+        } else {
416
+            0x14c
417
+        };
418
+        bytes[68..70].copy_from_slice(&native.to_le_bytes());
419
+        std::fs::write(&path, &bytes).unwrap();
420
+        assert!(compatible_library(&path));
421
+        bytes[68..70].copy_from_slice(&0xffffu16.to_le_bytes());
422
+        std::fs::write(&path, &bytes).unwrap();
423
+        assert!(!compatible_library(&path));
424
+    }
425
+
426
+    #[test]
427
+    fn identifier_preserves_order_null_bytes_and_missing_components() {
428
+        assert_eq!(
429
+            format_identifier(&[
430
+                Some(b"abc".to_vec()),
431
+                None,
432
+                Some(vec![]),
433
+                Some(b"abc\0".to_vec()),
434
+                None,
435
+                None,
436
+                None
437
+            ]),
438
+            "90015098.00000000.D41D8CD9.147A664A.00000000.00000000.00000000"
439
+        );
440
+    }
441
+}
... ...
@@ -29,6 +29,11 @@ rand = "0.10.2"
29 29
 reqwest.workspace = true
30 30
 serde = { version = "1.0.228", features = ["derive"] }
31 31
 sha1.workspace = true
32
+# Match the RustCrypto versions used by apple-codesign.
33
+der = { version = "0.7.10", features = ["alloc", "std"] }
34
+rsa = { version = "0.9.9", features = ["sha2", "getrandom"] }
35
+sha2_010 = { package = "sha2", version = "0.10" }
36
+x509-cert = { version = "0.2.5", features = ["builder"] }
32 37
 thiserror.workspace = true
33 38
 tempfile = "3.27"
34 39
 toml = "0.8.23"
... ...
@@ -43,6 +48,7 @@ core-foundation = "0.10.0"
43 48
 libc = "0.2.176"
44 49
 
45 50
 [target.'cfg(target_os = "windows")'.dependencies]
51
+windows-sys = { version = "0.61", features = ["Win32_System_SystemInformation"] }
46 52
 dlopen2 = "0.8.2"
47 53
 library-coreadi.workspace = true
48 54
 ouroboros = "0.18.5"
... ...
@@ -5,16 +5,12 @@ use crate::domain::{
5 5
     AdiRepairAction, MachineIdentity,
6 6
 };
7 7
 use adi::core_adi::CoreADIADIProxy;
8
-#[cfg(target_os = "windows")]
9
-use adi::core_adi::{CoreADIParameters, CoreADIProxy};
10 8
 use adi::ADIProxy;
11 9
 use async_zip::tokio::read::fs::ZipFileReader;
12 10
 use futures_lite::io::AsyncReadExt;
13
-use grandslam::bundle_information::BundleInformation;
11
+use grandslam::bundle_information::{BundleInformation, AKD_BUNDLE_INFORMATION};
14 12
 use grandslam::device::Device;
15 13
 use grandslam::http_session::AnisetteHTTPSession;
16
-#[cfg(target_os = "windows")]
17
-use ouroboros::self_referencing;
18 14
 use std::ffi::CString;
19 15
 use std::fs;
20 16
 use std::path::{Path, PathBuf};
... ...
@@ -109,8 +105,8 @@ async fn provision_adi_async(
109 105
 ) -> BackendResult<()> {
110 106
     let proxy = selected_adi_proxy(kind, &android_adi_identifier)?;
111 107
     let http_session = grandslam::http_session(
112
-        grandslam_device(&machine_identity),
113
-        XCODE_BUNDLE_INFORMATION,
108
+        grandslam_device(&machine_identity, kind),
109
+        AKD_BUNDLE_INFORMATION,
114 110
     )
115 111
     .await
116 112
     .map_err(|error| {
... ...
@@ -125,7 +121,18 @@ async fn provision_adi_async(
125 121
         .map_err(|error| BackendError::Adi(format!("Failed to provision ADI: {error}")))
126 122
 }
127 123
 
128
-pub(crate) fn grandslam_device(machine_identity: &MachineIdentity) -> Device {
124
+pub(crate) fn grandslam_device(machine_identity: &MachineIdentity, kind: AdiBackendKind) -> Device {
125
+    #[cfg(target_os = "windows")]
126
+    if kind == AdiBackendKind::WindowsCoreAdi {
127
+        let identity = crate::backend::system_identity::windows_machine_identity();
128
+        return Device {
129
+            device_model: identity.machine_name,
130
+            operating_system_information: format!("{}; {}", identity.os_name, identity.os_version),
131
+            device_uuid: identity.machine_id,
132
+        };
133
+    }
134
+    #[cfg(not(target_os = "windows"))]
135
+    let _ = kind;
129 136
     Device {
130 137
         device_model: machine_identity.machine_name.to_string(),
131 138
         operating_system_information: format!(
... ...
@@ -162,56 +169,22 @@ fn system_adi_proxy() -> BackendResult<Box<dyn ADIProxy>> {
162 169
     ))
163 170
 }
164 171
 
165
-#[cfg(target_os = "windows")]
166
-#[self_referencing]
167
-struct WindowsCoreADIProxy {
168
-    library: dlopen2::symbor::Library,
169
-
170
-    #[borrows(library)]
171
-    #[covariant]
172
-    proxy: library_coreadi::LibraryCoreADIProxy<'this>,
173
-}
174
-
175
-#[cfg(target_os = "windows")]
176
-impl WindowsCoreADIProxy {
177
-    fn open(path: PathBuf) -> BackendResult<Self> {
178
-        let library = dlopen2::symbor::Library::open(path.as_os_str())
179
-            .map_err(|error| BackendError::Adi(format!("Failed to load CoreADI.dll: {error}")))?;
180
-        let proxy = WindowsCoreADIProxyTryBuilder {
181
-            library,
182
-            proxy_builder: |library| {
183
-                library_coreadi::LibraryCoreADIProxy::new(library).map_err(|error| {
184
-                    BackendError::Adi(format!("CoreADI entry point could not be loaded: {error}"))
185
-                })
186
-            },
187
-        }
188
-        .try_build()?;
189
-
190
-        proxy
191
-            .with_proxy(|proxy| {
192
-                CoreADIADIProxy::initialize(proxy).map_err(|error| {
193
-                    BackendError::Adi(format!("CoreADI initialization failed: {error}"))
194
-                })
195
-            })
196
-            .map(|()| proxy)
197
-    }
198
-}
199
-
200
-#[cfg(target_os = "windows")]
201
-impl CoreADIProxy for WindowsCoreADIProxy {
202
-    unsafe fn dispatch(&self, function_code: u32, parameters: *const CoreADIParameters) -> i32 {
203
-        self.with_proxy(|proxy| unsafe { proxy.dispatch(function_code, parameters) })
204
-    }
205
-}
206
-
207 172
 #[cfg(target_os = "windows")]
208 173
 fn windows_coreadi_proxy() -> BackendResult<Box<dyn ADIProxy>> {
209 174
     let library_path = find_windows_coreadi_library().ok_or_else(|| {
210 175
         BackendError::Adi(
211
-            "CoreADI.dll was not found in the usual iTunes or iCloud folders.".to_string(),
176
+            "A compatible CoreADI library was not found in the installed iTunes or iCloud folders."
177
+                .into(),
212 178
         )
213 179
     })?;
214
-    Ok(Box::new(WindowsCoreADIProxy::open(library_path)?))
180
+    let proxy =
181
+        library_coreadi::windows::WindowsCoreADIProxy::open(&library_path).map_err(|error| {
182
+            BackendError::Adi(format!(
183
+                "Failed to load {}: {error}",
184
+                library_path.display()
185
+            ))
186
+        })?;
187
+    Ok(Box::new(proxy))
215 188
 }
216 189
 
217 190
 #[cfg(not(target_os = "windows"))]
... ...
@@ -322,6 +295,11 @@ fn windows_coreadi_backend() -> AdiBackend {
322 295
                 label: "Identity storage".into(),
323 296
                 value: "Apple managed".into(),
324 297
             },
298
+            #[cfg(target_os = "windows")]
299
+            AdiBackendDetail {
300
+                label: "Device identifier".into(),
301
+                value: library_coreadi::windows::device_identifier(),
302
+            },
325 303
         ],
326 304
         provisioning_state: AdiProvisioningState::Unknown,
327 305
         editable_identity: false,
... ...
@@ -539,23 +517,22 @@ async fn publish_partial_file(
539 517
 pub(crate) async fn download_and_install_android_coreadi(
540 518
     mut progress: impl FnMut(AndroidCoreAdiInstallEvent) + Send + 'static,
541 519
 ) -> BackendResult<PathBuf> {
542
-    backend_runtime::run_send("CoreADI install", async move {
543
-        let apk_path = download_android_coreadi_apk(|download| {
520
+    backend_runtime::run_send_with("CoreADI install", move || async move {
521
+        let apk_path = Box::pin(download_android_coreadi_apk(|download| {
544 522
             progress(AndroidCoreAdiInstallEvent::Downloading(download));
545
-        })
523
+        }))
546 524
         .await?;
547 525
         progress(AndroidCoreAdiInstallEvent::Installing);
548
-        install_android_coreadi_from_apk_async(apk_path).await
526
+        Box::pin(install_android_coreadi_from_apk_async(apk_path)).await
549 527
     })
550 528
     .await?
551 529
 }
552 530
 
553 531
 #[allow(dead_code)]
554 532
 pub(crate) async fn install_android_coreadi_from_apk(apk_path: PathBuf) -> BackendResult<PathBuf> {
555
-    backend_runtime::run_send(
556
-        "CoreADI APK reader",
557
-        install_android_coreadi_from_apk_async(apk_path),
558
-    )
533
+    backend_runtime::run_send_with("CoreADI APK reader", move || {
534
+        Box::pin(install_android_coreadi_from_apk_async(apk_path))
535
+    })
559 536
     .await?
560 537
 }
561 538
 
... ...
@@ -611,7 +588,8 @@ async fn install_android_coreadi_from_apk_async(apk_path: PathBuf) -> BackendRes
611 588
                 source: error,
612 589
             })?;
613 590
     let mut written_bytes = 0u64;
614
-    let mut buffer = [0u8; 64 * 1024];
591
+    // Keep the extraction buffer out of the async state machine on the caller stack.
592
+    let mut buffer = vec![0u8; 64 * 1024];
615 593
     loop {
616 594
         let read = entry_reader.read(&mut buffer).await.map_err(|error| {
617 595
             BackendError::Zip(format!("Failed to extract CoreADI from APK: {error}"))
... ...
@@ -693,48 +671,26 @@ fn c_string_from_path(path: &Path) -> BackendResult<CString> {
693 671
 }
694 672
 
695 673
 fn find_windows_coreadi_library() -> Option<PathBuf> {
696
-    if !cfg!(target_os = "windows") {
697
-        return None;
698
-    }
699
-
700
-    windows_coreadi_candidates()
701
-        .into_iter()
702
-        .find(|path| path.exists())
703
-}
704
-
705
-fn windows_coreadi_candidates() -> Vec<PathBuf> {
706
-    let mut candidates = Vec::new();
707
-    for env_var in ["ProgramFiles", "ProgramFiles(x86)"] {
708
-        if let Some(root) = std::env::var_os(env_var) {
709
-            let root = PathBuf::from(root);
710
-            candidates.push(
711
-                root.join("Common Files")
712
-                    .join("Apple")
713
-                    .join("Apple Application Support")
714
-                    .join("CoreADI.dll"),
715
-            );
716
-            candidates.push(
717
-                root.join("Apple")
718
-                    .join("Apple Application Support")
719
-                    .join("CoreADI.dll"),
720
-            );
721
-        }
674
+    #[cfg(target_os = "windows")]
675
+    {
676
+        library_coreadi::windows::find_library()
722 677
     }
723
-    if let Some(root) = std::env::var_os("CommonProgramFiles") {
724
-        candidates.push(
725
-            PathBuf::from(root)
726
-                .join("Apple")
727
-                .join("Apple Application Support")
728
-                .join("CoreADI.dll"),
729
-        );
678
+    #[cfg(not(target_os = "windows"))]
679
+    {
680
+        None
730 681
     }
731
-    candidates
732 682
 }
733 683
 
734 684
 #[cfg(test)]
735 685
 mod tests {
736 686
     use super::*;
737 687
 
688
+    #[test]
689
+    fn install_future_does_not_embed_the_extraction_buffer() {
690
+        let future = download_and_install_android_coreadi(|_| {});
691
+        assert!(std::mem::size_of_val(&future) < 16 * 1024);
692
+    }
693
+
738 694
     #[test]
739 695
     fn available_backends_return_domain_backends_and_filter_unavailable() {
740 696
         let backends = available_adi_backends("0123456789abcdef", false);
... ...
@@ -1,10 +1,18 @@
1 1
 use crate::backend::paths::app_data_dir;
2 2
 use crate::backend::{BackendError, BackendResult};
3
+use der::{Decode, Encode, EncodePem};
4
+use rsa::pkcs1::DecodeRsaPrivateKey;
5
+use rsa::pkcs1v15::{Signature, SigningKey};
6
+use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey, LineEnding};
7
+use rsa::rand_core::OsRng;
8
+use rsa::RsaPrivateKey;
3 9
 use sha1::Digest as _;
4 10
 use std::fs;
5 11
 use std::path::{Path, PathBuf};
6
-use std::process::{Command, Stdio};
7 12
 use uuid::Uuid;
13
+use x509_cert::builder::{Builder, RequestBuilder};
14
+use x509_cert::name::Name;
15
+use x509_cert::Certificate;
8 16
 
9 17
 #[cfg(unix)]
10 18
 use std::os::unix::fs::PermissionsExt;
... ...
@@ -27,44 +35,24 @@ pub(crate) fn generate_development_certificate_signing_request(
27 35
 ) -> BackendResult<GeneratedCertificateSigningRequest> {
28 36
     let machine_id = Uuid::new_v4().to_string().to_uppercase();
29 37
     let machine_name = "Super Sideloader".to_string();
30
-    let temp_dir = certificate_temp_dir()?;
31
-    fs::create_dir_all(&temp_dir).map_err(|source| BackendError::Io {
32
-        action: "Create certificate work folder",
33
-        path: temp_dir.clone(),
34
-        source,
35
-    })?;
36
-    let file_id = Uuid::new_v4().to_string();
37
-    let key_path = temp_dir.join(format!("{file_id}.key.pem"));
38
-    let csr_path = temp_dir.join(format!("{file_id}.csr.pem"));
39
-    let subject = format!("/CN={machine_name}");
40
-
41
-    let mut generate = Command::new("openssl");
42
-    generate
43
-        .args(["req", "-new", "-newkey", "rsa:2048", "-nodes"])
44
-        .arg("-keyout")
45
-        .arg(&key_path)
46
-        .arg("-out")
47
-        .arg(&csr_path)
48
-        .arg("-subj")
49
-        .arg(subject)
50
-        .arg("-batch");
51
-    run_command(generate, "generate a development certificate CSR")?;
52
-
53
-    let private_key_pem = fs::read(&key_path).map_err(|source| BackendError::Io {
54
-        action: "Read generated private key",
55
-        path: key_path.clone(),
56
-        source,
57
-    })?;
58
-    let csr_content = fs::read_to_string(&csr_path).map_err(|source| BackendError::Io {
59
-        action: "Read generated CSR",
60
-        path: csr_path.clone(),
61
-        source,
62
-    })?;
63
-    let public_key_der = public_key_der_from_private_key(&key_path)?;
64
-    let public_key_fingerprint = certificate_fingerprint(&public_key_der);
65
-
66
-    let _ = fs::remove_file(&key_path);
67
-    let _ = fs::remove_file(&csr_path);
38
+    let private_key = RsaPrivateKey::new(&mut OsRng, 2048).map_err(crypto_error)?;
39
+    let public_key_der = private_key
40
+        .to_public_key()
41
+        .to_public_key_der()
42
+        .map_err(crypto_error)?;
43
+    let public_key_fingerprint = certificate_fingerprint(public_key_der.as_bytes());
44
+    let private_key_pem = private_key
45
+        .to_pkcs8_pem(LineEnding::LF)
46
+        .map_err(crypto_error)?
47
+        .as_bytes()
48
+        .to_vec();
49
+    let signer = SigningKey::<sha2_010::Sha256>::new(private_key);
50
+    let subject: Name = format!("CN={machine_name}").parse().map_err(crypto_error)?;
51
+    let csr = RequestBuilder::new(subject, &signer)
52
+        .map_err(crypto_error)?
53
+        .build::<Signature>()
54
+        .map_err(crypto_error)?;
55
+    let csr_content = csr.to_pem(LineEnding::LF).map_err(crypto_error)?;
68 56
 
69 57
     Ok(GeneratedCertificateSigningRequest {
70 58
         machine_id,
... ...
@@ -85,7 +73,7 @@ pub(crate) fn import_app_managed_private_key(
85 73
         path: private_key_path.to_path_buf(),
86 74
         source,
87 75
     })?;
88
-    let public_key_der = public_key_der_from_private_key(private_key_path)?;
76
+    let public_key_der = public_key_der_from_private_key(&private_key_pem)?;
89 77
     let public_key_fingerprint = certificate_fingerprint(&public_key_der);
90 78
     if !public_key_fingerprint.eq_ignore_ascii_case(expected_public_key_fingerprint) {
91 79
         return Err(BackendError::Keychain(format!(
... ...
@@ -178,50 +166,37 @@ pub(crate) fn load_app_managed_signing_material(
178 166
     })
179 167
 }
180 168
 
181
-fn public_key_der_from_private_key(key_path: &Path) -> BackendResult<Vec<u8>> {
182
-    let mut command = Command::new("openssl");
183
-    command
184
-        .args(["pkey", "-in"])
185
-        .arg(key_path)
186
-        .args(["-pubout", "-outform", "DER"]);
187
-    command_output(command, "extract the private key public key")
169
+fn decode_private_key(pem: &[u8]) -> BackendResult<RsaPrivateKey> {
170
+    let pem = std::str::from_utf8(pem).map_err(crypto_error)?;
171
+    // Match the unencrypted RSA formats accepted by the app's CMS signer.
172
+    RsaPrivateKey::from_pkcs8_pem(pem)
173
+        .or_else(|_| RsaPrivateKey::from_pkcs1_pem(pem))
174
+        .map_err(|error| {
175
+            BackendError::Message(format!(
176
+                "Expected an unencrypted RSA private key in PKCS#8 or PKCS#1 PEM format: {error}"
177
+            ))
178
+        })
188 179
 }
189 180
 
190
-fn certificate_public_key_der(certificate_der: &[u8]) -> BackendResult<Vec<u8>> {
191
-    let temp_dir = certificate_temp_dir()?;
192
-    fs::create_dir_all(&temp_dir).map_err(|source| BackendError::Io {
193
-        action: "Create certificate work folder",
194
-        path: temp_dir.clone(),
195
-        source,
196
-    })?;
197
-    let file_id = Uuid::new_v4().to_string();
198
-    let certificate_path = temp_dir.join(format!("{file_id}.cer"));
199
-    let public_key_path = temp_dir.join(format!("{file_id}.pub.pem"));
200
-    fs::write(&certificate_path, certificate_der).map_err(|source| BackendError::Io {
201
-        action: "Stage certificate",
202
-        path: certificate_path.clone(),
203
-        source,
204
-    })?;
181
+fn public_key_der_from_private_key(private_key_pem: &[u8]) -> BackendResult<Vec<u8>> {
182
+    decode_private_key(private_key_pem)?
183
+        .to_public_key()
184
+        .to_public_key_der()
185
+        .map(|document| document.as_bytes().to_vec())
186
+        .map_err(crypto_error)
187
+}
205 188
 
206
-    let mut extract_public_key = Command::new("openssl");
207
-    extract_public_key
208
-        .args(["x509", "-inform", "DER", "-in"])
209
-        .arg(&certificate_path)
210
-        .args(["-pubkey", "-noout", "-out"])
211
-        .arg(&public_key_path);
212
-    let result =
213
-        run_command(extract_public_key, "extract the certificate public key").and_then(|_| {
214
-            let mut convert_public_key = Command::new("openssl");
215
-            convert_public_key
216
-                .args(["pkey", "-pubin", "-in"])
217
-                .arg(&public_key_path)
218
-                .args(["-outform", "DER"]);
219
-            command_output(convert_public_key, "encode the certificate public key")
220
-        });
189
+fn certificate_public_key_der(certificate_der: &[u8]) -> BackendResult<Vec<u8>> {
190
+    Certificate::from_der(certificate_der)
191
+        .map_err(crypto_error)?
192
+        .tbs_certificate
193
+        .subject_public_key_info
194
+        .to_der()
195
+        .map_err(crypto_error)
196
+}
221 197
 
222
-    let _ = fs::remove_file(&certificate_path);
223
-    let _ = fs::remove_file(&public_key_path);
224
-    result
198
+fn crypto_error(error: impl std::fmt::Display) -> BackendError {
199
+    BackendError::Message(format!("Certificate operation failed: {error}"))
225 200
 }
226 201
 
227 202
 pub(crate) fn save_app_managed_private_key(
... ...
@@ -304,45 +279,79 @@ fn signing_certificates_dir() -> BackendResult<PathBuf> {
304 279
         })
305 280
 }
306 281
 
307
-fn certificate_temp_dir() -> BackendResult<PathBuf> {
308
-    app_data_dir()
309
-        .map(|path| path.join("certificates").join("tmp"))
310
-        .ok_or_else(|| {
311
-            BackendError::Unsupported("The application data folder is not available.".to_string())
312
-        })
313
-}
314
-
315
-fn run_command(mut command: Command, action: &'static str) -> BackendResult<()> {
316
-    let output = command
317
-        .stdin(Stdio::null())
318
-        .stdout(Stdio::null())
319
-        .stderr(Stdio::piped())
320
-        .output()
321
-        .map_err(|source| BackendError::Command { action, source })?;
322
-    if output.status.success() {
323
-        return Ok(());
324
-    }
325
-    Err(command_error(action, &output.stderr))
282
+#[cfg(test)]
283
+mod tests {
284
+    use super::*;
285
+    use der::DecodePem;
286
+    use rsa::pkcs1::EncodeRsaPrivateKey;
287
+    use rsa::pkcs8::DecodePublicKey;
288
+    use rsa::signature::Verifier;
289
+    use rsa::traits::PublicKeyParts;
290
+    use rsa::{pkcs1v15::VerifyingKey, RsaPublicKey};
291
+    use x509_cert::builder::{CertificateBuilder, Profile};
292
+    use x509_cert::request::CertReq;
293
+    use x509_cert::time::Validity;
294
+
295
+    #[test]
296
+    fn csr_signature_keys_and_certificate_fingerprints_agree() {
297
+        let generated = generate_development_certificate_signing_request().unwrap();
298
+        let csr = CertReq::from_pem(&generated.csr_content).unwrap();
299
+        assert_eq!(csr.info.subject.to_string(), "CN=Super Sideloader");
300
+        assert_eq!(csr.algorithm.oid.to_string(), "1.2.840.113549.1.1.11");
301
+        let spki = csr.info.public_key.to_der().unwrap();
302
+        let public_key = RsaPublicKey::from_public_key_der(&spki).unwrap();
303
+        assert_eq!(public_key.n().bits(), 2048);
304
+        let verifier = VerifyingKey::<sha2_010::Sha256>::new(public_key);
305
+        let signature = Signature::try_from(csr.signature.as_bytes().unwrap()).unwrap();
306
+        let info = csr.info.to_der().unwrap();
307
+        verifier.verify(&info, &signature).unwrap();
308
+        let mut tampered = info.clone();
309
+        tampered[0] ^= 1;
310
+        assert!(verifier.verify(&tampered, &signature).is_err());
311
+        assert_eq!(
312
+            certificate_fingerprint(&spki),
313
+            generated.public_key_fingerprint
314
+        );
315
+        assert_eq!(
316
+            public_key_der_from_private_key(&generated.private_key_pem).unwrap(),
317
+            spki
318
+        );
319
+
320
+        let key = decode_private_key(&generated.private_key_pem).unwrap();
321
+        let pkcs1 = key.to_pkcs1_pem(LineEnding::LF).unwrap();
322
+        assert_eq!(
323
+            public_key_der_from_private_key(pkcs1.as_bytes()).unwrap(),
324
+            spki
325
+        );
326
+        let signer = SigningKey::<sha2_010::Sha256>::new(key);
327
+        let cert = CertificateBuilder::new(
328
+            Profile::Root,
329
+            1u32.into(),
330
+            Validity::from_now(std::time::Duration::from_secs(3600)).unwrap(),
331
+            csr.info.subject,
332
+            csr.info.public_key,
333
+            &signer,
334
+        )
335
+        .unwrap()
336
+        .build::<Signature>()
337
+        .unwrap();
338
+        assert_eq!(
339
+            certificate_public_key_fingerprint(&cert.to_der().unwrap()).unwrap(),
340
+            generated.public_key_fingerprint
341
+        );
342
+
343
+        let temp = tempfile::tempdir().unwrap();
344
+        let path = temp.path().join("key.pem");
345
+        fs::write(&path, pkcs1.as_bytes()).unwrap();
346
+        let error =
347
+            import_app_managed_private_key("test-certificate", &"0".repeat(40), &path).unwrap_err();
348
+        assert!(error.to_string().contains("does not match"));
326 349
     }
327 350
 
328
-fn command_output(mut command: Command, action: &'static str) -> BackendResult<Vec<u8>> {
329
-    let output = command
330
-        .stdin(Stdio::null())
331
-        .stderr(Stdio::piped())
332
-        .output()
333
-        .map_err(|source| BackendError::Command { action, source })?;
334
-    if output.status.success() {
335
-        return Ok(output.stdout);
351
+    #[test]
352
+    fn malformed_key_and_certificate_are_rejected() {
353
+        assert!(public_key_der_from_private_key(b"not a PEM key").is_err());
354
+        assert!(public_key_der_from_private_key(&[255, 254]).is_err());
355
+        assert!(certificate_public_key_fingerprint(b"not a certificate").is_none());
336 356
     }
337
-    Err(command_error(action, &output.stderr))
338
-}
339
-
340
-fn command_error(action: &'static str, stderr: &[u8]) -> BackendError {
341
-    let stderr = String::from_utf8_lossy(stderr).trim().to_string();
342
-    let detail = if stderr.is_empty() {
343
-        "Process exited with a non-zero status.".to_string()
344
-    } else {
345
-        stderr
346
-    };
347
-    BackendError::CommandFailed { action, detail }
348 357
 }
... ...
@@ -32,7 +32,7 @@ where
32 32
 {
33 33
     let proxy = selected_adi_proxy(config.adi_backend, &config.android_adi_identifier)?;
34 34
     let http_session = grandslam::http_session(
35
-        grandslam_device(&config.machine_identity),
35
+        grandslam_device(&config.machine_identity, config.adi_backend),
36 36
         XCODE_BUNDLE_INFORMATION,
37 37
     )
38 38
     .await
... ...
@@ -1,3 +1,7 @@
1
+#[cfg(target_os = "windows")]
2
+#[path = "windows_session_store.rs"]
3
+mod windows_session_store;
4
+
1 5
 use crate::backend::{BackendError, BackendResult};
2 6
 use grandslam::{AuthToken, Token};
3 7
 use serde::{Deserialize, Serialize};
... ...
@@ -75,6 +79,9 @@ pub(crate) fn save_keychain_session(
75 79
         return Ok(());
76 80
     }
77 81
 
82
+    #[cfg(target_os = "windows")]
83
+    windows_session_store::save(account_id, &contents).map_err(BackendError::Keychain)?;
84
+    #[cfg(not(target_os = "windows"))]
78 85
     keyring_entry(account_id)
79 86
         .map_err(BackendError::Keychain)?
80 87
         .set_password(&contents)
... ...
@@ -90,6 +97,11 @@ pub(crate) fn save_keychain_session(
90 97
 pub(crate) fn load_keychain_session(
91 98
     account_id: &str,
92 99
 ) -> BackendResult<Option<DeveloperAccountKeychainSession>> {
100
+    #[cfg(target_os = "windows")]
101
+    {
102
+        load_keychain_session_once(account_id, || windows_session_store::load(account_id))
103
+    }
104
+    #[cfg(not(target_os = "windows"))]
93 105
     load_keychain_session_once(account_id, || {
94 106
         let entry = keyring_entry(account_id)?;
95 107
         match entry.get_password() {
... ...
@@ -138,6 +150,11 @@ pub(crate) fn delete_keychain_session(account_id: &str) -> BackendResult<()> {
138 150
     let mut cache = lock_session_cache()?;
139 151
     cache.remove(account_id);
140 152
 
153
+    #[cfg(target_os = "windows")]
154
+    {
155
+        windows_session_store::delete(account_id).map_err(BackendError::Keychain)
156
+    }
157
+    #[cfg(not(target_os = "windows"))]
141 158
     match keyring_entry(account_id)
142 159
         .map_err(BackendError::Keychain)?
143 160
         .delete_credential()
... ...
@@ -24,6 +24,7 @@ use grandslam::{AuthOutcome, AuthenticatedHTTPSession, Token};
24 24
 use plist::{Dictionary, Value};
25 25
 use std::path::PathBuf;
26 26
 use std::time::{Duration, UNIX_EPOCH};
27
+use grandslam::bundle_information::AKD_BUNDLE_INFORMATION;
27 28
 use xcode::{
28 29
     AddAppIdAction, AddDeviceAction, AppIdFeature, DeleteAppIdAction, DeleteDeviceAction,
29 30
     DeveloperTeam, DownloadTeamProvisioningProfileAction, IOSRequest,
... ...
@@ -870,8 +871,8 @@ async fn login_developer_account_async(
870 871
     let account_id = new_account_id();
871 872
     let proxy = selected_adi_proxy(request.adi_backend, &request.android_adi_identifier)?;
872 873
     let http_session = grandslam::http_session(
873
-        grandslam_device(&request.machine_identity),
874
-        XCODE_BUNDLE_INFORMATION,
874
+        grandslam_device(&request.machine_identity, request.adi_backend),
875
+        AKD_BUNDLE_INFORMATION,
875 876
     )
876 877
     .await
877 878
     .map_err(|error| {
... ...
@@ -0,0 +1,200 @@
1
+//! Large sessions stay in Credential Manager, split into bounded binary entries.
2
+//! A generation-specific index is committed last so failed writes preserve the
3
+//! previous session.
4
+use super::{keyring_entry, KeychainResult};
5
+
6
+const PREFIX: &str = "super-sideloader-chunks-v1:";
7
+const CHUNK_BYTES: usize = 2400; // Below Windows' 2560-byte credential blob limit.
8
+const MAX_CHUNKS: usize = 4096;
9
+
10
+struct Index {
11
+    generation: uuid::Uuid,
12
+    count: usize,
13
+}
14
+
15
+impl Index {
16
+    fn parse(value: &str) -> KeychainResult<Self> {
17
+        let invalid = || "Invalid account session chunk index".to_string();
18
+        let value = value.strip_prefix(PREFIX).ok_or_else(invalid)?;
19
+        let (generation, count) = value.split_once(':').ok_or_else(invalid)?;
20
+        let generation = uuid::Uuid::parse_str(generation).map_err(|_| invalid())?;
21
+        let count = count.parse::<usize>().map_err(|_| invalid())?;
22
+        if count == 0 || count > MAX_CHUNKS {
23
+            return Err(invalid());
24
+        }
25
+        Ok(Self { generation, count })
26
+    }
27
+
28
+    fn encode(&self) -> String {
29
+        format!("{PREFIX}{}:{}", self.generation, self.count)
30
+    }
31
+    fn chunk(&self, account: &str, number: usize) -> KeychainResult<keyring::Entry> {
32
+        keyring_entry(&format!("{account}:chunks:{}:{number}", self.generation))
33
+    }
34
+    fn remove(&self, account: &str) -> KeychainResult<()> {
35
+        let mut failure = None;
36
+        for number in 0..self.count {
37
+            if let Err(error) = self
38
+                .chunk(account, number)
39
+                .and_then(|entry| remove_entry(&entry))
40
+            {
41
+                failure = Some(error);
42
+            }
43
+        }
44
+        failure.map_or(Ok(()), Err)
45
+    }
46
+}
47
+
48
+fn read_index(account: &str) -> KeychainResult<Option<String>> {
49
+    match keyring_entry(account)?.get_password() {
50
+        Ok(value) => Ok(Some(value)),
51
+        Err(keyring::Error::NoEntry) => Ok(None),
52
+        Err(error) => Err(format!("Failed to read account session: {error}")),
53
+    }
54
+}
55
+
56
+fn remove_entry(entry: &keyring::Entry) -> KeychainResult<()> {
57
+    match entry.delete_credential() {
58
+        Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
59
+        Err(error) => Err(format!("Failed to remove account session entry: {error}")),
60
+    }
61
+}
62
+
63
+pub(super) fn load(account: &str) -> KeychainResult<Option<String>> {
64
+    let Some(value) = read_index(account)? else {
65
+        return Ok(None);
66
+    };
67
+    let index = Index::parse(&value)?;
68
+    let mut bytes = Vec::new();
69
+    for number in 0..index.count {
70
+        let chunk = index
71
+            .chunk(account, number)?
72
+            .get_secret()
73
+            .map_err(|error| format!("Failed to read account session chunk {number}: {error}"))?;
74
+        if chunk.is_empty() || chunk.len() > CHUNK_BYTES {
75
+            return Err("Invalid account session chunk size".into());
76
+        }
77
+        bytes.extend_from_slice(&chunk);
78
+    }
79
+    String::from_utf8(bytes)
80
+        .map(Some)
81
+        .map_err(|_| "Invalid account session encoding".into())
82
+}
83
+
84
+pub(super) fn save(account: &str, contents: &str) -> KeychainResult<()> {
85
+    save_with_commit(account, contents, |entry, index| {
86
+        entry
87
+            .set_password(index)
88
+            .map_err(|error| format!("Failed to commit account session: {error}"))
89
+    })
90
+}
91
+
92
+fn save_with_commit(
93
+    account: &str,
94
+    contents: &str,
95
+    commit: impl FnOnce(&keyring::Entry, &str) -> KeychainResult<()>,
96
+) -> KeychainResult<()> {
97
+    let count = contents.len().div_ceil(CHUNK_BYTES);
98
+    if count == 0 || count > MAX_CHUNKS {
99
+        return Err("Account session size is invalid".into());
100
+    }
101
+    let old = read_index(account)?
102
+        .map(|value| Index::parse(&value))
103
+        .transpose()?;
104
+    let index = Index {
105
+        generation: uuid::Uuid::new_v4(),
106
+        count,
107
+    };
108
+    let result = (|| {
109
+        for (number, chunk) in contents.as_bytes().chunks(CHUNK_BYTES).enumerate() {
110
+            index
111
+                .chunk(account, number)?
112
+                .set_secret(chunk)
113
+                .map_err(|error| {
114
+                    format!("Failed to save account session chunk {number}: {error}")
115
+                })?;
116
+        }
117
+        commit(&keyring_entry(account)?, &index.encode())
118
+    })();
119
+    if result.is_err() {
120
+        if let Err(error) = index.remove(account) {
121
+            log::warn!("Failed to clean up incomplete session: {error}");
122
+        }
123
+        return result;
124
+    }
125
+    if let Some(old) = old {
126
+        if let Err(error) = old.remove(account) {
127
+            log::warn!("Failed to clean up old session: {error}");
128
+        }
129
+    }
130
+    Ok(())
131
+}
132
+
133
+pub(super) fn delete(account: &str) -> KeychainResult<()> {
134
+    let index = read_index(account)?
135
+        .map(|value| Index::parse(&value))
136
+        .transpose()?;
137
+    remove_entry(&keyring_entry(account)?)?;
138
+    if let Some(index) = index {
139
+        index.remove(account)?;
140
+    }
141
+    Ok(())
142
+}
143
+
144
+#[cfg(test)]
145
+mod tests {
146
+    use super::*;
147
+    #[test]
148
+    fn rejects_invalid_indexes() {
149
+        assert!(Index::parse("version = 1").is_err());
150
+        for suffix in [
151
+            "invalid:1",
152
+            "00000000-0000-0000-0000-000000000000:0",
153
+            "00000000-0000-0000-0000-000000000000:4097",
154
+        ] {
155
+            assert!(Index::parse(&format!("{PREFIX}{suffix}")).is_err());
156
+        }
157
+    }
158
+
159
+    #[test]
160
+    #[ignore = "writes synthetic entries to the native Windows Credential Manager"]
161
+    fn native_large_session_round_trip_update_failure_and_delete() {
162
+        let account = format!("storage-test-{}", uuid::Uuid::new_v4());
163
+        struct Cleanup(String);
164
+        impl Drop for Cleanup {
165
+            fn drop(&mut self) {
166
+                let _ = delete(&self.0);
167
+            }
168
+        }
169
+        let _cleanup = Cleanup(account.clone());
170
+        assert!(load(&account).unwrap().is_none());
171
+        let contents = "synthetic-token-😀-é\n".repeat(2000);
172
+        save(&account, &contents).unwrap();
173
+        assert_eq!(load(&account).unwrap().unwrap(), contents);
174
+        let old = Index::parse(&read_index(&account).unwrap().unwrap()).unwrap();
175
+        let mut failed_index = None;
176
+        let failed = save_with_commit(&account, &contents, |_, value| {
177
+            failed_index = Some(Index::parse(value).unwrap());
178
+            Err("Simulated commit failure".into())
179
+        });
180
+        assert!(failed.is_err());
181
+        assert_eq!(load(&account).unwrap().unwrap(), contents);
182
+        let failed_index = failed_index.unwrap();
183
+        for i in 0..failed_index.count {
184
+            assert!(matches!(
185
+                failed_index.chunk(&account, i).unwrap().get_secret(),
186
+                Err(keyring::Error::NoEntry)
187
+            ));
188
+        }
189
+        save(&account, "replacement").unwrap();
190
+        assert_eq!(load(&account).unwrap().unwrap(), "replacement");
191
+        for i in 0..old.count {
192
+            assert!(matches!(
193
+                old.chunk(&account, i).unwrap().get_secret(),
194
+                Err(keyring::Error::NoEntry)
195
+            ));
196
+        }
197
+        delete(&account).unwrap();
198
+        assert!(load(&account).unwrap().is_none());
199
+    }
200
+}
... ...
@@ -54,6 +54,26 @@ where
54 54
         .map_err(|_| BackendError::TaskCanceled { label })
55 55
 }
56 56
 
57
+// Build large I/O futures on the runtime thread, not on the UI thread's stack.
58
+pub(crate) async fn run_send_with<T, F, M>(
59
+    label: &'static str,
60
+    make_future: M,
61
+) -> Result<T, BackendError>
62
+where
63
+    T: Send + 'static,
64
+    F: Future<Output = T> + Send + 'static,
65
+    M: FnOnce() -> F + Send + 'static,
66
+{
67
+    let runtime = send_runtime(label)?;
68
+    let (sender, receiver) = oneshot::channel();
69
+    runtime.spawn(async move {
70
+        let _ = sender.send(Box::pin(make_future()).await);
71
+    });
72
+    receiver
73
+        .await
74
+        .map_err(|_| BackendError::TaskCanceled { label })
75
+}
76
+
57 77
 fn worker(label: &'static str) -> BackendResult<BackendWorker> {
58 78
     match BACKEND_WORKER.get_or_init(start_worker) {
59 79
         Ok(worker) => Ok(worker.clone()),
... ...
@@ -1,6 +1,10 @@
1 1
 use crate::domain::MachineIdentity;
2 2
 use std::env;
3 3
 
4
+#[cfg(target_os = "windows")]
5
+#[path = "windows_system.rs"]
6
+mod windows_system;
7
+
4 8
 pub(crate) fn machine_identity() -> MachineIdentity {
5 9
     platform_machine_identity().unwrap_or_else(fallback_machine_identity)
6 10
 }
... ...
@@ -36,7 +40,25 @@ fn platform_machine_identity() -> Option<MachineIdentity> {
36 40
     })
37 41
 }
38 42
 
39
-#[cfg(not(target_os = "macos"))]
43
+#[cfg(target_os = "windows")]
44
+pub(crate) fn windows_machine_identity() -> MachineIdentity {
45
+    MachineIdentity {
46
+        machine_name: "PC".to_string(),
47
+        os_name: "Windows".to_string(),
48
+        os_version: windows_system::information().unwrap_or_else(|error| {
49
+            log::warn!("Failed to read Windows system information: {error}");
50
+            "Unknown".into()
51
+        }),
52
+        machine_id: library_coreadi::windows::device_identifier(),
53
+    }
54
+}
55
+
56
+#[cfg(target_os = "windows")]
57
+fn platform_machine_identity() -> Option<MachineIdentity> {
58
+    Some(windows_machine_identity())
59
+}
60
+
61
+#[cfg(not(any(target_os = "macos", target_os = "windows")))]
40 62
 fn platform_machine_identity() -> Option<MachineIdentity> {
41 63
     None
42 64
 }
... ...
@@ -0,0 +1,130 @@
1
+use std::io;
2
+use windows_sys::Win32::System::SystemInformation::*;
3
+
4
+#[link(name = "ntdll")]
5
+unsafe extern "system" {
6
+    fn RtlGetVersion(version: *mut OSVERSIONINFOEXW) -> i32;
7
+}
8
+
9
+/// Read the real kernel version rather than the manifest-dependent GetVersionEx result.
10
+pub(super) fn information() -> io::Result<String> {
11
+    let mut version: OSVERSIONINFOEXW = unsafe { std::mem::zeroed() };
12
+    version.dwOSVersionInfoSize = std::mem::size_of_val(&version) as u32;
13
+    let status = unsafe { RtlGetVersion(&mut version) };
14
+    if status < 0 {
15
+        return Err(io::Error::other(format!(
16
+            "RtlGetVersion failed: {status:#x}"
17
+        )));
18
+    }
19
+    let mut product = 0;
20
+    if unsafe {
21
+        GetProductInfo(
22
+            version.dwMajorVersion,
23
+            version.dwMinorVersion,
24
+            version.wServicePackMajor as u32,
25
+            version.wServicePackMinor as u32,
26
+            &mut product,
27
+        )
28
+    } == 0
29
+    {
30
+        return Err(io::Error::last_os_error());
31
+    }
32
+    let mut system: SYSTEM_INFO = unsafe { std::mem::zeroed() };
33
+    unsafe {
34
+        GetNativeSystemInfo(&mut system);
35
+    }
36
+    let architecture = unsafe { system.Anonymous.Anonymous.wProcessorArchitecture };
37
+    Ok(format!(
38
+        "{}.{}.{}/SP{}.{}.{}; {} {}; {}",
39
+        version.dwMajorVersion,
40
+        version.dwMinorVersion,
41
+        version.dwBuildNumber,
42
+        version.wServicePackMajor,
43
+        version.wServicePackMinor,
44
+        version.dwBuildNumber,
45
+        release_name(&version),
46
+        edition_name(product),
47
+        architecture_name(architecture)
48
+    ))
49
+}
50
+
51
+fn release_name(version: &OSVERSIONINFOEXW) -> String {
52
+    // Server releases share kernel build numbers with client releases.
53
+    if version.wProductType != 1 {
54
+        return "Windows Server".into();
55
+    }
56
+    match (
57
+        version.dwMajorVersion,
58
+        version.dwMinorVersion,
59
+        version.dwBuildNumber,
60
+    ) {
61
+        (10, 0, 22000..) => "Win11".into(),
62
+        (10, 0, _) => "Win10".into(),
63
+        (6, 3, _) => "Win8.1".into(),
64
+        (6, 2, _) => "Win8".into(),
65
+        (6, 1, _) => "Win7".into(),
66
+        (major, minor, _) => format!("Win{major}.{minor}"),
67
+    }
68
+}
69
+
70
+fn edition_name(product: u32) -> String {
71
+    let name = match product {
72
+        // These newer SKU values are documented by GetProductInfo but absent
73
+        // from this windows-sys release.
74
+        0xbc => "IoT Enterprise",
75
+        0xbf => "IoT Enterprise LTSC",
76
+        0xa4 => "Pro Education",
77
+        0xa5 => "Pro Education N",
78
+        PRODUCT_PROFESSIONAL => "Pro",
79
+        PRODUCT_PROFESSIONAL_N => "Pro N",
80
+        PRODUCT_PRO_WORKSTATION => "Pro for Workstations",
81
+        PRODUCT_PRO_WORKSTATION_N => "Pro for Workstations N",
82
+        PRODUCT_CORE => "Home",
83
+        PRODUCT_CORE_N => "Home N",
84
+        PRODUCT_CORE_SINGLELANGUAGE => "Home Single Language",
85
+        PRODUCT_CORE_COUNTRYSPECIFIC => "Home China",
86
+        PRODUCT_ENTERPRISE => "Enterprise",
87
+        PRODUCT_ENTERPRISE_N => "Enterprise N",
88
+        PRODUCT_ENTERPRISE_S => "Enterprise LTSC",
89
+        PRODUCT_ENTERPRISE_S_N => "Enterprise LTSC N",
90
+        PRODUCT_EDUCATION => "Education",
91
+        PRODUCT_EDUCATION_N => "Education N",
92
+        PRODUCT_STANDARD_SERVER => "Standard",
93
+        PRODUCT_DATACENTER_SERVER => "Datacenter",
94
+        // Preserve the actual SKU when it has no friendly label here.
95
+        _ => return format!("SKU {product:#x}"),
96
+    };
97
+    name.into()
98
+}
99
+
100
+fn architecture_name(architecture: u16) -> &'static str {
101
+    match architecture {
102
+        PROCESSOR_ARCHITECTURE_AMD64 => "x64",
103
+        PROCESSOR_ARCHITECTURE_INTEL => "x86",
104
+        PROCESSOR_ARCHITECTURE_ARM64 => "ARM64",
105
+        PROCESSOR_ARCHITECTURE_ARM => "ARM",
106
+        PROCESSOR_ARCHITECTURE_IA64 => "IA64",
107
+        _ => "Unknown",
108
+    }
109
+}
110
+
111
+#[cfg(test)]
112
+mod tests {
113
+    use super::*;
114
+    #[test]
115
+    fn windows_11_is_distinguished_from_windows_10_and_server() {
116
+        let mut version: OSVERSIONINFOEXW = unsafe { std::mem::zeroed() };
117
+        version.dwMajorVersion = 10;
118
+        version.wProductType = 1;
119
+        version.dwBuildNumber = 19045;
120
+        assert_eq!(release_name(&version), "Win10");
121
+        version.dwBuildNumber = 22000;
122
+        assert_eq!(release_name(&version), "Win11");
123
+        version.wProductType = 3;
124
+        assert_eq!(release_name(&version), "Windows Server");
125
+        assert_eq!(edition_name(0xbf), "IoT Enterprise LTSC");
126
+        assert_eq!(edition_name(PRODUCT_CORE), "Home");
127
+        assert_eq!(edition_name(PRODUCT_PROFESSIONAL), "Pro");
128
+        assert_eq!(architecture_name(PROCESSOR_ARCHITECTURE_ARM64), "ARM64");
129
+    }
130
+}
... ...
@@ -47,12 +47,13 @@ fn main() {
47 47
 }
48 48
 
49 49
 fn init_logging() {
50
-    let mut builder = env_logger::Builder::from_default_env();
50
+    let mut builder = env_logger::Builder::new();
51 51
     builder
52 52
         .filter_module("reqwest", LevelFilter::Info)
53 53
         .filter_module("hyper", LevelFilter::Info)
54 54
         .filter_module("h2", LevelFilter::Info)
55 55
         .filter_module("rustls", LevelFilter::Info)
56 56
         .filter_module("elf_loader", LevelFilter::Info);
57
+    builder.parse_default_env();
57 58
     builder.init();
58 59
 }
59 60