tauri_plugin_licensemanager/core/
fingerprint.rs1use std::future::Future;
4use std::sync::Arc;
5
6use base64::engine::general_purpose::STANDARD;
7use base64::Engine;
8use futures::future::BoxFuture;
9use futures::FutureExt;
10use sha2::{Digest, Sha256};
11
12#[derive(Clone)]
15pub enum MachineFingerprint {
16 Static(String),
18 Fn(Arc<dyn Fn() -> BoxFuture<'static, String> + Send + Sync>),
20}
21
22impl MachineFingerprint {
23 pub fn from_fn<F, Fut>(f: F) -> Self
25 where
26 F: Fn() -> Fut + Send + Sync + 'static,
27 Fut: Future<Output = String> + Send + 'static,
28 {
29 Self::Fn(Arc::new(move || f().boxed()))
30 }
31
32 pub fn from_sync_fn<F>(f: F) -> Self
34 where
35 F: Fn() -> String + Send + Sync + 'static,
36 {
37 Self::Fn(Arc::new(move || futures::future::ready(f()).boxed()))
38 }
39}
40
41impl From<String> for MachineFingerprint {
42 fn from(value: String) -> Self {
43 Self::Static(value)
44 }
45}
46
47impl From<&str> for MachineFingerprint {
48 fn from(value: &str) -> Self {
49 Self::Static(value.to_owned())
50 }
51}
52
53impl std::fmt::Debug for MachineFingerprint {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 Self::Static(value) => f.debug_tuple("Static").field(value).finish(),
57 Self::Fn(_) => f.write_str("Fn(..)"),
58 }
59 }
60}
61
62pub(crate) fn hash_machine_value(raw: &str) -> String {
64 STANDARD.encode(Sha256::digest(raw))
65}
66
67pub(crate) async fn resolve_machine_fingerprint(option: Option<&MachineFingerprint>) -> String {
70 let raw = match option {
71 Some(MachineFingerprint::Static(value)) => value.clone(),
72 Some(MachineFingerprint::Fn(f)) => f().await,
73 None => read_machine_id().await,
74 };
75 hash_machine_value(&raw)
76}
77
78pub(crate) async fn read_machine_id() -> String {
82 match read_native_machine_id().await {
83 Some(id) => id,
84 None => fallback_machine_id(),
85 }
86}
87
88#[cfg(target_os = "macos")]
89async fn read_native_machine_id() -> Option<String> {
90 let output = tokio::time::timeout(
91 std::time::Duration::from_secs(5),
92 tokio::process::Command::new("ioreg")
93 .args(["-rd1", "-c", "IOPlatformExpertDevice"])
94 .kill_on_drop(true)
95 .output(),
96 )
97 .await
98 .ok()?
99 .ok()?;
100 if !output.status.success() {
101 return None;
102 }
103 parse_darwin(&String::from_utf8_lossy(&output.stdout))
104}
105
106#[cfg(target_os = "windows")]
107async fn read_native_machine_id() -> Option<String> {
108 tokio::task::spawn_blocking(|| {
109 use winreg::enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_64KEY};
110 let value: String = winreg::RegKey::predef(HKEY_LOCAL_MACHINE)
111 .open_subkey_with_flags(
112 r"SOFTWARE\Microsoft\Cryptography",
113 KEY_READ | KEY_WOW64_64KEY,
114 )
115 .ok()?
116 .get_value("MachineGuid")
117 .ok()?;
118 normalize_id(&value)
119 })
120 .await
121 .ok()
122 .flatten()
123}
124
125#[cfg(not(any(target_os = "macos", target_os = "windows")))]
126async fn read_native_machine_id() -> Option<String> {
127 read_linux_machine_id(&["/etc/machine-id", "/var/lib/dbus/machine-id"]).await
128}
129
130pub(crate) fn parse_darwin(output: &str) -> Option<String> {
132 let (_, after) = output.split_once("IOPlatformUUID")?;
133 let line = after.split('\n').next().unwrap_or_default();
134 let value: String = line
135 .chars()
136 .filter(|c| *c != '=' && *c != '"' && !c.is_whitespace())
137 .collect::<String>()
138 .to_lowercase();
139 (!value.is_empty()).then_some(value)
140}
141
142pub(crate) fn normalize_id(raw: &str) -> Option<String> {
145 let value: String = raw
146 .chars()
147 .filter(|c| !c.is_whitespace())
148 .collect::<String>()
149 .to_lowercase();
150 (!value.is_empty()).then_some(value)
151}
152
153#[cfg_attr(any(target_os = "macos", target_os = "windows"), allow(dead_code))]
155pub(crate) async fn read_linux_machine_id(paths: &[&str]) -> Option<String> {
156 for path in paths {
157 if let Ok(content) = tokio::fs::read_to_string(path).await {
158 if let Some(value) = normalize_id(&content) {
159 return Some(value);
160 }
161 }
162 }
163 None
164}
165
166pub(crate) fn fallback_machine_id() -> String {
168 match mac_address::get_mac_address() {
169 Ok(Some(mac)) if mac.bytes() != [0; 6] => mac.to_string().to_lowercase(),
170 _ => super::environment::hostname().to_lowercase(),
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use std::io::Write;
177
178 use super::*;
179
180 #[tokio::test]
181 async fn hashes_a_verbatim_string() {
182 let option = MachineFingerprint::from("my-machine");
183 assert_eq!(
184 resolve_machine_fingerprint(Some(&option)).await,
185 hash_machine_value("my-machine")
186 );
187 let option = MachineFingerprint::from(String::from("my-machine"));
188 assert_eq!(
189 resolve_machine_fingerprint(Some(&option)).await,
190 hash_machine_value("my-machine")
191 );
192 }
193
194 #[tokio::test]
195 async fn awaits_a_sync_function() {
196 let option = MachineFingerprint::from_sync_fn(|| "sync-value".into());
197 assert_eq!(
198 resolve_machine_fingerprint(Some(&option)).await,
199 hash_machine_value("sync-value")
200 );
201 }
202
203 #[tokio::test]
204 async fn awaits_an_async_function() {
205 let option = MachineFingerprint::from_fn(|| async { "async-value".to_owned() });
206 assert_eq!(
207 resolve_machine_fingerprint(Some(&option)).await,
208 hash_machine_value("async-value")
209 );
210 assert_eq!(format!("{option:?}"), "Fn(..)");
211 assert_eq!(
212 format!("{:?}", MachineFingerprint::from("x")),
213 "Static(\"x\")"
214 );
215 }
216
217 #[test]
218 fn parses_the_macos_io_platform_uuid() {
219 assert_eq!(
220 parse_darwin(
221 "+-o Root\n \"IOPlatformUUID\" = \"ABCDEF12-3456-7890-ABCD-EF1234567890\"\n \"x\" = 1\n"
222 )
223 .as_deref(),
224 Some("abcdef12-3456-7890-abcd-ef1234567890")
225 );
226 }
227
228 #[test]
229 fn rejects_a_missing_or_empty_macos_uuid() {
230 assert_eq!(parse_darwin("nothing here"), None);
231 assert_eq!(parse_darwin("\"IOPlatformUUID\" = \"\"\n"), None);
232 }
233
234 #[test]
235 fn normalizes_the_windows_machine_guid() {
236 assert_eq!(
237 normalize_id(" 11112222-3333-4444-5555-666677778888\r\n").as_deref(),
238 Some("11112222-3333-4444-5555-666677778888")
239 );
240 assert_eq!(normalize_id(" \r\n"), None);
241 }
242
243 #[tokio::test]
244 async fn reads_the_linux_machine_id_file() {
245 let mut file = tempfile::NamedTempFile::new().unwrap();
246 writeln!(file, "DEADBEEFdeadbeefdeadbeefdeadbeef").unwrap();
247 let path = file.path().to_str().unwrap().to_owned();
248 assert_eq!(
249 read_linux_machine_id(&[&path]).await.as_deref(),
250 Some("deadbeefdeadbeefdeadbeefdeadbeef")
251 );
252 }
253
254 #[tokio::test]
255 async fn tries_the_dbus_path_when_etc_machine_id_is_empty() {
256 let mut empty = tempfile::NamedTempFile::new().unwrap();
257 writeln!(empty, " ").unwrap();
258 let mut dbus = tempfile::NamedTempFile::new().unwrap();
259 writeln!(dbus, "cafecafecafecafecafecafecafecafe").unwrap();
260 let paths = [
261 empty.path().to_str().unwrap().to_owned(),
262 dbus.path().to_str().unwrap().to_owned(),
263 ];
264 assert_eq!(
265 read_linux_machine_id(&[&paths[0], &paths[1]])
266 .await
267 .as_deref(),
268 Some("cafecafecafecafecafecafecafecafe")
269 );
270 }
271
272 #[tokio::test]
273 async fn returns_none_when_no_linux_machine_id_file_is_readable() {
274 assert_eq!(
275 read_linux_machine_id(&["/nonexistent/machine-id"]).await,
276 None
277 );
278 }
279
280 #[test]
281 fn falls_back_to_a_mac_address_or_the_hostname() {
282 let value = fallback_machine_id();
283 assert!(!value.is_empty());
284 assert_eq!(value, value.to_lowercase());
285 assert!(!value.ends_with('='));
287 }
288
289 #[tokio::test]
290 async fn reads_a_native_machine_id_without_failing() {
291 let value = read_machine_id().await;
292 assert!(!value.is_empty());
293 assert_eq!(value, value.to_lowercase());
294 assert_eq!(read_machine_id().await, value);
296 }
297
298 #[test]
299 fn golden_machine_hash() {
300 assert_eq!(
301 hash_machine_value("4c4c4544-0042-3510-8051-b4c04f4b4e32"),
302 "WDpaNFX/NZW0yp1UEWsHi36rKjpX4d0zVnasSsrpJOA="
303 );
304 }
305
306 #[test]
307 fn hash_is_a_stable_base64_sha256_digest() {
308 assert_eq!(
309 hash_machine_value("x"),
310 STANDARD.encode(Sha256::digest("x"))
311 );
312 assert_eq!(hash_machine_value("x").len(), 44);
313 }
314}