app_machine_id/lib.rs
1// app-machine-id - Generate app-specific machine IDs
2// Copyright (C) 2023 d-k-bo
3// SPDX-License-Identifier: LGPL-2.1-or-later
4
5//! Generate app-specific machine IDs derived from the machine ID defined in
6//! `/etc/machine-id` and an application ID.
7//!
8//! Unlike the default machine ID, which should be considered "confidential",
9//! this implementation uses HMAC-SHA256 to generate an app-specific machine ID
10//! which could be used in less secure contexts.
11//!
12//! This implementation is based on systemd's `sd_id128_get_machine_app_specific()`.
13//!
14//! See [`man machine-id(5)`](https://www.freedesktop.org/software/systemd/man/machine-id.html)
15//! and [`man sd_id128_get_machine(3)`](https://www.freedesktop.org/software/systemd/man/sd_id128_get_machine_app_specific.html)
16//! for details.
17
18use std::{
19 fmt::{Debug, Display},
20 fs::read_to_string,
21 io,
22};
23
24use hmac_sha256::HMAC;
25use uuid::Uuid;
26
27const MACHINE_ID_PATH: &str = "/etc/machine-id";
28
29/// Generate an app-specific machine ID derived from the machine ID defined in
30/// `/etc/machine-id` and an application ID.
31///
32/// Unlike the default machine ID, which should be considered "confidential",
33/// this implementation uses HMAC-SHA256 to generate an app-specific machine UUID
34/// which could be used in less secure contexts.
35///
36/// This implementation is based on systemd's `sd_id128_get_machine_app_specific()`.
37///
38/// See [`man machine-id(5)`](https://www.freedesktop.org/software/systemd/man/machine-id.html)
39/// and [`man sd_id128_get_machine(3)`](https://www.freedesktop.org/software/systemd/man/sd_id128_get_machine_app_specific.html)
40/// for details.
41pub fn get(app_id: Uuid) -> Result<Uuid, Error> {
42 let machine_id = machine_id()?;
43 let hmac = HMAC::mac(app_id, machine_id);
44 let id = Uuid::from_slice(&hmac[0..16])?;
45 let id = make_v4_uuid(id);
46 Ok(id)
47}
48
49fn machine_id() -> Result<Uuid, Error> {
50 let id = Uuid::try_parse(read_to_string(MACHINE_ID_PATH)?.trim_end())?;
51 Ok(id)
52}
53
54/// Turn the ID into a valid UUIDv4.
55///
56/// This code is inspired by `generate_random_uuid()` of drivers/char/random.c from the Linux kernel sources.
57fn make_v4_uuid(id: Uuid) -> Uuid {
58 let mut id = id.into_bytes();
59
60 // Set UUID version to 4 --- truly random generation
61 id[6] = (id[6] & 0x0F) | 0x40;
62 // Set the UUID variant to DCE
63 id[8] = (id[8] & 0x3F) | 0x80;
64
65 Uuid::from_bytes(id)
66}
67
68#[derive(Debug)]
69/// Returned when reading the machine ID fails.
70pub enum Error {
71 /// Could not read `/etc/machine-id`.
72 Io(io::Error),
73 /// The machine ID doesn't match the machine-id(5) format.
74 InvalidId(uuid::Error),
75}
76impl Display for Error {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 Self::Io(err) => write!(f, "Could not read {MACHINE_ID_PATH}: {err}"),
80 Self::InvalidId(_) => {
81 write!(
82 f,
83 "The machine ID in {MACHINE_ID_PATH} does not \
84 match the format descibed in machine-id(5)"
85 )
86 }
87 }
88 }
89}
90impl std::error::Error for Error {
91 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
92 Some(match self {
93 Self::Io(err) => err,
94 Self::InvalidId(err) => err,
95 })
96 }
97}
98impl From<io::Error> for Error {
99 fn from(err: io::Error) -> Self {
100 Self::Io(err)
101 }
102}
103impl From<uuid::Error> for Error {
104 fn from(err: uuid::Error) -> Self {
105 Self::InvalidId(err)
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 static APP_ID: Uuid = uuid::uuid!("8e9b38ad-0ef8-4b14-894a-83bef002c713");
114
115 #[test]
116 fn test_machine_id() {
117 let systemd = Uuid::try_parse_ascii(
118 &std::process::Command::new("systemd-id128")
119 .args(["machine-id"])
120 .output()
121 .unwrap()
122 .stdout[0..32],
123 )
124 .unwrap();
125 let this = machine_id().unwrap();
126 assert_eq!(systemd, this);
127 }
128
129 #[test]
130 fn test_app_specific_machine_id() {
131 let systemd = Uuid::try_parse_ascii(
132 &std::process::Command::new("systemd-id128")
133 .args(["machine-id", "--app-specific", &APP_ID.to_string()])
134 .output()
135 .unwrap()
136 .stdout[0..32],
137 )
138 .unwrap();
139 let this = get(APP_ID).unwrap();
140 assert_eq!(systemd, this);
141 }
142}