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> {
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(())
}
}