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}