fix: minimize and zeroize owned password buffers
This commit is contained in:
Generated
+7
@@ -278,6 +278,7 @@ dependencies = [
|
|||||||
"shellexpand",
|
"shellexpand",
|
||||||
"sqlparser",
|
"sqlparser",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1719,3 +1720,9 @@ name = "wit-bindgen"
|
|||||||
version = "0.57.1"
|
version = "0.57.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ native-tls = "0.2"
|
|||||||
rusqlite = { version = "0.40", default-features = false, features = ["bundled"] }
|
rusqlite = { version = "0.40", default-features = false, features = ["bundled"] }
|
||||||
shellexpand = { version = "3.1", features = ["path"] }
|
shellexpand = { version = "3.1", features = ["path"] }
|
||||||
sqlparser = { version = "0.63", default-features = false, features = ["std"] }
|
sqlparser = { version = "0.63", default-features = false, features = ["std"] }
|
||||||
|
zeroize = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
+69
-18
@@ -9,6 +9,7 @@ use ratatui::{
|
|||||||
text::{Line, Span, Text},
|
text::{Line, Span, Text},
|
||||||
widgets::{Block, Clear, List, ListItem, ListState, Paragraph, Wrap},
|
widgets::{Block, Clear, List, ListItem, ListState, Paragraph, Wrap},
|
||||||
};
|
};
|
||||||
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Result,
|
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 => {
|
Event::Paste(text) if show_form => {
|
||||||
|
let text = Zeroizing::new(text);
|
||||||
form.error = None;
|
form.error = None;
|
||||||
if let Some(input) = form.input_mut() {
|
if let Some(input) = form.input_mut() {
|
||||||
input.paste(&text);
|
input.paste(&text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Paste(text) if focus == Focus::Query => {
|
Event::Paste(text) if focus == Focus::Query => {
|
||||||
|
let text = Zeroizing::new(text);
|
||||||
if let Some(browser) = &mut browser {
|
if let Some(browser) = &mut browser {
|
||||||
browser.paste_query(&text);
|
browser.paste_query(&text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Event::Paste(mut text) => text.zeroize(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -643,13 +647,25 @@ impl Profiles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn save(&mut self, form: &Form) -> Result<()> {
|
fn save(&mut self, form: &Form) -> Result<()> {
|
||||||
let mut saved = form.clone();
|
let mut saved = Form {
|
||||||
saved.clear_password();
|
id: form.id,
|
||||||
saved.reconnect = false;
|
name: Input::from(form.name.value.trim()),
|
||||||
saved.error = None;
|
provider: form.provider,
|
||||||
saved.name = Input::from(saved.name.value.trim());
|
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 {
|
if form.provider == 0 {
|
||||||
form.postgres_config()?;
|
saved.postgres_config()?;
|
||||||
} else if form.path.value.is_empty() {
|
} else if form.path.value.is_empty() {
|
||||||
return Err("Enter a SQLite database file path.".into());
|
return Err("Enter a SQLite database file path.".into());
|
||||||
} else {
|
} else {
|
||||||
@@ -803,7 +819,8 @@ impl Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clear_password(&mut self) {
|
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) {
|
fn edit(&mut self, key: KeyEvent) {
|
||||||
@@ -1076,6 +1093,12 @@ pub(crate) struct Input {
|
|||||||
cursor: usize,
|
cursor: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for Input {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.value.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&str> for Input {
|
impl From<&str> for Input {
|
||||||
fn from(value: &str) -> Self {
|
fn from(value: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -1240,7 +1263,7 @@ impl Input {
|
|||||||
self.value.remove(self.cursor);
|
self.value.remove(self.cursor);
|
||||||
}
|
}
|
||||||
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
self.value.clear();
|
self.value.zeroize();
|
||||||
self.cursor = 0;
|
self.cursor = 0;
|
||||||
}
|
}
|
||||||
KeyCode::Char(character)
|
KeyCode::Char(character)
|
||||||
@@ -1257,20 +1280,24 @@ impl Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paste(&mut self, value: &str) {
|
fn paste(&mut self, value: &str) {
|
||||||
let text: String = value
|
let text = Zeroizing::new(
|
||||||
.chars()
|
value
|
||||||
.filter(|character| !character.is_control())
|
.chars()
|
||||||
.collect();
|
.filter(|character| !character.is_control())
|
||||||
|
.collect::<String>(),
|
||||||
|
);
|
||||||
self.value.insert_str(self.cursor, &text);
|
self.value.insert_str(self.cursor, &text);
|
||||||
self.cursor += text.len();
|
self.cursor += text.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn paste_multiline(&mut self, value: &str) {
|
pub(crate) fn paste_multiline(&mut self, value: &str) {
|
||||||
let text: String = value
|
let text = Zeroizing::new(
|
||||||
.chars()
|
value
|
||||||
.map(|character| if character == '\t' { ' ' } else { character })
|
.chars()
|
||||||
.filter(|character| *character == '\n' || !character.is_control())
|
.map(|character| if character == '\t' { ' ' } else { character })
|
||||||
.collect();
|
.filter(|character| *character == '\n' || !character.is_control())
|
||||||
|
.collect::<String>(),
|
||||||
|
);
|
||||||
self.value.insert_str(self.cursor, &text);
|
self.value.insert_str(self.cursor, &text);
|
||||||
self.cursor += text.len();
|
self.cursor += text.len();
|
||||||
}
|
}
|
||||||
@@ -1347,6 +1374,30 @@ impl Input {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn sql_gutter_tracks_scrolling_digits_and_cursor_without_entering_sql() -> Result<()> {
|
fn sql_gutter_tracks_scrolling_digits_and_cursor_without_entering_sql() -> Result<()> {
|
||||||
use ratatui::{Terminal, backend::TestBackend};
|
use ratatui::{Terminal, backend::TestBackend};
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ Navigation:
|
|||||||
Left/Right changes TLS when focused. Enter connects and updates that saved entry
|
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.
|
only on success; failures keep the previous settings and connection. Esc discards edits.
|
||||||
Passwords are masked, never saved, and clear on success or cancellation.
|
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.
|
Ctrl-U clears a field; pasting is supported.
|
||||||
Enter the database user explicitly; the operating-system username is not used.
|
Enter the database user explicitly; the operating-system username is not used.
|
||||||
Configurations persist in $XDG_CONFIG_HOME/dbdash/connections.sqlite3, falling
|
Configurations persist in $XDG_CONFIG_HOME/dbdash/connections.sqlite3, falling
|
||||||
|
|||||||
Reference in New Issue
Block a user