aperion_shield/orgmode/
state.rs1use std::path::PathBuf;
9
10use anyhow::{anyhow, Context};
11use serde::{Deserialize, Serialize};
12
13pub const ORG_STATE_FILE: &str = "orgmode.json";
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct OrgState {
18 pub smartflow_url: String,
21
22 pub vkey: String,
25
26 pub device_id: String,
28
29 pub policy_group: String,
32
33 #[serde(default)]
36 pub owner_email: Option<String>,
37
38 pub enrolled_at: String,
40
41 pub platform: String,
45
46 pub device_name: String,
49
50 pub device_fingerprint: String,
53}
54
55impl OrgState {
56 pub fn default_path() -> anyhow::Result<PathBuf> {
60 let dir = if let Ok(custom) = std::env::var("APERION_SHIELD_HOME") {
61 PathBuf::from(custom)
62 } else {
63 let mut home = dirs::home_dir()
64 .ok_or_else(|| anyhow!("could not resolve home directory"))?;
65 home.push(".aperion-shield");
66 home
67 };
68 std::fs::create_dir_all(&dir).context("create ~/.aperion-shield/")?;
69 Ok(dir.join(ORG_STATE_FILE))
70 }
71
72 pub fn load() -> anyhow::Result<Option<Self>> {
75 let path = Self::default_path()?;
76 if !path.exists() {
77 return Ok(None);
78 }
79 let raw = std::fs::read_to_string(&path)
80 .with_context(|| format!("read {}", path.display()))?;
81 let state: OrgState =
82 serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))?;
83 Ok(Some(state))
84 }
85
86 pub fn save(&self) -> anyhow::Result<()> {
88 let path = Self::default_path()?;
89 let tmp = path.with_extension("json.tmp");
90 let json = serde_json::to_string_pretty(self)?;
91 std::fs::write(&tmp, json).with_context(|| format!("write {}", tmp.display()))?;
92 #[cfg(unix)]
93 {
94 use std::os::unix::fs::PermissionsExt;
95 let mut perms = std::fs::metadata(&tmp)?.permissions();
96 perms.set_mode(0o600);
97 std::fs::set_permissions(&tmp, perms)?;
98 }
99 std::fs::rename(&tmp, &path).with_context(|| format!("rename {}", path.display()))?;
100 Ok(())
101 }
102
103 pub fn remove() -> anyhow::Result<()> {
105 let path = Self::default_path()?;
106 if path.exists() {
107 std::fs::remove_file(&path)
108 .with_context(|| format!("remove {}", path.display()))?;
109 }
110 Ok(())
111 }
112
113 pub fn fingerprint() -> String {
117 use sha2::{Digest, Sha256};
118 let hostname = hostname_string();
119 let os = std::env::consts::OS.to_string();
120 let machine_id = machine_id_string();
121 let mut hasher = Sha256::new();
122 hasher.update(format!("{}|{}|{}", hostname, os, machine_id).as_bytes());
123 hex::encode(hasher.finalize())
124 }
125}
126
127fn hostname_string() -> String {
128 std::env::var("HOSTNAME")
130 .ok()
131 .or_else(|| std::env::var("COMPUTERNAME").ok())
132 .or_else(|| {
133 std::process::Command::new("hostname")
134 .output()
135 .ok()
136 .and_then(|o| String::from_utf8(o.stdout).ok())
137 .map(|s| s.trim().to_string())
138 .filter(|s| !s.is_empty())
139 })
140 .unwrap_or_else(|| "unknown".to_string())
141}
142
143fn machine_id_string() -> String {
144 if let Ok(s) = std::fs::read_to_string("/etc/machine-id") {
148 return s.trim().to_string();
149 }
150 if cfg!(target_os = "macos") {
151 if let Ok(out) = std::process::Command::new("ioreg")
152 .args(["-rd1", "-c", "IOPlatformExpertDevice"])
153 .output()
154 {
155 if let Ok(s) = String::from_utf8(out.stdout) {
156 for line in s.lines() {
157 if let Some(idx) = line.find("IOPlatformUUID") {
158 if let Some(uuid) = line[idx..].split('"').nth(3) {
159 return uuid.to_string();
160 }
161 }
162 }
163 }
164 }
165 }
166 "unknown".to_string()
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn fingerprint_is_stable() {
175 let a = OrgState::fingerprint();
176 let b = OrgState::fingerprint();
177 assert_eq!(a, b);
178 assert_eq!(a.len(), 64);
179 }
180}