use std::path::Path;
use aes_gcm::KeyInit;
use chacha20poly1305::{XChaCha20Poly1305, XNonce};
use freenet_stdlib::client_api::DelegateRequest;
use super::*;
const NONCE_SIZE: usize = 24;
const CIPHER_SIZE: usize = 32;
impl ConfigArgs {
pub(super) fn read_secrets(
path_to_key: Option<PathBuf>,
path_to_nonce: Option<PathBuf>,
path_to_cipher: Option<PathBuf>,
) -> std::io::Result<Secrets> {
let transport_keypair = if let Some(ref path_to_key) = path_to_key {
read_transport_keypair(path_to_key)?
} else {
TransportKeypair::new()
};
let nonce = if let Some(ref path_to_nonce) = path_to_nonce {
read_nonce(path_to_nonce)?
} else {
DelegateRequest::DEFAULT_NONCE
};
let cipher = if let Some(ref path_to_cipher) = path_to_cipher {
read_cipher(path_to_cipher)?
} else {
DelegateRequest::DEFAULT_CIPHER
};
Ok(Secrets {
transport_keypair,
transport_keypair_path: path_to_key,
nonce,
nonce_path: path_to_nonce,
cipher,
cipher_path: path_to_cipher,
})
}
}
#[derive(Debug, Default, Clone, clap::Parser, serde::Serialize, serde::Deserialize)]
pub struct SecretArgs {
#[clap(long, value_parser, default_value=None, env = "TRANSPORT_KEYPAIR")]
pub transport_keypair: Option<PathBuf>,
#[clap(long, value_parser, default_value=None, env = "NONCE")]
pub nonce: Option<PathBuf>,
#[clap(long, value_parser, default_value=None, env = "CIPHER")]
pub cipher: Option<PathBuf>,
}
impl SecretArgs {
pub(super) fn build(self, secrets_dir: Option<&Path>) -> std::io::Result<Secrets> {
let (transport_keypair_path, transport_keypair) =
if let Some(ref explicit_path) = self.transport_keypair {
let keypair = read_transport_keypair(explicit_path)?;
(self.transport_keypair, keypair)
} else if let Some(dir) = secrets_dir {
let default_path = dir.join("transport_keypair");
if default_path.exists() {
tracing::info!(
path = %default_path.display(),
"Loading persisted transport keypair"
);
let keypair = read_transport_keypair(&default_path)?;
(Some(default_path), keypair)
} else {
std::fs::create_dir_all(dir)?;
let keypair = TransportKeypair::new();
keypair.save(&default_path)?;
tracing::info!(
path = %default_path.display(),
"Generated and saved new transport keypair"
);
(Some(default_path), keypair)
}
} else {
(None, TransportKeypair::new())
};
let nonce = self.nonce.as_ref().map(read_nonce).transpose()?;
let (nonce_path, nonce) = if let Some(nonce) = nonce {
(self.nonce, nonce)
} else {
(None, DelegateRequest::DEFAULT_NONCE)
};
let cipher = self.cipher.as_ref().map(read_cipher).transpose()?;
let (cipher_path, cipher) = if let Some(cipher) = cipher {
(self.cipher, cipher)
} else {
(None, DelegateRequest::DEFAULT_CIPHER)
};
Ok(Secrets {
transport_keypair,
transport_keypair_path,
nonce,
nonce_path,
cipher,
cipher_path,
})
}
pub(super) fn merge(&mut self, other: Secrets) {
if self.transport_keypair.is_none() {
self.transport_keypair = other.transport_keypair_path;
}
if self.nonce.is_none() {
self.nonce = other.nonce_path;
}
if self.cipher.is_none() {
self.cipher = other.cipher_path;
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Secrets {
#[serde(skip)]
pub transport_keypair: TransportKeypair,
#[serde(rename = "transport_keypair", skip_serializing_if = "Option::is_none")]
pub transport_keypair_path: Option<PathBuf>,
#[serde(skip)]
pub nonce: [u8; 24],
#[serde(rename = "nonce", skip_serializing_if = "Option::is_none")]
pub nonce_path: Option<PathBuf>,
#[serde(skip)]
pub cipher: [u8; 32],
#[serde(rename = "cipher", skip_serializing_if = "Option::is_none")]
pub cipher_path: Option<PathBuf>,
}
#[cfg(test)]
impl Default for Secrets {
fn default() -> Self {
let transport_keypair = TransportKeypair::new();
let nonce = DelegateRequest::DEFAULT_NONCE;
let cipher = DelegateRequest::DEFAULT_CIPHER;
Secrets {
transport_keypair,
transport_keypair_path: None,
nonce,
nonce_path: None,
cipher,
cipher_path: None,
}
}
}
impl Secrets {
#[inline]
pub fn nonce(&self) -> XNonce {
self.nonce.into()
}
#[inline]
pub fn cipher(&self) -> XChaCha20Poly1305 {
XChaCha20Poly1305::new((&self.cipher).into())
}
#[inline]
pub fn transport_keypair(&self) -> &TransportKeypair {
&self.transport_keypair
}
}
fn read_nonce(path_to_nonce: impl AsRef<Path>) -> std::io::Result<[u8; NONCE_SIZE]> {
let path_to_nonce = path_to_nonce.as_ref();
let mut nonce_file = File::open(path_to_nonce).map_err(|e| {
std::io::Error::new(
e.kind(),
format!("Failed to open key file {}: {e}", path_to_nonce.display()),
)
})?;
let mut buf = [0u8; NONCE_SIZE];
nonce_file.read_exact(&mut buf).map_err(|e| {
std::io::Error::new(
e.kind(),
format!("Failed to read key file {}: {e}", path_to_nonce.display()),
)
})?;
Ok::<_, std::io::Error>(buf)
}
fn read_cipher(path_to_cipher: impl AsRef<Path>) -> std::io::Result<[u8; CIPHER_SIZE]> {
let path_to_cipher = path_to_cipher.as_ref();
let mut cipher_file = File::open(path_to_cipher).map_err(|e| {
std::io::Error::new(
e.kind(),
format!("Failed to open key file {}: {e}", path_to_cipher.display()),
)
})?;
let mut buf = [0u8; CIPHER_SIZE];
cipher_file.read_exact(&mut buf).map_err(|e| {
std::io::Error::new(
e.kind(),
format!("Failed to read key file {}: {e}", path_to_cipher.display()),
)
})?;
Ok::<_, std::io::Error>(buf)
}
fn read_transport_keypair(path_to_key: impl AsRef<Path>) -> std::io::Result<TransportKeypair> {
let path = path_to_key.as_ref();
match TransportKeypair::load(path) {
Ok(keypair) => Ok(keypair),
Err(e) => {
if let Ok(content) = std::fs::read_to_string(path) {
if content.trim().starts_with("-----BEGIN") {
tracing::warn!(
path = %path.display(),
"Found RSA PEM key (legacy format). Generating new X25519 keypair. \
The old key file will be overwritten."
);
let keypair = TransportKeypair::new();
keypair.save(path)?;
if let Some(parent) = path.parent() {
let filename = path.file_name().and_then(|f| f.to_str()).unwrap_or("");
let public_filename = filename.replace("private", "public");
let public_key_path = parent.join(&public_filename);
if public_key_path.exists() {
if let Err(e) = keypair.public().save(&public_key_path) {
tracing::warn!(
path = %public_key_path.display(),
error = %e,
"Failed to update public key file"
);
} else {
tracing::info!(
path = %public_key_path.display(),
"Updated public key file to X25519 format"
);
}
}
}
return Ok(keypair);
}
}
Err(e)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_from_different_files() {
let transport_keypair = TransportKeypair::new();
let nonce = [0u8; NONCE_SIZE];
let cipher = [0u8; CIPHER_SIZE];
let transport_keypair_file = tempfile::NamedTempFile::new().unwrap();
let mut nonce_file = tempfile::NamedTempFile::new().unwrap();
let mut cipher_file = tempfile::NamedTempFile::new().unwrap();
transport_keypair
.save(transport_keypair_file.path())
.unwrap();
nonce_file.write_all(&nonce).unwrap();
cipher_file.write_all(&cipher).unwrap();
let secrets = Secrets {
transport_keypair,
transport_keypair_path: Some(transport_keypair_file.path().to_path_buf()),
nonce,
nonce_path: Some(nonce_file.path().to_path_buf()),
cipher,
cipher_path: Some(cipher_file.path().to_path_buf()),
};
let secret_args = SecretArgs {
transport_keypair: Some(transport_keypair_file.path().to_path_buf()),
nonce: Some(nonce_file.path().to_path_buf()),
cipher: Some(cipher_file.path().to_path_buf()),
};
let loaded_secrets = secret_args.build(None).unwrap();
assert_eq!(secrets, loaded_secrets);
}
#[test]
fn test_load_default() {
let secret_args = SecretArgs::default();
let loaded_secrets = secret_args.build(None).unwrap();
assert_eq!(DelegateRequest::DEFAULT_CIPHER, loaded_secrets.cipher);
assert_eq!(DelegateRequest::DEFAULT_NONCE, loaded_secrets.nonce);
}
#[test]
fn test_keypair_auto_persist_and_reload() {
let tmp_dir = tempfile::tempdir().unwrap();
let secrets_dir = tmp_dir.path();
let args1 = SecretArgs::default();
let secrets1 = args1.build(Some(secrets_dir)).unwrap();
let keypair_path = secrets_dir.join("transport_keypair");
assert!(keypair_path.exists(), "keypair file should be created");
assert_eq!(
secrets1.transport_keypair_path.as_deref(),
Some(keypair_path.as_path())
);
let args2 = SecretArgs::default();
let secrets2 = args2.build(Some(secrets_dir)).unwrap();
assert_eq!(
secrets1.transport_keypair.public(),
secrets2.transport_keypair.public(),
"reloaded keypair should have the same public key"
);
assert_eq!(
secrets2.transport_keypair_path.as_deref(),
Some(keypair_path.as_path()),
"reloaded keypair should preserve the path"
);
}
#[test]
fn test_explicit_keypair_overrides_secrets_dir() {
let tmp_dir = tempfile::tempdir().unwrap();
let secrets_dir = tmp_dir.path();
let args_seed = SecretArgs::default();
let seeded = args_seed.build(Some(secrets_dir)).unwrap();
let explicit_file = tempfile::NamedTempFile::new().unwrap();
let different_keypair = TransportKeypair::new();
different_keypair.save(explicit_file.path()).unwrap();
let args = SecretArgs {
transport_keypair: Some(explicit_file.path().to_path_buf()),
..Default::default()
};
let loaded = args.build(Some(secrets_dir)).unwrap();
assert_eq!(
loaded.transport_keypair.public(),
different_keypair.public()
);
assert_ne!(
loaded.transport_keypair.public(),
seeded.transport_keypair.public(),
"explicit path should override auto-persisted keypair"
);
}
}