Skip to content

Commit

Permalink
feat: zeroize secrets (#119)
Browse files Browse the repository at this point in the history
1. `SecretData` implements `Zeroize` and `ZeroizeOnDrop` for the
password and hashed password
2. `SecureKey` provides a safe wrapper around `Key` with:
   - Thread-safe storage of key bytes using `Arc<Mutex<Vec<u8>>>`
   - Thread-safe storage of key bytes using `Arc<Mutex<Vec<u8>>>`
   - Automatic zeroization through `Drop`
   - Safe dereferencing to the underlying `Key`
3. User-provided passwords are zeroized after verification
4. All sensitive data is properly cleaned up when dropped
  • Loading branch information
storopoli authored Dec 14, 2024
1 parent d4ce5cb commit b2904da
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 46 deletions.
22 changes: 19 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[workspace]
resolver = "2"
members = [
"crates/dead-man-switch",
"crates/dead-man-switch-tui",
"crates/dead-man-switch-web",
"crates/dead-man-switch",
"crates/dead-man-switch-tui",
"crates/dead-man-switch-web",
]
default-members = ["crates/dead-man-switch", "crates/dead-man-switch-tui"]

Expand All @@ -15,6 +15,11 @@ description = "A simple no-BS Dead Man's Switch"
license = "AGPL-3.0-only"
readme = "README.md"

[workspace.dependencies]
dead-man-switch = { path = "crates/dead-man-switch", version = "0.5.0" }

zeroize = { version = "1.8.1", features = ["derive"] }

[profile.release]
opt-level = "z" # Optimized for size, use 3 for speed
lto = true # Enable Link Time Optimization
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ To check-in, you just need to press the `c` key as in **c**heck-in.

There are several ways to install Dead Man's Switch:

1. [Crates.io](https://crates.io/crates/dead-man-switch): `cargo install dead-man-switch-tui`.
1. [GitHub](https://github.com/storopoli/dead-man-switch): `cargo install --git https://github.com/storopoli/dead-man-switch -p dead-man-switch-tui`.
1. From source: Clone the repository and run `cargo install --path .`.
1. [Crates.io](https://crates.io/crates/dead-man-switch): `cargo install --locked dead-man-switch-tui`.
1. [GitHub](https://github.com/storopoli/dead-man-switch): `cargo install --git --locked https://github.com/storopoli/dead-man-switch -p dead-man-switch-tui`.
1. From source: Clone the repository and run `cargo install --locked --path .`.
1. Using Nix: `nix run github:storopoli/dead-man-switch`.
1. Using Nix Flakes: add this to your `flake.nix`:

Expand Down Expand Up @@ -110,7 +110,7 @@ To do so you can add the following to your `Cargo.toml`:

```toml
[dependencies]
dead-man-switch = "0.4"
dead-man-switch = "0.5"
```

## Web Interface
Expand Down
5 changes: 3 additions & 2 deletions crates/dead-man-switch-tui/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
[package]
name = "dead-man-switch-tui"
edition = "2021"
version = "0.4.2"
version = "0.5.0"
authors = ["Jose Storopoli <[email protected]>"]
description = "A simple no-BS Dead Man's Switch Tui Interface"
license = "AGPL-3.0-only"
readme = "../../README.md"

[dependencies]
dead-man-switch = { version = "0.4.2", path = "../dead-man-switch" }
dead-man-switch.workspace = true

thiserror = "2"
ratatui = "0.28"
crossterm = "0.28"
4 changes: 2 additions & 2 deletions crates/dead-man-switch-tui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,14 @@ fn ascii_block(content: &[&'static str]) -> Paragraph<'static> {
/// Contains a [`Gauge`] widget to display the timer.
/// The timer will be updated every second.
///
/// ## Parameters
/// # Parameters
///
/// - `title`: The title for the timer.
/// - `current_percent`: The current percentage of the timer.
/// - `label`: The label for the timer.
/// - `gauge_style`: The [`Style`] for the timer.
///
/// ## Notes
/// # Notes
///
/// The timer will be green if is still counting the warning time.
/// Eventually, it will turn red when the warning time is done,
Expand Down
7 changes: 5 additions & 2 deletions crates/dead-man-switch-web/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
[package]
name = "dead-man-switch-web"
edition = "2021"
version = "0.4.2"
version = "0.5.0"
authors = ["Jose Storopoli <[email protected]>"]
description = "A simple no-BS Dead Man's Switch Web Interface"
license = "AGPL-3.0-only"
readme = "../../README.md"

[dependencies]
dead-man-switch = { version = "0.4.2", path = "../dead-man-switch" }
dead-man-switch.workspace = true

zeroize.workspace = true

anyhow = "1.0.92"
askama = "0.12.1"
axum = "0.7.9"
Expand Down
108 changes: 96 additions & 12 deletions crates/dead-man-switch-web/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Web implementation for the Dead Man's Switch.
use std::{collections::HashMap, sync::Arc, time::Duration};
use std::{collections::HashMap, ops::Deref, sync::Arc, time::Duration};

use anyhow::Context;
use askama::Template;
Expand All @@ -19,15 +19,19 @@ use dead_man_switch::{
timer::{Timer, TimerType},
};
use serde::Serialize;
use tokio::sync::{Mutex, RwLock};
use tokio::{net::TcpListener, time::sleep};
use tokio::{
runtime::Handle,
sync::{Mutex, RwLock},
};
use tower::{buffer::BufferLayer, limit::RateLimitLayer, ServiceBuilder};
use tower_http::{
cors::{Any, CorsLayer},
trace::TraceLayer,
};
use tracing::{info, subscriber, warn, Level};
use tracing_subscriber::FmtSubscriber;
use zeroize::{Zeroize, ZeroizeOnDrop};

/// App state.
struct AppState {
Expand All @@ -36,21 +40,88 @@ struct AppState {
timer: Mutex<Timer>,
}

/// Secret data to be zeroized.
#[derive(Zeroize, ZeroizeOnDrop)]
struct SecretData {
/// Password from the config.
password: String,
/// Hashed password from the config.
hashed_password: String,
}

/// Wrapper for [`Key`] that provides secure zeroization.
#[derive(Clone)]
struct SecureKey {
/// The wrapped [`Key`].
key: Key,
/// The pointer to the key's memory.
///
/// Using an `Arc<Mutex<Vec<u8>>>` to make the pointer thread-safe.
bytes: Arc<Mutex<Vec<u8>>>,
}

impl SecureKey {
/// Create a new [`SecureKey`] from a [`Key`].
fn new(key: Key) -> Self {
let bytes = key.master().to_vec();
Self {
key,
bytes: Arc::new(Mutex::new(bytes)),
}
}
}

impl Zeroize for SecureKey {
fn zeroize(&mut self) {
match Handle::try_current() {
Ok(rt) => {
// block_on returns the MutexGuard directly
let mut guard = rt.block_on(async { self.bytes.lock().await });
guard.zeroize();
}
Err(_) => {
// No runtime available, try to zeroize synchronously
if let Ok(mut guard) = self.bytes.try_lock() {
guard.zeroize();
}
}
}
}
}

impl Drop for SecureKey {
fn drop(&mut self) {
// Use try_lock() instead of depending on the runtime
if let Ok(guard) = self.bytes.try_lock() {
let mut bytes = guard.to_vec();
bytes.zeroize();
}
}
}

impl Deref for SecureKey {
type Target = Key;

fn deref(&self) -> &Self::Target {
&self.key
}
}

/// Combined state containing both AppState and SecretState.
#[derive(Clone)]
struct SharedState {
/// Dead Man's Switch [`AppState`].
app_state: Arc<AppState>,
/// Hashed password from the config
hashed_password: String,
/// [`SecretData`] from the config.
secret_data: Arc<SecretData>,
/// Secret key for cookie encryption.
key: Key,
key: SecureKey,
}

/// Tells [`PrivateCookieJar`] how to access the key from a [`SharedState`].
impl FromRef<SharedState> for Key {
fn from_ref(state: &SharedState) -> Self {
state.key.clone()
state.key.key.clone()
}
}

Expand Down Expand Up @@ -139,11 +210,17 @@ async fn handle_login(
State(state): State<SharedState>,
Form(params): Form<HashMap<String, String>>,
) -> impl IntoResponse {
let jar = PrivateCookieJar::new(state.key.clone());
let jar = PrivateCookieJar::new(state.key.key.clone());

let mut user_password = params.get("password").expect("Password not found").clone();

let user_password = params.get("password").expect("Password not found").clone();
let is_valid = verify(&user_password, &state.secret_data.hashed_password)
.expect("Failed to verify password");

if verify(user_password, &state.hashed_password).expect("Failed to verify password") {
// Zeroize the user-provided password after use
user_password.zeroize();

if is_valid {
let updated_jar = jar.add(Cookie::new("auth", "true"));
(updated_jar, Redirect::to("/dashboard"))
} else {
Expand Down Expand Up @@ -246,7 +323,13 @@ async fn main() -> anyhow::Result<()> {
let config = load_or_initialize_config().context("Failed to load or initialize config")?;
// Hash the password
let password = config.web_password.clone();
let hashed_password = hash(password, DEFAULT_COST).expect("Failed to hash password");
let hashed_password = hash(&password, DEFAULT_COST).expect("Failed to hash password");

// Create a new SecretData
let secret_data = Arc::new(SecretData {
password,
hashed_password,
});

// Create a new Timer
let timer = Timer::new(
Expand All @@ -261,8 +344,8 @@ async fn main() -> anyhow::Result<()> {
// Create combined shared state
let shared_state = SharedState {
app_state: app_state.clone(),
key: Key::generate(),
hashed_password,
key: SecureKey::new(Key::generate()),
secret_data,
};

// CORS Layer
Expand Down Expand Up @@ -303,5 +386,6 @@ async fn main() -> anyhow::Result<()> {
serve(addr, app)
.await
.context("error while starting server")?;

Ok(())
}
3 changes: 2 additions & 1 deletion crates/dead-man-switch/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "dead-man-switch"
edition = "2021"
version = "0.4.2"
version = "0.5.0"
authors = ["Jose Storopoli <[email protected]>"]
description = "A simple no-BS Dead Man's Switch"
license = "AGPL-3.0-only"
Expand All @@ -16,3 +16,4 @@ lettre = { version = "0.11", features = ["rustls-tls", "builder"] }
lettre_email = "0.9"
mime_guess = "2"
chrono = "0.4"
zeroize.workspace = true
Loading

0 comments on commit b2904da

Please sign in to comment.