use std::{
fs::{self, OpenOptions},
io::Write,
path::{Path, PathBuf},
};
use crate::{ConnectionError, util::validation::validate_connection_name};
pub fn store_inline_cert(
connection_name: &str,
cert_type: &str,
pem_data: &str,
) -> Result<PathBuf, ConnectionError> {
let dir = connection_cert_dir(connection_name)?;
fs::create_dir_all(&dir).map_err(|e| {
ConnectionError::VpnFailed(format!(
"cert store: create directory {}: {e}",
dir.display()
))
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&dir, fs::Permissions::from_mode(0o700)).map_err(|e| {
ConnectionError::VpnFailed(format!(
"cert store: chmod directory {}: {e}",
dir.display()
))
})?;
}
let filename = filename_for(cert_type)?;
let path = dir.join(filename);
let tmp_path = dir.join(format!(".{filename}.tmp"));
{
let mut opts = OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut file = opts.open(&tmp_path).map_err(|e| {
ConnectionError::VpnFailed(format!(
"cert store: open {} for write: {e}",
tmp_path.display(),
))
})?;
file.write_all(pem_data.as_bytes()).map_err(|e| {
ConnectionError::VpnFailed(format!("cert store: write {}: {e}", tmp_path.display(),))
})?;
file.sync_all().map_err(|e| {
ConnectionError::VpnFailed(format!("cert store: sync {}: {e}", tmp_path.display()))
})?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&tmp_path, fs::Permissions::from_mode(0o600)).map_err(|e| {
let _ = fs::remove_file(&tmp_path);
ConnectionError::VpnFailed(format!("cert store: chmod {}: {e}", tmp_path.display()))
})?;
}
fs::rename(&tmp_path, &path).map_err(|e| {
let _ = fs::remove_file(&tmp_path);
ConnectionError::VpnFailed(format!(
"cert store: rename {} -> {}: {e}",
tmp_path.display(),
path.display()
))
})?;
path.canonicalize().map_err(|e| {
ConnectionError::VpnFailed(format!("cert store: canonicalize {}: {e}", path.display()))
})
}
pub fn cleanup_certs(connection_name: &str) -> Result<(), ConnectionError> {
let dir = connection_cert_dir(connection_name)?;
match fs::remove_dir_all(&dir) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(ConnectionError::VpnFailed(format!(
"cert store: remove {}: {e}",
dir.display()
))),
}
}
fn xdg_data_home() -> Result<PathBuf, ConnectionError> {
match std::env::var_os("XDG_DATA_HOME") {
Some(p) if !p.is_empty() => Ok(PathBuf::from(p)),
_ => {
let home = std::env::var_os("HOME").ok_or_else(|| {
ConnectionError::VpnFailed(
"cert store: HOME is not set (cannot resolve XDG data directory)".into(),
)
})?;
Ok(Path::new(&home).join(".local/share"))
}
}
}
fn connection_cert_dir(connection_name: &str) -> Result<PathBuf, ConnectionError> {
validate_connection_name(connection_name)?;
if connection_name.contains('/') || connection_name.contains('\\') {
return Err(ConnectionError::InvalidAddress(
"connection name must not contain path separators".into(),
));
}
if connection_name == "." || connection_name == ".." {
return Err(ConnectionError::InvalidAddress(
"invalid connection name".into(),
));
}
Ok(xdg_data_home()?
.join("nmrs")
.join("certs")
.join(connection_name))
}
fn filename_for(cert_type: &str) -> Result<&'static str, ConnectionError> {
match cert_type {
"ca" => Ok("ca.pem"),
"cert" => Ok("cert.pem"),
"key" => Ok("key.pem"),
"ta" => Ok("ta.key"),
"tls-crypt" => Ok("tls-crypt.key"),
_ => Err(ConnectionError::InvalidAddress(format!(
"unknown cert_type {cert_type:?} (expected ca, cert, key, ta, tls-crypt)"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::test_utils::with_fake_xdg;
#[test]
fn write_read_cleanup_cycle() {
with_fake_xdg(|| {
let pem = "-----BEGIN CERTIFICATE-----\nABC\n-----END CERTIFICATE-----\n";
let p = store_inline_cert("MyVPN", "ca", pem).unwrap();
let got = std::fs::read_to_string(&p).unwrap();
assert_eq!(got, pem);
cleanup_certs("MyVPN").unwrap();
assert!(!p.exists());
});
}
#[test]
fn cleanup_nonexistent_is_ok() {
with_fake_xdg(|| {
cleanup_certs("does-not-exist").unwrap();
});
}
#[test]
fn double_cleanup_ok() {
with_fake_xdg(|| {
store_inline_cert("x", "ca", "pem").unwrap();
cleanup_certs("x").unwrap();
cleanup_certs("x").unwrap();
});
}
#[cfg(unix)]
#[test]
fn permissions_are_rw_for_owner_only() {
use std::os::unix::fs::PermissionsExt;
with_fake_xdg(|| {
let p = store_inline_cert("perm", "key", "secret").unwrap();
let mode = std::fs::metadata(&p).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600);
});
}
}