diff --git a/examples/ssb-cli.rs b/examples/ssb-cli.rs index 4e41251..6df35f2 100644 --- a/examples/ssb-cli.rs +++ b/examples/ssb-cli.rs @@ -10,9 +10,13 @@ use async_std::io::{Read, Write}; use async_std::net::TcpStream; use kuska_handshake::async_std::{handshake_client, BoxStream}; +use kuska_ssb::api::{ + ApiHelper, CreateHistoryStreamArgs, CreateStreamArgs, LatestUserMessage, WhoAmI, +}; +use kuska_ssb::discovery::ssb_net_id; use kuska_ssb::feed::{is_privatebox, privatebox_decipher, Feed, Message}; -use kuska_ssb::patchwork::*; -use kuska_ssb::patchwork::{ApiHelper, LatestUserMessage, WhoAmI}; +use kuska_ssb::keystore::from_patchwork_local; +use kuska_ssb::keystore::OwnedIdentity; use kuska_ssb::rpc::{RecvMsg, RequestNo, RpcStream}; type AnyResult = std::result::Result>; @@ -116,8 +120,7 @@ async fn main() -> AnyResult<()> { env_logger::init(); log::set_max_level(log::LevelFilter::max()); - let IdentitySecret { pk, sk, .. } = - IdentitySecret::from_local_config().expect("read local secret"); + let OwnedIdentity { pk, sk, .. } = from_patchwork_local().expect("read local secret"); let mut socket = TcpStream::connect("127.0.0.1:8080").await?; let handshake = handshake_client(&mut socket, ssb_net_id(), pk, sk.clone(), pk).await?; diff --git a/src/patchwork/error.rs b/src/api/error.rs similarity index 100% rename from src/patchwork/error.rs rename to src/api/error.rs diff --git a/src/patchwork/api.rs b/src/api/helper.rs similarity index 100% rename from src/patchwork/api.rs rename to src/api/helper.rs diff --git a/src/patchwork/mod.rs b/src/api/mod.rs similarity index 62% rename from src/patchwork/mod.rs rename to src/api/mod.rs index 9298386..fbb83dc 100644 --- a/src/patchwork/mod.rs +++ b/src/api/mod.rs @@ -1,11 +1,8 @@ -mod api; -mod config; mod error; +mod helper; pub mod msgs; -pub mod pubs; -pub use api::{ +pub use error::{Error, Result}; +pub use helper::{ ApiHelper, ApiMethod, CreateHistoryStreamArgs, CreateStreamArgs, LatestUserMessage, WhoAmI, }; -pub use config::{ssb_net_id, IdentitySecret}; -pub use error::{Error, Result}; diff --git a/src/patchwork/msgs.rs b/src/api/msgs.rs similarity index 100% rename from src/patchwork/msgs.rs rename to src/api/msgs.rs diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 1c9a67f..bd068ed 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -2,4 +2,6 @@ mod error; mod sodium; pub use error::{Error, Result}; -pub use sodium::{ToSodiumObject, ToSsbId}; +pub use sodium::{ + ToSodiumObject, ToSsbId, CURVE_ED25519_SUFFIX, ED25519_SIGNATURE_SUFFIX, SHA256_SUFFIX, +}; diff --git a/src/crypto/sodium.rs b/src/crypto/sodium.rs index 4333da3..4202e50 100644 --- a/src/crypto/sodium.rs +++ b/src/crypto/sodium.rs @@ -4,9 +4,9 @@ use sodiumoxide::crypto::sign::ed25519; use super::error::{Error, Result}; -const CURVE_ED25519_SUFFIX: &str = ".ed25519"; -const ED25519_SIGNATURE_SUFFIX: &str = ".sig.ed25519"; -const SHA256_SUFFIX: &str = ".sha256"; +pub const CURVE_ED25519_SUFFIX: &str = ".ed25519"; +pub const ED25519_SIGNATURE_SUFFIX: &str = ".sig.ed25519"; +pub const SHA256_SUFFIX: &str = ".sha256"; pub trait ToSodiumObject { fn to_ed25519_pk(&self) -> Result; diff --git a/src/discovery/error.rs b/src/discovery/error.rs new file mode 100644 index 0000000..05cb7f3 --- /dev/null +++ b/src/discovery/error.rs @@ -0,0 +1,27 @@ +#[derive(Debug)] +pub enum Error { + ParseInt(std::num::ParseIntError), + InvalidInviteCode, + CryptoFormat(crate::crypto::Error), +} + +impl From for Error { + fn from(err: crate::crypto::Error) -> Self { + Error::CryptoFormat(err) + } +} + +impl From for Error { + fn from(err: std::num::ParseIntError) -> Self { + Error::ParseInt(err) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} +impl std::error::Error for Error {} + +pub type Result = std::result::Result; diff --git a/src/discovery/mod.rs b/src/discovery/mod.rs new file mode 100644 index 0000000..7f3b6a9 --- /dev/null +++ b/src/discovery/mod.rs @@ -0,0 +1,6 @@ +mod error; +mod network; +mod pubs; + +pub use network::ssb_net_id; +pub use pubs::Invite; diff --git a/src/discovery/network.rs b/src/discovery/network.rs new file mode 100644 index 0000000..6f3205c --- /dev/null +++ b/src/discovery/network.rs @@ -0,0 +1,6 @@ +use sodiumoxide::crypto::auth; + +pub const SSB_NET_ID: &str = "d4a1cb88a66f02f8db635ce26441cc5dac1b08420ceaac230839b755845a9ffb"; +pub fn ssb_net_id() -> auth::Key { + auth::Key::from_slice(&hex::decode(SSB_NET_ID).unwrap()).unwrap() +} diff --git a/src/patchwork/pubs.rs b/src/discovery/pubs.rs similarity index 100% rename from src/patchwork/pubs.rs rename to src/discovery/pubs.rs diff --git a/src/feed/message.rs b/src/feed/message.rs index c583ffa..b908061 100644 --- a/src/feed/message.rs +++ b/src/feed/message.rs @@ -7,7 +7,7 @@ use sodiumoxide::crypto::sign::ed25519; use super::error::{Error, Result}; use super::{ssb_sha256, stringify_json}; use crate::crypto::ToSodiumObject; -use crate::patchwork::IdentitySecret; +use crate::keystore::OwnedIdentity; use sodiumoxide::crypto::hash::sha256; const MSG_PREVIOUS: &str = "previous"; @@ -58,7 +58,7 @@ pub struct Message { } impl Message { - pub fn sign(prev: Option<&Message>, identity: &IdentitySecret, content: Value) -> Result { + pub fn sign(prev: Option<&Message>, identity: &OwnedIdentity, content: Value) -> Result { let mut value: serde_json::Map = serde_json::Map::new(); if let Some(prev) = prev { value.insert( @@ -206,7 +206,7 @@ mod test { #[test] fn test_sign_verify() -> Result<()> { let content = Value::String("thistest".to_string()); - let id = IdentitySecret::new(); + let id = OwnedIdentity::new(); let msg1 = Message::sign(None, &id, content.clone())?.to_string(); let msg1 = Message::from_str(&msg1)?; let msg2 = Message::sign(Some(&msg1), &id, content)?.to_string(); diff --git a/src/feed/privatebox.rs b/src/feed/privatebox.rs index c207a62..ea38d50 100644 --- a/src/feed/privatebox.rs +++ b/src/feed/privatebox.rs @@ -125,7 +125,7 @@ fn decipher(ciphertext: &[u8], sk: &SecretKey) -> Result>> { #[cfg(test)] mod test { use super::*; - use crate::patchwork::IdentitySecret; + use crate::keystore::OwnedIdentity; #[test] fn test_msg_cipher_to_one() -> Result<()> { @@ -139,7 +139,7 @@ mod test { #[test] fn test_msg_cipher_to_one_helper() -> Result<()> { - let id = IdentitySecret::new(); + let id = OwnedIdentity::new(); let plaintext = "holar"; let ciphertext = privatebox_cipher(plaintext, &[&id.id])?; assert_eq!(is_privatebox(&ciphertext), true); diff --git a/src/keystore/error.rs b/src/keystore/error.rs new file mode 100644 index 0000000..7f71ead --- /dev/null +++ b/src/keystore/error.rs @@ -0,0 +1,27 @@ +#[derive(Debug)] +pub enum Error { + HomeNotFound, + InvalidConfig, + CryptoFormat(crate::crypto::Error), + Io(std::io::Error), +} +impl From for Error { + fn from(err: crate::crypto::Error) -> Self { + Error::CryptoFormat(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} +impl std::error::Error for Error {} + +pub type Result = std::result::Result; diff --git a/src/keystore/identity.rs b/src/keystore/identity.rs new file mode 100644 index 0000000..8be7520 --- /dev/null +++ b/src/keystore/identity.rs @@ -0,0 +1,20 @@ +use crate::crypto::CURVE_ED25519_SUFFIX; +use sodiumoxide::crypto::sign::ed25519; + +#[derive(Debug, Clone)] +pub struct OwnedIdentity { + pub id: String, + pub pk: ed25519::PublicKey, + pub sk: ed25519::SecretKey, +} + +impl OwnedIdentity { + pub fn new() -> OwnedIdentity { + let (pk, sk) = ed25519::gen_keypair(); + OwnedIdentity { + pk, + sk, + id: format!("@{}{}", base64::encode(&pk), CURVE_ED25519_SUFFIX), + } + } +} diff --git a/src/keystore/mod.rs b/src/keystore/mod.rs new file mode 100644 index 0000000..6d3629f --- /dev/null +++ b/src/keystore/mod.rs @@ -0,0 +1,6 @@ +mod error; +mod identity; +pub mod patchwork; + +pub use identity::OwnedIdentity; +pub use patchwork::{from_patchwork_config, from_patchwork_local}; diff --git a/src/keystore/patchwork.rs b/src/keystore/patchwork.rs new file mode 100644 index 0000000..56e72ad --- /dev/null +++ b/src/keystore/patchwork.rs @@ -0,0 +1,53 @@ +use std::io; +use std::string::ToString; + +use crate::crypto::ToSodiumObject; + +use super::error::{Error, Result}; +use super::OwnedIdentity; + +pub const CURVE_ED25519: &str = "ed25519"; + +#[derive(Deserialize)] +struct JsonSSBSecret { + id: String, + curve: String, + public: String, + private: String, +} + +fn to_ioerr(err: T) -> io::Error { + io::Error::new(io::ErrorKind::Other, err.to_string()) +} + +#[allow(clippy::new_without_default)] + +pub fn from_patchwork_local() -> Result { + let home_dir = dirs::home_dir().ok_or(Error::HomeNotFound)?; + let local_key_file = format!("{}/.ssb/secret", home_dir.to_string_lossy()); + let content = std::fs::read_to_string(local_key_file)?; + Ok(from_patchwork_config(content)?) +} + +pub fn from_patchwork_config>(config: T) -> Result { + // strip all comments + let json = config + .as_ref() + .lines() + .filter(|line| !line.starts_with('#')) + .collect::>() + .join(""); + + // parse json + let secret: JsonSSBSecret = serde_json::from_str(json.as_ref()).map_err(to_ioerr)?; + + if secret.curve != CURVE_ED25519 { + return Err(Error::InvalidConfig); + } + + Ok(OwnedIdentity { + id: secret.id, + pk: secret.public.to_ed25519_pk()?, + sk: secret.private.to_ed25519_sk()?, + }) +} diff --git a/src/lib.rs b/src/lib.rs index b9a8581..89448b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,9 @@ extern crate serde; extern crate async_std; extern crate serde_json; +pub mod api; pub mod crypto; +pub mod discovery; pub mod feed; -pub mod patchwork; +pub mod keystore; pub mod rpc; diff --git a/src/patchwork/config.rs b/src/patchwork/config.rs deleted file mode 100644 index a0e60ce..0000000 --- a/src/patchwork/config.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::io; -use std::string::ToString; - -use sodiumoxide::crypto::auth; -use sodiumoxide::crypto::sign::ed25519; - -use crate::crypto::ToSodiumObject; - -use super::error::{Error, Result}; - -const CURVE_ED25519: &str = "ed25519"; -pub const SSB_NET_ID: &str = "d4a1cb88a66f02f8db635ce26441cc5dac1b08420ceaac230839b755845a9ffb"; - -#[derive(Debug, Clone)] -pub struct IdentitySecret { - pub id: String, - pub pk: ed25519::PublicKey, - pub sk: ed25519::SecretKey, -} - -#[derive(Deserialize)] -struct JsonSSBSecret { - id: String, - curve: String, - public: String, - private: String, -} - -pub fn ssb_net_id() -> auth::Key { - auth::Key::from_slice(&hex::decode(SSB_NET_ID).unwrap()).unwrap() -} - -fn to_ioerr(err: T) -> io::Error { - io::Error::new(io::ErrorKind::Other, err.to_string()) -} - -#[allow(clippy::new_without_default)] -impl IdentitySecret { - pub fn new() -> IdentitySecret { - let (pk, sk) = ed25519::gen_keypair(); - IdentitySecret { - pk, - sk, - id: format!("@{}.{}", base64::encode(&pk), CURVE_ED25519), - } - } - - pub fn from_local_config() -> Result { - let home_dir = dirs::home_dir().ok_or(Error::HomeNotFound)?; - let local_key_file = format!("{}/.ssb/secret", home_dir.to_string_lossy()); - let content = std::fs::read_to_string(local_key_file)?; - Ok(IdentitySecret::from_config(content)?) - } - - pub fn from_config>(config: T) -> Result { - // strip all comments - let json = config - .as_ref() - .lines() - .filter(|line| !line.starts_with('#')) - .collect::>() - .join(""); - - // parse json - let secret: JsonSSBSecret = serde_json::from_str(json.as_ref()).map_err(to_ioerr)?; - - if secret.curve != CURVE_ED25519 { - return Err(Error::InvalidConfig); - } - - Ok(IdentitySecret { - id: secret.id, - pk: secret.public.to_ed25519_pk()?, - sk: secret.private.to_ed25519_sk()?, - }) - } -}