macula_rust/node_key/
key_file.rs1use 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
15const MAX_KEY_FILE_BYTES: u64 = 64 * 1024;
17impl NodeKey {
18 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 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 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
106fn 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 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
131fn 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}