securegit 0.8.5

Zero-trust git replacement with 12 built-in security scanners, LLM redteam bridge, universal undo, durable backups, and a 50-tool MCP server
Documentation
use std::fmt;

/// A string wrapper that zeros memory on drop to prevent credential leakage.
pub struct SecureString {
    inner: Vec<u8>,
}

impl SecureString {
    pub fn new(s: &str) -> Self {
        Self {
            inner: s.as_bytes().to_vec(),
        }
    }

    pub fn from_string(s: String) -> Self {
        Self {
            inner: s.into_bytes(),
        }
    }

    pub fn as_str(&self) -> &str {
        std::str::from_utf8(&self.inner).unwrap_or("")
    }

    pub fn is_empty(&self) -> bool {
        self.inner.is_empty()
    }
}

impl Drop for SecureString {
    fn drop(&mut self) {
        // Zero out the memory before deallocation
        for byte in self.inner.iter_mut() {
            unsafe {
                std::ptr::write_volatile(byte, 0);
            }
        }
        // Fence to prevent reordering past the volatile writes
        std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
        self.inner.clear();
    }
}

impl From<String> for SecureString {
    fn from(s: String) -> Self {
        Self::from_string(s)
    }
}

impl Clone for SecureString {
    fn clone(&self) -> Self {
        Self {
            inner: self.inner.clone(),
        }
    }
}

impl fmt::Debug for SecureString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "SecureString(***)")
    }
}

impl fmt::Display for SecureString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "***")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_secure_string_from_and_expose() {
        let ss = SecureString::new("my-secret-token");
        assert_eq!(ss.as_str(), "my-secret-token");

        let ss2 = SecureString::from_string("another-token".to_string());
        assert_eq!(ss2.as_str(), "another-token");

        let ss3: SecureString = "from-trait".to_string().into();
        assert_eq!(ss3.as_str(), "from-trait");
    }

    #[test]
    fn test_secure_string_debug_masked() {
        let ss = SecureString::new("super-secret-password-12345");
        let debug_output = format!("{:?}", ss);
        assert_eq!(debug_output, "SecureString(***)");
        assert!(
            !debug_output.contains("super-secret"),
            "Debug output must not leak the secret value"
        );
    }

    #[test]
    fn test_secure_string_display_masked() {
        let ss = SecureString::new("super-secret-password-12345");
        let display_output = format!("{}", ss);
        assert_eq!(display_output, "***");
        assert!(
            !display_output.contains("super-secret"),
            "Display output must not leak the secret value"
        );
    }

    #[test]
    fn test_secure_string_clone_independent() {
        let ss1 = SecureString::new("original-value");
        let ss2 = ss1.clone();

        // Both should expose the same value
        assert_eq!(ss1.as_str(), "original-value");
        assert_eq!(ss2.as_str(), "original-value");

        // Dropping one should not affect the other
        drop(ss1);
        assert_eq!(ss2.as_str(), "original-value");
    }

    #[test]
    fn test_secure_string_empty() {
        let ss = SecureString::new("");
        assert_eq!(ss.as_str(), "");
        assert!(ss.is_empty());

        let ss2 = SecureString::from_string(String::new());
        assert_eq!(ss2.as_str(), "");
        assert!(ss2.is_empty());
    }

    #[test]
    fn test_secure_string_long_value() {
        // 10KB+ credential
        let long_value: String = "A".repeat(10_240);
        let ss = SecureString::new(&long_value);
        assert_eq!(ss.as_str(), long_value.as_str());
        assert_eq!(ss.as_str().len(), 10_240);
        assert!(!ss.is_empty());
    }

    #[test]
    fn test_secure_string_special_chars() {
        // Unicode
        let ss_unicode = SecureString::new("p\u{00e4}ssw\u{00f6}rd-\u{1f512}");
        assert_eq!(ss_unicode.as_str(), "p\u{00e4}ssw\u{00f6}rd-\u{1f512}");

        // Control characters (tabs, newlines)
        let ss_control = SecureString::new("token\twith\nnewlines\r\nand\ttabs");
        assert_eq!(ss_control.as_str(), "token\twith\nnewlines\r\nand\ttabs");

        // Mixed special characters
        let ss_mixed = SecureString::new("ghp_\u{00e9}\u{00e8}\u{00ea}!@#$%^&*()");
        assert_eq!(ss_mixed.as_str(), "ghp_\u{00e9}\u{00e8}\u{00ea}!@#$%^&*()");
    }
}