Skip to main content

macula_rust/node_key/
key_file.rs

1//! Key files in the seed form (`seed_form`), readable by their owner only:
2//! written owner-only (0o600 in a 0o700 directory) and refused on load unless
3//! the effective user owns the file and its group and others cannot read it.
4//! Unix only. On Windows a node key is kept in Credential Manager instead, and
5//! these calls refuse with [`KeyFileError::NoKeyFile`] (`no_key_file`,
6//! macula-rust#19).
7
8use std::io::{Read, Write};
9use std::path::Path;
10
11use super::seed_form::{parse, round_trip};
12use super::{KeyFileError, NodeKey, Purpose};
13use crate::profile::Profile;
14
15/// The most a load reads: a key file is a few KiB.
16const MAX_KEY_FILE_BYTES: u64 = 64 * 1024;
17impl NodeKey {
18    /// Writes the key to `path` in the seed form, readable by its owner only.
19    /// The file is created in a new owner-only directory beside `path`,
20    /// written, synced and renamed over any file at `path`; then `path`'s
21    /// directory is synced and the new one removed. Nothing else in the
22    /// directory is read, written or removed.
23    pub fn save(&self, path: &Path) -> Result<(), KeyFileError> {
24        let dir = match path.parent() {
25            Some(d) if !d.as_os_str().is_empty() => d,
26            _ => Path::new("."),
27        };
28        create_dir_owner_only(dir, true)?;
29        let base = path
30            .file_name()
31            .ok_or(KeyFileError::NotRegular)?
32            .to_string_lossy();
33        let staging = dir.join(format!(".{base}.saving-{}", random_suffix()?));
34        create_dir_owner_only(&staging, false)?;
35        let result = write_staged(&staging, path, &self.file_bytes()?).and_then(|()| sync_dir(dir));
36        let removed = std::fs::remove_dir_all(&staging);
37        result?;
38        removed.map_err(KeyFileError::from)
39    }
40
41    /// The key saved at `path` for `purpose` in `profile`, checked before it
42    /// is returned. A path that names anything but a regular file, directly
43    /// or through a symlink, is refused before it is opened, and the opened
44    /// file is checked again: a regular file, owned by the effective user,
45    /// that its group and others cannot read, of at most 64 KiB. Then a key
46    /// for another purpose or profile, halves that do not fit the profile, a
47    /// stored public key its private key does not derive, and a key that
48    /// fails a sign-and-verify round trip are refused.
49    pub fn load(path: &Path, purpose: Purpose, profile: Profile) -> Result<NodeKey, KeyFileError> {
50        let contents = read_key_file(path)?;
51        let key = parse(&contents, purpose, profile)?;
52        round_trip(&key)?;
53        Ok(key)
54    }
55
56    /// The identity key at `path` in `profile`, or, when nothing is there, a
57    /// new one with the admission puzzle solved, saved there first. Anything
58    /// at `path` that does not load as such a key is refused and left as it
59    /// is, never replaced.
60    pub fn load_or_create(path: &Path, profile: Profile) -> Result<NodeKey, KeyFileError> {
61        match std::fs::symlink_metadata(path) {
62            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
63                let key = NodeKey::generate_identity(profile, super::PUZZLE_DIFFICULTY)
64                    .map_err(KeyFileError::Generate)?;
65                key.save(path)?;
66                Ok(key)
67            }
68            _ => NodeKey::load(path, Purpose::Identity, profile),
69        }
70    }
71}
72
73fn random_suffix() -> Result<String, KeyFileError> {
74    let mut bytes = [0u8; 8];
75    aws_lc_rs::rand::fill(&mut bytes)
76        .map_err(|_| KeyFileError::Io(std::io::Error::other("no randomness")))?;
77    Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
78}
79
80fn write_staged(staging: &Path, path: &Path, contents: &[u8]) -> Result<(), KeyFileError> {
81    let staged = staging.join("key");
82    let mut options = std::fs::OpenOptions::new();
83    options.write(true).create_new(true);
84    std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
85    let mut file = options.open(&staged)?;
86    file.write_all(contents)?;
87    file.sync_all()?;
88    drop(file);
89    std::fs::rename(&staged, path)?;
90    Ok(())
91}
92
93fn create_dir_owner_only(dir: &Path, recursive: bool) -> Result<(), KeyFileError> {
94    let mut builder = std::fs::DirBuilder::new();
95    builder.recursive(recursive);
96    std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700);
97    builder.create(dir)?;
98    Ok(())
99}
100
101fn sync_dir(dir: &Path) -> Result<(), KeyFileError> {
102    std::fs::File::open(dir)?.sync_all()?;
103    Ok(())
104}
105
106/// The contents of the key file at `path`, read only once the path names a
107/// regular file and the opened file passes [`owner_only`].
108fn read_key_file(path: &Path) -> Result<Vec<u8>, KeyFileError> {
109    if !std::fs::metadata(path)?.is_file() {
110        return Err(KeyFileError::NotRegular);
111    }
112    let mut options = std::fs::OpenOptions::new();
113    options.read(true);
114    // Without waiting, should the path have become a FIFO since it was
115    // checked.
116    std::os::unix::fs::OpenOptionsExt::custom_flags(
117        &mut options,
118        rustix::fs::OFlags::NONBLOCK.bits() as i32,
119    );
120    let file = options.open(path)?;
121    owner_only(&file.metadata()?)?;
122    let mut contents = Vec::new();
123    file.take(MAX_KEY_FILE_BYTES + 1)
124        .read_to_end(&mut contents)?;
125    if contents.len() as u64 > MAX_KEY_FILE_BYTES {
126        return Err(KeyFileError::TooLarge);
127    }
128    Ok(contents)
129}
130
131/// Refuses an opened key file that is not a regular file, not the effective
132/// user's, or readable by its group or others.
133fn owner_only(metadata: &std::fs::Metadata) -> Result<(), KeyFileError> {
134    if !metadata.is_file() {
135        return Err(KeyFileError::NotRegular);
136    }
137    use std::os::unix::fs::MetadataExt;
138    if metadata.uid() != rustix::process::geteuid().as_raw() {
139        return Err(KeyFileError::Owner);
140    }
141    if metadata.mode() & 0o077 != 0 {
142        return Err(KeyFileError::Permissions);
143    }
144    Ok(())
145}