//! Radicle node profile. //! //! $RAD_HOME/ # Radicle home //! storage/ # Storage root //! zEQNunJUqkNahQ8VvQYuWZZV7EJB/ # Project git repository //! ... # More projects... //! keys/ //! radicle # Secret key (PKCS 8) //! radicle.pub # Public key (PKCS 8) //! node/ //! radicle.sock # Node control socket //! use std::path::{Path, PathBuf}; use std::{fs, io}; use thiserror::Error; use crate::crypto::ssh::agent::Agent; use crate::crypto::ssh::{keystore, Keystore, Passphrase}; use crate::crypto::{PublicKey, Signer}; use crate::node; use crate::prelude::Did; use crate::storage::git::transport; use crate::storage::git::Storage; /// Environment variables used by radicle. pub mod env { pub use std::env::*; /// Path to the radicle home folder. pub const RAD_HOME: &str = "RAD_HOME"; /// Path to the radicle node socket file. pub const RAD_SOCKET: &str = "RAD_SOCKET"; /// Passphrase for the encrypted radicle secret key. pub const RAD_PASSPHRASE: &str = "RAD_PASSPHRASE"; pub fn passphrase() -> Option { let Ok(passphrase) = std::env::var(RAD_PASSPHRASE) else { return None; }; Some(super::Passphrase::from(passphrase)) } } #[derive(Debug, Error)] pub enum Error { #[error(transparent)] Io(#[from] io::Error), #[error(transparent)] Keystore(#[from] keystore::Error), #[error(transparent)] MemorySigner(#[from] keystore::MemorySignerError), #[error("no profile found at the filepath '{0}'")] NotFound(PathBuf), #[error("error connecting to ssh-agent: {0}")] Agent(#[from] crate::crypto::ssh::agent::Error), #[error("profile key `{0}` is not registered with ssh-agent")] KeyNotRegistered(PublicKey), } #[derive(Debug, Clone)] pub struct Profile { pub home: Home, pub storage: Storage, pub keystore: Keystore, pub public_key: PublicKey, } impl Profile { pub fn init(home: Home, passphrase: impl Into) -> Result { let storage = Storage::open(home.storage())?; let keystore = Keystore::new(&home.keys()); let public_key = keystore.init("radicle", passphrase)?; transport::local::register(storage.clone()); Ok(Profile { home, storage, keystore, public_key, }) } pub fn load() -> Result { let home = self::home()?; let storage = Storage::open(home.storage())?; let keystore = Keystore::new(&home.keys()); let public_key = keystore .public_key()? .ok_or_else(|| Error::NotFound(home.path().to_path_buf()))?; transport::local::register(storage.clone()); Ok(Profile { home, storage, keystore, public_key, }) } pub fn id(&self) -> &PublicKey { &self.public_key } pub fn did(&self) -> Did { Did::from(self.public_key) } pub fn signer(&self) -> Result, Error> { if let Some(passphrase) = env::passphrase() { let signer = keystore::MemorySigner::load(&self.keystore, passphrase)?; return Ok(signer.boxed()); } match Agent::connect() { Ok(agent) => { let signer = agent.signer(self.public_key); if signer.is_ready()? { Ok(signer.boxed()) } else { Err(Error::KeyNotRegistered(self.public_key)) } } Err(err) => Err(err.into()), } } /// Return the path to the keys folder. pub fn keys(&self) -> PathBuf { self.home.keys() } /// Get the profile home directory. pub fn home(&self) -> &Path { self.home.path() } /// Get the path to the radicle node socket. pub fn socket(&self) -> PathBuf { self.home.socket() } } /// Get the path to the radicle home folder. pub fn home() -> Result { if let Some(home) = env::var_os(env::RAD_HOME) { Ok(Home::new(PathBuf::from(home))?) } else if let Some(home) = env::var_os("HOME") { Ok(Home::new(PathBuf::from(home).join(".radicle"))?) } else { Err(io::Error::new( io::ErrorKind::NotFound, "Neither `RAD_HOME` nor `HOME` are set", )) } } /// Radicle home. #[derive(Debug, Clone)] pub struct Home { path: PathBuf, } impl TryFrom for Home { type Error = io::Error; fn try_from(home: PathBuf) -> Result { Self::new(home) } } impl Home { /// Creates the Radicle Home directories. /// /// The `home` path is used as the base directory for all /// necessary subdirectories. /// /// If `home` does not already exist then it and any /// subdirectories are created using [`fs::create_dir_all`]. /// /// The `home` path is also canonicalized using [`fs::canonicalize`]. /// /// All necessary subdirectories are also created. pub fn new(home: impl Into) -> Result { let path = home.into(); if !path.exists() { fs::create_dir_all(path.clone())?; } let home = Self { path: path.canonicalize()?, }; for dir in &[home.storage(), home.keys(), home.node()] { if !dir.exists() { fs::create_dir_all(dir)?; } } Ok(home) } pub fn path(&self) -> &Path { self.path.as_path() } pub fn storage(&self) -> PathBuf { self.path.join("storage") } pub fn keys(&self) -> PathBuf { self.path.join("keys") } pub fn node(&self) -> PathBuf { self.path.join("node") } pub fn socket(&self) -> PathBuf { env::var_os(env::RAD_SOCKET) .map(PathBuf::from) .unwrap_or_else(|| self.node().join(node::DEFAULT_SOCKET_NAME)) } } #[cfg(test)] mod test { use std::fs; use std::path::Path; use super::Home; // Checks that if we have: // '/run/user/1000/.tmpqfK6ih/../.tmpqfK6ih/Radicle/Home' // // that it gets normalized to: // '/run/user/1000/.tmpqfK6ih/Radicle/Home' #[test] fn canonicalize_home() { let tmp = tempfile::tempdir().unwrap(); let path = tmp.path().join("Home").join("Radicle"); fs::create_dir_all(path.clone()).unwrap(); let last = tmp.path().components().last().unwrap(); let mut home = Home::new( tmp.path() .join("..") .join(last) .join("Home") .join("Radicle"), ) .unwrap(); if cfg!(target_os = "macos") { home.path = Path::new("/").join(home.path.strip_prefix("/private").unwrap_or(&home.path)) }; assert_eq!(home.path, path); } }