From 7888ca09d281e6b6726e86d514ae00c8775a05ed Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Fri, 26 Aug 2022 09:06:23 -0400 Subject: [PATCH] fs-mistrust: Add a `anonymize_home` extension fn for Path. This function transforms `/home/nickm/.config` to `${HOME}/.config/`, so that we can expose the username less in our logs. --- Cargo.lock | 1 + crates/fs-mistrust/Cargo.toml | 6 +- crates/fs-mistrust/semver.md | 2 + crates/fs-mistrust/src/anon_home.rs | 153 ++++++++++++++++++++++++++++ crates/fs-mistrust/src/lib.rs | 2 + crates/fs-mistrust/src/walk.rs | 1 - 6 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 crates/fs-mistrust/semver.md create mode 100644 crates/fs-mistrust/src/anon_home.rs diff --git a/Cargo.lock b/Cargo.lock index 1eea1a407..992d7e678 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1210,6 +1210,7 @@ dependencies = [ "anyhow", "derive_builder_fork_arti", "derive_more", + "dirs", "educe", "libc", "once_cell", diff --git a/crates/fs-mistrust/Cargo.toml b/crates/fs-mistrust/Cargo.toml index a25ddc4de..dd8e51794 100644 --- a/crates/fs-mistrust/Cargo.toml +++ b/crates/fs-mistrust/Cargo.toml @@ -11,12 +11,16 @@ categories = ["filesystem"] repository = "https://gitlab.torproject.org/tpo/core/arti.git/" [features] -default = ["walkdir"] +default = ["walkdir", "anon_home"] + +anon_home = ["once_cell", "dirs"] [dependencies] derive_builder = { version = "0.11", package = "derive_builder_fork_arti" } derive_more = "0.99" +dirs = { version = "4", optional = true } educe = "0.4.6" +once_cell = { version = "1", optional = true } serde = { version = "1.0.103", features = ["derive"], optional = true } thiserror = "1" walkdir = { version = "2", optional = true } diff --git a/crates/fs-mistrust/semver.md b/crates/fs-mistrust/semver.md new file mode 100644 index 000000000..77e567a21 --- /dev/null +++ b/crates/fs-mistrust/semver.md @@ -0,0 +1,2 @@ +ADDED: compact_home() feature. + diff --git a/crates/fs-mistrust/src/anon_home.rs b/crates/fs-mistrust/src/anon_home.rs new file mode 100644 index 000000000..6a41c5e7c --- /dev/null +++ b/crates/fs-mistrust/src/anon_home.rs @@ -0,0 +1,153 @@ +//! Replace the home-directory in a filename with `${HOME}` or `%UserProfile%` +//! +//! In some privacy-sensitive applications, we want to lower the amount of +//! personally identifying information in our logs. In such environments, it's +//! good to avoid logging the actual value of the home directory, since those +//! frequently identify the user. + +use std::{ + collections::HashSet, + path::{Path, PathBuf}, +}; + +use once_cell::sync::Lazy; + +/// Cached value of our observed home directory. +static HOMEDIRS: Lazy> = Lazy::new(default_homedirs); + +/// Return a list of home directories in official and canonical forms. +fn default_homedirs() -> Vec { + if let Some(basic_home) = dirs::home_dir() { + // Build as a HashSet, to de-duplicate. + let mut homedirs = HashSet::new(); + + // We like our home directory. + homedirs.insert(basic_home.clone()); + // We like the canonical version of our home directory. + if let Ok(canonical) = std::fs::canonicalize(&basic_home) { + homedirs.insert(canonical); + } + // We like the version of our home directory generated by `ResolvePath`. + if let Ok(rp) = crate::walk::ResolvePath::new(basic_home) { + let (mut p, rest) = rp.into_result(); + p.extend(rest); + homedirs.insert(p); + } + + homedirs.into_iter().collect() + } else { + vec![] + } +} + +/// The string that we use to represent our home directory in a compacted path. +const HOME_SUBSTITUTION: &str = { + if cfg!(target_family = "windows") { + "%UserProfile%" + } else { + "${HOME}" + } +}; + +/// An extension trait for [`Path`]. + +pub trait PathExt { + /// If this is a path within our home directory, try to replace the home + /// directory component with a symbolic reference to our home directory. + /// + /// This function can be useful for outputting paths while reducing the risk + /// of exposing usernames in the log. + /// + /// # Examples + /// + /// ```no_run + /// use std::path::{Path,PathBuf}; + /// use fs_mistrust::anon_home::PathExt as _; + /// + /// let path = PathBuf::from("/home/arachnidsGrip/.config/arti.toml"); + /// assert_eq!(path.anonymize_home().to_string(), + /// "${HOME}/.config/arti.toml"); + /// panic!(); + /// ``` + fn anonymize_home(&self) -> AnonHomePath<'_>; +} + +impl PathExt for Path { + fn anonymize_home(&self) -> AnonHomePath<'_> { + AnonHomePath(self) + } +} + +/// A wrapper for `Path` which, when displayed, replaces the home directory with +/// a symbolic reference. +#[derive(Debug, Clone)] +pub struct AnonHomePath<'a>(&'a Path); + +impl<'a> std::fmt::Display for AnonHomePath<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // We compare against both the home directory and the canonical home + // directory, since sometimes we'll want to canonicalize a path before + // passing it to this function and still have it work. + for home in HOMEDIRS.iter() { + if let Ok(suffix) = self.0.strip_prefix(home) { + return write!( + f, + "{}{}{}", + HOME_SUBSTITUTION, + std::path::MAIN_SEPARATOR, + suffix.display() + ); + } + } + + // Didn't match any homedir. + + self.0.display().fmt(f) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn no_change() { + // This is not your home directory + let path = PathBuf::from("/completely/untoucha8le"); + assert_eq!(path.anonymize_home().to_string(), path.to_string_lossy()); + } + + fn check_with_home(homedir: &Path) { + let arti_conf = homedir.join("here/is/a/path"); + #[cfg(target_family = "windows")] + assert_eq!( + arti_conf.compact_home().to_string(), + "%UserProfile%\\here/is/a/path" + ); + + #[cfg(not(target_family = "windows"))] + assert_eq!( + arti_conf.anonymize_home().to_string(), + "${HOME}/here/is/a/path" + ); + } + + #[test] + fn in_home() { + if let Some(home) = dirs::home_dir() { + check_with_home(&home); + } + } + + #[test] + fn in_canonical_home() { + if let Some(canonical_home) = dirs::home_dir() + .map(std::fs::canonicalize) + .transpose() + .ok() + .flatten() + { + check_with_home(&canonical_home); + } + } +} diff --git a/crates/fs-mistrust/src/lib.rs b/crates/fs-mistrust/src/lib.rs index ffb5ae395..8d298bfd2 100644 --- a/crates/fs-mistrust/src/lib.rs +++ b/crates/fs-mistrust/src/lib.rs @@ -291,6 +291,8 @@ mod imp; ))] mod user; +#[cfg(feature = "compact_home")] +pub mod compact_homedir; #[cfg(test)] pub(crate) mod testing; pub mod walk; diff --git a/crates/fs-mistrust/src/walk.rs b/crates/fs-mistrust/src/walk.rs index 7d528de93..22738e4aa 100644 --- a/crates/fs-mistrust/src/walk.rs +++ b/crates/fs-mistrust/src/walk.rs @@ -171,7 +171,6 @@ impl ResolvePath { /// ended with an error), we return the part that we were able to resolve, /// and a path that would need to be joined onto it to reach the intended /// destination. - #[cfg(test)] pub(crate) fn into_result(self) -> (PathBuf, Option) { let remainder = if self.stack.is_empty() { None