Skip to main content

macula_rust/node_key/
stored_identity.rs

1//! Stored identity keys, by name and profile: identity layout v1
2//! (macula#76), the key a program under one user account uses when it is
3//! given none. Keys sit one file per name and profile,
4//! `<identity_dir>/<name>.<profile>.key`, and the single `identity.key` of
5//! earlier macula releases, beside the identity directory, is moved into the
6//! layout before anything is loaded. Unix only, as key files are.
7//!
8//! The shared vectors are macula's `test/vectors/identity_layout_v1.json`,
9//! copied to `tests/vectors/identity/` and run by `tests/identity_layout.rs`.
10
11use std::path::{Path, PathBuf};
12
13use super::key_file::create_dir_owner_only;
14use super::{KeyFileError, NodeKey, Purpose};
15use crate::profile::Profile;
16
17/// The name an identity is stored under unless the program names itself.
18pub const DEFAULT_IDENTITY_NAME: &str = "default";
19
20/// The single key file of earlier macula releases, beside the identity
21/// directory.
22const OLD_KEY_FILE: &str = "identity.key";
23
24/// The longest identity name.
25const MAX_NAME_BYTES: usize = 64;
26
27/// Where identities are stored when a program names no directory: `identity`
28/// in the platform's per-user data directory, where macula's
29/// `filename:basedir(user_data, "macula")` puts it:
30/// `$XDG_DATA_HOME/macula/identity`, or `~/.local/share/macula/identity`, and
31/// on macOS `~/Library/Application Support/macula/identity`. None when HOME
32/// is needed and unset.
33pub 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
46/// The file of the identity `name` in `profile` under `dir`:
47/// `<name>.<profile>.key`. A name is 1 to 64 lowercase ASCII letters,
48/// digits, `-` and `_`, starting with a letter or digit, so it names no other
49/// directory and no other profile's key; any other is refused
50/// [`KeyFileError::IdentityName`].
51pub 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    /// The identity a program stores for `name` in `profile` under `dir`
68    /// ([`default_identity_dir`] unless it names its own), with the path it
69    /// is stored at: loaded, or generated with the admission puzzle solved
70    /// and stored there if there is none. Two first starts at once end with
71    /// one key. A key works in one profile only, so a program in both
72    /// profiles is two nodes. Log the path and the node_id: a program that
73    /// switches name or profile becomes another node, with no error.
74    ///
75    /// The old `identity.key` beside `dir` is moved first to
76    /// `default.<its profile>.key`, its profile being the one it loads in,
77    /// and is never read in place. The move only ever creates: another key
78    /// in that place is refused [`KeyFileError::OldKeyPlaceTaken`], and an
79    /// old key that will not load [`KeyFileError::OldKey`], each leaving
80    /// both files as they are. A stored key that will not load is refused
81    /// [`KeyFileError::StoredKey`] and never replaced.
82    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
99/// Moves the old single key file beside `dir`, when there is one, to
100/// `default.<its profile>.key` in `dir`: linked, then the old name removed.
101/// The same file already in place is a move cut short, and is finished.
102fn 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
120/// The profile the old key loads in, as an identity key.
121fn 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
131/// Ok when `to` holds the old key's bytes; otherwise the place is taken.
132fn 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}