1use std::path::Path;
16
17use crate::error::{Error, Result};
18use data_encoding::HEXLOWER;
19use rpassword::prompt_password;
20use std::fs::File;
21
22use zeroize::Zeroizing;
23
24#[cfg(any(target_os = "linux", target_os = "android"))]
25use linux_keyutils::Key;
26
27pub fn read_password(
35 prompt: &str,
36 key_id: Option<i32>,
37 _tty: Option<&File>,
38) -> Result<Zeroizing<Vec<u8>>> {
39 #[cfg(any(target_os = "linux", target_os = "android"))]
40 if let Some(id) = key_id {
41 let key = Key::from_id(linux_keyutils::KeySerialId(id));
43
44 let mut buf = Zeroizing::new(vec![0u8; 1024]);
49 let len = key.read(&mut buf).map_err(Error::Keyring)?;
50
51 if len > buf.len() {
52 return Err(Error::InvalidKeyLength);
53 }
54
55 buf.truncate(len);
56 return Ok(buf);
57 }
58
59 #[cfg(not(any(target_os = "linux", target_os = "android")))]
60 if key_id.is_some() {
61 return Err(Error::KeyringDisabled);
62 }
63
64 #[cfg(unix)]
65 if let Some(tty) = _tty {
66 return read_password_unix(prompt, tty);
67 }
68
69 let pass = prompt_password(prompt).map_err(|_e| Error::PasswordReadFailed)?;
71 Ok(Zeroizing::new(pass.into_bytes()))
72}
73
74#[cfg(unix)]
75fn read_password_unix<Fd: std::os::fd::AsFd>(prompt: &str, tty: Fd) -> Result<Zeroizing<Vec<u8>>> {
76 use nix::errno::Errno;
77 use nix::sys::termios::{tcgetattr, tcsetattr, LocalFlags, SetArg};
78 use nix::unistd::{read, write};
79
80 let oterm = tcgetattr(&tty)?;
81 let mut nterm = oterm.clone();
82
83 nterm.local_flags.remove(LocalFlags::ECHO);
85 nterm.local_flags.insert(LocalFlags::ECHONL);
87 tcsetattr(&tty, SetArg::TCSANOW, &nterm)?;
88
89 let mut nwrite = 0;
91 let bytes = prompt.as_bytes();
92 while nwrite < bytes.len() {
93 match write(&tty, &bytes[nwrite..]) {
94 Ok(n) => nwrite = nwrite.checked_add(n).ok_or(Error::Overflow)?,
95 Err(Errno::EINTR) => {}
96 Err(errno) => {
97 let _ = tcsetattr(&tty, SetArg::TCSANOW, &oterm);
99 return Err(errno.into());
100 }
101 }
102 }
103
104 let mut buf = Zeroizing::new([0u8; 1]);
106 let mut pass = Zeroizing::new(Vec::with_capacity(128));
107 loop {
108 match read(tty.as_fd(), buf.as_mut()) {
109 Ok(0) => break, Ok(_) => {
111 let c = buf[0];
112 if matches!(c, b'\n' | b'\r') {
113 break;
114 }
115 pass.push(c);
116 }
117 Err(Errno::EINTR) => {}
118 Err(errno) => {
119 let _ = tcsetattr(&tty, SetArg::TCSANOW, &oterm);
121 return Err(errno.into());
122 }
123 }
124 }
125
126 let _ = tcsetattr(&tty, SetArg::TCSANOW, &oterm);
128
129 Ok(pass)
130}
131
132#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
142pub fn check_password_strength(password: &[u8]) -> Result<()> {
143 let password = std::str::from_utf8(password).or(Err(Error::InvalidPasswordUtf8))?;
144
145 let entropy = zxcvbn::zxcvbn(password, &[]);
146 if entropy.score() < zxcvbn::Score::Four {
147 let feedback = entropy.feedback().map(|feedback| feedback.to_string());
148 return Err(Error::WeakPassword(feedback));
149 }
150
151 Ok(())
152}
153
154#[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))]
156pub fn check_password_strength(password: &[u8]) -> Result<()> {
157 let password = std::str::from_utf8(password).or(Err(Error::InvalidPasswordUtf8))?;
158
159 let strength = password_strength::estimate_strength(password);
160 if strength < 0.8 {
161 let feedback = if strength < 0.3 {
162 Some("very weak - use a longer passphrase with mixed characters".to_string())
163 } else if strength < 0.6 {
164 Some("weak - add more characters and avoid common patterns".to_string())
165 } else {
166 Some("moderate - add uncommon words or symbols".to_string())
167 };
168 return Err(Error::WeakPassword(feedback));
169 }
170
171 Ok(())
172}
173
174pub fn check_keyname_compliance(pubkey_path: Option<&Path>, seckey_path: &Path) -> Result<String> {
183 if !seckey_path
184 .extension()
185 .is_some_and(|ext| ext.eq_ignore_ascii_case("sec"))
186 {
187 return Err(Error::InvalidKeyName);
188 }
189
190 let seckey_stem = seckey_path.file_stem().ok_or(Error::InvalidPath)?;
191
192 if let Some(pk_path) = pubkey_path {
193 if !pk_path
194 .extension()
195 .is_some_and(|ext| ext.eq_ignore_ascii_case("pub"))
196 {
197 return Err(Error::InvalidKeyName);
198 }
199
200 let pubkey_stem = pk_path.file_stem().ok_or(Error::InvalidPath)?;
201
202 if seckey_stem != pubkey_stem {
203 return Err(Error::InvalidKeyName);
204 }
205 }
206
207 seckey_stem
208 .to_str()
209 .map(ToOwned::to_owned)
210 .ok_or(Error::InvalidPath)
211}
212
213pub fn log_untrusted_buf(buf: &[u8]) -> String {
215 if contains_ascii_unprintable(buf) {
216 HEXLOWER.encode(buf)
217 } else if let Ok(s) = std::str::from_utf8(buf) {
218 s.to_string()
219 } else {
220 HEXLOWER.encode(buf)
221 }
222}
223
224fn contains_ascii_unprintable(buf: &[u8]) -> bool {
226 buf.iter().any(|byte| !is_ascii_printable(*byte))
227}
228
229fn is_ascii_printable(byte: u8) -> bool {
231 (0x20..=0x7e).contains(&byte)
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use std::path::PathBuf;
238
239 #[test]
240 fn test_compliance_seckey_only() -> std::result::Result<(), Box<dyn std::error::Error>> {
241 let sec = PathBuf::from("test.sec");
242 assert_eq!(check_keyname_compliance(None, &sec)?, "test");
243
244 let sec = PathBuf::from("foo/bar/baz.sec");
245 assert_eq!(check_keyname_compliance(None, &sec)?, "baz");
246 Ok(())
247 }
248
249 #[test]
250 fn test_compliance_invalid_seckey() {
251 let sec = PathBuf::from("test.key");
252 check_keyname_compliance(None, &sec).unwrap_err();
253
254 let sec = PathBuf::from("test");
255 check_keyname_compliance(None, &sec).unwrap_err();
256 }
257
258 #[test]
259 fn test_compliance_pair_match() -> std::result::Result<(), Box<dyn std::error::Error>> {
260 let sec = PathBuf::from("test.sec");
261 let pubk = PathBuf::from("test.pub");
262 assert_eq!(check_keyname_compliance(Some(&pubk), &sec)?, "test");
263 Ok(())
264 }
265
266 #[test]
267 fn test_compliance_pair_mismatch() {
268 let sec = PathBuf::from("test.sec");
269 let pubk = PathBuf::from("other.pub");
270 check_keyname_compliance(Some(&pubk), &sec).unwrap_err();
271 }
272
273 #[test]
274 fn test_compliance_invalid_pubkey() {
275 let sec = PathBuf::from("test.sec");
276 let pubk = PathBuf::from("test.key");
277 check_keyname_compliance(Some(&pubk), &sec).unwrap_err();
278 }
279
280 #[test]
281 fn test_compliance_invalid_path() {
282 let sec = PathBuf::from("foo/bar/..");
283 assert!(matches!(
284 check_keyname_compliance(None, &sec),
285 Err(Error::InvalidKeyName) | Err(Error::InvalidPath)
286 ));
287 }
288
289 #[test]
290 fn test_log_untrusted_buf() {
291 let buf = b"hello\x00world";
292 assert_eq!(log_untrusted_buf(buf), "68656c6c6f00776f726c64");
293
294 let buf = b"hello world";
295 assert_eq!(log_untrusted_buf(buf), "hello world");
296 }
297
298 #[test]
299 fn test_password_strength_invalid_utf8() {
300 let password = b"\xff\xfe";
301 assert!(matches!(
302 check_password_strength(password),
303 Err(Error::InvalidPasswordUtf8)
304 ));
305 }
306
307 #[test]
308 fn test_password_strength_weak() {
309 let password = b"sekrit";
310 assert!(matches!(
311 check_password_strength(password),
312 Err(Error::WeakPassword(_))
313 ));
314 }
315
316 #[test]
317 fn test_password_strength_strong() {
318 let password = b"Shine-On-You-Crazy-Diamond-1975!";
319 assert!(check_password_strength(password).is_ok());
320 }
321}