1use std::path::{Path, PathBuf};
12
13const LOOSE_MASK: u32 = 0o077;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct PermissionWarning {
20 pub path: PathBuf,
22 pub mode: u32,
24}
25
26impl std::fmt::Display for PermissionWarning {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(
29 f,
30 "{} is mode {:04o}, readable beyond its owner; a secret file should be 0600 (chmod 600 {})",
31 self.path.display(),
32 self.mode,
33 self.path.display()
34 )
35 }
36}
37
38#[cfg(unix)]
45pub fn check_secret_permissions(path: &Path) -> std::io::Result<Option<PermissionWarning>> {
46 use std::os::unix::fs::PermissionsExt;
47 let mode = std::fs::metadata(path)?.permissions().mode() & 0o777;
48 Ok((mode & LOOSE_MASK != 0).then(|| PermissionWarning {
49 path: path.to_owned(),
50 mode,
51 }))
52}
53
54#[cfg(not(unix))]
57pub fn check_secret_permissions(_path: &Path) -> std::io::Result<Option<PermissionWarning>> {
58 Ok(None)
59}
60
61#[cfg(all(test, unix))]
62mod tests {
63 use super::*;
64 use std::fs;
65 use std::os::unix::fs::PermissionsExt;
66
67 fn write_with_mode(dir: &Path, mode: u32) -> PathBuf {
68 let path = dir.join("secret");
69 fs::write(&path, b"shh").unwrap();
70 fs::set_permissions(&path, fs::Permissions::from_mode(mode)).unwrap();
71 path
72 }
73
74 #[test]
75 fn a_tight_file_is_not_reported() {
76 let dir = tempdir();
77 let path = write_with_mode(dir.path(), 0o600);
78 assert_eq!(check_secret_permissions(&path).unwrap(), None);
79 }
80
81 #[test]
82 fn group_readable_is_reported() {
83 let dir = tempdir();
84 let path = write_with_mode(dir.path(), 0o640);
85 let warning = check_secret_permissions(&path).unwrap().unwrap();
86 assert_eq!(warning.mode, 0o640);
87 assert!(warning.to_string().contains("chmod 600"));
88 }
89
90 #[test]
91 fn world_readable_is_reported() {
92 let dir = tempdir();
93 let path = write_with_mode(dir.path(), 0o644);
94 assert!(check_secret_permissions(&path).unwrap().is_some());
95 }
96
97 #[test]
98 fn a_missing_file_is_an_io_error_not_a_warning() {
99 let dir = tempdir();
100 let path = dir.path().join("nope");
101 assert!(check_secret_permissions(&path).is_err());
102 }
103
104 struct TempDir(PathBuf);
107 impl TempDir {
108 fn path(&self) -> &Path {
109 &self.0
110 }
111 }
112 impl Drop for TempDir {
113 fn drop(&mut self) {
114 let _ = fs::remove_dir_all(&self.0);
115 }
116 }
117 fn tempdir() -> TempDir {
118 let dir = std::env::temp_dir().join(format!(
119 "didbot-config-test-{}-{}",
120 std::process::id(),
121 std::time::SystemTime::now()
122 .duration_since(std::time::UNIX_EPOCH)
123 .unwrap()
124 .as_nanos()
125 ));
126 fs::create_dir_all(&dir).unwrap();
127 TempDir(dir)
128 }
129}