use super::TrustStore;
use crate::{Error, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
pub struct NssTrustStore {
cert_path: PathBuf,
unique_name: String,
}
impl NssTrustStore {
pub fn new(cert_path: &Path, unique_name: String) -> Self {
Self {
cert_path: cert_path.to_path_buf(),
unique_name,
}
}
fn get_nss_dbs() -> Vec<PathBuf> {
let mut dbs = Vec::new();
if let Some(home) = dirs::home_dir() {
dbs.push(home.join(".pki/nssdb"));
dbs.push(home.join("snap/chromium/current/.pki/nssdb"));
}
dbs.push(PathBuf::from("/etc/pki/nssdb"));
dbs
}
#[cfg(target_os = "macos")]
fn get_firefox_profile_globs() -> Vec<String> {
let mut globs = Vec::new();
if let Some(home) = dirs::home_dir() {
globs.push(format!(
"{}/Library/Application Support/Firefox/Profiles/*",
home.display()
));
}
globs
}
#[cfg(target_os = "linux")]
fn get_firefox_profile_globs() -> Vec<String> {
let mut globs = Vec::new();
if let Some(home) = dirs::home_dir() {
globs.push(format!("{}/.mozilla/firefox/*", home.display()));
globs.push(format!(
"{}/snap/firefox/common/.mozilla/firefox/*",
home.display()
));
}
globs
}
#[cfg(target_os = "windows")]
fn get_firefox_profile_globs() -> Vec<String> {
let mut globs = Vec::new();
if let Ok(profile) = std::env::var("USERPROFILE") {
globs.push(format!(
"{}\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\*",
profile
));
}
globs
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn get_firefox_profile_globs() -> Vec<String> {
Vec::new()
}
fn has_firefox() -> bool {
let firefox_paths = vec![
"/usr/bin/firefox",
"/usr/bin/firefox-nightly",
"/usr/bin/firefox-developer-edition",
"/snap/firefox",
"/Applications/Firefox.app",
"/Applications/FirefoxDeveloperEdition.app",
"/Applications/Firefox Developer Edition.app",
"/Applications/Firefox Nightly.app",
"C:\\Program Files\\Mozilla Firefox",
];
for path in firefox_paths {
if Path::new(path).exists() {
return true;
}
}
false
}
pub fn is_available() -> bool {
if Self::has_firefox() {
return true;
}
for db in Self::get_nss_dbs() {
if db.exists() {
return true;
}
}
false
}
fn find_nss_profiles() -> Vec<(String, PathBuf)> {
let mut profiles = Vec::new();
for db in Self::get_nss_dbs() {
if db.exists() && db.is_dir() {
if db.join("cert9.db").exists() {
profiles.push(("sql".to_string(), db.clone()));
} else if db.join("cert8.db").exists() {
profiles.push(("dbm".to_string(), db.clone()));
}
}
}
for pattern in Self::get_firefox_profile_globs() {
if let Ok(paths) = glob::glob(&pattern) {
for entry in paths.flatten() {
if entry.is_dir() {
if entry.join("cert9.db").exists() {
profiles.push(("sql".to_string(), entry.clone()));
} else if entry.join("cert8.db").exists() {
profiles.push(("dbm".to_string(), entry.clone()));
}
}
}
}
}
profiles
}
fn find_certutil() -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
if let Ok(output) = Command::new("which").arg("certutil").output()
&& output.status.success()
{
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() {
return Some(PathBuf::from(path));
}
}
let homebrew_path = PathBuf::from("/usr/local/opt/nss/bin/certutil");
if homebrew_path.exists() {
return Some(homebrew_path);
}
if let Ok(output) = Command::new("brew").args(["--prefix", "nss"]).output()
&& output.status.success()
{
let prefix = String::from_utf8_lossy(&output.stdout).trim().to_string();
let certutil_path = PathBuf::from(prefix).join("bin/certutil");
if certutil_path.exists() {
return Some(certutil_path);
}
}
}
#[cfg(target_os = "linux")]
{
if let Ok(output) = Command::new("which").arg("certutil").output()
&& output.status.success()
{
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() {
return Some(PathBuf::from(path));
}
}
}
#[cfg(target_os = "windows")]
{
if let Ok(output) = Command::new("where").arg("certutil").output() {
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() {
return Some(PathBuf::from(path));
}
}
}
}
None
}
pub fn has_certutil() -> bool {
Self::find_certutil().is_some()
}
fn exec_certutil(args: &[&str]) -> Result<std::process::Output> {
let certutil_path = Self::find_certutil()
.ok_or_else(|| Error::TrustStore("certutil not found".to_string()))?;
let output = Command::new(&certutil_path)
.args(args)
.output()
.map_err(|e| Error::CommandFailed(format!("Failed to execute certutil: {}", e)))?;
#[cfg(unix)]
{
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("SEC_ERROR_READ_ONLY") {
let output = Command::new("sudo")
.arg(&certutil_path)
.args(args)
.output()
.map_err(|e| {
Error::CommandFailed(format!(
"Failed to execute certutil with sudo: {}",
e
))
})?;
return Ok(output);
}
}
}
Ok(output)
}
}
impl TrustStore for NssTrustStore {
fn check(&self) -> Result<bool> {
if !Self::has_certutil() {
return Ok(false);
}
let profiles = Self::find_nss_profiles();
if profiles.is_empty() {
return Ok(false);
}
let mut success = true;
for (db_type, profile_path) in profiles {
let db_arg = format!("{}:{}", db_type, profile_path.display());
let args = vec!["-V", "-d", &db_arg, "-u", "L", "-n", &self.unique_name];
match Self::exec_certutil(&args) {
Ok(output) => {
if !output.status.success() {
success = false;
}
}
Err(_) => {
success = false;
}
}
}
Ok(success)
}
fn install(&self) -> Result<()> {
if !Self::has_certutil() {
return Err(Error::TrustStore(
"certutil not found. Please install NSS tools.".to_string(),
));
}
let profiles = Self::find_nss_profiles();
if profiles.is_empty() {
return Err(Error::TrustStore(
"No NSS security databases found. Please start Firefox at least once.".to_string(),
));
}
let cert_path_str = self
.cert_path
.to_str()
.ok_or_else(|| Error::TrustStore("Invalid certificate path".to_string()))?;
for (db_type, profile_path) in &profiles {
let db_arg = format!("{}:{}", db_type, profile_path.display());
let args = vec![
"-A",
"-d",
&db_arg,
"-t",
"C,,",
"-n",
&self.unique_name,
"-i",
cert_path_str,
];
let output = Self::exec_certutil(&args)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::TrustStore(format!(
"Failed to install certificate in NSS database {}: {}",
profile_path.display(),
stderr
)));
}
}
if !self.check()? {
return Err(Error::TrustStore(
"Certificate installation verification failed. Please report this issue."
.to_string(),
));
}
Ok(())
}
fn uninstall(&self) -> Result<()> {
if !Self::has_certutil() {
return Ok(());
}
let profiles = Self::find_nss_profiles();
if profiles.is_empty() {
return Ok(());
}
for (db_type, profile_path) in &profiles {
let db_arg = format!("{}:{}", db_type, profile_path.display());
let check_args = vec!["-V", "-d", &db_arg, "-u", "L", "-n", &self.unique_name];
match Self::exec_certutil(&check_args) {
Ok(output) => {
if !output.status.success() {
continue;
}
}
Err(_) => {
continue;
}
}
let delete_args = vec!["-D", "-d", &db_arg, "-n", &self.unique_name];
let output = Self::exec_certutil(&delete_args)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!(
"Warning: Failed to remove certificate from NSS database {}: {}",
profile_path.display(),
stderr
);
}
}
Ok(())
}
}