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 |
|
| ... | ... |
@@ -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 |
| ... | ... |
@@ -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" |
| ... | ... |
@@ -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 |