cairn_mod/
credential_file.rs1use std::fs;
16use std::io;
17use std::path::{Path, PathBuf};
18
19use thiserror::Error;
20
21#[derive(Debug, Error)]
27pub enum CredentialFileError {
28 #[error("io: {0}")]
30 Io(#[from] io::Error),
31 #[error("credential file {path} has insecure permissions (mode {mode:o}); expected 600")]
35 InsecurePermissions {
36 path: PathBuf,
38 mode: u32,
40 },
41 #[error("credential file {path} is owned by another user")]
44 ForeignOwner {
45 path: PathBuf,
47 },
48 #[error(
52 "{env} is set — key material is file-only (§5.1); unset the env var and use the file path"
53 )]
54 EnvOverrideRejected {
55 env: &'static str,
57 },
58 #[error("cairn CLI on this platform is not supported in v1; use a POSIX filesystem")]
61 UnsupportedPlatform,
62}
63
64pub fn check_mode_and_owner(path: &Path) -> Result<(), CredentialFileError> {
68 #[cfg(unix)]
69 {
70 use std::os::unix::fs::MetadataExt;
71 let meta = fs::metadata(path)?;
72 let mode = meta.mode() & 0o777;
73 check_mode(path, mode)?;
74 let current = current_uid();
75 check_owner(path, meta.uid(), current)?;
76 Ok(())
77 }
78 #[cfg(not(unix))]
79 {
80 let _ = path;
81 Err(CredentialFileError::UnsupportedPlatform)
82 }
83}
84
85pub fn reject_env_override(env: &'static str) -> Result<(), CredentialFileError> {
89 if std::env::var_os(env).is_some() {
90 Err(CredentialFileError::EnvOverrideRejected { env })
91 } else {
92 Ok(())
93 }
94}
95
96#[cfg(unix)]
97fn check_mode(path: &Path, mode: u32) -> Result<(), CredentialFileError> {
98 if mode == 0o600 {
99 Ok(())
100 } else {
101 Err(CredentialFileError::InsecurePermissions {
102 path: path.to_path_buf(),
103 mode,
104 })
105 }
106}
107
108#[cfg(unix)]
111fn check_owner(path: &Path, file_uid: u32, current_uid: u32) -> Result<(), CredentialFileError> {
112 if file_uid == current_uid {
113 Ok(())
114 } else {
115 Err(CredentialFileError::ForeignOwner {
116 path: path.to_path_buf(),
117 })
118 }
119}
120
121#[cfg(unix)]
122fn current_uid() -> u32 {
123 rustix::process::geteuid().as_raw()
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn check_owner_accepts_matching_uid() {
132 assert!(check_owner(Path::new("x"), 1000, 1000).is_ok());
133 }
134
135 #[test]
136 fn check_owner_rejects_foreign_uid() {
137 let err = check_owner(Path::new("x"), 0, 1000).unwrap_err();
138 assert!(matches!(err, CredentialFileError::ForeignOwner { .. }));
139 }
140
141 #[test]
142 fn check_mode_accepts_0600() {
143 assert!(check_mode(Path::new("x"), 0o600).is_ok());
144 }
145
146 #[test]
147 fn check_mode_rejects_wider_modes() {
148 for bad in [0o644, 0o640, 0o666, 0o700, 0o755, 0o777] {
149 let err = check_mode(Path::new("x"), bad).unwrap_err();
150 assert!(
151 matches!(
152 err,
153 CredentialFileError::InsecurePermissions { mode, .. } if mode == bad
154 ),
155 "expected InsecurePermissions for mode {bad:o}, got {err:?}"
156 );
157 }
158 }
159
160 #[test]
161 fn reject_env_override_passes_when_unset() {
162 assert!(reject_env_override("CAIRN_DOES_NOT_EXIST_FOR_TEST").is_ok());
164 }
165}