steamkit/util/
hardware_utils.rs1use std::sync::{Arc, Mutex, OnceLock, Weak};
8
9use sha1::{Digest, Sha1};
10
11use crate::error::SteamError;
12use crate::steam::steam_client::configuration::i_machine_info_provider::IMachineInfoProvider;
13use crate::types::key_value::KeyValue;
14use crate::types::message_object::MessageObject;
15use crate::util::debug_log::DebugLog;
16use crate::util::utils::Utils;
17
18#[derive(Debug)]
20pub struct MachineInfoProvider;
21
22impl MachineInfoProvider {
23 pub fn get_default_provider() -> Arc<dyn IMachineInfoProvider> {
25 #[cfg(target_os = "windows")]
26 {
27 Arc::new(WindowsMachineInfoProvider)
28 }
29
30 #[cfg(target_os = "macos")]
31 {
32 Arc::new(MacOSMachineInfoProvider)
33 }
34
35 #[cfg(target_os = "linux")]
36 {
37 Arc::new(LinuxMachineInfoProvider)
38 }
39
40 #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
41 {
42 Arc::new(DefaultMachineInfoProvider)
43 }
44 }
45}
46
47#[derive(Debug)]
50pub struct DefaultMachineInfoProvider;
51
52impl DefaultMachineInfoProvider {
53 pub fn instance() -> Arc<dyn IMachineInfoProvider> {
55 static INSTANCE: OnceLock<Arc<dyn IMachineInfoProvider>> = OnceLock::new();
56
57 INSTANCE
58 .get_or_init(|| Arc::new(DefaultMachineInfoProvider))
59 .clone()
60 }
61
62 pub fn get_combined_mac_address(addresses: Vec<Vec<u8>>) -> Vec<u8> {
66 let mut sequences: Vec<Vec<u8>> = addresses
67 .into_iter()
68 .map(|mut address| {
69 address.push(0);
70 address.push(0);
71 address
72 })
73 .collect();
74
75 sequences.push(vec![0u8; 8]);
76 sequences.push(vec![0u8; 8]);
77
78 let mut result = Vec::with_capacity(16);
79
80 for sequence in sequences.into_iter().take(2) {
81 result.extend(sequence);
82 }
83
84 result
85 }
86}
87
88impl IMachineInfoProvider for DefaultMachineInfoProvider {
89 fn machine_guid(&self) -> Option<Vec<u8>> {
90 Some(format!("{}-SteamKit", machine_name()).into_bytes())
91 }
92
93 fn mac_address(&self) -> Option<Vec<u8>> {
94 let addresses = read_mac_addresses(false);
97
98 match addresses.into_iter().next() {
99 Some(address) => Some(address),
100 None => Some(b"SteamKit-MacAddress".to_vec()),
101 }
102 }
103
104 fn disk_id(&self) -> Option<Vec<u8>> {
105 Some(b"SteamKit-DiskId".to_vec())
106 }
107}
108
109#[derive(Debug)]
111pub struct WindowsMachineInfoProvider;
112
113impl IMachineInfoProvider for WindowsMachineInfoProvider {
114 fn machine_guid(&self) -> Option<Vec<u8>> {
115 query_machine_guid().map(|guid| guid.into_bytes())
116 }
117
118 fn mac_address(&self) -> Option<Vec<u8>> {
119 let addresses = read_physical_mac_addresses();
121
122 Some(DefaultMachineInfoProvider::get_combined_mac_address(addresses))
123 }
124
125 fn disk_id(&self) -> Option<Vec<u8>> {
126 crate::util::win32_helpers::Win32Helpers::get_boot_disk_serial_number()
127 .map(|serial_number| serial_number.into_bytes())
128 }
129}
130
131#[derive(Debug)]
133pub struct LinuxMachineInfoProvider;
134
135impl IMachineInfoProvider for LinuxMachineInfoProvider {
136 fn machine_guid(&self) -> Option<Vec<u8>> {
137 let machine_files = [
138 "/var/lib/dbus/machine-id",
139 "/etc/machine-id",
140 "/etc/hostname",
141 ];
142
143 for file_name in machine_files {
144 if let Ok(data) = std::fs::read(file_name) {
145 return Some(data);
146 }
147 }
148
149 None
150 }
151
152 fn mac_address(&self) -> Option<Vec<u8>> {
153 Some(DefaultMachineInfoProvider::get_combined_mac_address(
154 read_mac_addresses(true),
155 ))
156 }
157
158 fn disk_id(&self) -> Option<Vec<u8>> {
159 let boot_params = get_boot_options();
160
161 for param in ["root=UUID=", "root=PARTUUID="] {
162 if let Some(value) = get_param_value(&boot_params, param) {
163 if !value.is_empty() {
164 return Some(value.into_bytes());
165 }
166 }
167 }
168
169 let disk_uuids = get_disk_uuids();
170
171 disk_uuids
172 .into_iter()
173 .next()
174 .map(|uuid| uuid.into_bytes())
175 }
176}
177
178#[derive(Debug)]
180pub struct MacOSMachineInfoProvider;
181
182impl IMachineInfoProvider for MacOSMachineInfoProvider {
183 fn machine_guid(&self) -> Option<Vec<u8>> {
184 match crate::util::mac_helpers::platform_serial_number() {
186 Some(serial_number) => Some(serial_number.into_bytes()),
187 None => DefaultMachineInfoProvider::instance().machine_guid(),
188 }
189 }
190
191 fn mac_address(&self) -> Option<Vec<u8>> {
192 None
194 }
195
196 fn disk_id(&self) -> Option<Vec<u8>> {
197 match crate::util::mac_helpers::media_uuid("/") {
198 Some(uuid) => Some(uuid.into_bytes()),
199 None => DefaultMachineInfoProvider::instance().disk_id(),
200 }
201 }
202}
203
204#[derive(Debug, Default)]
206struct MachineIdSlot {
207 value: OnceLock<Option<Vec<u8>>>,
209}
210
211#[derive(Debug)]
213struct GenerationEntry {
214 provider: Weak<dyn IMachineInfoProvider>,
216 slot: Arc<MachineIdSlot>,
218}
219
220impl GenerationEntry {
221 fn matches(&self, provider: &Arc<dyn IMachineInfoProvider>) -> bool {
223 match self.provider.upgrade() {
224 Some(strong) => Arc::ptr_eq(&strong, provider),
225 None => false,
226 }
227 }
228}
229
230fn generation_table() -> &'static Mutex<Vec<GenerationEntry>> {
232 static TABLE: OnceLock<Mutex<Vec<GenerationEntry>>> = OnceLock::new();
233
234 TABLE.get_or_init(|| Mutex::new(Vec::new()))
235}
236
237fn register_provider(provider: &Arc<dyn IMachineInfoProvider>) -> Arc<MachineIdSlot> {
239 let mut table = generation_table()
240 .lock()
241 .unwrap_or_else(|error| error.into_inner());
242
243 if let Some(entry) = table.iter().find(|entry| entry.matches(provider)) {
244 return entry.slot.clone();
245 }
246
247 let slot = Arc::new(MachineIdSlot::default());
248
249 table.push(GenerationEntry {
250 provider: Arc::downgrade(provider),
251 slot: slot.clone(),
252 });
253
254 slot
255}
256
257fn lookup_provider(provider: &Arc<dyn IMachineInfoProvider>) -> Option<Arc<MachineIdSlot>> {
259 let mut table = generation_table()
260 .lock()
261 .unwrap_or_else(|error| error.into_inner());
262
263 table.retain(|entry| entry.provider.strong_count() > 0);
264
265 table
266 .iter()
267 .find(|entry| entry.matches(provider))
268 .map(|entry| entry.slot.clone())
269}
270
271fn generate_machine_id(provider: &dyn IMachineInfoProvider) -> Result<Vec<u8>, SteamError> {
273 let fallback = DefaultMachineInfoProvider::instance();
279
280 let mut message_object = MessageObject::new();
281
282 {
283 let key_values = message_object.key_values_mut();
284
285 key_values.set("BB3", KeyValue::new());
286 key_values.set("FF2", KeyValue::new());
287 key_values.set("3B3", KeyValue::new());
288 }
289
290 let bb3 = provider
293 .machine_guid()
294 .or_else(|| fallback.machine_guid())
295 .unwrap_or_default();
296 let ff2 = provider
297 .mac_address()
298 .or_else(|| fallback.mac_address())
299 .unwrap_or_default();
300 let three_b3 = provider
301 .disk_id()
302 .or_else(|| fallback.disk_id())
303 .unwrap_or_default();
304
305 set_child_value(&mut message_object, "BB3", &get_hex_string(&bb3));
306 set_child_value(&mut message_object, "FF2", &get_hex_string(&ff2));
307 set_child_value(&mut message_object, "3B3", &get_hex_string(&three_b3));
308
309 let mut stream: Vec<u8> = Vec::new();
312 message_object.write_to_stream(&mut stream)?;
313
314 Ok(stream)
315}
316
317fn set_child_value(message_object: &mut MessageObject, key: &str, value: &str) {
319 let children = message_object.key_values_mut().children_mut();
320
321 if let Some(child) = children
322 .iter_mut()
323 .find(|child| child.name().is_some_and(|name| name.eq_ignore_ascii_case(key)))
324 {
325 child.set_value(Some(value.to_string()));
326 }
327}
328
329fn get_hex_string(data: &[u8]) -> String {
331 let digest = Sha1::digest(data);
332
333 Utils::encode_hex_string(digest.as_slice())
334}
335
336fn machine_name() -> String {
338 #[cfg(target_os = "windows")]
339 {
340 std::env::var("COMPUTERNAME").unwrap_or_else(|_| "unknown".to_string())
341 }
342
343 #[cfg(not(target_os = "windows"))]
344 {
345 std::env::var("HOSTNAME").unwrap_or_else(|_| "unknown".to_string())
346 }
347}
348
349#[cfg(target_os = "windows")]
351fn query_machine_guid() -> Option<String> {
352 let output = std::process::Command::new("reg")
353 .args([
354 "query",
355 r"HKLM\SOFTWARE\Microsoft\Cryptography",
356 "/v",
357 "MachineGuid",
358 ])
359 .output()
360 .ok()?;
361
362 if !output.status.success() {
363 return None;
364 }
365
366 let text = String::from_utf8(output.stdout).ok()?;
367
368 for line in text.lines() {
369 let trimmed = line.trim();
370
371 if trimmed.starts_with("MachineGuid") {
372 return trimmed.split_whitespace().last().map(str::to_string);
373 }
374 }
375
376 None
377}
378
379#[cfg(target_os = "windows")]
387const PHYSICAL_MAC_ADDRESSES_SCRIPT: &str = r#"
388$ErrorActionPreference = 'SilentlyContinue'
389$classKey = 'SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}'
390[System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() | ForEach-Object {
391 $adapter = $_
392 $connection = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey("$classKey\$($adapter.Id)\Connection")
393 if ($null -ne $connection) {
394 $instanceId = $connection.GetValue('PnpInstanceID', '')
395 $connection.Close()
396 if ($instanceId.Length -gt 3 -and $instanceId.StartsWith('PCI', [System.StringComparison]::Ordinal)) {
397 $address = $adapter.GetPhysicalAddress().GetAddressBytes()
398 if ($address.Length -gt 0) { -join ($address | ForEach-Object { $_.ToString('X2') }) } else { '-' }
399 }
400 }
401}
402"#;
403
404#[cfg(target_os = "windows")]
407fn read_physical_mac_addresses() -> Vec<Vec<u8>> {
408 let text = match crate::util::win32_helpers::run_powershell(PHYSICAL_MAC_ADDRESSES_SCRIPT) {
409 Ok(text) => text,
410 Err(error) => {
411 DebugLog::write_line(
412 "HardwareUtils",
413 &format!("Failed to read the MAC addresses of the network adapters: {error}"),
414 );
415
416 return Vec::new();
417 }
418 };
419
420 text.lines()
421 .filter_map(|line| {
422 let hex: String = line
423 .trim()
424 .chars()
425 .filter(|character| character.is_ascii_hexdigit())
426 .collect();
427
428 if hex.len() % 2 != 0 {
429 return None;
430 }
431
432 let mut bytes = Vec::with_capacity(hex.len() / 2);
433
434 for pair in hex.as_bytes().chunks(2) {
435 let text = std::str::from_utf8(pair).ok()?;
436 bytes.push(u8::from_str_radix(text, 16).ok()?);
437 }
438
439 Some(bytes)
440 })
441 .collect()
442}
443
444#[cfg(target_os = "linux")]
446fn read_mac_addresses(check_for_physical_device: bool) -> Vec<Vec<u8>> {
447 let Ok(entries) = std::fs::read_dir("/sys/class/net") else {
448 return Vec::new();
449 };
450
451 let mut addresses = Vec::new();
452
453 for entry in entries.flatten() {
454 let interface = entry.file_name().to_string_lossy().into_owned();
455
456 if check_for_physical_device
457 && !std::path::Path::new(&format!("/sys/class/net/{interface}/device")).exists()
458 {
459 continue;
460 }
461
462 let Ok(text) = std::fs::read_to_string(format!("/sys/class/net/{interface}/address")) else {
463 continue;
464 };
465
466 let hex: String = text
467 .trim()
468 .chars()
469 .filter(|character| character.is_ascii_hexdigit())
470 .collect();
471
472 if hex.len() < 2 || hex.len() % 2 != 0 {
473 continue;
474 }
475
476 let mut bytes = Vec::with_capacity(hex.len() / 2);
477
478 let mut valid = true;
479
480 for pair in hex.as_bytes().chunks(2) {
481 match std::str::from_utf8(pair).ok().and_then(|text| u8::from_str_radix(text, 16).ok()) {
482 Some(byte) => bytes.push(byte),
483 None => {
484 valid = false;
485 break;
486 }
487 }
488 }
489
490 if valid {
491 addresses.push(bytes);
492 }
493 }
494
495 if check_for_physical_device && addresses.is_empty() {
496 return read_mac_addresses(false);
497 }
498
499 addresses
500}
501
502#[cfg(not(target_os = "linux"))]
505fn read_mac_addresses(_check_for_physical_device: bool) -> Vec<Vec<u8>> {
506 Vec::new()
507}
508
509#[cfg(target_os = "linux")]
511fn get_boot_options() -> Vec<String> {
512 let Ok(text) = std::fs::read_to_string("/proc/cmdline") else {
513 return Vec::new();
514 };
515
516 text.split(' ')
517 .map(|option| option.to_string())
518 .collect()
519}
520
521#[cfg(not(target_os = "linux"))]
523fn get_boot_options() -> Vec<String> {
524 Vec::new()
525}
526
527fn get_param_value(boot_options: &[String], param: &str) -> Option<String> {
529 let option = boot_options
530 .iter()
531 .find(|option| option.len() >= param.len() && option[..param.len()].eq_ignore_ascii_case(param))?;
532
533 Some(option[param.len()..].to_string())
534}
535
536#[cfg(target_os = "linux")]
538fn get_disk_uuids() -> Vec<String> {
539 let Ok(entries) = std::fs::read_dir("/dev/disk/by-uuid") else {
540 return Vec::new();
541 };
542
543 let mut files: Vec<(std::time::SystemTime, String)> = entries
544 .flatten()
545 .filter_map(|entry| {
546 let name = entry.file_name().to_string_lossy().into_owned();
547 let modified = entry.metadata().ok()?.modified().ok()?;
548
549 Some((modified, name))
550 })
551 .collect();
552
553 files.sort_by_key(|(modified, _)| *modified);
554
555 files.into_iter().map(|(_, name)| name).collect()
556}
557
558#[cfg(not(target_os = "linux"))]
560fn get_disk_uuids() -> Vec<String> {
561 Vec::new()
562}
563
564#[derive(Debug)]
566pub struct HardwareUtils;
567
568impl HardwareUtils {
569 pub fn init(machine_info_provider: Arc<dyn IMachineInfoProvider>) {
573 let slot = register_provider(&machine_info_provider);
574
575 let Ok(handle) = tokio::runtime::Handle::try_current() else {
576 return;
577 };
578
579 let provider = machine_info_provider.clone();
580
581 handle.spawn(async move {
582 let _ = tokio::task::spawn_blocking(move || {
583 slot.value.get_or_init(|| generate_machine_id(provider.as_ref()).ok());
584 })
585 .await;
586 });
587 }
588
589 pub fn get_machine_id(
592 machine_info_provider: &Arc<dyn IMachineInfoProvider>,
593 ) -> Option<Vec<u8>> {
594 let slot = lookup_provider(machine_info_provider)?;
595
596 let value = slot
597 .value
598 .get_or_init(|| generate_machine_id(machine_info_provider.as_ref()).ok());
599
600 value.clone()
601 }
602}
603
604pub fn get_machine_id(
606 machine_info_provider: &Arc<dyn IMachineInfoProvider>,
607) -> Option<Vec<u8>> {
608 let Some(slot) = lookup_provider(machine_info_provider) else {
609 DebugLog::write_line("HardwareUtils", "GetMachineID() called before Init()");
610 return None;
611 };
612
613 let value = slot
614 .value
615 .get_or_init(|| generate_machine_id(machine_info_provider.as_ref()).ok());
616
617 value.clone()
618}