//! Key storage utilities for x0x identity persistence.
//!
//! This module provides serialization and storage functionality for
//! MachineKeypair and AgentKeypair types, enabling persistence of
//! identities across application restarts.
use crate::error::{IdentityError, Result};
use crate::identity::{AgentKeypair, MachineKeypair};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use tokio::fs;
/// Serialized keypair representation for storage.
///
/// Uses raw bytes for efficiency rather than base64 encoding.
#[derive(Serialize, Deserialize)]
struct SerializedKeypair {
/// Raw public key bytes
public_key: Vec<u8>,
/// Raw secret key bytes
secret_key: Vec<u8>,
}
/// Serialize a MachineKeypair to bytes for storage.
///
/// # Arguments
///
/// * `kp` - The MachineKeypair to serialize
///
/// # Returns
///
/// A vector containing the serialized keypair data
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()))
}
/// Deserialize a MachineKeypair from bytes.
///
/// # Arguments
///
/// * `bytes` - The serialized keypair data
///
/// # Returns
///
/// A deserialized MachineKeypair
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)
}
/// Serialize an AgentKeypair to bytes for storage.
///
/// # Arguments
///
/// * `kp` - The AgentKeypair to serialize
///
/// # Returns
///
/// A vector containing the serialized keypair data
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()))
}
/// Deserialize an AgentKeypair from bytes.
///
/// # Arguments
///
/// * `bytes` - The serialized keypair data
///
/// # Returns
///
/// A deserialized AgentKeypair
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)
}
/// x0x configuration directory path.
const X0X_DIR: &str = ".x0x";
/// Machine keypair file name.
const MACHINE_KEY_FILE: &str = "machine.key";
/// Get the x0x configuration directory path.
///
/// # Returns
///
/// The path to the .x0x directory in the user's home directory
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))
}
/// Save a MachineKeypair to the default storage location.
///
/// Stores the keypair in ~/.x0x/machine.key with appropriate
/// file permissions. The directory will be created if it doesn't exist.
///
/// # Arguments
///
/// * `kp` - The MachineKeypair to save
///
/// # Returns
///
/// Ok(()) on success, or an error if the operation fails
pub async fn save_machine_keypair(kp: &MachineKeypair) -> Result<()> {
let dir = x0x_dir().await?;
fs::create_dir_all(&dir)
.await
.map_err(IdentityError::from)?;
let path = dir.join(MACHINE_KEY_FILE);
let bytes = serialize_machine_keypair(kp)?;
fs::write(&path, bytes).await.map_err(IdentityError::from)?;
Ok(())
}
/// Load a MachineKeypair from the default storage location.
///
/// # Returns
///
/// The loaded MachineKeypair, or an error if the file doesn't exist or is invalid
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)
}
/// Check if a machine keypair exists in the default storage location.
///
/// # Returns
///
/// true if the machine key file exists, false otherwise
pub async fn machine_keypair_exists() -> bool {
let Ok(path) = x0x_dir().await else {
return false;
};
path.join(MACHINE_KEY_FILE).exists()
}
/// Save an AgentKeypair to the specified file path.
///
/// # Arguments
///
/// * `kp` - The AgentKeypair to save
/// * `path` - The file path to save to
///
/// # Returns
///
/// Ok(()) on success, or an error if the operation fails
pub async fn save_agent_keypair<P: AsRef<Path>>(kp: &AgentKeypair, path: P) -> Result<()> {
let bytes = serialize_agent_keypair(kp)?;
let parent = path.as_ref().parent().ok_or_else(|| {
IdentityError::from(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"invalid path: missing parent directory",
))
})?;
fs::create_dir_all(parent)
.await
.map_err(IdentityError::from)?;
fs::write(path, bytes).await.map_err(IdentityError::from)?;
Ok(())
}
/// Save a MachineKeypair to the specified file path.
///
/// Creates the parent directory if it doesn't exist.
///
/// # Arguments
///
/// * `kp` - The MachineKeypair to save
/// * `path` - The file path to save to
///
/// # Returns
///
/// `Ok(())` on success, or an error if file I/O fails
pub async fn save_machine_keypair_to<P: AsRef<Path>>(kp: &MachineKeypair, path: P) -> Result<()> {
let bytes = serialize_machine_keypair(kp)?;
// Ensure parent directory exists
if let Some(parent) = path.as_ref().parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(IdentityError::from)?;
}
tokio::fs::write(path, bytes)
.await
.map_err(IdentityError::from)?;
Ok(())
}
/// Load a MachineKeypair from the specified file path.
///
/// # Arguments
///
/// * `path` - The file path to load from
///
/// # Returns
///
/// The loaded MachineKeypair, or an error if the file doesn't exist or is invalid
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)
}
/// Load an AgentKeypair from the specified file path.
///
/// # Arguments
///
/// * `path` - The file path to load from
///
/// # Returns
///
/// The loaded AgentKeypair, or an error if the file doesn't exist or is invalid
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)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::identity::{AgentKeypair, MachineKeypair};
#[tokio::test]
async fn test_keypair_serialization_roundtrip() {
// Test MachineKeypair
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()
);
// Test AgentKeypair
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();
// Save to temp file
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("test_machine.key");
save_machine_keypair_to_path(&keypair, &path).await.unwrap();
// Load and verify
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);
// Initially should not exist
assert!(!machine_keypair_exists_in_dir(temp_dir.path()).await);
// Create a key file
let keypair = MachineKeypair::generate().unwrap();
save_machine_keypair_to_path(&keypair, &path).await.unwrap();
// Should now exist
assert!(machine_keypair_exists_in_dir(temp_dir.path()).await);
}
#[tokio::test]
async fn test_invalid_deserialization() {
// Test with invalid bytes
let result = deserialize_machine_keypair(&[1u8, 2u8, 3u8]).unwrap_err();
assert!(matches!(result, IdentityError::Serialization(_)));
}
// Helper functions for testing (since the main functions use ~/.x0x)
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()
}
}