diff --git a/radicle-crypto/src/lib.rs b/radicle-crypto/src/lib.rs index 116a8094..26e51f70 100644 --- a/radicle-crypto/src/lib.rs +++ b/radicle-crypto/src/lib.rs @@ -45,6 +45,23 @@ pub trait Signer: Send + Sync { fn try_sign(&self, msg: &[u8]) -> Result; } +impl Signer for Box +where + T: Signer + ?Sized, +{ + fn public_key(&self) -> &PublicKey { + self.deref().public_key() + } + + fn sign(&self, msg: &[u8]) -> Signature { + self.deref().sign(msg) + } + + fn try_sign(&self, msg: &[u8]) -> Result { + self.deref().try_sign(msg) + } +} + /// Cryptographic signature. #[derive(PartialEq, Eq, Copy, Clone)] pub struct Signature(pub ed25519::Signature); diff --git a/radicle-crypto/src/ssh/agent.rs b/radicle-crypto/src/ssh/agent.rs index 3feafdc1..35bf06a4 100644 --- a/radicle-crypto/src/ssh/agent.rs +++ b/radicle-crypto/src/ssh/agent.rs @@ -1,21 +1,92 @@ -use radicle_ssh::agent::client::AgentClient; -use radicle_ssh::{self as ssh, agent::client::ClientStream}; +use std::ops::{Deref, DerefMut}; +use std::sync::Mutex; + +pub use radicle_ssh::agent::client::AgentClient; +pub use radicle_ssh::agent::client::Error; +pub use radicle_ssh::{self as ssh, agent::client::ClientStream}; -use crate as crypto; use crate::ssh::SecretKey; +use crate::{self as crypto}; +use crate::{PublicKey, Signature, Signer, SignerError}; #[cfg(not(unix))] -use std::net::TcpStream as Stream; +pub use std::net::TcpStream as Stream; #[cfg(unix)] -use std::os::unix::net::UnixStream as Stream; +pub use std::os::unix::net::UnixStream as Stream; -pub fn connect() -> Result, ssh::agent::client::Error> { - Stream::connect_env() +pub struct Agent { + client: AgentClient, } -pub fn register(key: &crypto::SecretKey) -> Result<(), ssh::agent::client::Error> { - let mut agent = self::connect()?; - agent.add_identity(&SecretKey::from(*key), &[])?; +impl Agent { + /// Connect to a running SSH agent. + pub fn connect() -> Result { + Stream::connect_env().map(|client| Self { client }) + } - Ok(()) + /// Register a key with the agent. + pub fn register(&mut self, key: &crypto::SecretKey) -> Result<(), ssh::Error> { + self.client.add_identity(&SecretKey::from(*key), &[]) + } +} + +impl Deref for Agent { + type Target = AgentClient; + + fn deref(&self) -> &Self::Target { + &self.client + } +} + +impl DerefMut for Agent { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.client + } +} + +/// A [`Signer`] that uses `ssh-agent`. +pub struct AgentSigner { + agent: Mutex, + public: PublicKey, +} + +impl AgentSigner { + pub fn new(agent: Agent, public: PublicKey) -> Self { + let agent = Mutex::new(agent); + + Self { agent, public } + } + + pub fn is_ready(&self) -> Result { + let ids = self.agent.lock().unwrap().request_identities()?; + + Ok(ids.contains(&self.public)) + } + + /// Box this signer into a [`Signer`]. + pub fn boxed(self) -> Box { + Box::new(self) + } +} + +impl Signer for AgentSigner { + fn public_key(&self) -> &PublicKey { + &self.public + } + + fn sign(&self, msg: &[u8]) -> Signature { + self.try_sign(msg).unwrap() + } + + fn try_sign(&self, msg: &[u8]) -> Result { + let sig = self + .agent + .lock() + // We'll take our chances here; the worse that can happen is the agent returns an error. + .unwrap_or_else(|e| e.into_inner()) + .sign(&self.public, msg) + .map_err(SignerError::new)?; + + Ok(Signature::from(sig)) + } } diff --git a/radicle-crypto/src/ssh/keystore.rs b/radicle-crypto/src/ssh/keystore.rs index 4d7e8167..5d6ddb94 100644 --- a/radicle-crypto/src/ssh/keystore.rs +++ b/radicle-crypto/src/ssh/keystore.rs @@ -1,11 +1,15 @@ +use std::os::unix::fs::DirBuilderExt; use std::path::{Path, PathBuf}; +use std::{fs, io}; use thiserror::Error; -use crate::{KeyPair, PublicKey, SecretKey}; +use crate::{KeyPair, PublicKey, SecretKey, Signature, Signer, SignerError}; #[derive(Debug, Error)] pub enum Error { + #[error(transparent)] + Io(#[from] io::Error), #[error("ssh keygen: {0}")] Ssh(#[from] ssh_key::Error), #[error("invalid key type, expected ed25519 key")] @@ -28,17 +32,21 @@ impl Keystore { } } - /// Initialize a keystore by generate a key pair and storing the secret and public keys + /// Get the path to the keystore. + pub fn path(&self) -> &Path { + self.path.as_path() + } + + /// Initialize a keystore by generating a key pair and storing the secret and public key /// at the given path. /// - /// The comment is associated with the private key. - /// The passphrase is used to encrypt the private key. - /// - pub fn init(self, comment: &str, passphrase: &str) -> Result { + /// The `comment` is associated with the private key. + /// The `passphrase` is used to encrypt the private key. + pub fn init(&self, comment: &str, passphrase: &str) -> Result { let pair = KeyPair::generate(); - let pair = ssh_key::private::Ed25519Keypair::from_bytes(&*pair)?; - let pair = ssh_key::private::KeypairData::Ed25519(pair); - let secret = ssh_key::PrivateKey::new(pair, comment)?; + let ssh_pair = ssh_key::private::Ed25519Keypair::from_bytes(&*pair)?; + let ssh_pair = ssh_key::private::KeypairData::Ed25519(ssh_pair); + let secret = ssh_key::PrivateKey::new(ssh_pair, comment)?; let secret = secret.encrypt(ssh_key::rand_core::OsRng, passphrase)?; let public = secret.public_key(); let path = self.path.join("radicle"); @@ -47,10 +55,15 @@ impl Keystore { return Err(Error::AlreadyInitialized); } + fs::DirBuilder::new() + .recursive(true) + .mode(0o700) + .create(&self.path)?; + secret.write_openssh_file(&path, ssh_key::LineEnding::default())?; public.write_openssh_file(&path.with_extension("pub"))?; - Ok(self) + Ok(pair.pk.into()) } /// Load the public key from the store. Returns `None` if it wasn't found. @@ -61,10 +74,8 @@ impl Keystore { } let public = ssh_key::PublicKey::read_openssh_file(&path)?; - match public.key_data() { - ssh_key::public::KeyData::Ed25519(ssh_key::public::Ed25519PublicKey(data)) => { - Ok(Some(PublicKey::from(*data))) - } + match public.try_into() { + Ok(public) => Ok(Some(public)), _ => Err(Error::InvalidKeyType), } } @@ -89,6 +100,70 @@ impl Keystore { } } +#[derive(Debug, Error)] +pub enum MemorySignerError { + #[error(transparent)] + Keystore(#[from] Error), + #[error("key not found in '{0}'")] + NotFound(PathBuf), +} + +/// An in-memory signer that keeps its secret key internally +/// so that signing never fails. +/// +/// Can be created from a [`Keystore`] with the [`MemorySigner::load`] function. +#[derive(Debug)] +pub struct MemorySigner { + public: PublicKey, + secret: SecretKey, +} + +impl Signer for MemorySigner { + fn public_key(&self) -> &PublicKey { + &self.public + } + + fn sign(&self, msg: &[u8]) -> Signature { + Signature(self.secret.sign(msg, None)) + } + + fn try_sign(&self, msg: &[u8]) -> Result { + Ok(self.sign(msg)) + } +} + +impl MemorySigner { + /// Load this signer from a keystore, given a secret key passphrase. + pub fn load(keystore: &Keystore, passphrase: &str) -> Result { + let public = keystore + .public_key()? + .ok_or_else(|| MemorySignerError::NotFound(keystore.path().to_path_buf()))?; + let secret = keystore + .secret_key(passphrase)? + .ok_or_else(|| MemorySignerError::NotFound(keystore.path().to_path_buf()))?; + + Ok(Self { public, secret }) + } + + /// Box this signer into a trait object. + pub fn boxed(self) -> Box { + Box::new(self) + } +} + +impl TryFrom for PublicKey { + type Error = Error; + + fn try_from(public: ssh_key::PublicKey) -> Result { + match public.key_data() { + ssh_key::public::KeyData::Ed25519(ssh_key::public::Ed25519PublicKey(data)) => { + Ok(Self::from(*data)) + } + _ => Err(Error::InvalidKeyType), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -98,11 +173,23 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let store = Keystore::new(&tmp.path()); - let store = store.init("test", "hunter").unwrap(); - let public = store.public_key().unwrap().unwrap(); - let secret = store.secret_key("hunter").unwrap().unwrap(); + let public = store.init("test", "hunter").unwrap(); + assert_eq!(public, store.public_key().unwrap().unwrap()); + let secret = store.secret_key("hunter").unwrap().unwrap(); assert_eq!(PublicKey::from(secret.public_key()), public); + store.secret_key("blunder").unwrap_err(); // Wrong passphrase. } + + #[test] + fn test_signer() { + let tmp = tempfile::tempdir().unwrap(); + let store = Keystore::new(&tmp.path()); + + let public = store.init("test", "hunter").unwrap(); + let signer = MemorySigner::load(&store, "hunter").unwrap(); + + assert_eq!(public, *signer.public_key()); + } } diff --git a/radicle-node/src/client.rs b/radicle-node/src/client.rs index f0417f18..58cd5d12 100644 --- a/radicle-node/src/client.rs +++ b/radicle-node/src/client.rs @@ -4,6 +4,8 @@ use crossbeam_channel as chan; use nakamoto_net::{LocalTime, Reactor}; use thiserror::Error; +use radicle::crypto::Signer; + use crate::clock::RefClock; use crate::profile::Profile; use crate::service::routing; @@ -70,7 +72,6 @@ impl Default for Config { pub struct Client { reactor: R, - profile: Profile, handle: chan::Sender, commands: chan::Receiver, @@ -80,7 +81,7 @@ pub struct Client { } impl Client { - pub fn new(profile: Profile) -> Result { + pub fn new() -> Result { let (handle, commands) = chan::unbounded::(); let (shutdown, shutdown_recv) = chan::bounded(1); let (listening_send, listening) = chan::bounded(1); @@ -88,7 +89,6 @@ impl Client { let events = Events {}; Ok(Self { - profile, reactor, handle, commands, @@ -98,13 +98,17 @@ impl Client { }) } - pub fn run(mut self, config: Config) -> Result<(), Error> { + pub fn run( + mut self, + config: Config, + profile: Profile, + signer: G, + ) -> Result<(), Error> { let network = config.service.network; let rng = fastrand::Rng::new(); let time = LocalTime::now(); - let storage = self.profile.storage; - let signer = self.profile.signer; - let node_dir = self.profile.home.join(NODE_DIR); + let storage = profile.storage; + let node_dir = profile.home.join(NODE_DIR); let address_db = node_dir.join(ADDRESS_DB_FILE); let routing_db = node_dir.join(ROUTING_DB_FILE); diff --git a/radicle-node/src/main.rs b/radicle-node/src/main.rs index e09f0ea0..8185a45b 100644 --- a/radicle-node/src/main.rs +++ b/radicle-node/src/main.rs @@ -1,8 +1,8 @@ -use std::thread; -use std::{net, process}; +use std::{env, net, process, thread}; use anyhow::Context as _; +use radicle_node::crypto::ssh::keystore::MemorySigner; use radicle_node::logger; use radicle_node::prelude::Address; use radicle_node::{client, control, service}; @@ -48,8 +48,17 @@ fn main() -> anyhow::Result<()> { let options = Options::from_env()?; let profile = radicle::Profile::load().context("Failed to load node profile")?; - let socket = profile.socket(); - let client = client::Client::::new(profile).context("Failed to initialize client")?; + let node = profile.node(); + let client = client::Client::::new().context("Failed to initialize client")?; + let signer = match profile.signer() { + Ok(signer) => signer.boxed(), + Err(err) => { + let passphrase = env::var("RAD_PASSPHRASE") + .context("Either ssh-agent must be initialized, or `RAD_PASSPHRASE` must be set") + .context(err)?; + MemorySigner::load(&profile.keystore, &passphrase)?.boxed() + } + }; let handle = client.handle(); let config = client::Config { service: service::Config { @@ -59,8 +68,8 @@ fn main() -> anyhow::Result<()> { listen: options.listen, }; - let t1 = thread::spawn(move || control::listen(socket, handle)); - let t2 = thread::spawn(move || client.run(config)); + let t1 = thread::spawn(move || control::listen(node, handle)); + let t2 = thread::spawn(move || client.run(config, profile, signer)); t1.join().unwrap()?; t2.join().unwrap()?; diff --git a/radicle-remote-helper/src/lib.rs b/radicle-remote-helper/src/lib.rs index c189b8aa..1ed458ae 100644 --- a/radicle-remote-helper/src/lib.rs +++ b/radicle-remote-helper/src/lib.rs @@ -4,8 +4,7 @@ use std::{env, io, process}; use thiserror::Error; -use radicle::crypto::ssh; -use radicle::crypto::{PublicKey, Signer}; +use radicle::crypto::PublicKey; use radicle::node::Handle; use radicle::storage::git::transport::local::{Url, UrlError}; use radicle::storage::{ReadRepository, WriteRepository, WriteStorage}; @@ -52,9 +51,7 @@ pub fn run(profile: radicle::Profile) -> Result<(), Box Result<(), Box()? - .contains(&namespace) - { - return Err(Error::KeyNotRegistered(namespace).into()); - } - } + let signer = profile.signer()?; + + Some(signer) + } else { + None + }; println!(); // Empty line signifies connection is established. let mut child = process::Command::new(service) @@ -106,13 +102,13 @@ pub fn run(profile: radicle::Profile) -> Result<(), Box AgentClient { } pub trait ClientStream: Sized + Send + Sync { - /// Send an agent request to through the stream and read the response. + /// Send an agent request through the stream and read the response. fn request(&mut self, req: &[u8]) -> Result; /// How to connect the streaming socket diff --git a/radicle-ssh/src/lib.rs b/radicle-ssh/src/lib.rs index 632188e7..bd07d900 100644 --- a/radicle-ssh/src/lib.rs +++ b/radicle-ssh/src/lib.rs @@ -1,2 +1,4 @@ pub mod agent; pub mod encoding; + +pub use agent::client::Error; diff --git a/radicle-tools/src/rad-agent.rs b/radicle-tools/src/rad-agent.rs index 4400863b..ca066584 100644 --- a/radicle-tools/src/rad-agent.rs +++ b/radicle-tools/src/rad-agent.rs @@ -1,30 +1,39 @@ +use anyhow::anyhow; use radicle::{crypto, crypto::ssh}; use std::io::prelude::*; use std::{env, io}; fn main() -> anyhow::Result<()> { let profile = radicle::Profile::load()?; + let mut agent = ssh::agent::Agent::connect()?; println!("({})", ssh::fmt::key(profile.id())); match env::args().nth(1).as_deref() { Some("add") => { - ssh::agent::register(&profile.signer.secret)?; + let mut passphrase = String::new(); + io::stdin().lock().read_line(&mut passphrase)?; + + let secret = profile + .keystore + .secret_key(&passphrase)? + .ok_or_else(|| anyhow!("Key not found in {:?}", profile.keystore.path()))?; + + agent.register(&secret)?; println!("ok"); } Some("remove") => { - ssh::agent::connect()?.remove_identity(profile.id())?; + agent.remove_identity(profile.id())?; println!("ok"); } Some("remove-all") => { - ssh::agent::connect()?.remove_all_identities()?; + agent.remove_all_identities()?; println!("ok"); } Some("sign") => { let mut stdin = Vec::new(); io::stdin().read_to_end(&mut stdin)?; - let mut agent = ssh::agent::connect()?; let sig = agent.sign(profile.id(), &stdin)?; let sig = crypto::Signature::from(sig); diff --git a/radicle-tools/src/rad-auth.rs b/radicle-tools/src/rad-auth.rs index 04f5642b..878642d9 100644 --- a/radicle-tools/src/rad-auth.rs +++ b/radicle-tools/src/rad-auth.rs @@ -1,16 +1,12 @@ -use radicle::profile; -use radicle::Profile; +use radicle::profile::{Error, Profile}; fn main() -> anyhow::Result<()> { let profile = match Profile::load() { - Ok(v) => v, - Err(profile::Error::NotFound(_)) => { - let keypair = radicle::crypto::KeyPair::generate(); - radicle::crypto::ssh::agent::register(&keypair.sk)?; - radicle::Profile::init(keypair)? - } + Ok(profile) => profile, + Err(Error::NotFound(_)) => Profile::init("radicle")?, Err(err) => anyhow::bail!(err), }; + println!("id: {}", profile.id()); println!("home: {}", profile.home.display()); diff --git a/radicle-tools/src/rad-clone.rs b/radicle-tools/src/rad-clone.rs index 2dba981b..85eb2859 100644 --- a/radicle-tools/src/rad-clone.rs +++ b/radicle-tools/src/rad-clone.rs @@ -7,11 +7,12 @@ use radicle::identity::Id; fn main() -> anyhow::Result<()> { let cwd = Path::new(".").canonicalize()?; let profile = radicle::Profile::load()?; + let signer = profile.signer()?; if let Some(id) = env::args().nth(1) { let id = Id::from_str(&id)?; - let node = profile.node()?; - let repo = radicle::rad::clone(id, &cwd, &profile.signer, &profile.storage, &node)?; + let node = radicle::node::connect(profile.node())?; + let repo = radicle::rad::clone(id, &cwd, &signer, &profile.storage, &node)?; println!( "ok: project {id} cloned into `{}`", diff --git a/radicle-tools/src/rad-init.rs b/radicle-tools/src/rad-init.rs index 5b4e7b88..b0b1f759 100644 --- a/radicle-tools/src/rad-init.rs +++ b/radicle-tools/src/rad-init.rs @@ -1,18 +1,19 @@ use std::path::Path; -use radicle::git; +use radicle::{git, Profile}; fn main() -> anyhow::Result<()> { let cwd = Path::new(".").canonicalize()?; let name = cwd.file_name().unwrap().to_string_lossy().to_string(); let repo = radicle::git::raw::Repository::open(cwd)?; - let profile = radicle::Profile::load()?; + let profile = Profile::load()?; + let signer = profile.signer()?; let (id, _) = radicle::rad::init( &repo, &name, "", git::refname!("master"), - &profile.signer, + &signer, &profile.storage, )?; diff --git a/radicle-tools/src/rad-push.rs b/radicle-tools/src/rad-push.rs index fc11ac95..528bbe19 100644 --- a/radicle-tools/src/rad-push.rs +++ b/radicle-tools/src/rad-push.rs @@ -11,11 +11,12 @@ fn main() -> anyhow::Result<()> { let output = radicle::git::run::<_, _, &str, &str>(&cwd, &["push", "rad"], None)?; println!("{}", output); + let signer = profile.signer()?; let project = profile.storage.repository(id)?; - let sigrefs = project.sign_refs(&profile.signer)?; + let sigrefs = project.sign_refs(&signer)?; let head = project.set_head()?; - profile.node()?.announce_refs(&id)?; + radicle::node::connect(&profile.node())?.announce_refs(&id)?; println!("head: {}", head); println!("ok: {}", sigrefs.signature); diff --git a/radicle/src/identity/project.rs b/radicle/src/identity/project.rs index 2d807ed6..1c0eb73a 100644 --- a/radicle/src/identity/project.rs +++ b/radicle/src/identity/project.rs @@ -486,7 +486,7 @@ impl Identity { #[cfg(test)] mod test { use radicle_crypto::test::signer::MockSigner; - use radicle_crypto::Signer; + use radicle_crypto::Signer as _; use crate::rad; use crate::storage::git::Storage; diff --git a/radicle/src/keystore.rs b/radicle/src/keystore.rs deleted file mode 100644 index ce1429bb..00000000 --- a/radicle/src/keystore.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::fs; -use std::io; -use std::io::Write; -use std::os::unix::fs::DirBuilderExt; -use std::os::unix::prelude::OpenOptionsExt; -use std::path::{Path, PathBuf}; - -use thiserror::Error; - -use crate::crypto; -use crate::crypto::{PublicKey, SecretKey}; - -#[derive(Debug, Error)] -pub enum Error { - #[error(transparent)] - Io(#[from] io::Error), - #[error("invalid key file format: {0}")] - FromUtf8Error(#[from] std::string::FromUtf8Error), - #[error("invalid key format: {0}")] - Crypto(#[from] crypto::Error), -} - -pub struct UnsafeKeystore { - path: PathBuf, -} - -impl UnsafeKeystore { - pub fn new>(path: &P) -> Self { - Self { - path: path.as_ref().to_path_buf(), - } - } - - pub fn put(&mut self, public: &PublicKey, secret: &SecretKey) -> Result<(), Error> { - // TODO: Zeroize secret key. - let public = public.to_pem(); - let secret = secret.to_pem(); - - fs::DirBuilder::new() - .recursive(true) - .mode(0o700) - .create(&self.path)?; - - fs::OpenOptions::new() - .mode(0o644) - .create_new(true) - .write(true) - .open(self.path.join("radicle.pub"))? - .write_all(public.as_bytes())?; - - fs::OpenOptions::new() - .mode(0o600) - .create_new(true) - .write(true) - .open(self.path.join("radicle"))? - .write_all(secret.as_bytes())?; - - Ok(()) - } - - pub fn get(&self) -> Result, Error> { - let public = self.path.join("radicle.pub"); - let secret = self.path.join("radicle"); - if !public.exists() && !secret.exists() { - return Ok(None); - } - - let public = fs::read(public)?; - let public = String::from_utf8(public)?; - let public = PublicKey::from_pem(&public)?; - - let secret = fs::read(secret)?; - let secret = String::from_utf8(secret)?; - let secret = SecretKey::from_pem(&secret)?; - - Ok(Some((public, secret))) - } -} diff --git a/radicle/src/lib.rs b/radicle/src/lib.rs index de16aa0a..913cac12 100644 --- a/radicle/src/lib.rs +++ b/radicle/src/lib.rs @@ -7,7 +7,6 @@ pub mod collections; pub mod git; pub mod hash; pub mod identity; -pub mod keystore; pub mod node; pub mod profile; pub mod rad; @@ -18,6 +17,5 @@ pub mod storage; #[cfg(any(test, feature = "test"))] pub mod test; -pub use keystore::UnsafeKeystore; pub use profile::Profile; pub use storage::git::Storage; diff --git a/radicle/src/node.rs b/radicle/src/node.rs index 88e8ff00..6aed0356 100644 --- a/radicle/src/node.rs +++ b/radicle/src/node.rs @@ -33,19 +33,21 @@ pub trait Handle { fn shutdown(self) -> Result<(), Error>; } -/// Node control socket. +/// Node controller. #[derive(Debug)] -pub struct Connection { +pub struct Node { stream: UnixStream, } -impl Connection { +impl Node { + /// Connect to the node, via the socket at the given path. pub fn connect>(path: P) -> Result { let stream = UnixStream::connect(path).map_err(Error::Connect)?; Ok(Self { stream }) } + /// Call a command on the node. pub fn call( &self, cmd: &str, @@ -57,7 +59,7 @@ impl Connection { } } -impl Handle for Connection { +impl Handle for Node { fn fetch(&self, id: &Id) -> Result<(), Error> { for line in self.call("fetch", id)? { let line = line?; @@ -94,3 +96,8 @@ impl Handle for Connection { todo!(); } } + +/// Connect to the local node. +pub fn connect>(path: P) -> Result { + Node::connect(path) +} diff --git a/radicle/src/profile.rs b/radicle/src/profile.rs index 9deb79d7..0f773177 100644 --- a/radicle/src/profile.rs +++ b/radicle/src/profile.rs @@ -15,8 +15,9 @@ use std::{env, io}; use thiserror::Error; -use crate::crypto::{KeyPair, PublicKey, SecretKey, Signature, Signer, SignerError}; -use crate::keystore::UnsafeKeystore; +use crate::crypto::ssh::agent::{Agent, AgentSigner}; +use crate::crypto::ssh::keystore::Keystore; +use crate::crypto::PublicKey; use crate::node; use crate::storage::git::transport; use crate::storage::git::Storage; @@ -26,87 +27,86 @@ pub enum Error { #[error(transparent)] Io(#[from] io::Error), #[error(transparent)] - KeyStore(#[from] crate::keystore::Error), + Keystore(#[from] crate::crypto::ssh::keystore::Error), #[error("no profile found at the filepath '{0}'")] NotFound(PathBuf), -} - -#[derive(Debug)] -pub struct UnsafeSigner { - pub public: PublicKey, - pub secret: SecretKey, -} - -impl Signer for UnsafeSigner { - fn public_key(&self) -> &PublicKey { - &self.public - } - - fn sign(&self, msg: &[u8]) -> Signature { - Signature(self.secret.sign(msg, None)) - } - - fn try_sign(&self, msg: &[u8]) -> Result { - Ok(self.sign(msg)) - } + #[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)] pub struct Profile { pub home: PathBuf, - pub signer: UnsafeSigner, pub storage: Storage, + pub keystore: Keystore, + pub public_key: PublicKey, } impl Profile { - pub fn init(keypair: KeyPair) -> Result { + pub fn init(passphrase: &str) -> Result { let home = self::home()?; - let mut keystore = UnsafeKeystore::new(&home.join("keys")); - let public = keypair.pk.into(); - let signer = UnsafeSigner { - public, - secret: keypair.sk, - }; let storage = Storage::open(&home.join("storage"))?; + // TODO: This should generate a keypair at the given location. + // For now, we use ssh-keygen? + let keystore = Keystore::new(&home.join("keys")); + let public_key = keystore.init("radicle", passphrase)?; transport::local::register(storage.clone()); - keystore.put(&signer.public, &signer.secret)?; Ok(Profile { home, - signer, storage, + keystore, + public_key, }) } pub fn load() -> Result { let home = self::home()?; - let (public, secret) = UnsafeKeystore::new(&home.join("keys")) - .get()? - .ok_or_else(|| Error::NotFound(home.clone()))?; - let signer = UnsafeSigner { public, secret }; let storage = Storage::open(&home.join("storage"))?; + let keystore = Keystore::new(&home.join("keys")); + let public_key = keystore + .public_key()? + .ok_or_else(|| Error::NotFound(home.clone()))?; transport::local::register(storage.clone()); Ok(Profile { home, - signer, storage, + keystore, + public_key, }) } - /// Return a connection to the locally running node. - pub fn node(&self) -> Result { - node::Connection::connect(self.socket()) + pub fn id(&self) -> &PublicKey { + &self.public_key } - pub fn id(&self) -> &PublicKey { - self.signer.public_key() + pub fn signer(&self) -> Result { + match Agent::connect() { + Ok(agent) => { + let signer = AgentSigner::new(agent, self.public_key); + + if signer.is_ready()? { + Ok(signer) + } 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.join("keys") } /// Get the path to the radicle node socket. - pub fn socket(&self) -> PathBuf { + pub fn node(&self) -> PathBuf { env::var_os("RAD_SOCKET") .map(PathBuf::from) .unwrap_or_else(|| self.home.join("node").join(node::DEFAULT_SOCKET_NAME))