macula_rust/node_key/
stored_identity.rs1use std::path::{Path, PathBuf};
12
13use super::key_file::create_dir_owner_only;
14use super::{KeyFileError, NodeKey, Purpose};
15use crate::profile::Profile;
16
17pub const DEFAULT_IDENTITY_NAME: &str = "default";
19
20const OLD_KEY_FILE: &str = "identity.key";
23
24const MAX_NAME_BYTES: usize = 64;
26
27pub fn default_identity_dir() -> Option<PathBuf> {
34 let set = |var: &str| std::env::var_os(var).filter(|v| !v.is_empty());
35 let home = || set("HOME").map(PathBuf::from);
36 let data = if cfg!(target_os = "macos") {
37 home()?.join("Library/Application Support")
38 } else {
39 set("XDG_DATA_HOME")
40 .map(PathBuf::from)
41 .or_else(|| home().map(|h| h.join(".local/share")))?
42 };
43 Some(data.join("macula").join("identity"))
44}
45
46pub fn identity_path(dir: &Path, name: &str, profile: Profile) -> Result<PathBuf, KeyFileError> {
52 if !valid_name(name) {
53 return Err(KeyFileError::IdentityName(name.to_string()));
54 }
55 Ok(dir.join(format!("{name}.{}.key", profile.name())))
56}
57
58fn valid_name(name: &str) -> bool {
59 let named = |c: &u8| c.is_ascii_lowercase() || c.is_ascii_digit();
60 let bytes = name.as_bytes();
61 bytes.first().is_some_and(named)
62 && bytes.len() <= MAX_NAME_BYTES
63 && bytes.iter().all(|c| named(c) || *c == b'-' || *c == b'_')
64}
65
66impl NodeKey {
67 pub fn stored_identity(
83 dir: &Path,
84 name: &str,
85 profile: Profile,
86 ) -> Result<(NodeKey, PathBuf), KeyFileError> {
87 let path = identity_path(dir, name, profile)?;
88 move_old_key(dir)?;
89 match NodeKey::load_or_create(&path, profile) {
90 Ok(key) => Ok((key, path)),
91 Err(reason) => Err(KeyFileError::StoredKey {
92 path,
93 reason: Box::new(reason),
94 }),
95 }
96 }
97}
98
99fn move_old_key(dir: &Path) -> Result<(), KeyFileError> {
103 let old = dir.parent().unwrap_or(Path::new("")).join(OLD_KEY_FILE);
104 match std::fs::symlink_metadata(&old) {
105 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
106 Err(e) => return Err(old_key(&old, e.into())),
107 Ok(_) => {}
108 }
109 let profile = old_key_profile(&old).map_err(|e| old_key(&old, e))?;
110 let to = identity_path(dir, DEFAULT_IDENTITY_NAME, profile)?;
111 create_dir_owner_only(dir, true).map_err(|e| old_key(&old, e))?;
112 match std::fs::hard_link(&old, &to) {
113 Ok(()) => {}
114 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => same_key(&old, &to)?,
115 Err(e) => return Err(old_key(&old, e.into())),
116 }
117 std::fs::remove_file(&old).map_err(|e| old_key(&old, e.into()))
118}
119
120fn old_key_profile(old: &Path) -> Result<Profile, KeyFileError> {
122 match NodeKey::load(old, Purpose::Identity, Profile::PqPure) {
123 Ok(_) => Ok(Profile::PqPure),
124 Err(KeyFileError::WrongProfile(found)) => {
125 NodeKey::load(old, Purpose::Identity, found).map(|_| found)
126 }
127 Err(e) => Err(e),
128 }
129}
130
131fn same_key(old: &Path, to: &Path) -> Result<(), KeyFileError> {
133 let read = |p: &Path| std::fs::read(p).map_err(|e| old_key(old, e.into()));
134 if read(old)? == read(to)? {
135 return Ok(());
136 }
137 Err(KeyFileError::OldKeyPlaceTaken {
138 from: old.to_path_buf(),
139 to: to.to_path_buf(),
140 })
141}
142
143fn old_key(from: &Path, reason: KeyFileError) -> KeyFileError {
144 KeyFileError::OldKey {
145 from: from.to_path_buf(),
146 reason: Box::new(reason),
147 }
148}