lean_ctx/core/
secure_fs.rs1pub(crate) fn write_secret_file(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> {
8 use std::io::Write;
9
10 let parent = path
11 .parent()
12 .ok_or_else(|| std::io::Error::other("secret path has no parent directory"))?;
13 let name = path
14 .file_name()
15 .ok_or_else(|| std::io::Error::other("secret path has no file name"))?
16 .to_string_lossy();
17 let mut nonce = [0u8; 8];
20 getrandom::fill(&mut nonce).map_err(std::io::Error::other)?;
21 let tmp = parent.join(format!(
22 ".{name}.tmp.{}.{}",
23 std::process::id(),
24 hex::encode(nonce)
25 ));
26
27 let mut opts = std::fs::OpenOptions::new();
28 opts.write(true).create_new(true);
29 #[cfg(unix)]
30 {
31 use std::os::unix::fs::OpenOptionsExt;
32 opts.mode(0o600);
33 }
34
35 let result = (|| {
36 let mut f = opts.open(&tmp)?;
37 f.write_all(bytes)?;
38 f.sync_all()?;
39 drop(f);
40 crate::core::atomic_fs::replace_file(&tmp, path)
44 })();
45
46 if result.is_err() {
47 let _ = std::fs::remove_file(&tmp);
48 }
49 result
50}
51
52pub fn private_dir(dir: &std::path::Path) -> std::io::Result<()> {
55 let step = |what: &str, e: std::io::Error| {
56 std::io::Error::new(e.kind(), format!("{what} {}: {e}", dir.display()))
57 };
58 std::fs::create_dir_all(dir).map_err(|e| step("create", e))?;
59 restrict_to_owner(dir).map_err(|e| step("restrict", e))
60}
61
62pub fn restrict_to_owner(path: &std::path::Path) -> std::io::Result<()> {
69 #[cfg(unix)]
70 {
71 use std::os::unix::fs::PermissionsExt as _;
72 if std::fs::metadata(path)?.permissions().mode() & 0o7777 != 0o700 {
73 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
74 }
75 }
76 #[cfg(not(unix))]
77 let _ = path;
78 Ok(())
79}
80
81pub fn write_private(dir: &std::path::Path, name: &str, bytes: &[u8]) -> std::io::Result<()> {
84 private_dir(dir)?;
85 write_secret_file(&dir.join(name), bytes).map_err(|e| {
86 std::io::Error::new(e.kind(), format!("write {name} in {}: {e}", dir.display()))
87 })
88}
89
90#[cfg(test)]
91mod tests {
92 use super::write_secret_file;
93
94 #[test]
95 fn concurrent_writers_of_one_path_all_succeed_and_leave_one_whole_file() {
96 let dir = tempfile::tempdir().unwrap();
97 let path = dir.path().join("secret.json");
98 let writers: Vec<_> = (0..8u8)
99 .map(|i| {
100 let path = path.clone();
101 std::thread::spawn(move || write_secret_file(&path, &[i; 64]))
102 })
103 .collect();
104 for writer in writers {
105 writer.join().unwrap().unwrap();
106 }
107 let written = std::fs::read(&path).unwrap();
108 assert_eq!(written.len(), 64);
109 assert!(written.iter().all(|byte| *byte == written[0]));
110 let names: Vec<_> = std::fs::read_dir(dir.path())
111 .unwrap()
112 .map(|entry| entry.unwrap().file_name())
113 .collect();
114 assert_eq!(names, ["secret.json"], "no temporary file is left");
115 #[cfg(unix)]
116 {
117 use std::os::unix::fs::PermissionsExt;
118 let mode = std::fs::metadata(&path).unwrap().permissions().mode();
119 assert_eq!(mode & 0o777, 0o600);
120 }
121 }
122
123 #[cfg(unix)]
126 #[test]
127 fn restrict_to_owner_leaves_a_private_dir_untouched() {
128 use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
129 let tmp = tempfile::tempdir().unwrap();
130 let dir = tmp.path().join("data");
131 std::fs::create_dir(&dir).unwrap();
132 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
133 let ctime = |p: &std::path::Path| {
134 let m = std::fs::metadata(p).unwrap();
135 (m.ctime(), m.ctime_nsec())
136 };
137 let before = ctime(&dir);
138 std::thread::sleep(std::time::Duration::from_millis(1100));
140 super::restrict_to_owner(&dir).unwrap();
141 super::private_dir(&dir).unwrap();
142 crate::core::data_dir::ensure_dir_permissions(&dir);
143 assert_eq!(
144 ctime(&dir),
145 before,
146 "an already private dir is not chmodded"
147 );
148 }
149
150 #[cfg(unix)]
153 #[test]
154 fn restrict_to_owner_still_tightens_an_open_dir() {
155 use std::os::unix::fs::PermissionsExt as _;
156 let tmp = tempfile::tempdir().unwrap();
157 let mode =
158 |p: &std::path::Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o7777;
159 for (i, restrict) in [
160 (|p: &std::path::Path| super::restrict_to_owner(p).unwrap()) as fn(&std::path::Path),
161 |p| super::private_dir(p).unwrap(),
162 |p| crate::core::data_dir::ensure_dir_permissions(p),
163 ]
164 .into_iter()
165 .enumerate()
166 {
167 let dir = tmp.path().join(format!("open-{i}"));
168 std::fs::create_dir(&dir).unwrap();
169 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap();
170 restrict(&dir);
171 assert_eq!(mode(&dir), 0o700);
172 }
173 }
174}