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 => std::fs::hard_link(&staged, path)?,
129 }
130 Ok(())
131}
132
133pub(super) fn create_dir_owner_only(dir: &Path, recursive: bool) -> Result<(), KeyFileError> {
134 let mut builder = std::fs::DirBuilder::new();
135 builder.recursive(recursive);
136 std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700);
137 builder.create(dir)?;
138 Ok(())
139}
140
141fn sync_dir(dir: &Path) -> Result<(), KeyFileError> {
142 std::fs::File::open(dir)?.sync_all()?;
143 Ok(())
144}
145
146fn read_key_file(path: &Path) -> Result<Vec<u8>, KeyFileError> {
149 if !std::fs::metadata(path)?.is_file() {
150 return Err(KeyFileError::NotRegular);
151 }
152 let mut options = std::fs::OpenOptions::new();
153 options.read(true);
154 std::os::unix::fs::OpenOptionsExt::custom_flags(
157 &mut options,
158 rustix::fs::OFlags::NONBLOCK.bits() as i32,
159 );
160 let file = options.open(path)?;
161 owner_only(&file.metadata()?)?;
162 let mut contents = Vec::new();
163 file.take(MAX_KEY_FILE_BYTES + 1)
164 .read_to_end(&mut contents)?;
165 if contents.len() as u64 > MAX_KEY_FILE_BYTES {
166 return Err(KeyFileError::TooLarge);
167 }
168 Ok(contents)
169}
170
171fn owner_only(metadata: &std::fs::Metadata) -> Result<(), KeyFileError> {
174 if !metadata.is_file() {
175 return Err(KeyFileError::NotRegular);
176 }
177 use std::os::unix::fs::MetadataExt;
178 if metadata.uid() != rustix::process::geteuid().as_raw() {
179 return Err(KeyFileError::Owner);
180 }
181 if metadata.mode() & 0o077 != 0 {
182 return Err(KeyFileError::Permissions);
183 }
184 Ok(())
185}