diff --git a/Cargo.lock b/Cargo.lock index d9355b97..d6eaf66a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -826,6 +826,14 @@ name = "radicle-std-ext" version = "0.1.0" source = "git+https://github.com/radicle-dev/radicle-link?tag=cycle/2022-07-12#541a8161cb24c3b7b10d44f958cc5c5ed05cf443" +[[package]] +name = "radicle-tools" +version = "0.2.0" +dependencies = [ + "anyhow", + "radicle", +] + [[package]] name = "rand" version = "0.8.5" diff --git a/Cargo.toml b/Cargo.toml index 139b4f77..64fb76f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["radicle", "radicle-node"] +members = ["radicle", "radicle-node", "radicle-tools"] [patch.crates-io.radicle-git-ext] git = "https://github.com/radicle-dev/radicle-link" diff --git a/radicle-tools/Cargo.toml b/radicle-tools/Cargo.toml new file mode 100644 index 00000000..3a30e69d --- /dev/null +++ b/radicle-tools/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "radicle-tools" +license = "MIT OR Apache-2.0" +version = "0.2.0" +authors = ["Alexis Sellier "] +edition = "2021" + +[dependencies] +anyhow = { version = "1" } + +[dependencies.radicle] +version = "0" +path = "../radicle" + +[[bin]] +name = "rad-init" +path = "src/rad-init.rs" + +[[bin]] +name = "rad-auth" +path = "src/rad-auth.rs" + +[[bin]] +name = "rad-self" +path = "src/rad-self.rs" diff --git a/radicle-tools/src/rad-auth.rs b/radicle-tools/src/rad-auth.rs new file mode 100644 index 00000000..fb5f0c7f --- /dev/null +++ b/radicle-tools/src/rad-auth.rs @@ -0,0 +1,9 @@ +fn main() -> anyhow::Result<()> { + let keypair = radicle::crypto::KeyPair::generate(); + let profile = radicle::Profile::init(keypair)?; + + println!("id: {}", profile.id()); + println!("home: {}", profile.home.display()); + + Ok(()) +} diff --git a/radicle-tools/src/rad-init.rs b/radicle-tools/src/rad-init.rs new file mode 100644 index 00000000..522aaea6 --- /dev/null +++ b/radicle-tools/src/rad-init.rs @@ -0,0 +1,20 @@ +use std::path::Path; + +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 (id, _) = radicle::rad::init( + &repo, + &name, + "", + radicle::git::BranchName::from("master"), + &profile.signer, + &profile.storage, + )?; + + println!("ok: {}", id); + + Ok(()) +} diff --git a/radicle-tools/src/rad-self.rs b/radicle-tools/src/rad-self.rs new file mode 100644 index 00000000..f012247b --- /dev/null +++ b/radicle-tools/src/rad-self.rs @@ -0,0 +1,8 @@ +fn main() -> anyhow::Result<()> { + let profile = radicle::Profile::load()?; + + println!("id: {}", profile.id()); + println!("home: {}", profile.home.display()); + + Ok(()) +} diff --git a/radicle/src/crypto.rs b/radicle/src/crypto.rs index 46829555..f24010be 100644 --- a/radicle/src/crypto.rs +++ b/radicle/src/crypto.rs @@ -116,6 +116,12 @@ impl TryFrom<&[u8]> for Signature { #[serde(into = "String", try_from = "String")] pub struct PublicKey(pub ed25519::PublicKey); +impl PublicKey { + pub fn from_pem(pem: &str) -> Result { + ed25519::PublicKey::from_pem(pem).map(Self) + } +} + /// The private/signing key. pub type SecretKey = ed25519::SecretKey; diff --git a/radicle/src/git.rs b/radicle/src/git.rs index 11d38338..67bfc607 100644 --- a/radicle/src/git.rs +++ b/radicle/src/git.rs @@ -6,6 +6,7 @@ use once_cell::sync::Lazy; use crate::collections::HashMap; use crate::crypto::PublicKey; +use crate::storage; use crate::storage::refs::Refs; use crate::storage::RemoteId; @@ -17,6 +18,7 @@ pub use git_ref_format::{refname, RefStr, RefString}; pub use git_url as url; pub use git_url::Url; pub use radicle_git_ext as ext; +pub use storage::BranchName; /// Default port of the `git` transport protocol. pub const PROTOCOL_PORT: u16 = 9418; diff --git a/radicle/src/keystore.rs b/radicle/src/keystore.rs new file mode 100644 index 00000000..9e7caa17 --- /dev/null +++ b/radicle/src/keystore.rs @@ -0,0 +1,59 @@ +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 crate::crypto::{PublicKey, SecretKey}; + +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<(), io::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<(PublicKey, SecretKey), io::Error> { + let public = fs::read(self.path.join("radicle.pub"))?; + let public = String::from_utf8(public).unwrap(); + let public = PublicKey::from_pem(&public).unwrap(); + + let secret = fs::read(self.path.join("radicle"))?; + let secret = String::from_utf8(secret).unwrap(); + let secret = SecretKey::from_pem(&secret).unwrap(); + + Ok((public, secret)) + } +} diff --git a/radicle/src/lib.rs b/radicle/src/lib.rs index db61136e..716071f1 100644 --- a/radicle/src/lib.rs +++ b/radicle/src/lib.rs @@ -3,9 +3,15 @@ pub mod crypto; pub mod git; pub mod hash; pub mod identity; +pub mod keystore; pub mod node; +pub mod profile; pub mod rad; pub mod serde_ext; pub mod storage; #[cfg(feature = "test")] pub mod test; + +pub use keystore::UnsafeKeystore; +pub use profile::Profile; +pub use storage::git::Storage; diff --git a/radicle/src/profile.rs b/radicle/src/profile.rs new file mode 100644 index 00000000..edd2fa7e --- /dev/null +++ b/radicle/src/profile.rs @@ -0,0 +1,79 @@ +use std::path::PathBuf; +use std::{env, io}; + +use crate::crypto::{KeyPair, PublicKey, SecretKey, Signature, Signer}; +use crate::keystore::UnsafeKeystore; +use crate::storage::git::Storage; + +pub struct UnsafeSigner { + public: PublicKey, + 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)) + } +} + +pub struct Profile { + pub home: PathBuf, + pub signer: UnsafeSigner, + pub storage: Storage, +} + +impl Profile { + pub fn init(keypair: KeyPair) -> 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"))?; + + keystore.put(&signer.public, &signer.secret)?; + + Ok(Profile { + home, + signer, + storage, + }) + } + + pub fn load() -> Result { + let home = self::home()?; + let (public, secret) = UnsafeKeystore::new(&home.join("keys")).get()?; + let signer = UnsafeSigner { public, secret }; + let storage = Storage::open(&home.join("storage"))?; + + Ok(Profile { + home, + signer, + storage, + }) + } + + pub fn id(&self) -> &PublicKey { + self.signer.public_key() + } +} + +/// Get the path to the radicle home folder. +pub fn home() -> Result { + if let Some(home) = env::var_os("RAD_HOME") { + Ok(PathBuf::from(home)) + } else if let Some(home) = env::var_os("HOME") { + Ok(PathBuf::from(home).join(".radicle")) + } else { + Err(io::Error::new( + io::ErrorKind::NotFound, + "Neither `RAD_HOME` nor `HOME` are set", + )) + } +}