gardesk/garcard / 00dce95

Browse files

Reduce prompt credential lifetime

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
00dce953964d79ea0fad4a00ef61668d52c23a06
Parents
08a18d9
Tree
824c362

4 changed files

StatusFile+-
M RELEASE_NOTES.md 1 0
M examples/sprint-04-validation-report-2026-02-20.md 1 0
M garcard/src/prompt.rs 31 15
M garcard/src/prompt_ui.rs 19 1
RELEASE_NOTES.mdmodified
@@ -10,6 +10,7 @@
1010
 1. Same-UID enforcement for local IPC control clients.
1111
 2. Reduced panic surface in prompt color setup paths.
1212
 3. Best-effort scrubbing of helper prompt response buffers after use.
13
+4. Reduced prompt credential lifetime by moving submitted input without cloning and scrubbing prompt output buffers.
1314
 
1415
 ## Validation Coverage
1516
 1. Sprint 02 live callback and reconnect validation:
examples/sprint-04-validation-report-2026-02-20.mdmodified
@@ -30,6 +30,7 @@
3030
 1. IPC control path now validates same-UID peer credentials.
3131
 2. Prompt UI runtime path no longer relies on panic/`expect` for color parsing.
3232
 3. Helper response buffers are scrubbed after sending to helper socket.
33
+4. Prompt input handling now moves submitted secrets without cloning and scrubs prompt/output buffers after use.
3334
 
3435
 ## Remaining Manual Sprint 04 Checks
3536
 1. Optional interactive acceptance pass (enter valid credentials, wrong-then-retry, explicit cancel) in full desktop session.
garcard/src/prompt.rsmodified
@@ -66,7 +66,7 @@ impl PromptProvider for CommandPrompt {
6666
 
6767
 fn run_custom_prompt_command(command: &str, prompt: &str, visible: bool) -> Result<PromptResponse> {
6868
     let mode = if visible { "plain" } else { "secret" };
69
-    let output = Command::new("sh")
69
+    let mut output = Command::new("sh")
7070
         .arg("-c")
7171
         .arg(command)
7272
         .env("GARCARD_PROMPT", prompt)
@@ -74,10 +74,9 @@ fn run_custom_prompt_command(command: &str, prompt: &str, visible: bool) -> Resu
7474
         .output()
7575
         .with_context(|| format!("failed to run custom prompt command: {}", command))?;
7676
 
77
-    Ok(map_output_to_prompt_response(
78
-        &output.status,
79
-        &output.stdout,
80
-    ))
77
+    let response = map_output_to_prompt_response(&output.status, &output.stdout);
78
+    scrub_bytes(&mut output.stdout);
79
+    Ok(response)
8180
 }
8281
 
8382
 fn run_gartk_prompt_subcommand(
@@ -88,7 +87,7 @@ fn run_gartk_prompt_subcommand(
8887
     let mode = if visible { "plain" } else { "secret" };
8988
     let executable =
9089
         std::env::current_exe().context("failed to resolve current executable path")?;
91
-    let output = Command::new(executable)
90
+    let mut output = Command::new(executable)
9291
         .arg("prompt")
9392
         .arg("--mode")
9493
         .arg(mode)
@@ -101,13 +100,15 @@ fn run_gartk_prompt_subcommand(
101100
 
102101
     if output.status.code() == Some(2) {
103102
         let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
103
+        scrub_bytes(&mut output.stdout);
104
+        scrub_bytes(&mut output.stderr);
104105
         anyhow::bail!("garcard prompt subcommand unavailable: {}", stderr);
105106
     }
106107
 
107
-    Ok(map_output_to_prompt_response(
108
-        &output.status,
109
-        &output.stdout,
110
-    ))
108
+    let response = map_output_to_prompt_response(&output.status, &output.stdout);
109
+    scrub_bytes(&mut output.stdout);
110
+    scrub_bytes(&mut output.stderr);
111
+    Ok(response)
111112
 }
112113
 
113114
 fn run_systemd_ask_password(
@@ -124,13 +125,13 @@ fn run_systemd_ask_password(
124125
     }
125126
     command.arg(prompt);
126127
 
127
-    let output = command
128
+    let mut output = command
128129
         .output()
129130
         .context("failed to run systemd-ask-password")?;
130
-    Ok(map_output_to_prompt_response(
131
-        &output.status,
132
-        &output.stdout,
133
-    ))
131
+    let response = map_output_to_prompt_response(&output.status, &output.stdout);
132
+    scrub_bytes(&mut output.stdout);
133
+    scrub_bytes(&mut output.stderr);
134
+    Ok(response)
134135
 }
135136
 
136137
 fn map_output_to_prompt_response(status: &ExitStatus, stdout: &[u8]) -> PromptResponse {
@@ -156,6 +157,14 @@ fn extract_response(stdout: &[u8]) -> Option<String> {
156157
     }
157158
 }
158159
 
160
+fn scrub_bytes(value: &mut Vec<u8>) {
161
+    if value.is_empty() {
162
+        return;
163
+    }
164
+    value.fill(0);
165
+    value.clear();
166
+}
167
+
159168
 #[cfg(test)]
160169
 mod tests {
161170
     use super::*;
@@ -181,4 +190,11 @@ mod tests {
181190
         let mapped = map_output_to_prompt_response(&status, b"");
182191
         assert_eq!(mapped, PromptResponse::TimedOut);
183192
     }
193
+
194
+    #[test]
195
+    fn scrub_bytes_clears_vec() {
196
+        let mut value = b"top-secret".to_vec();
197
+        scrub_bytes(&mut value);
198
+        assert!(value.is_empty());
199
+    }
184200
 }
garcard/src/prompt_ui.rsmodified
@@ -195,7 +195,9 @@ impl PromptDialog {
195195
                 self.exit = Some(PromptExit::Canceled);
196196
             }
197197
             Key::Return => {
198
-                self.exit = Some(PromptExit::Submitted(self.input.clone()));
198
+                let submitted = std::mem::take(&mut self.input);
199
+                self.cursor = 0;
200
+                self.exit = Some(PromptExit::Submitted(submitted));
199201
             }
200202
             Key::Left => {
201203
                 if self.cursor > 0 {
@@ -364,6 +366,7 @@ impl PromptDialog {
364366
 
365367
 impl Drop for PromptDialog {
366368
     fn drop(&mut self) {
369
+        scrub_string(&mut self.input);
367370
         let _ = self.window.connection().inner().free_gc(self.gc);
368371
     }
369372
 }
@@ -421,6 +424,14 @@ fn remove_char_at(input: &mut String, cursor: usize) {
421424
     input.drain(start..end);
422425
 }
423426
 
427
+fn scrub_string(value: &mut String) {
428
+    if value.is_empty() {
429
+        return;
430
+    }
431
+    let mut bytes = std::mem::take(value).into_bytes();
432
+    bytes.fill(0);
433
+}
434
+
424435
 #[cfg(test)]
425436
 mod tests {
426437
     use super::*;
@@ -451,4 +462,11 @@ mod tests {
451462
         assert_eq!(display_prefix("hello", 2, PromptMode::Plain), "he");
452463
         assert_eq!(display_prefix("hello", 2, PromptMode::Secret), "**");
453464
     }
465
+
466
+    #[test]
467
+    fn scrub_string_clears_input() {
468
+        let mut value = "top-secret".to_string();
469
+        scrub_string(&mut value);
470
+        assert!(value.is_empty());
471
+    }
454472
 }