rightkit_daemon/
private.rs1use rightkit_child_process::{OwnedChild, OwnedCommand};
26use std::io::{self, BufRead, Read, Write};
27use std::process::Stdio;
28use subtle::ConstantTimeEq;
29
30pub const SECRET_BYTES: usize = 32;
32pub const SECRET_HEX_LEN: usize = SECRET_BYTES * 2;
34
35#[derive(Debug)]
37pub enum SecretError {
38 Random,
40 Malformed,
42 Missing,
44 Io(io::Error),
46}
47
48impl std::fmt::Display for SecretError {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 SecretError::Random => f.write_str("the OS random source failed"),
52 SecretError::Malformed => {
53 write!(
54 f,
55 "the secret must be {SECRET_HEX_LEN} lowercase hex digits"
56 )
57 }
58 SecretError::Missing => f.write_str("stdin closed before a secret arrived"),
59 SecretError::Io(e) => write!(f, "reading the secret: {e}"),
60 }
61 }
62}
63impl std::error::Error for SecretError {}
64
65#[derive(Clone)]
67pub struct PrivateSecret {
68 hex: [u8; SECRET_HEX_LEN],
70}
71
72impl std::fmt::Debug for PrivateSecret {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.write_str("PrivateSecret(<redacted>)")
75 }
76}
77
78impl Drop for PrivateSecret {
79 fn drop(&mut self) {
80 for byte in self.hex.iter_mut() {
81 unsafe { std::ptr::write_volatile(byte, 0) };
83 }
84 std::sync::atomic::compiler_fence(std::sync::atomic::Ordering::SeqCst);
85 }
86}
87
88fn hex_digit(nibble: u8) -> u8 {
89 b"0123456789abcdef"[usize::from(nibble & 0x0f)]
90}
91
92impl PrivateSecret {
93 pub fn generate() -> Result<Self, SecretError> {
95 let mut raw = [0u8; SECRET_BYTES];
96 getrandom::fill(&mut raw).map_err(|_| SecretError::Random)?;
97 let mut hex = [0u8; SECRET_HEX_LEN];
98 for (index, byte) in raw.iter().enumerate() {
99 hex[index * 2] = hex_digit(byte >> 4);
100 hex[index * 2 + 1] = hex_digit(*byte);
101 }
102 for byte in raw.iter_mut() {
103 unsafe { std::ptr::write_volatile(byte, 0) };
105 }
106 Ok(Self { hex })
107 }
108
109 pub fn from_hex(text: &str) -> Result<Self, SecretError> {
112 let bytes = text.as_bytes();
113 if bytes.len() != SECRET_HEX_LEN
114 || !bytes
115 .iter()
116 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(b))
117 {
118 return Err(SecretError::Malformed);
119 }
120 let mut hex = [0u8; SECRET_HEX_LEN];
121 hex.copy_from_slice(bytes);
122 Ok(Self { hex })
123 }
124
125 pub fn expose(&self) -> &str {
127 std::str::from_utf8(&self.hex).unwrap_or("")
129 }
130
131 pub fn verify(&self, presented: &str) -> bool {
135 let presented = presented.as_bytes();
136 let mut padded = [0u8; SECRET_HEX_LEN];
137 let take = presented.len().min(SECRET_HEX_LEN);
138 padded[..take].copy_from_slice(&presented[..take]);
139 let same = self.hex[..].ct_eq(&padded[..]);
140 let length_ok = (presented.len() as u64).ct_eq(&(SECRET_HEX_LEN as u64));
141 bool::from(same & length_ok)
142 }
143
144 pub fn read_from(reader: &mut impl BufRead) -> Result<Self, SecretError> {
147 let mut line = String::new();
148 let read = (&mut *reader)
149 .take((SECRET_HEX_LEN + 2) as u64)
150 .read_line(&mut line)
151 .map_err(SecretError::Io)?;
152 if read == 0 {
153 return Err(SecretError::Missing);
154 }
155 let result = Self::from_hex(line.trim_end_matches(['\n', '\r']));
156 unsafe {
159 for byte in line.as_bytes_mut() {
160 std::ptr::write_volatile(byte, b'0');
161 }
162 }
163 result
164 }
165
166 pub fn read_from_stdin() -> Result<Self, SecretError> {
169 Self::read_from(&mut io::stdin().lock())
170 }
171}
172
173pub fn watch_reader_eof<R, F>(mut reader: R, on_eof: F) -> std::thread::JoinHandle<()>
176where
177 R: Read + Send + 'static,
178 F: FnOnce() + Send + 'static,
179{
180 std::thread::spawn(move || {
181 let mut sink = [0u8; 256];
182 loop {
183 match reader.read(&mut sink) {
184 Ok(0) | Err(_) => break,
185 Ok(_) => {}
186 }
187 }
188 on_eof();
189 })
190}
191
192pub fn watch_stdin_eof<F>(on_eof: F) -> std::thread::JoinHandle<()>
195where
196 F: FnOnce() + Send + 'static,
197{
198 watch_reader_eof(io::stdin(), on_eof)
199}
200
201pub fn exit_when_stdin_closes(code: i32) -> std::thread::JoinHandle<()> {
205 watch_stdin_eof(move || std::process::exit(code))
206}
207
208#[derive(Debug)]
211pub struct PrivateChild {
212 stdin: Option<std::process::ChildStdin>,
213 secret: PrivateSecret,
214 child: OwnedChild,
215}
216
217impl PrivateChild {
218 pub fn spawn(command: OwnedCommand) -> io::Result<Self> {
222 let secret = PrivateSecret::generate().map_err(io::Error::other)?;
223 Self::spawn_with(command, secret)
224 }
225
226 pub fn spawn_with(command: OwnedCommand, secret: PrivateSecret) -> io::Result<Self> {
228 Self::spawn_with_extra(command, secret, None)
229 }
230
231 pub fn spawn_with_extra(
236 mut command: OwnedCommand,
237 secret: PrivateSecret,
238 extra_line: Option<&str>,
239 ) -> io::Result<Self> {
240 if let Some(line) = extra_line {
241 check_line(line)?;
242 }
243 command.command_mut().stdin(Stdio::piped());
244 let mut child = command.spawn()?;
245 let Some(mut stdin) = child.take_stdin() else {
246 let _ = child.terminate_tree();
247 return Err(io::Error::other("the child's stdin was not piped"));
248 };
249 let mut written = stdin
250 .write_all(secret.expose().as_bytes())
251 .and_then(|()| stdin.write_all(b"\n"));
252 if let Some(line) = extra_line {
253 if written.is_ok() {
254 written = stdin
255 .write_all(line.as_bytes())
256 .and_then(|()| stdin.write_all(b"\n"));
257 }
258 }
259 let written = written.and_then(|()| stdin.flush());
260 if let Err(error) = written {
261 let _ = child.terminate_tree();
262 return Err(error);
263 }
264 Ok(Self {
265 stdin: Some(stdin),
266 secret,
267 child,
268 })
269 }
270
271 pub fn write_line(&mut self, line: &str) -> io::Result<()> {
274 check_line(line)?;
275 let Some(stdin) = self.stdin.as_mut() else {
276 return Err(io::Error::new(
277 io::ErrorKind::BrokenPipe,
278 "the child's stdin is closed",
279 ));
280 };
281 stdin.write_all(line.as_bytes())?;
282 stdin.write_all(b"\n")?;
283 stdin.flush()
284 }
285
286 pub fn secret(&self) -> &PrivateSecret {
288 &self.secret
289 }
290
291 pub fn child(&self) -> &OwnedChild {
293 &self.child
294 }
295
296 pub fn child_mut(&mut self) -> &mut OwnedChild {
298 &mut self.child
299 }
300
301 pub fn close_stdin(&mut self) {
303 drop(self.stdin.take());
304 }
305}
306
307fn check_line(line: &str) -> io::Result<()> {
309 if line.contains('\n') {
310 return Err(io::Error::new(
311 io::ErrorKind::InvalidInput,
312 "a stdin line cannot contain a newline",
313 ));
314 }
315 Ok(())
316}
317
318impl Drop for PrivateChild {
319 fn drop(&mut self) {
320 self.close_stdin();
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn generated_secrets_are_hex_and_distinct() {
332 let a = PrivateSecret::generate().unwrap();
333 let b = PrivateSecret::generate().unwrap();
334 assert_eq!(a.expose().len(), SECRET_HEX_LEN);
335 assert!(a.expose().bytes().all(|c| c.is_ascii_hexdigit()));
336 assert_ne!(a.expose(), b.expose());
337 }
338
339 #[test]
340 fn verify_accepts_only_the_exact_secret() {
341 let secret = PrivateSecret::generate().unwrap();
342 assert!(secret.verify(secret.expose()));
343 assert!(!secret.verify(""));
344 assert!(!secret.verify("x"));
345 let mut wrong = secret.expose().to_string();
346 wrong.replace_range(0..1, if wrong.starts_with('0') { "1" } else { "0" });
347 assert!(!secret.verify(&wrong));
348 assert!(!secret.verify(&secret.expose()[..SECRET_HEX_LEN - 1]));
350 assert!(!secret.verify(&format!("{}0", secret.expose())));
351 assert!(!secret.verify(&"0".repeat(SECRET_HEX_LEN - 1)));
353 }
354
355 #[test]
356 fn debug_never_shows_the_secret() {
357 let secret = PrivateSecret::generate().unwrap();
358 assert!(!format!("{secret:?}").contains(secret.expose()));
359 }
360
361 #[test]
362 fn from_hex_rejects_malformed_text() {
363 assert!(PrivateSecret::from_hex("").is_err());
364 assert!(PrivateSecret::from_hex(&"A".repeat(SECRET_HEX_LEN)).is_err());
365 assert!(PrivateSecret::from_hex(&"g".repeat(SECRET_HEX_LEN)).is_err());
366 assert!(PrivateSecret::from_hex(&"a".repeat(SECRET_HEX_LEN)).is_ok());
367 }
368
369 #[test]
370 fn read_from_takes_the_first_line_only() {
371 let secret = PrivateSecret::generate().unwrap();
372 let input = format!("{}\nrest\n", secret.expose()).into_bytes();
373 let mut reader = io::Cursor::new(input);
374 let read = PrivateSecret::read_from(&mut reader).unwrap();
375 assert!(secret.verify(read.expose()));
376 }
377
378 #[test]
379 fn read_from_reports_missing_and_oversized_input() {
380 let mut empty = io::Cursor::new(Vec::<u8>::new());
381 assert!(matches!(
382 PrivateSecret::read_from(&mut empty),
383 Err(SecretError::Missing)
384 ));
385 let mut long = io::Cursor::new("a".repeat(10_000).into_bytes());
386 assert!(matches!(
387 PrivateSecret::read_from(&mut long),
388 Err(SecretError::Malformed)
389 ));
390 }
391
392 #[test]
393 fn eof_watcher_fires_when_the_reader_ends() {
394 let (tx, rx) = std::sync::mpsc::channel();
395 let handle = watch_reader_eof(io::Cursor::new(b"ignored bytes".to_vec()), move || {
396 tx.send(()).unwrap();
397 });
398 rx.recv_timeout(std::time::Duration::from_secs(5)).unwrap();
399 handle.join().unwrap();
400 }
401}