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 self.place(path, Place::Replace)
25 }
26
27 fn save_new(&self, path: &Path) -> Result<(), KeyFileError> {
31 self.place(path, Place::CreateNew)
32 }
33
34 fn place(&self, path: &Path, place: Place) -> Result<(), KeyFileError> {
35 let dir = match path.parent() {
36 Some(d) if !d.as_os_str().is_empty() => d,
37 _ => Path::new("."),
38 };
39 create_dir_owner_only(dir, true)?;
40 let base = path
41 .file_name()
42 .ok_or(KeyFileError::NotRegular)?
43 .to_string_lossy();
44 let staging = dir.join(format!(".{base}.saving-{}", random_suffix()?));
45 create_dir_owner_only(&staging, false)?;
46 let result =
47 write_staged(&staging, path, &self.file_bytes()?, place).and_then(|()| sync_dir(dir));
48 let removed = std::fs::remove_dir_all(&staging);
49 result?;
50 removed.map_err(KeyFileError::from)
51 }
52
53 pub fn load(path: &Path, purpose: Purpose, profile: Profile) -> Result<NodeKey, KeyFileError> {
62 let contents = read_key_file(path)?;
63 let key = parse(&contents, purpose, profile)?;
64 round_trip(&key)?;
65 Ok(key)
66 }
67
68 pub fn load_or_create(path: &Path, profile: Profile) -> Result<NodeKey, KeyFileError> {
74 match std::fs::symlink_metadata(path) {
75 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
76 let key = NodeKey::generate_identity(profile, super::PUZZLE_DIFFICULTY)
77 .map_err(KeyFileError::Generate)?;
78 stored_first(key, path, profile)
79 }
80 _ => NodeKey::load(path, Purpose::Identity, profile),
81 }
82 }
83}
84
85fn stored_first(key: NodeKey, path: &Path, profile: Profile) -> Result<NodeKey, KeyFileError> {
88 match key.save_new(path) {
89 Ok(()) => Ok(key),
90 Err(KeyFileError::Io(e)) if e.kind() == std::io::ErrorKind::AlreadyExists => {
91 NodeKey::load(path, Purpose::Identity, profile)
92 }
93 Err(e) => Err(e),
94 }
95}
96
97fn random_suffix() -> Result<String, KeyFileError> {
98 let mut bytes = [0u8; 8];
99 aws_lc_rs::rand::fill(&mut bytes)
100 .map_err(|_| KeyFileError::Io(std::io::Error::other("no randomness")))?;
101 Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
102}
103
104#[derive(Clone, Copy)]
107enum Place {
108 Replace,
109 CreateNew,
110}
111
112fn write_staged(
113 staging: &Path,
114 path: &Path,
115 contents: &[u8],
116 place: Place,
117) -> Result<(), KeyFileError> {
118 let staged = staging.join("key");
119 let mut options = std::fs::OpenOptions::new();
120 options.write(true).create_new(true);
121 std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
122 let mut file = options.open(&staged)?;
123 file.write_all(contents)?;
124 file.sync_all()?;
125 drop(file);
126 match place {
127 Place::Replace => std::fs::rename(&staged, path)?,
128 Place::CreateNew => move_new(&staged, path)?,
129 }
130 Ok(())
131}
132
133pub(super) fn move_new(from: &Path, to: &Path) -> std::io::Result<()> {
139 match rename_no_replace(from, to) {
140 Err(e) if no_such_rename(&e) => {
141 std::fs::hard_link(from, to)?;
142 std::fs::remove_file(from)
143 }
144 moved => moved,
145 }
146}
147
148#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
149fn rename_no_replace(from: &Path, to: &Path) -> std::io::Result<()> {
150 use rustix::fs::{renameat_with, RenameFlags, CWD};
151 renameat_with(CWD, from, CWD, to, RenameFlags::NOREPLACE).map_err(std::io::Error::from)
152}
153
154#[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
155fn rename_no_replace(_from: &Path, _to: &Path) -> std::io::Result<()> {
156 Err(rustix::io::Errno::NOSYS.into())
157}
158
159fn no_such_rename(e: &std::io::Error) -> bool {
161 use rustix::io::Errno;
162 [Errno::INVAL, Errno::NOSYS, Errno::OPNOTSUPP]
163 .iter()
164 .any(|errno| e.raw_os_error() == Some(errno.raw_os_error()))
165}
166
167pub(super) fn create_dir_owner_only(dir: &Path, recursive: bool) -> Result<(), KeyFileError> {
168 let mut builder = std::fs::DirBuilder::new();
169 builder.recursive(recursive);
170 std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700);
171 builder.create(dir)?;
172 Ok(())
173}
174
175fn sync_dir(dir: &Path) -> Result<(), KeyFileError> {
176 std::fs::File::open(dir)?.sync_all()?;
177 Ok(())
178}
179
180fn read_key_file(path: &Path) -> Result<Vec<u8>, KeyFileError> {
183 if !std::fs::metadata(path)?.is_file() {
184 return Err(KeyFileError::NotRegular);
185 }
186 let mut options = std::fs::OpenOptions::new();
187 options.read(true);
188 std::os::unix::fs::OpenOptionsExt::custom_flags(
191 &mut options,
192 rustix::fs::OFlags::NONBLOCK.bits() as i32,
193 );
194 let file = options.open(path)?;
195 owner_only(&file.metadata()?)?;
196 let mut contents = Vec::new();
197 file.take(MAX_KEY_FILE_BYTES + 1)
198 .read_to_end(&mut contents)?;
199 if contents.len() as u64 > MAX_KEY_FILE_BYTES {
200 return Err(KeyFileError::TooLarge);
201 }
202 Ok(contents)
203}
204
205fn owner_only(metadata: &std::fs::Metadata) -> Result<(), KeyFileError> {
208 if !metadata.is_file() {
209 return Err(KeyFileError::NotRegular);
210 }
211 use std::os::unix::fs::MetadataExt;
212 if metadata.uid() != rustix::process::geteuid().as_raw() {
213 return Err(KeyFileError::Owner);
214 }
215 if metadata.mode() & 0o077 != 0 {
216 return Err(KeyFileError::Permissions);
217 }
218 Ok(())
219}