use crate::error::{IdentityError, Result};
use crate::identity::{AgentCertificate, AgentKeypair, MachineKeypair, UserKeypair};
use serde::{Deserialize, Serialize};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::fs;
#[derive(Serialize, Deserialize)]
struct SerializedKeypair {
public_key: Vec<u8>,
secret_key: Vec<u8>,
}
pub fn serialize_machine_keypair(kp: &MachineKeypair) -> Result<Vec<u8>> {
let data = SerializedKeypair {
public_key: kp.public_key().as_bytes().to_vec(),
secret_key: kp.secret_key().as_bytes().to_vec(),
};
bincode::serialize(&data).map_err(|e| IdentityError::Serialization(e.to_string()))
}
pub fn deserialize_machine_keypair(bytes: &[u8]) -> Result<MachineKeypair> {
let data: SerializedKeypair =
bincode::deserialize(bytes).map_err(|e| IdentityError::Serialization(e.to_string()))?;
MachineKeypair::from_bytes(&data.public_key, &data.secret_key)
}
pub fn serialize_agent_keypair(kp: &AgentKeypair) -> Result<Vec<u8>> {
let data = SerializedKeypair {
public_key: kp.public_key().as_bytes().to_vec(),
secret_key: kp.secret_key().as_bytes().to_vec(),
};
bincode::serialize(&data).map_err(|e| IdentityError::Serialization(e.to_string()))
}
pub fn deserialize_agent_keypair(bytes: &[u8]) -> Result<AgentKeypair> {
let data: SerializedKeypair =
bincode::deserialize(bytes).map_err(|e| IdentityError::Serialization(e.to_string()))?;
AgentKeypair::from_bytes(&data.public_key, &data.secret_key)
}
const X0X_DIR: &str = ".x0x";
const MACHINE_KEY_FILE: &str = "machine.key";
const AGENT_KEY_FILE: &str = "agent.key";
const USER_KEY_FILE: &str = "user.key";
const AGENT_CERT_FILE: &str = "agent.cert";
static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
async fn write_private_file(path: &Path, bytes: Vec<u8>) -> Result<()> {
let parent = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
fs::create_dir_all(parent)
.await
.map_err(IdentityError::from)?;
let file_name = path.file_name().ok_or_else(|| {
IdentityError::from(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"invalid path: missing file name",
))
})?;
let unique = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
let tmp_path = parent.join(format!(
".{}.{}.{}.tmp",
file_name.to_string_lossy(),
std::process::id(),
unique
));
fs::write(&tmp_path, bytes)
.await
.map_err(IdentityError::from)?;
#[cfg(unix)]
{
let mut perms = fs::metadata(&tmp_path)
.await
.map_err(IdentityError::from)?
.permissions();
perms.set_mode(0o600);
fs::set_permissions(&tmp_path, perms)
.await
.map_err(IdentityError::from)?;
}
if let Err(err) = fs::rename(&tmp_path, path).await {
let _ = fs::remove_file(&tmp_path).await;
return Err(IdentityError::from(err));
}
Ok(())
}
async fn x0x_dir() -> Result<std::path::PathBuf> {
let home = dirs::home_dir().ok_or_else(|| {
IdentityError::from(std::io::Error::new(
std::io::ErrorKind::NotFound,
"home directory not found",
))
})?;
Ok(home.join(X0X_DIR))
}
pub async fn save_machine_keypair(kp: &MachineKeypair) -> Result<()> {
let dir = x0x_dir().await?;
let path = dir.join(MACHINE_KEY_FILE);
let bytes = serialize_machine_keypair(kp)?;
write_private_file(&path, bytes).await
}
pub async fn load_machine_keypair() -> Result<MachineKeypair> {
let path = x0x_dir().await?.join(MACHINE_KEY_FILE);
let bytes = fs::read(&path).await.map_err(IdentityError::from)?;
deserialize_machine_keypair(&bytes)
}
pub async fn machine_keypair_exists() -> bool {
let Ok(path) = x0x_dir().await else {
return false;
};
tokio::fs::try_exists(path.join(MACHINE_KEY_FILE))
.await
.unwrap_or(false)
}
pub async fn save_agent_keypair<P: AsRef<Path>>(kp: &AgentKeypair, path: P) -> Result<()> {
let bytes = serialize_agent_keypair(kp)?;
write_private_file(path.as_ref(), bytes).await
}
pub async fn save_machine_keypair_to<P: AsRef<Path> + Clone>(
kp: &MachineKeypair,
path: P,
) -> Result<()> {
let bytes = serialize_machine_keypair(kp)?;
write_private_file(path.as_ref(), bytes).await
}
pub async fn load_machine_keypair_from<P: AsRef<Path>>(path: P) -> Result<MachineKeypair> {
let bytes = tokio::fs::read(path).await.map_err(IdentityError::from)?;
deserialize_machine_keypair(&bytes)
}
pub async fn load_agent_keypair<P: AsRef<Path>>(path: P) -> Result<AgentKeypair> {
let bytes = tokio::fs::read(path).await.map_err(IdentityError::from)?;
deserialize_agent_keypair(&bytes)
}
pub async fn save_agent_keypair_default(kp: &AgentKeypair) -> Result<()> {
let dir = x0x_dir().await?;
let path = dir.join(AGENT_KEY_FILE);
let bytes = serialize_agent_keypair(kp)?;
write_private_file(&path, bytes).await
}
pub async fn load_agent_keypair_default() -> Result<AgentKeypair> {
let path = x0x_dir().await?.join(AGENT_KEY_FILE);
let bytes = fs::read(&path).await.map_err(IdentityError::from)?;
deserialize_agent_keypair(&bytes)
}
pub async fn agent_keypair_exists() -> bool {
let Ok(path) = x0x_dir().await else {
return false;
};
tokio::fs::try_exists(path.join(AGENT_KEY_FILE))
.await
.unwrap_or(false)
}
pub async fn save_agent_keypair_to<P: AsRef<Path> + Clone>(
kp: &AgentKeypair,
path: P,
) -> Result<()> {
let bytes = serialize_agent_keypair(kp)?;
write_private_file(path.as_ref(), bytes).await
}
pub async fn load_agent_keypair_from<P: AsRef<Path>>(path: P) -> Result<AgentKeypair> {
let bytes = tokio::fs::read(path).await.map_err(IdentityError::from)?;
deserialize_agent_keypair(&bytes)
}
pub fn serialize_user_keypair(kp: &UserKeypair) -> Result<Vec<u8>> {
let data = SerializedKeypair {
public_key: kp.public_key().as_bytes().to_vec(),
secret_key: kp.secret_key().as_bytes().to_vec(),
};
bincode::serialize(&data).map_err(|e| IdentityError::Serialization(e.to_string()))
}
pub fn deserialize_user_keypair(bytes: &[u8]) -> Result<UserKeypair> {
let data: SerializedKeypair =
bincode::deserialize(bytes).map_err(|e| IdentityError::Serialization(e.to_string()))?;
UserKeypair::from_bytes(&data.public_key, &data.secret_key)
}
pub async fn save_user_keypair(kp: &UserKeypair) -> Result<()> {
let dir = x0x_dir().await?;
let path = dir.join(USER_KEY_FILE);
let bytes = serialize_user_keypair(kp)?;
write_private_file(&path, bytes).await
}
pub async fn load_user_keypair() -> Result<UserKeypair> {
let path = x0x_dir().await?.join(USER_KEY_FILE);
let bytes = fs::read(&path).await.map_err(IdentityError::from)?;
deserialize_user_keypair(&bytes)
}
pub async fn user_keypair_exists() -> bool {
let Ok(path) = x0x_dir().await else {
return false;
};
tokio::fs::try_exists(path.join(USER_KEY_FILE))
.await
.unwrap_or(false)
}
pub async fn save_user_keypair_to<P: AsRef<Path> + Clone>(kp: &UserKeypair, path: P) -> Result<()> {
let bytes = serialize_user_keypair(kp)?;
write_private_file(path.as_ref(), bytes).await
}
pub async fn load_user_keypair_from<P: AsRef<Path>>(path: P) -> Result<UserKeypair> {
let bytes = tokio::fs::read(path).await.map_err(IdentityError::from)?;
deserialize_user_keypair(&bytes)
}
pub async fn save_agent_certificate(cert: &AgentCertificate) -> Result<()> {
let dir = x0x_dir().await?;
let path = dir.join(AGENT_CERT_FILE);
let bytes =
bincode::serialize(cert).map_err(|e| IdentityError::Serialization(e.to_string()))?;
write_private_file(&path, bytes).await
}
pub async fn load_agent_certificate() -> Result<AgentCertificate> {
let path = x0x_dir().await?.join(AGENT_CERT_FILE);
let bytes = fs::read(&path).await.map_err(IdentityError::from)?;
bincode::deserialize(&bytes).map_err(|e| IdentityError::Serialization(e.to_string()))
}
pub async fn agent_certificate_exists() -> bool {
let Ok(path) = x0x_dir().await else {
return false;
};
tokio::fs::try_exists(path.join(AGENT_CERT_FILE))
.await
.unwrap_or(false)
}
pub async fn save_agent_certificate_to<P: AsRef<Path> + Clone>(
cert: &AgentCertificate,
path: P,
) -> Result<()> {
let bytes =
bincode::serialize(cert).map_err(|e| IdentityError::Serialization(e.to_string()))?;
write_private_file(path.as_ref(), bytes).await
}
pub async fn load_agent_certificate_from<P: AsRef<Path>>(path: P) -> Result<AgentCertificate> {
let bytes = tokio::fs::read(path).await.map_err(IdentityError::from)?;
bincode::deserialize(&bytes).map_err(|e| IdentityError::Serialization(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::{AgentKeypair, MachineKeypair};
#[tokio::test]
async fn test_keypair_serialization_roundtrip() {
let original = MachineKeypair::generate().unwrap();
let serialized = serialize_machine_keypair(&original).unwrap();
let deserialized = deserialize_machine_keypair(&serialized).unwrap();
assert_eq!(original.machine_id(), deserialized.machine_id());
assert_eq!(
original.public_key().as_bytes(),
deserialized.public_key().as_bytes()
);
let original_agent = AgentKeypair::generate().unwrap();
let serialized_agent = serialize_agent_keypair(&original_agent).unwrap();
let deserialized_agent = deserialize_agent_keypair(&serialized_agent).unwrap();
assert_eq!(original_agent.agent_id(), deserialized_agent.agent_id());
assert_eq!(
original_agent.public_key().as_bytes(),
deserialized_agent.public_key().as_bytes()
);
}
#[tokio::test]
async fn test_save_and_load_machine_keypair() {
let keypair = MachineKeypair::generate().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test_machine.key");
save_machine_keypair_to_path(&keypair, &path).await.unwrap();
let loaded = load_machine_keypair_from_path(&path).await.unwrap();
assert_eq!(keypair.machine_id(), loaded.machine_id());
}
#[tokio::test]
async fn test_machine_keypair_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join(MACHINE_KEY_FILE);
assert!(!machine_keypair_exists_in_dir(temp_dir.path()).await);
let keypair = MachineKeypair::generate().unwrap();
save_machine_keypair_to_path(&keypair, &path).await.unwrap();
assert!(machine_keypair_exists_in_dir(temp_dir.path()).await);
}
#[tokio::test]
async fn test_invalid_deserialization() {
let result = deserialize_machine_keypair(&[1u8, 2u8, 3u8]).unwrap_err();
assert!(matches!(result, IdentityError::Serialization(_)));
}
async fn save_machine_keypair_to_path(kp: &MachineKeypair, path: &Path) -> Result<()> {
let bytes = serialize_machine_keypair(kp)?;
let parent = path.parent().unwrap();
fs::create_dir_all(parent)
.await
.map_err(IdentityError::from)?;
fs::write(path, bytes).await.map_err(IdentityError::from)?;
Ok(())
}
async fn load_machine_keypair_from_path(path: &Path) -> Result<MachineKeypair> {
let bytes = fs::read(path).await.map_err(IdentityError::from)?;
deserialize_machine_keypair(&bytes)
}
async fn machine_keypair_exists_in_dir(dir: &Path) -> bool {
dir.join(MACHINE_KEY_FILE).exists()
}
}