pijul 0.12.2

A patch-based distributed version control system, easy to use and fast. Command-line interface.
use commands::remote::{parse_remote, Remote};
use dirs;
use error::Error;
use libpijul::fs_representation::RepoRoot;
use sequoia_openpgp::parse::Parse;
use sequoia_openpgp::serialize::Serialize;
use sequoia_openpgp::{
    TPK,
    tpk::{CipherSuite, TPKBuilder},
};
use std;
use std::collections::BTreeMap;
use std::fs::{create_dir_all, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use thrussh_keys::key::KeyPair;
use toml;

pub const DEFAULT_REMOTE: &'static str = "remote";

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Repository {
    pub address: String,
    pub port: Option<u16>,
}

impl std::fmt::Display for Repository {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self.port {
            Some(port) => write!(f, "{}:{}", self.address, port),
            None => write!(f, "{}", self.address),
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Meta {
    #[serde(default)]
    pub authors: Vec<String>,
    pub editor: Option<String>,
    pub pull: Option<String>,
    pub push: Option<String>,
    #[serde(default)]
    pub remote: BTreeMap<String, Repository>,
    pub signing_key: Option<String>,
}

impl Meta {
    pub fn load(r: &RepoRoot<impl AsRef<Path>>) -> Result<Meta, Error> {
        let mut str = String::new();
        {
            let mut f = File::open(r.meta_file())?;
            f.read_to_string(&mut str)?;
        }
        Ok(toml::from_str(&str)?)
    }
    pub fn new() -> Meta {
        Meta {
            authors: Vec::new(),
            editor: None,
            pull: None,
            push: None,
            remote: BTreeMap::new(),
            signing_key: None,
        }
    }
    pub fn save(&self, r: &RepoRoot<impl AsRef<Path>>) -> Result<(), Error> {
        let mut f = File::create(r.meta_file())?;
        let s: String = toml::to_string(&self)?;
        f.write_all(s.as_bytes())?;
        Ok(())
    }

    fn parse_remote<'a>(
        &'a self,
        remote: &'a str,
        port: Option<u16>,
        base_path: Option<&'a Path>,
        local_repo_root: Option<&'a Path>,
    ) -> Remote<'a> {
        if let Some(repo) = self.remote.get(remote) {
            parse_remote(
                &repo.address,
                port.or(repo.port),
                base_path,
                local_repo_root,
            )
        } else {
            parse_remote(remote, port, base_path, local_repo_root)
        }
    }

    fn get_remote<'a>(
        &'a self,
        remote: Option<&'a str>,
        default_remote: Option<&'a String>,
        port: Option<u16>,
        base_path: Option<&'a Path>,
        local_repo_root: Option<&'a Path>,
    ) -> Result<Remote<'a>, Error> {
        if let Some(remote) = remote {
            Ok(self.parse_remote(remote, port, base_path, local_repo_root))
        } else if let Some(ref remote) = default_remote {
            Ok(self.parse_remote(remote, port, base_path, local_repo_root))
        } else if self.remote.len() == 1 {
            let remote = self.remote.keys().next().unwrap();
            Ok(self.parse_remote(remote, port, base_path, local_repo_root))
        } else {
            Err(Error::MissingRemoteRepository)
        }
    }

    pub fn pull<'a>(
        &'a self,
        remote: Option<&'a str>,
        port: Option<u16>,
        base_path: Option<&'a Path>,
        local_repo_root: Option<&'a Path>,
    ) -> Result<Remote<'a>, Error> {
        self.get_remote(remote, self.pull.as_ref(), port, base_path, local_repo_root)
    }

    pub fn push<'a>(
        &'a self,
        remote: Option<&'a str>,
        port: Option<u16>,
        base_path: Option<&'a Path>,
        local_repo_root: Option<&'a Path>,
    ) -> Result<Remote<'a>, Error> {
        self.get_remote(remote, self.push.as_ref(), port, base_path, local_repo_root)
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Global {
    pub author: Option<String>,
    pub editor: Option<String>,
    pub signing_key: Option<String>,
}

pub fn global_path() -> Result<PathBuf, Error> {
    if let Ok(var) = std::env::var("PIJUL_CONFIG_DIR") {
        let mut path = PathBuf::new();
        path.push(var);
        Ok(path)
    } else if let Ok(var) = std::env::var("XDG_DATA_HOME") {
        let mut path = PathBuf::new();
        path.push(var);
        path.push("pijul");
        std::fs::create_dir_all(&path)?;
        path.push("config");
        Ok(path)
    } else {
        if let Some(mut path) = dirs::home_dir() {
            path.push(".pijulconfig");
            Ok(path)
        } else {
            Err(Error::NoHomeDir)
        }
    }
}

pub fn generate_ssh_key<P: AsRef<Path>>(
    dot_pijul: P,
    password: Option<(u32, &[u8])>,
) -> Result<(), Error> {
    use thrussh_keys::{encode_pkcs8_pem, encode_pkcs8_pem_encrypted, write_public_key_base64};
    let key = KeyPair::generate_ed25519().unwrap();
    create_dir_all(dot_pijul.as_ref())?;

    let mut f = dot_pijul.as_ref().join("id_ed25519");
    debug!("generate_key: {:?}", f);
    if std::fs::metadata(&f).is_err() {
        let mut f = File::create(&f)?;
        if let Some((rounds, pass)) = password {
            encode_pkcs8_pem_encrypted(&key, pass, rounds, &mut f)?
        } else {
            encode_pkcs8_pem(&key, &mut f)?
        }
        f.flush().unwrap();
        set_key_permissions(&f, "private key file");
    } else {
        return Err(Error::WillNotOverwriteKeyFile { path: f });
    }
    f.set_extension("pub");
    {
        let mut f = File::create(&f)?;
        let pk = key.clone_public_key();
        write_public_key_base64(&mut f, &pk)?;
        f.write(b"\n")?;
        f.flush()?;
        set_key_permissions(&f, "public key file");
    }
    Ok(())
}

pub fn generate_signing_key<P: AsRef<Path>>(
    dot_pijul: P,
    identity: &str,
    password: Option<String>,
) -> Result<PathBuf, Error> {
    create_dir_all(dot_pijul.as_ref())?;

    let mut f = dot_pijul.as_ref().join("signing_secret_key");
    if std::fs::metadata(&f).is_err() {
        let (tpk, sig) = TPKBuilder::new()
            .set_cipher_suite(CipherSuite::Cv25519)
            .add_userid(identity)
            .add_signing_subkey()
            .set_password(password.map(From::from))
            .generate()
            .unwrap();

        let mut keyfile = File::create(&f)?;
        tpk.as_tsk().serialize(&mut keyfile).unwrap();
        set_key_permissions(&keyfile, "signing key file");

        f.set_extension("revocation_cert");
        let mut revoc_cert = File::create(&f)?;
        sig.serialize(&mut revoc_cert).unwrap();
        set_key_permissions(&revoc_cert, "revocation certificate");

        f.set_extension("");
        Ok(f)
    } else {
        return Err(Error::WillNotOverwriteKeyFile { path: f });
    }
}

pub struct SigningKeys {
    pub keys: Vec<sequoia_openpgp::crypto::KeyPair>,
    pub tpk: TPK,
    pub user_id: String,
}

impl SigningKeys {
    pub fn check_author(&self, authors: &[String]) -> Result<(), Error> {
        let author_uid = regex::Regex::new("^[^<>]*<?([^<>]+)>?[^<>]*$").unwrap();
        if self.keys.is_empty() {
            if !authors.iter().any(|auth| {
                auth == &self.user_id
                    || (if let Some(cap) = author_uid.captures(&auth) {
                        cap[1] == self.user_id
                    } else {
                        false
                    })
            }) {
                return Err(Error::NotSigningAuthor);
            }
        }
        Ok(())
    }
}

pub fn load_signing_key<P: AsRef<Path>>(path: P) -> Result<SigningKeys, Error> {
    // let path: PathBuf = path.as_ref().join("signing_secret_key");
    debug!("load_signing_key: {:?}", path.as_ref());
    let tpk = sequoia_openpgp::TPK::from_reader(&std::fs::File::open(&path)?)?;
    use sequoia_openpgp::crypto::KeyPair;
    use sequoia_openpgp::packet::key::SecretKey;
    let mut keys = Vec::new();
    for (_, _, key) in tpk.keys_valid().secret(Some(true)) {
        if let Some(secret) = key.secret() {
            debug!("secret");
            let secret_mpis = match secret {
                SecretKey::Encrypted(secret) => {
                    let password = rpassword::prompt_password_stderr(&format!(
                        "Please enter password to decrypt {:?}/{}: ",
                        path.as_ref(),
                        key
                    ))?;
                    secret.decrypt(key.pk_algo(), &password.into())?
                }
                SecretKey::Unencrypted(mpis) => mpis.clone(),
            };
            keys.push(KeyPair::new(key.clone(), secret_mpis)?)
        } else {
            debug!("no secret");
        }
    }
    debug!("found {:?} keys", keys.len());
    Ok(SigningKeys {
        keys,
        tpk,
        user_id: String::new(),
    })
}

pub fn generate_global_ssh_key() -> Result<(), Error> {
    generate_ssh_key(&global_path()?, None)
}

#[cfg(unix)]
fn set_key_permissions(f: &File, key_type: &str) {
    use std::os::unix::fs::PermissionsExt;
    match f.set_permissions(std::fs::Permissions::from_mode(0o600)) {
        Err(e) => eprintln!(
            "Warning: failed to set permissions on {}: {:?}",
            key_type, e
        ),
        Ok(()) => (),
    };
}

#[cfg(not(unix))]
fn set_key_permissions(_: &File, _: &str) {}

impl Global {
    pub fn new() -> Self {
        Global {
            author: None,
            editor: None,
            signing_key: None,
        }
    }

    pub fn load() -> Result<Self, Error> {
        let mut path = global_path()?;
        path.push("config.toml");
        let mut str = String::new();
        {
            let mut f = File::open(&path)?;
            f.read_to_string(&mut str)?;
        }
        Ok(toml::from_str(&str)?)
    }

    pub fn save(&self) -> Result<(), Error> {
        let mut path = global_path()?;
        create_dir_all(&path)?;
        path.push("config.toml");
        let mut f = File::create(&path)?;
        let s: String = toml::to_string(&self)?;
        f.write_all(s.as_bytes())?;
        Ok(())
    }

    pub fn generate_global_signing_key(
        &mut self,
        identity: &str,
        password: Option<String>,
    ) -> Result<(), Error> {
        if self.signing_key.is_none() {
            self.signing_key = Some(
                generate_signing_key(&global_path()?, identity, password)?
                    .to_str()
                    .unwrap()
                    .to_string(),
            );
            self.save()?;
        }
        Ok(())
    }
}