use std::path::PathBuf;
use anyhow::{anyhow, Context};
use serde::{Deserialize, Serialize};
pub const ORG_STATE_FILE: &str = "orgmode.json";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrgState {
pub smartflow_url: String,
pub vkey: String,
pub device_id: String,
pub policy_group: String,
#[serde(default)]
pub owner_email: Option<String>,
pub enrolled_at: String,
pub platform: String,
pub device_name: String,
pub device_fingerprint: String,
}
impl OrgState {
pub fn default_path() -> anyhow::Result<PathBuf> {
let dir = if let Ok(custom) = std::env::var("APERION_SHIELD_HOME") {
PathBuf::from(custom)
} else {
let mut home = dirs::home_dir()
.ok_or_else(|| anyhow!("could not resolve home directory"))?;
home.push(".aperion-shield");
home
};
std::fs::create_dir_all(&dir).context("create ~/.aperion-shield/")?;
Ok(dir.join(ORG_STATE_FILE))
}
pub fn load() -> anyhow::Result<Option<Self>> {
let path = Self::default_path()?;
if !path.exists() {
return Ok(None);
}
let raw = std::fs::read_to_string(&path)
.with_context(|| format!("read {}", path.display()))?;
let state: OrgState =
serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))?;
Ok(Some(state))
}
pub fn save(&self) -> anyhow::Result<()> {
let path = Self::default_path()?;
let tmp = path.with_extension("json.tmp");
let json = serde_json::to_string_pretty(self)?;
std::fs::write(&tmp, json).with_context(|| format!("write {}", tmp.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&tmp)?.permissions();
perms.set_mode(0o600);
std::fs::set_permissions(&tmp, perms)?;
}
std::fs::rename(&tmp, &path).with_context(|| format!("rename {}", path.display()))?;
Ok(())
}
pub fn remove() -> anyhow::Result<()> {
let path = Self::default_path()?;
if path.exists() {
std::fs::remove_file(&path)
.with_context(|| format!("remove {}", path.display()))?;
}
Ok(())
}
pub fn fingerprint() -> String {
use sha2::{Digest, Sha256};
let hostname = hostname_string();
let os = std::env::consts::OS.to_string();
let machine_id = machine_id_string();
let mut hasher = Sha256::new();
hasher.update(format!("{}|{}|{}", hostname, os, machine_id).as_bytes());
hex::encode(hasher.finalize())
}
}
fn hostname_string() -> String {
std::env::var("HOSTNAME")
.ok()
.or_else(|| std::env::var("COMPUTERNAME").ok())
.or_else(|| {
std::process::Command::new("hostname")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
})
.unwrap_or_else(|| "unknown".to_string())
}
fn machine_id_string() -> String {
if let Ok(s) = std::fs::read_to_string("/etc/machine-id") {
return s.trim().to_string();
}
if cfg!(target_os = "macos") {
if let Ok(out) = std::process::Command::new("ioreg")
.args(["-rd1", "-c", "IOPlatformExpertDevice"])
.output()
{
if let Ok(s) = String::from_utf8(out.stdout) {
for line in s.lines() {
if let Some(idx) = line.find("IOPlatformUUID") {
if let Some(uuid) = line[idx..].split('"').nth(3) {
return uuid.to_string();
}
}
}
}
}
}
"unknown".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fingerprint_is_stable() {
let a = OrgState::fingerprint();
let b = OrgState::fingerprint();
assert_eq!(a, b);
assert_eq!(a.len(), 64);
}
}