confitul 0.1.4

ConfitUL contains utilities for ConfitDB which is an experimental, distributed, real-time database, giving full control on conflict resolution.
Documentation
use crate::crypto::{sign, verify};
use crate::host_id::HostId;
use crate::host_info::HostInfo;
use crate::local_host_options::LocalHostOptions;
use crate::Host;
use ed25519_dalek;
use ed25519_dalek::Keypair;
use rand07::rngs::OsRng;
use serde::{Deserialize, Serialize};
use std::fmt::Formatter;
use url::{ParseError, Url};

/// LocalHost is a host for which this instance of the program
/// is responsible.
///
/// It holds the private key for the host, which
/// should never be transmitted over the wire to any other peer.
///
/// It is serializable because you may want to re-use a host
/// after a program restart, to inform other peers that yes,
/// you are still the same host, after all.
#[derive(Debug, Serialize, Deserialize)]
pub struct LocalHost {
    info: HostInfo,
    keypair: ed25519_dalek::Keypair,
}

impl LocalHost {
    /// Create a new local host.
    ///
    /// Parameters typically come from a UI or config, user specifying
    /// what is the name of the host and how it can be reached.
    ///
    /// # Examples
    /// ```
    /// use confitul::LocalHost;
    ///
    /// let lh = LocalHost::new(None).unwrap();
    /// print!("{}", lh);
    /// ```
    pub fn new(options: Option<LocalHostOptions>) -> Result<LocalHost, ParseError> {
        let mut csprng = OsRng {};
        let keypair: Keypair = Keypair::generate(&mut csprng);
        let real_options = options.unwrap_or(LocalHostOptions::default());
        let mut local_host = LocalHost {
            info: HostInfo {
                id: HostId::new(&keypair),
                name: real_options.name,
                description: real_options.description,
                urls: Vec::new(),
                sig: None,
            },
            keypair: keypair,
        };
        local_host.update_urls(&real_options.urls)?; // update_urls will update the sig
        Ok(local_host)
    }

    fn update_sig(&mut self) {
        let sig = sign(&self.keypair, &self.info.content_to_verify());
        self.info.sig = Some(sig);
    }

    /// Sign a message.
    ///
    /// Anything sent over the wire should be signed. Only local hosts
    /// can sign, by design, as the only host you can trust for this
    /// is yourself.
    ///
    /// # Examples
    /// ```
    /// use confitul::LocalHost;
    /// use confitul::Host;
    ///
    /// let local_host = LocalHost::new(None).unwrap();
    /// let msg = "a message".as_bytes();
    /// let sig = local_host.sign_msg(msg);
    /// assert!(matches!(local_host.verify_msg(msg, &sig), Ok(())));
    /// assert!(matches!(local_host.verify_msg(msg, msg), Err(_)));
    /// ```
    pub fn sign_msg(&self, msg: &[u8]) -> Vec<u8> {
        sign(&self.keypair, msg)
    }

    /// Update local host name.
    ///
    /// A special call is needed for this as the signature depends on
    /// the name, so this function ensure that the signature
    /// is updated after the name is modified.
    ///
    /// # Examples
    /// ```
    /// use confitul::LocalHost;
    /// use confitul::Host;
    ///
    /// let mut local_host = LocalHost::new(None).unwrap();
    /// local_host.update_name("another test");
    /// ```
    pub fn update_name(&mut self, name: &str) {
        self.info.name = name.to_string();
        self.update_sig();
    }

    /// Update local host description.
    ///
    /// A special call is needed for this as the signature depends on
    /// the description, so this function ensure that the signature
    /// is updated after the description is modified.
    ///
    /// # Examples
    /// ```
    /// use confitul::LocalHost;
    /// use confitul::Host;
    ///
    /// let mut local_host = LocalHost::new(None).unwrap();
    /// local_host.update_description("another test");
    /// ```
    pub fn update_description(&mut self, description: &str) {
        self.info.description = description.to_string();
        self.update_sig();
    }

    /// Update local host URLs.
    ///
    /// A special call is needed for this as the signature depends on
    /// the URLs, so this function ensure that the signature
    /// is updated after the URLs are modified.
    ///
    /// # Examples
    /// ```
    /// use confitul::LocalHost;
    /// use confitul::Host;
    /// use url::Url;
    ///
    /// let mut local_host = LocalHost::new(None).unwrap();
    /// local_host.update_urls(&vec![String::from("https://a-location"), String::from("https://another-location")]).unwrap();
    /// ```
    pub fn update_urls(&mut self, urls: &Vec<String>) -> Result<(), ParseError> {
        let mut parsed_urls: Vec<Url> = Vec::new();
        for url in urls {
            let parsed_url = Url::parse(url)?;
            parsed_urls.push(parsed_url);
        }
        self.info.urls = parsed_urls;
        self.update_sig();
        Ok(())
    }
}

impl Host for LocalHost {
    fn info(&self) -> &HostInfo {
        &self.info
    }
    fn verify_msg(&self, msg: &[u8], sig: &[u8]) -> Result<(), signature::Error> {
        verify(&self.keypair.public, msg, sig)
    }
}

impl std::fmt::Display for LocalHost {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{{\"type\":\"local\",\"info\":{}}}", self.info)
    }
}

#[cfg(test)]
mod tests {
    use super::Host;
    use super::LocalHost;
    use serde_json;

    #[test]
    fn test_local_host_serde_json() {
        let host = LocalHost::new(None).unwrap();
        let serialized = serde_json::to_string(&host).unwrap();
        let deserialized: LocalHost = serde_json::from_str(&serialized).unwrap();
        assert_eq!(host.info(), deserialized.info());
        let msg = "message".as_bytes();
        let sig = deserialized.sign_msg(msg);
        assert!(matches!(host.verify_msg(msg, &sig), Ok(())));
    }

    #[test]
    fn test_local_host_description_update() {
        use super::Host;
        use super::LocalHost;

        let mut local_host = LocalHost::new(None).unwrap();
        let msg = "message".as_bytes();
        let sig = local_host.sign_msg(msg);

        local_host.update_description("another test");
        assert_eq!("another test", local_host.info().description.as_str());
        assert!(
            matches!(local_host.verify_self(), Ok(())),
            "updating description should update signature as well"
        );

        assert!(
            matches!(local_host.verify_msg(msg, &sig), Ok(())),
            "description update has no impact on previous sig"
        );
    }

    #[test]
    fn test_local_host_urls_update() {
        use super::Host;
        use super::LocalHost;
        use url::Url;

        let mut local_host = LocalHost::new(None).unwrap();
        let msg = "message".as_bytes();
        let sig = local_host.sign_msg(msg);

        local_host
            .update_urls(&vec![
                String::from("https://a-location"),
                String::from("https://another-location"),
            ])
            .unwrap();
        assert_eq!(
            vec![
                Url::parse("https://a-location").unwrap(),
                Url::parse("https://another-location").unwrap()
            ],
            local_host.info().urls
        );
        assert!(
            matches!(local_host.verify_self(), Ok(())),
            "updating URLs should update signature as well"
        );

        assert!(
            matches!(local_host.verify_msg(msg, &sig), Ok(())),
            "URLs update has no impact on previous sig"
        );
    }
}