fix: minimize and zeroize owned password buffers
This commit is contained in:
+69
-18
@@ -9,6 +9,7 @@ use ratatui::{
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Clear, List, ListItem, ListState, Paragraph, Wrap},
|
||||
};
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
@@ -373,18 +374,21 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Paste(_) if info.is_some() || help.is_some() || remove.is_some() => {}
|
||||
Event::Paste(mut text) if info.is_some() || help.is_some() || remove.is_some() => text.zeroize(),
|
||||
Event::Paste(text) if show_form => {
|
||||
let text = Zeroizing::new(text);
|
||||
form.error = None;
|
||||
if let Some(input) = form.input_mut() {
|
||||
input.paste(&text);
|
||||
}
|
||||
}
|
||||
Event::Paste(text) if focus == Focus::Query => {
|
||||
let text = Zeroizing::new(text);
|
||||
if let Some(browser) = &mut browser {
|
||||
browser.paste_query(&text);
|
||||
}
|
||||
}
|
||||
Event::Paste(mut text) => text.zeroize(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -643,13 +647,25 @@ impl Profiles {
|
||||
}
|
||||
|
||||
fn save(&mut self, form: &Form) -> Result<()> {
|
||||
let mut saved = form.clone();
|
||||
saved.clear_password();
|
||||
saved.reconnect = false;
|
||||
saved.error = None;
|
||||
saved.name = Input::from(saved.name.value.trim());
|
||||
let mut saved = Form {
|
||||
id: form.id,
|
||||
name: Input::from(form.name.value.trim()),
|
||||
provider: form.provider,
|
||||
focus: form.focus,
|
||||
postgres: std::array::from_fn(|index| {
|
||||
if index == 4 {
|
||||
Input::default()
|
||||
} else {
|
||||
form.postgres[index].clone()
|
||||
}
|
||||
}),
|
||||
tls: form.tls,
|
||||
path: form.path.clone(),
|
||||
error: None,
|
||||
reconnect: false,
|
||||
};
|
||||
if form.provider == 0 {
|
||||
form.postgres_config()?;
|
||||
saved.postgres_config()?;
|
||||
} else if form.path.value.is_empty() {
|
||||
return Err("Enter a SQLite database file path.".into());
|
||||
} else {
|
||||
@@ -803,7 +819,8 @@ impl Form {
|
||||
}
|
||||
|
||||
fn clear_password(&mut self) {
|
||||
self.postgres[4] = Input::default();
|
||||
self.postgres[4].value.zeroize();
|
||||
self.postgres[4].cursor = 0;
|
||||
}
|
||||
|
||||
fn edit(&mut self, key: KeyEvent) {
|
||||
@@ -1076,6 +1093,12 @@ pub(crate) struct Input {
|
||||
cursor: usize,
|
||||
}
|
||||
|
||||
impl Drop for Input {
|
||||
fn drop(&mut self) {
|
||||
self.value.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Input {
|
||||
fn from(value: &str) -> Self {
|
||||
Self {
|
||||
@@ -1240,7 +1263,7 @@ impl Input {
|
||||
self.value.remove(self.cursor);
|
||||
}
|
||||
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.value.clear();
|
||||
self.value.zeroize();
|
||||
self.cursor = 0;
|
||||
}
|
||||
KeyCode::Char(character)
|
||||
@@ -1257,20 +1280,24 @@ impl Input {
|
||||
}
|
||||
|
||||
fn paste(&mut self, value: &str) {
|
||||
let text: String = value
|
||||
.chars()
|
||||
.filter(|character| !character.is_control())
|
||||
.collect();
|
||||
let text = Zeroizing::new(
|
||||
value
|
||||
.chars()
|
||||
.filter(|character| !character.is_control())
|
||||
.collect::<String>(),
|
||||
);
|
||||
self.value.insert_str(self.cursor, &text);
|
||||
self.cursor += text.len();
|
||||
}
|
||||
|
||||
pub(crate) fn paste_multiline(&mut self, value: &str) {
|
||||
let text: String = value
|
||||
.chars()
|
||||
.map(|character| if character == '\t' { ' ' } else { character })
|
||||
.filter(|character| *character == '\n' || !character.is_control())
|
||||
.collect();
|
||||
let text = Zeroizing::new(
|
||||
value
|
||||
.chars()
|
||||
.map(|character| if character == '\t' { ' ' } else { character })
|
||||
.filter(|character| *character == '\n' || !character.is_control())
|
||||
.collect::<String>(),
|
||||
);
|
||||
self.value.insert_str(self.cursor, &text);
|
||||
self.cursor += text.len();
|
||||
}
|
||||
@@ -1347,6 +1374,30 @@ impl Input {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn clearing_passwords_wipes_the_live_buffer_and_resets_unicode_cursor() {
|
||||
for cancel in [true, false] {
|
||||
let mut form = Form::default();
|
||||
form.postgres[4].paste("secret-é界");
|
||||
let length = form.postgres[4].value.len();
|
||||
if cancel {
|
||||
form.clear_password();
|
||||
} else {
|
||||
form.postgres[4].edit(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
|
||||
}
|
||||
let input = &mut form.postgres[4];
|
||||
assert!(input.value.is_empty());
|
||||
assert_eq!(input.position(), (1, 1));
|
||||
assert!(input.value.capacity() >= length);
|
||||
// The allocation is still owned, and zeroize initialized its former contents to zero.
|
||||
let wiped = unsafe { std::slice::from_raw_parts(input.value.as_ptr(), length) };
|
||||
assert!(wiped.iter().all(|byte| *byte == 0));
|
||||
input.paste("next");
|
||||
assert_eq!(input.value, "next");
|
||||
assert_eq!(input.position(), (1, 5));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn sql_gutter_tracks_scrolling_digits_and_cursor_without_entering_sql() -> Result<()> {
|
||||
use ratatui::{Terminal, backend::TestBackend};
|
||||
|
||||
@@ -103,6 +103,8 @@ Navigation:
|
||||
Left/Right changes TLS when focused. Enter connects and updates that saved entry
|
||||
only on success; failures keep the previous settings and connection. Esc discards edits.
|
||||
Passwords are masked, never saved, and clear on success or cancellation.
|
||||
Owned input buffers are zeroized on clear/drop; profile saving does not copy passwords.
|
||||
This does not erase driver copies, prior input allocations, or protect a compromised host.
|
||||
Ctrl-U clears a field; pasting is supported.
|
||||
Enter the database user explicitly; the operating-system username is not used.
|
||||
Configurations persist in $XDG_CONFIG_HOME/dbdash/connections.sqlite3, falling
|
||||
|
||||
Reference in New Issue
Block a user