commit 9b560174c1bea79d9cb89cbb7fbac297d441aee5 Author: mateoferon Date: Thu Feb 16 17:16:28 2023 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84e6995 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +/zpasspath +/conf +Zmakefile +Cargo.lock +TODO diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3e2c0db --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "zrpass" +version = "0.1.0" +authors = ["zawz "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +openssl = "0.10" +flate2 = { version = "1.0", features = ["zlib-ng"], default-features = false } +rand = "0.8" +tar = "0.4" +clipboard = "0.5" +dotenvy = "0.15" +libc = "0.2" +which = "4.2" +md-5 = "0.10" +redis = "0.21" +fork = "0.1" +ssh2 = "0.9" diff --git a/build-musl.sh b/build-musl.sh new file mode 100644 index 0000000..7927048 --- /dev/null +++ b/build-musl.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -x + +#docker run -i --rm -v $PWD:/build \ +# rust:alpine sh -c ' +# apk add --update --no-cache openssl openssl-dev make cmake gcc +# cd /build +# "$@" +#' sh "$@" +docker run --rm -i -v "$PWD":/home/rust/src \ + -v $PWD/cache/registry:/root/.cargo/registry \ + messense/rust-musl-cross:x86_64-musl "$@" diff --git a/completion/zrpass.bash b/completion/zrpass.bash new file mode 100644 index 0000000..da4fa40 --- /dev/null +++ b/completion/zrpass.bash @@ -0,0 +1,109 @@ +#/usr/bin/env bash + +# $1 = input arg , $@ = completions +_completion_check_expands() { + local arg=$1 + shift 1 + local matching=() + local N=0 + + if [ -z "$arg" ] ; then + local superset=${1:0:1} + for N ; do + [ "${N#"$superset"}" = "$N" ] && return 1 + done + return 0 + fi + + for I ; do + [ "$I" != "${I#"$arg"}" ] && matching[N++]=$I + done + local superset=${matching[0]:0:${#arg}} + [ "${superset#"$arg"}" = "$superset" ] && return 1 + for N in "${matching[@]}" ; do + [ "${N#"$superset"}" = "$N" ] && return 1 + done + return 0 +} + +_zrpass_completion() +{ + local _cw1="list-files cache-clear help rm-file ls tree create get copy set file add new rm mv exec cached rm-cache" + local _cw1_val_all="l ls g get a add n new r rm m mv" + local _cw1_val1="s set f file x copy" + local _cw1_files="rmf rm-file" + local N=0 + local _compwords= + local WORDS=() + local cur=$2 + COMPREPLY=() + + export _ZPASS_USE_CACHE=true + + if { [ "$COMP_CWORD" -eq "2" ] && echo "$_cw1_val1" | grep -qw -- "${COMP_WORDS[1]}" ; } || + { [ "$COMP_CWORD" -gt "1" ] && echo "$_cw1_val_all" | grep -qw -- "${COMP_WORDS[1]}"; } ; then + + zrpass cached || return 0 + + local dir=$2 + [ "${dir}" = "${dir%/}" ] && dir=$(dirname "$2") + + N=0 + for j in $(zrpass ls "$dir" 2>/dev/null) ; do + [ "$j" = "${j%/}" ] && j="$j " + WORDS[N++]=$j + done + + cur=$(basename "$cur") + [ "$2" != "${2%/}" ] && cur="" + + if _completion_check_expands "$cur" "${WORDS[@]}" ; then + N=0 + if [ -n "$cur" ] ; then + for I in "${WORDS[@]}" ; do + [ "$I" != "${I#"$cur"}" ] && COMPREPLY[N++]=$I + done + else + COMPREPLY=("${WORDS[@]}") + fi + N=0 + for I in "${COMPREPLY[@]}" ; do + local tt="${dir%/}/$I" + COMPREPLY[N++]="${tt#./}" + done + else + if [ -n "$cur" ] ; then + N=0 + for I in "${WORDS[@]}" ; do + [ "$I" != "${I#"$cur"}" ] && COMPREPLY[N++]=$I + done + else + COMPREPLY=("${WORDS[@]}") + fi + fi + return + + else + + if [ "$COMP_CWORD" = "1" ] ; then + _compwords="$_cw1" + elif [ "$COMP_CWORD" -gt "1" ] && echo " $_cw1_files " | grep -qF -- " ${COMP_WORDS[1]} " ; then + _compwords=$(zrpass lsf) + fi + for I in $_compwords ; do + WORDS[N++]="$I " + done + + N=0 + if [ -n "$cur" ] ; then + for I in "${WORDS[@]}" ; do + [ "$I" != "${I#"$cur"}" ] && COMPREPLY[N++]=$I + done + else + COMPREPLY=("${WORDS[@]}") + fi + [ ${#COMPREPLY[@]} -eq 0 ] && _filedir + fi +} + +complete -o nospace -F _zrpass_completion -o dirnames zrpass diff --git a/src/archive.rs b/src/archive.rs new file mode 100644 index 0000000..2afa1f6 --- /dev/null +++ b/src/archive.rs @@ -0,0 +1,450 @@ +use std::io::{Read}; +use std::path::{Path,PathBuf}; +use std::borrow::Borrow; + +use std::time::{SystemTime, UNIX_EPOCH}; +use std::ffi::CStr; + +use crate::process::ProcessError; + +#[derive(Debug)] +pub struct Entry { + pub header: tar::Header, + pub contents: Vec, +} + +#[derive(Debug)] +pub struct Archive { + entries: Vec, +} +#[derive(Debug)] +pub struct ArchiveRef<'a> { + entries: Vec<&'a Entry>, +} + +impl Clone for Entry { + fn clone(&self) -> Entry { + Entry { + header: self.header.clone(), + contents: self.contents.clone(), + } + } +} + +fn new_header() -> tar::Header { + let mut header = tar::Header::new_gnu(); + let (uid,gid) = ( unsafe {libc::getuid()} , unsafe {libc::getgid()} ); + let (username,groupname) = ( + unsafe { CStr::from_ptr( (*libc::getpwuid(uid)).pw_name ) } , + unsafe { CStr::from_ptr( (*libc::getgrgid(uid)).gr_name ) } + ); + header.set_uid(uid.into()); + header.set_gid(gid.into()); + header.set_username(username.to_str().unwrap()).unwrap(); + header.set_groupname(groupname.to_str().unwrap()).unwrap(); + header.set_mtime(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()); + header +} + +impl Archive { + pub fn new() -> Archive { + Archive { entries: Vec::new() } + } + + pub fn from(data: &[u8]) -> Archive { + let mut a = tar::Archive::new(data); + let mut ret: Vec = Vec::new(); + for entry in a.entries().unwrap() { + let mut file = entry.unwrap(); + let mut s = Vec::new(); + file.read_to_end(&mut s).unwrap(); + ret.push( Entry { + header: file.header().clone(), + contents: s, + } ); + } + Archive { entries: ret } + } + + pub fn to_ref<'a>(&'a self) -> ArchiveRef<'a> { + let mut ret: Vec<&Entry> = Vec::new(); + for it in &self.entries { + ret.push(&it); + } + ArchiveRef { entries: ret } + } + + pub fn get<'a>(&'a self, path: &Path) -> Option<&'a Entry> { + for it in &self.entries { + if it.header.path().unwrap() == path { + return Some(it); + } + } + None + } + + pub fn append(&mut self, e: Entry) { + self.entries.push(e); + } + + fn erase(&mut self, paths: &[&Path]) { + let mut i = 0; + while i < self.entries.len() { + if is_in(self.entries[i].header.path().unwrap().borrow(), paths) { + self.entries.remove(i); + } + else { + i += 1; + } + } + } + + pub fn link(&mut self, path: &Path, target: &Path) -> Result<&mut Archive,ProcessError> { + // follow symlink + let entry = self.get(path); + if entry.is_some() && entry.unwrap().header.entry_type() != tar::EntryType::Symlink { + return Err(ProcessError::PathAlreadyExists); + } + + let mut folders: Vec<&Path> = Vec::new(); + let mut ignores: Vec<&Path> = Vec::from([path]); + let mut dir = path.parent().unwrap(); + let mut nparents = 0; + while dir != Path::new("") { + nparents += 1; + ignores.push(dir); + folders.push(dir); + dir = dir.parent().unwrap(); + } + self.erase(&ignores[..]); + for it in folders { + let mut header = new_header(); + let s = String::from(it.to_str().unwrap()); + header.set_path(s+"/").unwrap(); + header.set_mode(0o700); + header.set_size(0); + header.set_entry_type(tar::EntryType::Directory); + header.set_cksum(); + + self.append(Entry { header: header, contents: Vec::new() }); + } + let mut header = new_header(); + header.set_path(path).unwrap(); + header.set_mode(0o600); + header.set_size(0); + header.set_entry_type(tar::EntryType::Symlink); + let mut s = String::new(); + for _ in 0..nparents { + s += "../"; + }; + s += target.to_str().unwrap(); + header.set_link_name(s).unwrap(); + header.set_cksum(); + + self.append(Entry {header: header, contents: Vec::new()} ); + Ok(self) + } + + + pub fn set_one(&mut self, path: &Path, val: &[u8]) -> Result<&mut Archive, ProcessError> { + // follow symlink + let entry = self.get(path); + if entry.is_some() && entry.unwrap().header.entry_type() == tar::EntryType::Symlink { + let linkdest = entry.unwrap().header.link_name().unwrap().unwrap(); + return self.set_one(&path.parent().unwrap().join(linkdest), val); + } + + // TODO: calculate file/folder conflicts + // let selfref = self.to_ref(); + // if(selfref.filter(|x: &Entry| is_in(x, folders))) { + // return Err(ProcessError::PathConflict); + // } + + let mut folders: Vec<&Path> = Vec::new(); + let mut ignores: Vec<&Path> = Vec::from([path]); + let mut dir = path.parent().unwrap(); + while dir != Path::new("") { + ignores.push(dir); + folders.push(dir); + dir = dir.parent().unwrap(); + } + self.erase(&ignores[..]); + for it in folders { + let mut header = new_header(); + let s = String::from(it.to_str().unwrap()); + header.set_path(s+"/").unwrap(); + header.set_mode(0o700); + header.set_size(0); + header.set_entry_type(tar::EntryType::Directory); + header.set_cksum(); + + self.append(Entry { header: header, contents: Vec::new() }); + } + let mut header = new_header(); + header.set_path(path).unwrap(); + header.set_mode(0o600); + header.set_size(val.len() as u64); + header.set_cksum(); + + self.append(Entry {header: header, contents: Vec::from(val)} ); + Ok(self) + } + + pub fn mv(&mut self, path: &Path, target: &Path) -> Result<&mut Archive,ProcessError> { + // follow symlink + let entry = self.get(path); + if entry.is_none() { + eprintln!("{} not found", path.to_str().unwrap()); + return Err(ProcessError::PathNotFound); + } + //let srcisdir = entry.header.entry_type() == tar::EntryType::Directory; + let targetisdir = match self.get(target) { + Some(v) => v.header.entry_type() == tar::EntryType::Directory, + None => false, + }; + + for it in &mut self.entries { + let entrypath = it.header.path().unwrap(); + if entrypath.starts_with(path) { + let newpath = match targetisdir { + true => { + target.join(entrypath.strip_prefix(path.parent().unwrap()).unwrap()) + }, + false => target.join(entrypath.strip_prefix(path).unwrap()), + }; + it.header.set_path( newpath ).unwrap(); + it.header.set_cksum(); + } + } + Ok(self) + } +} + +impl ArchiveRef<'_> { + pub fn clone(&self) -> Archive { + let mut e: Vec = Vec::new(); + for it in &self.entries { + e.push((*it).clone()); + } + Archive { entries: e } + } + + pub fn tree<'a>(&'a self, path: &Path) -> Option> { + let mut found: bool = false; + let mut ret: Vec<&Entry> = Vec::new(); + let getall: bool = path == Path::new(".") || path == Path::new("") ; + if getall { + found = true; + } + for entry in &self.entries { + let pathobj = entry.header.path().unwrap(); + if pathobj == path { + found = true; + } + if getall || pathobj.starts_with(path) { + ret.push(entry); + } + } + if found { + return Some(ArchiveRef {entries: ret} ) + } + else { + return None + } + } + + pub fn prune_type<'a>(&'a self, entry_type: tar::EntryType) -> ArchiveRef<'a> { + self.filter(&|x: &Entry| x.header.entry_type() != entry_type ) + } + + pub fn list<'a>(&'a self, path: &Path) -> Option> { + let treelist = self.tree(path); + let path = if path == Path::new(".") { + Path::new("") + } else { + path + }; + match treelist { + Some(p) => { + let mut ret: Vec<&Entry> = Vec::new(); + for entry in p.entries { + if entry.header.path().unwrap().parent().unwrap() == path || (entry.header.path().unwrap() == path && entry.header.entry_type() != tar::EntryType::Directory) { + ret.push(entry); + } + } + Some(ArchiveRef { entries: ret } ) + }, + _ => None, + } + } + + // TODO: change return to [u8] and process to string only on print + pub fn get_one(&self, path: &Path) -> Option> { + for entry in &self.entries { + let pathobj = entry.header.path().unwrap(); + if path == pathobj { + return match entry.header.entry_type() { + // dir/symlink: recurse with modified path + tar::EntryType::Directory => { + self.get_one(&pathobj.join("default")) + }, + tar::EntryType::Symlink => { + let linkdest = entry.header.link_name().unwrap().unwrap(); + self.get_one(&pathobj.parent().unwrap().join(linkdest)) + }, + tar::EntryType::Fifo => None, + tar::EntryType::Char => None, + tar::EntryType::Block => None, + _ => { + // prune trailing newlines + let mut ret = entry.contents.clone(); + while ret.len() > 0 && ret[ret.len()-1] == 0x0A { + ret.pop(); + } + Some( ret ) + } + }; + } + } + None + } + + pub fn get(&self, paths: Vec) -> Vec>> { + paths.iter().map( + |x| self.get_one(x.as_path()) + ).collect() + } + + pub fn filter<'a>(&'a self, fct: &dyn Fn(&Entry) -> bool ) -> ArchiveRef<'a> + { + let mut newvec: Vec<&Entry> = Vec::new(); + for entry in &self.entries { + if fct(entry) { + newvec.push(entry); + } + } + ArchiveRef { entries: newvec } + } + + pub fn map(&self, mut f: F) -> Vec + where + Self: Sized, + F: FnMut(&Entry) -> B, + { + let mut newvec: Vec = Vec::new(); + for entry in &self.entries { + newvec.push(f(entry)); + } + newvec + } + + pub fn export_paths(&self) -> Vec { + self.map(|x| x.header.path().unwrap().to_path_buf() ) + } + + pub fn entries(&self) -> &Vec<&Entry> { + &self.entries + } + + pub fn prune_paths<'a>(&'a self, paths: &[&Path]) -> ArchiveRef<'a> { + self.filter(&|x: &Entry| !is_in(x.header.path().unwrap().borrow(), paths) ) + } + + pub fn filter_paths<'a>(&'a self, paths: &[&Path]) -> ArchiveRef<'a> { + self.filter(&|x: &Entry| is_in(x.header.path().unwrap().borrow(), paths) ) + } + + pub fn build(&self) -> tar::Builder> { + let mut ret = tar::Builder::new(Vec::new()); + for entry in &self.entries { + ret.append(&entry.header, &entry.contents[..]).unwrap(); + } + ret + } + + pub fn sort(&mut self) { + self.entries.sort_by(|a,b| { + let (s1,s2) = ( + String::from(a.header.path().unwrap().to_str().unwrap()), + String::from(b.header.path().unwrap().to_str().unwrap()) + ); + s1.cmp(&s2) + }); + } + + pub fn generate(&self) -> Vec { + self.build().into_inner().unwrap() + } + + pub fn set(&self, values: &[(PathBuf, Vec)]) -> Result { + let mut newa = self.clone(); + for it in values { + match newa.set_one(it.0.as_path(), &it.1[..]) { + Ok(_) => (), + Err(e) => return Err(e), + } + } + Ok(newa) + } + + pub fn link(&self, path: &Path, target: &Path) -> Result { + let mut newa = self.clone(); + match newa.link(path, target) { + Ok(_) => Ok(newa), + Err(e) => Err(e), + } + } + + pub fn mv(&self, path: &Path, target: &Path) -> Result { + let mut newa = self.clone(); + match newa.mv(path, target) { + Ok(_) => Ok(newa), + Err(e) => Err(e), + } + } + + pub fn folder_is_empty(&self, path: &Path) -> bool { + let t = self.filter(&|x: &Entry| x.header.path().unwrap().starts_with(path) && x.header.entry_type() != tar::EntryType::Directory ); + t.entries.len() == 0 + } + + pub fn del<'a>(&'a self, values: &[&Path]) -> ArchiveRef<'a> { + let t = self.filter( &|x: &Entry| { + for it in values { + if x.header.path().unwrap().starts_with(it) { + return false; + } + } + true + } + ); + let mut emptydirs: Vec<&Path> = Vec::new(); + let emptypath = Path::new(""); + for it in values { + let mut dir = it.parent().unwrap(); + while dir != emptypath && t.folder_is_empty(dir) { + emptydirs.push(dir); + dir = dir.parent().unwrap(); + } + } + self.filter(&|x: &Entry| + ! is_in(x.header.path().unwrap().borrow(), &emptydirs[..]) && { + for it in values { + if x.header.path().unwrap().starts_with(it) { + return false; + } + } + true + } + ) + } +} + +pub fn is_in(val: T, paths: &[T]) -> bool { + for it in paths { + if *it == val { + return true; + } + } + false +} diff --git a/src/cache/file.rs b/src/cache/file.rs new file mode 100644 index 0000000..20bef96 --- /dev/null +++ b/src/cache/file.rs @@ -0,0 +1,64 @@ +use std::fs; +use std::time::Duration; +use std::thread::sleep; + +use fork::{fork, Fork, daemon}; + +use crate::Config; + +pub fn get(conf: &Config, key: &str) -> Option { + let cachefile = conf.cache_path.as_path().join(&key); + if cachefile.exists() { + let fullcontents: String = fs::read_to_string(&cachefile).expect("Error on read"); + Some(String::from(fullcontents.trim_end_matches('\n'))) + } else { + None + } +} + +pub fn set(conf: &Config, key: &str, val: &str, time: u64) { + if time == 0 { + return (); + } + match fork() { + Ok(Fork::Parent(_)) => { + () + }, + Ok(Fork::Child) => { + if let Ok(Fork::Child) = daemon(false, true) { + let cachefile = conf.cache_path.as_path().join(&key); + let contents = String::from(val)+"\n"; + fs::write(&cachefile, &contents).expect("Error on write"); + sleep(Duration::from_secs(time)); + if cachefile.is_file() { + let rcon = fs::read_to_string(&cachefile).expect("Error on read"); + if rcon == contents { + fs::remove_file(&cachefile).unwrap(); + } + } + std::process::exit(0); + } + std::process::exit(0); + }, + Err(_) => panic!("fork failed"), + } +} + +pub fn del(conf: &Config, key: &str) { + let cachefile = conf.cache_path.as_path().join(&key); + if cachefile.exists() { + fs::remove_file(&cachefile).unwrap(); + } +} + +pub fn clear(conf: &Config) { + for entry in conf.cache_path.read_dir().expect("read_dir failed") { + if let Ok(entry) = entry { + let path = entry.path(); + let t = String::from(path.file_name().unwrap().to_str().unwrap()); + if path.is_file() && t.ends_with(".key") { + fs::remove_file(&path).unwrap(); + } + } + } +} diff --git a/src/cache/mod.rs b/src/cache/mod.rs new file mode 100644 index 0000000..ae47993 --- /dev/null +++ b/src/cache/mod.rs @@ -0,0 +1,83 @@ +use std::fmt::Write; +use std::process::Command; + +use md5::{Md5, Digest}; + +use crate::config::Config; + +use crate::remote::Protocol; + +pub mod redis; +pub mod file; + +fn md5hash(val: &str) -> Vec { + let mut hasher = Md5::new(); + hasher.update(val); + hasher.finalize()[..].to_vec() +} + +fn getpath(conf: &Config) -> String { + let path = conf.path.join(conf.file().clone() + &conf.extension); + match &conf.remote_addr { + Some(_) => { + conf.remote_port.unwrap().to_string() + ":" + match conf.remote_method { + Protocol::SCP => "scp", + Protocol::SFTP => "sftp", + Protocol::FTPS => "ftps", + Protocol::WebDAV => "webdav", + } + ":" + path.to_str().unwrap() + }, + None => String::from(path.to_str().unwrap()), + } +} + + +fn keyfile(conf: &Config) -> String { + let bytes = md5hash(&(getpath(conf)+"\n")); + let mut s = String::with_capacity(2 * bytes.len() + 4); + for byte in bytes { + write!(s, "{:02x}", byte).unwrap(); + } + s += ".key"; + s +} + +pub fn start_agent(conf: &Config) { + Command::new("redis-server").args( + ["--save", "", "--port", "0", "--unixsocket", conf.cache_sock.to_str().unwrap(), "--unixsocketperm", "700"] + ).status().expect("Execution failure"); +} + +pub fn clear(conf: &Config) { + if redis::available(conf) { + redis::clear(conf); + } + file::clear(conf); +} + +pub fn read(conf: &Config) -> Option { + let keyfile = keyfile(conf); + if redis::available(conf) { + redis::get(conf, &keyfile) + } else { + file::get(conf, &keyfile) + } +} + +pub fn write(conf: &Config, val: &str, cache_time: u64) { + let keyfile = keyfile(conf); + if redis::available(conf) { + redis::set(conf, &keyfile, val, cache_time) + } else { + file::set(conf, &keyfile, val, cache_time) + } +} + +pub fn del(conf: &Config) { + let keyfile = keyfile(conf); + if redis::available(conf) { + redis::del(conf, &keyfile) + } else { + file::del(conf, &keyfile) + } +} diff --git a/src/cache/redis.rs b/src/cache/redis.rs new file mode 100644 index 0000000..0dcf709 --- /dev/null +++ b/src/cache/redis.rs @@ -0,0 +1,48 @@ +use std::fs; +use std::os::unix::fs::FileTypeExt; + +use redis::Commands; + +use crate::config::Config; + +fn open(conf: &Config) -> redis::Connection { + let url = String::from("unix://")+conf.cache_sock.as_path().to_str().unwrap(); + let client = redis::Client::open(&url[..]).unwrap(); + client.get_connection().unwrap() +} + + +pub fn available(conf: &Config) -> bool { + let t = fs::metadata(conf.cache_sock.as_path()); + match t { + Ok(v) => v.file_type().is_socket(), + _ => false, + } +} + +pub fn get(conf: &Config, key: &str) -> Option { + let mut con = open(conf); + match con.get(key) { + Ok(v) => Some(v), + _ => None, + } +} + +pub fn set(conf: &Config, key: &str, val: &str, time: u64) { + if time == 0 { + return (); + } + let mut con = open(conf); + let _ : () = con.set(key, val).unwrap(); + let _ : () = con.expire(key, time.try_into().unwrap()).unwrap(); +} + +pub fn del(conf: &Config, key: &str) { + let mut con = open(conf); + let _ : () = con.del(key).unwrap(); +} + +pub fn clear(conf: &Config) { + let mut con = open(conf); + let _ : () = redis::cmd("FLUSHDB").query(&mut con).unwrap(); +} diff --git a/src/clipboard.rs b/src/clipboard.rs new file mode 100644 index 0000000..f5805ce --- /dev/null +++ b/src/clipboard.rs @@ -0,0 +1,44 @@ +use std::time::Duration; +use std::thread::sleep; +use std::process::{Command}; + +use crate::config::Config; +use crate::archive::Archive; +use fork::{fork, Fork, daemon}; + +use clipboard::{ClipboardProvider,ClipboardContext}; + +pub fn cond_copy(conf: &Config, args: &[String], data: &Archive) -> bool { + if conf.copy_on_edit { + let mut newargs = Vec::new(); + newargs.push(args[0].clone()); + newargs.push(String::from("copy")); + newargs.push(args[2].clone()); + return crate::process::process(conf, &newargs[..], &data.to_ref()).is_ok(); + } + false +} + +pub fn timed_copy(data: &[u8], time: Duration) { + match daemon(false,false) { + Ok(Fork::Parent(_)) => { + () + }, + Ok(Fork::Child) => { + + if std::env::var("XDG_SESSION_TYPE").unwrap_or(String::new()) == "wayland" { + Command::new("wl-copy").args([std::str::from_utf8(data).unwrap()]).status().expect("Execution failure"); + sleep(time); + Command::new("wl-copy").args([""]).status().expect("Execution failure"); + } + else { + let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); + ctx.set_contents(unsafe {std::str::from_utf8_unchecked(data)}.into()).unwrap(); + sleep(time); + ctx.set_contents("".into()).unwrap(); + } + std::process::exit(0); + }, + Err(_) => panic!("fork failed"), + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..550c42a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,244 @@ +use std::env; +use std::path::{Path,PathBuf}; +use std::collections::HashMap; + +use crate::remote; + +// CONSTS + +pub const ENVVARS: [(&str, &str); 20] = [ + ("CONFIGFILE", "Path to the config file to load"), + ("ZPASS_PATH", "Folder containing password files"), + ("ZPASS_CACHE_PATH", "Path used for caching keys"), + ("ZPASS_CACHE_SOCK", "Socket file for redis caching server"), + ("ZPASS_FILE", "File to use for operations"), + ("ZPASS_EXTENSION", "Filename extension"), + ("ZPASS_KEY", "Key to use for encrypting/decrypting files"), + ("ZPASS_KEY_CACHE_TIME", "Time a key stays in cache for decrypting, in seconds"), + ("ZPASS_CLIPBOARD_TIME", "Time until clipboard gets cleared after copy, in seconds"), + ("ZPASS_UNK_OP_CALL", "Operation to call on unrecognized first argument"), + ("ZPASS_RAND_LEN", "Length of random passwords generated by 'new'"), + ("ZPASS_RAND_SET", "Character set to use for 'new' generation"), + ("ZPASS_REMOTE_METHOD", "Method to use for remote file. scp/sftp/ftps/webdav"), + ("ZPASS_REMOTE_ADDR", "Server the file is on"), + ("ZPASS_REMOTE_PORT", "Server port"), + ("ZPASS_SSH_ID", "SSH private key to use for scp/sftp"), + ("ZPASS_REMOTE_USER", "User for login"), + ("ZPASS_REMOTE_PASSWORD", "Password for ftps/webdav login"), + ("ZPASS_PRIORITIZE_CLI", "Key prompt will always be done on CLI if stdin is a tty"), + ("ZPASS_COPY_ON_EDIT", "Set to true, edit operations will copy the affected element"), +]; + +// ENUM/STRUCTS + +#[derive(Debug)] +pub struct EnvConf { + pub env: HashMap, +} + +#[derive(Debug)] +pub struct Config { + pub cache_path: PathBuf, + pub cache_sock: PathBuf, + pub path: PathBuf, + pub ssh_id: Option, + pub copy_on_edit: bool, + pub prioritize_cli: bool, + pub clipboard_time: u64, + pub key_cache_time: u64, + pub rand_len: u32, + pub remote_port: Option, + pub file: String, + pub extension: String, + pub key: Option, + pub rand_set: String, + pub remote_addr: Option, + pub remote_method: remote::Protocol, + pub remote_password: Option, + pub remote_user: Option, + pub unk_op_call: String, +} + +// ENVCONF // +impl EnvConf { + pub fn get<'a>(&'a self, key: &str) -> Option<&'a String> { + self.env.get(&String::from(key)) + } + + pub fn get_str(&self, key: &str) -> String { + self.env.get(&String::from(key)).unwrap().clone() + } + + pub fn get_str_opt(&self, key: &str) -> Option { + match self.env.get(&String::from(key)) { + Some(v) => Some(v.clone()), + _ => None, + } + } + + pub fn get_path(&self, key: &str) -> PathBuf { + Path::new(self.env.get(&String::from(key)).unwrap()).to_path_buf() + } + + pub fn get_bool(&self, key: &str) -> bool { + str_to_bool(&self.get_str_opt(key).unwrap_or(String::from("false"))) + } + + pub fn get_path_opt(&self, key: &str) -> Option { + match self.env.get(&String::from(key)) { + Some(v) => Some(Path::new(v).to_path_buf()), + _ => None, + } + } + + pub fn get_parse(&self, key: &str) -> T { + match self.env.get(&String::from(key)).unwrap().parse::() { + Ok(v) => v, + _ => panic!("Invalid format for {}", key), + } + } + + pub fn get_parse_opt(&self, key: &str) -> Option { + match self.env.get(&String::from(key)) { + Some(s) => match s.parse::() { + Ok(v) => Some(v), + _ => panic!("Invalid format for {}", key), + } + _ => None, + } + } + + pub fn load_from_env() -> EnvConf { + let mut env = HashMap::new(); + for it in ENVVARS { + match env::var(it.0) { + Ok(val) => env.insert(String::from(it.0), val), + _ => None, + }; + } + EnvConf {env: env} + } + + pub fn load_from_file(path: &Path) -> EnvConf { + let iter = dotenvy::from_filename_iter(path).unwrap(); + let mut env = HashMap::new(); + for item in iter { + let (key, val) = item.unwrap(); + env.insert(String::from(key), val); + } + EnvConf {env: env} + } + + pub fn merge(&mut self, conf2: &EnvConf) { + for (key, val) in &conf2.env { + // let (key, val) = it.unwrap(); + if ! self.env.contains_key(key) { + self.env.insert(key.clone(), val.clone()); + } + } + } + + pub fn default() -> EnvConf { + let mut env = HashMap::new(); + env.insert(String::from("CONFIGFILE"), default_xdg_homepath("XDG_CONFIG_HOME", ".config", "/etc")+"/zpass/default.conf" ); + env.insert(String::from("ZPASS_PATH"), default_xdg_homepath("XDG_DATA_HOME", ".local/share", "/usr/local/share")+"/zpass"); + env.insert(String::from("ZPASS_CACHE_PATH"), default_xdg_homepath("XDG_CACHE_HOME", ".cache", "/var/cache")+"/zpass"); + env.insert(String::from("ZPASS_FILE"), String::from("default")); + env.insert(String::from("ZPASS_EXTENSION"), String::from(".tgz.enc")); + env.insert(String::from("ZPASS_KEY_CACHE_TIME"), String::from("60")); + env.insert(String::from("ZPASS_CLIPBOARD_TIME"), String::from("30")); + env.insert(String::from("ZPASS_UNK_OP_CALL"), String::from("copy")); + env.insert(String::from("ZPASS_RAND_LEN"), String::from("20")); + env.insert(String::from("ZPASS_RAND_SET"), String::from("a-zA-Z0-9!-.")); + env.insert(String::from("ZPASS_REMOTE_METHOD"), String::from("scp")); + env.insert(String::from("ZPASS_CACHE_SOCK"), ( + match env::var("XDG_RUNTIME_DIR") { + Ok(val) => val, + //TODO: get current user uid + _ => String::from("/tmp"), + }) + "/zpass.socket"); + EnvConf {env: env} + } +} + +impl Clone for EnvConf { + fn clone(&self) -> EnvConf { + EnvConf {env: self.env.clone()} + } +} + +// CONFIG // + +impl Config { + + pub fn from(envconf: EnvConf) -> Config { + Config { + cache_path: envconf.get_path("ZPASS_CACHE_PATH"), + cache_sock: envconf.get_path("ZPASS_CACHE_SOCK"), + path: envconf.get_path("ZPASS_PATH"), + copy_on_edit: envconf.get_bool("ZPASS_COPY_ON_EDIT"), + prioritize_cli: envconf.get_bool("ZPASS_PRIORITIZE_CLI"), + clipboard_time: envconf.get_parse::("ZPASS_CLIPBOARD_TIME"), + key_cache_time: envconf.get_parse::("ZPASS_KEY_CACHE_TIME"), + rand_len: envconf.get_parse::("ZPASS_RAND_LEN"), + file: envconf.get_str("ZPASS_FILE"), + extension: envconf.get_str("ZPASS_EXTENSION"), + rand_set: envconf.get_str("ZPASS_RAND_SET"), + unk_op_call: envconf.get_str("ZPASS_UNK_OP_CALL"), + remote_method: remote::Protocol::from(&envconf.get_str("ZPASS_REMOTE_METHOD")), + + ssh_id: envconf.get_path_opt("ZPASS_SSH_ID"), + remote_port: envconf.get_parse_opt::("ZPASS_REMOTE_PORT"), + key: envconf.get_str_opt("ZPASS_KEY"), + remote_addr: envconf.get_str_opt("ZPASS_REMOTE_ADDR"), + remote_password: envconf.get_str_opt("ZPASS_REMOTE_PASSWORD"), + remote_user: envconf.get_str_opt("ZPASS_REMOTE_USER"), + + } + } + + pub fn key(&self) -> Option<&String> { + match &self.key { + Some(v) => Some(&v), + None => None, + } + } + + pub fn file(&self) -> &String { + &self.file + } + + pub fn get(fileconf: Option<&Path>) -> Config { + + let mut envconf = EnvConf::load_from_env(); + let fileconf = match fileconf { + Some(file) => EnvConf::load_from_file(file), + _ => EnvConf { env: HashMap::new() }, + }; + let defaults = EnvConf::default(); + envconf.merge(&fileconf); + envconf.merge(&defaults); + + Config::from(envconf) + } +} + +// HELPER FCTS // + +pub fn default_xdg_homepath(s: &str, s2: &str, s3: &str) -> String { + match env::var(s) { + Ok(val) => val, + _ => match env::var("HOME") { + Ok(val) => val + "/" + s2, + _ => String::from(s3), + } + } +} + + +fn str_to_bool(s: &str) -> bool { + match s { + "true"|"True"|"TRUE"|"yes"|"y"|"Yes"|"YES" => true, + _ => false, + } +} diff --git a/src/crypt/compress.rs b/src/crypt/compress.rs new file mode 100644 index 0000000..f5571ba --- /dev/null +++ b/src/crypt/compress.rs @@ -0,0 +1,21 @@ +use flate2::read::{GzDecoder,GzEncoder}; +use flate2::Compression; +use std::io::Read; + +use crate::crypt::DecryptError; + +pub fn decompress(data: &[u8]) -> Result, DecryptError> { + let mut d = GzDecoder::new(data); + let mut s = Vec::new(); + match d.read_to_end(&mut s) { + Ok(_) => Ok(s), + Err(_) => Err(DecryptError::BadFormat), + } +} + +pub fn compress(data: &[u8], level: u32) -> Vec { + let mut d = GzEncoder::new(data, Compression::new(level)); + let mut s = Vec::new(); + d.read_to_end(&mut s).unwrap(); + s +} diff --git a/src/crypt/mod.rs b/src/crypt/mod.rs new file mode 100644 index 0000000..e786a95 --- /dev/null +++ b/src/crypt/mod.rs @@ -0,0 +1,24 @@ +pub mod ssl; +pub mod compress; + +#[derive(Debug)] +pub enum DecryptError { + BadFormat, + BadDecrypt, + Abort, +} + +pub fn decrypt(data: &[u8], password: &str) -> Result, DecryptError> { + let t = ssl::ssl_decrypt(&data, &password.as_bytes()); + if t.is_ok() { + compress::decompress(&t.unwrap()) + } else { + t + } +} + +pub fn encrypt(data: &[u8], password: &str) -> Vec { + let compressed = compress::compress(&data, 6); + let result = ssl::ssl_encrypt(&compressed, &password.as_bytes()); + result +} diff --git a/src/crypt/ssl.rs b/src/crypt/ssl.rs new file mode 100644 index 0000000..cc76345 --- /dev/null +++ b/src/crypt/ssl.rs @@ -0,0 +1,58 @@ +use openssl::symm::{encrypt, decrypt, Cipher}; +use openssl::hash::MessageDigest; +use rand::Rng; + +use crate::crypt::DecryptError; + +fn password_to_key(password: &[u8], salt: &[u8]) -> ([u8; 32], [u8; 16]) { + let iterations = 10_000; + let mut derivedkey = [0u8; 48]; + openssl::pkcs5::pbkdf2_hmac(password, &salt, iterations, MessageDigest::sha256(), &mut derivedkey).unwrap(); + let key = &derivedkey[..32]; + let iv = &derivedkey[32..]; + (key.try_into().expect(""), iv.try_into().expect("")) +} + +fn random_salt() -> [u8; 8] { + let mut rng = rand::thread_rng(); + let data: u64 = rng.gen(); + unsafe { std::mem::transmute(data) } +} + +pub fn ssl_decrypt(data: &[u8], password: &[u8]) -> Result, DecryptError> { + if &data[..8] != "Salted__".as_bytes() { + return Err(DecryptError::BadFormat); + } + let cipher = Cipher::aes_256_cbc(); + + let salt = &data[8..16]; + let data = &data[16..]; + + let (key, iv) = password_to_key(password, &salt); + + let r = decrypt(cipher, &key, Some(&iv), data); + + match r { + Ok(ret) => Ok(ret), + _ => Err(DecryptError::BadDecrypt), + } +} + +pub fn ssl_encrypt(data: &[u8], password: &[u8]) -> Vec { + let salt = random_salt(); + let cipher = Cipher::aes_256_cbc(); + + let (key, iv) = password_to_key(password, &salt); + let r = encrypt(cipher, &key, Some(&iv), data); + + let encrypteddata = match r { + Ok(ret) => ret, + _ => panic!("Unexpected encryption error"), + }; + + let mut ret = Vec::new(); + ret.extend_from_slice("Salted__".as_bytes()); + ret.extend_from_slice(&salt); + ret.extend_from_slice(&encrypteddata); + ret +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a9bd811 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,109 @@ +use std::env; +use std::fs; +use std::path::Path; + +pub mod crypt; +pub mod archive; +pub mod clipboard; +pub mod print; +pub mod config; +pub mod random; +pub mod prompt; +pub mod cache; +pub mod process; +pub mod remote; + +use archive::Archive; +use config::Config; +use crypt::DecryptError; +use process::ProcessError; + + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() <= 1 { + print::help(&args[0]); + std::process::exit(1); + } + + // load config + let configfile = match env::var("CONFIGFILE") { + Ok(val) => val, + _ => config::default_xdg_homepath("XDG_CONFIG_HOME", ".config", "/etc") + "/zpass/default.conf" + }; + let configpath = Path::new(&configfile); + let conf = Config::get( + match configpath.is_file() || configpath.is_symlink() { + true => Some(configpath), + false => None, + } + ); + if conf.remote_addr.is_some() { + todo!(); + } + + // check integrity of file + let filepath = conf.path.join(conf.file().clone() + &conf.extension); + if filepath.exists() && ! filepath.is_file() { + eprintln!("File error"); + eprintln!("{} exists and is not a file", filepath.to_str().unwrap()); + std::process::exit(2); + } + + // non-decrypt operations + let t = process::predecrypt_process(&conf, &args[..]); + if t != None { + return (); + } + + // read and decrypt file + let fullcontents = fs::read(&filepath).expect("Error on read"); + let t = process::try_decrypt(&conf, &fullcontents); + if t.is_err() { + let r = match t.err().unwrap() { + DecryptError::BadFormat => (11, "Bad file format"), + DecryptError::BadDecrypt => (12, "Failed to decrypt file"), + DecryptError::Abort => (13, "Aborted"), + }; + eprintln!("{}", r.1); + std::process::exit(r.0); + } + let (decrypted, mut password) = t.unwrap(); + // generate archive + let archive = Archive::from(&decrypted); + + let r = process::process(&conf, &args[..], &archive.to_ref()); + if r.is_err() { + let status = match r.err().unwrap() { + ProcessError::BadFile => (11, "bad input file"), + ProcessError::BadDecrypt => (12, "cannot decrypt file"), + ProcessError::Abort => (13, "aborted"), + ProcessError::KeysDontMatch => (14, "keys don't match"), + ProcessError::MissingArg => (15, "missing argument(s)"), + ProcessError::PathNotFound => (16, "path not found"), + ProcessError::PathAlreadyExists => (17, "path already exists"), + ProcessError::PathConflict => (18, "path conflict"), + ProcessError::FileNotFound => (19, "file not found"), + ProcessError::FileExists => (20, "file exists"), + ProcessError::FileRead => (21, "file read error"), + ProcessError::FileWrite => (22, "file write error"), + ProcessError::FileDelete => (23, "file delete error"), + ProcessError::RemoteError => (24, "remote error"), + }; + eprintln!("{}", status.1); + std::process::exit(status.0); + } + match r.ok().unwrap() { + Some(d) => { + let mut t = d.0.to_ref(); + t.sort(); + let data = t.generate(); + if d.1.is_some() { + password = d.1.unwrap(); + } + fs::write(&filepath, crypt::encrypt(&data, &password) ).expect("Error on write"); + }, + None => (), + }; +} diff --git a/src/print.rs b/src/print.rs new file mode 100644 index 0000000..78fe5f2 --- /dev/null +++ b/src/print.rs @@ -0,0 +1,154 @@ +use std::path::{PathBuf,Path}; + +use crate::archive::Entry; +use crate::config::{ENVVARS,EnvConf}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub fn print_vec(val: Vec) { + for el in val { + println!("{}", el.to_str().unwrap()); + } +} + +pub struct Color { + color: ColorUnit, + bold: bool, +} + +pub enum ColorUnit { + Reset, + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, +} + +impl ColorUnit { + fn code(&self) -> u8 { + match self { + ColorUnit::Reset => 0, + ColorUnit::Black => 30, + ColorUnit::Red => 31, + ColorUnit::Green => 32, + ColorUnit::Yellow => 33, + ColorUnit::Blue => 34, + ColorUnit::Magenta => 35, + ColorUnit::Cyan => 36, + ColorUnit::White => 37, + } + } +} + +impl Color { + pub fn new(c: ColorUnit, bold: bool) -> Color { + Color { + color: c, + bold: bold, + } + } + + pub fn code(&self) -> String { + String::from("\u{1b}[") + if self.bold { "1;" } else { "" } + &self.color.code().to_string() + "m" + } +} + +pub fn version() { + println!("zrpass {}", VERSION); +} + +pub fn color_str(s: String, c: Color) -> String { + if unsafe {libc::isatty(1)} > 0 { + c.code() + &s + &Color::new(ColorUnit::Reset, false).code() + } + else { + s.clone() + } +} + +pub fn print_paths_fancy(val: &Vec<&Entry>, prefix_strip: &Path) { + for el in val { + let newpath = if el.header.path().unwrap() == prefix_strip { + prefix_strip.to_path_buf() + } else { + el.header.path().unwrap().strip_prefix(prefix_strip).unwrap().to_path_buf() + }; + let strname = String::from(newpath.as_path().to_str().unwrap()); + let outputstring = match el.header.entry_type() { + tar::EntryType::Directory => color_str(strname, Color::new(ColorUnit::Blue, true)) + "/", + tar::EntryType::Symlink => color_str(strname, Color::new(ColorUnit::Cyan, true)), + tar::EntryType::Fifo => color_str(strname, Color::new(ColorUnit::Yellow, false)), + tar::EntryType::Char => color_str(strname, Color::new(ColorUnit::Yellow, true)), + tar::EntryType::Block => color_str(strname, Color::new(ColorUnit::Yellow, true)), + tar::EntryType::Regular => strname, + _ => String::new(), + }; + println!("{}", outputstring); + } +} + +pub fn print_info(val: &Vec<&Entry>) { + for el in val { + let path = el.header.path().unwrap(); + let typestr = match el.header.entry_type() { + tar::EntryType::Directory => "directory", + tar::EntryType::Symlink => "symlink", + tar::EntryType::Fifo => "fifo", + tar::EntryType::Char => "char", + tar::EntryType::Block => "block", + tar::EntryType::Regular => "file", + _ => "unknown", + }; + println!("path: {}", path.display()); + println!("type: {}", typestr); + if el.header.entry_type() == tar::EntryType::Symlink { + println!("dest: {}", el.header.link_name().unwrap().unwrap().display()); + } + println!("size: {}", el.header.size().unwrap()); + } +} + +pub fn help(arg0: &str) { + println!("{} ", arg0); + println!("[Global Operations]:"); + println!(" agent Start caching agent, based on redis server"); + println!(" list-files List eligible files in data path. Shortcut 'lsf'"); + println!(" cache-clear Delete all cached keys. Shortcut 'cc'"); + println!(" help Display help"); + println!(" rm-file Remove files. Shortcut 'rmf'"); + println!("[File Operations]:"); + println!(" ls [path] List contents at given path"); + println!(" tree [path] List all contents"); + println!(" create Create a file or change key"); + println!(" get Get values of targets"); + println!(" copy Copy the target value to clipboard. Shortcut 'x'"); + println!(" set Set directly the value of target"); + println!(" file Set the targetted value from file content"); + println!(" add Prompt for input value on paths"); + println!(" new Generate a random password at target"); + println!(" rm Delete targets"); + println!(" mv Move targets"); + println!(" link Create a link to target"); + println!(" exec Execute the following command inside the archive."); + println!(" cached Returns wether or not a key is currently cached. Shortcut 'ch'"); + println!(" rm-cache Delete the cached key for this file. Shortcut 'rmc'"); + println!(""); + println!("[Config]:"); + + let defaultconf = EnvConf::default(); + let varlen = Vec::from(ENVVARS).iter().max_by(|a, b| a.0.len().cmp(&b.0.len())).unwrap().0.len(); + let vallen = defaultconf.env.iter().max_by(|a, b| a.1.len().cmp(&b.1.len())).unwrap().1.len(); + let desclen = Vec::from(ENVVARS).iter().max_by(|a, b| a.1.len().cmp(&b.1.len())).unwrap().1.len(); + + println!(" {:(s: &'a str, p: &str) -> &'a str { + if s.ends_with(p) { + &s[..s.len() - p.len()] + } else { + s + } +} + +pub fn try_decrypt(conf: &Config, data: &[u8]) -> Result<(Vec,String), DecryptError> { + let mut prompted = false; + let mut maxtries = match conf.key() { + Some(_) => 1, + None => 3, + }; + let mut password; + if conf.key().is_some() { + password = conf.key().unwrap().clone(); + } + else { + let cacheval = cache::read(&conf); + if cacheval.is_some() { + password = cacheval.unwrap(); + maxtries += 1; + } else { + let t = prompt::hidden(&conf, "Enter key"); + if t.is_none() { + return Err(DecryptError::Abort); + } + prompted = true; + password = t.unwrap() + } + } + + let mut err = DecryptError::Abort; + let mut tries = 0; + while tries < maxtries { + let t = crypt::decrypt(&data, &password); + if t.is_ok() { + if prompted { + cache::write(&conf, &password, conf.key_cache_time); + } + return Ok( (t.unwrap(), password) ); + } else { + err = t.err().unwrap(); + } + eprintln!("Decrypt failed"); + tries += 1; + if tries < maxtries { + let t = prompt::hidden(&conf, "Enter key"); + if t.is_none() { + return Err(DecryptError::Abort); + } + prompted = true; + password = t.unwrap(); + } + } + Err(err) +} + +pub fn predecrypt_process(conf: &Config, args: &[String]) -> Option<()> { + //let filepath = conf.path.join(conf.file().clone() + &conf.extension); + match &args[1][..] { + "-v"|"v"|"--version"|"version" => { + print::version(); + Some(()) + }, + "-h"|"--help"|"help" => { + print::help(&args[0]); + Some(()) + }, + "agent" => { + cache::start_agent(&conf); + Some(()) + }, + "ch"|"cached" => { + match cache::read(&conf) { + Some(_) => Some(()), + None => std::process::exit(1), + } + }, + "cc"|"cache-clear" => { + cache::clear(&conf); + Some(()) + }, + "rmc"|"rc"|"rm-cache" => { + cache::del(&conf); + Some(()) + }, + "lsf"|"lf"|"list-files" => { + for entry in conf.path.read_dir().expect("read_dir failed") { + if let Ok(entry) = entry { + let t = String::from(entry.path().file_name().unwrap().to_str().unwrap()); + if t.ends_with(&conf.extension) { + println!("{}", remove_suffix(&t, &conf.extension)); + } + } + } + Some(()) + }, + "rmf"|"rf"|"rm-file" => { + remote::operation(conf, &remote::Operation::Delete, None).unwrap(); + Some(()) + // if filepath.exists() { + // std::fs::remove_file(&filepath).unwrap(); + // Some(()) + // } + // else { + // eprintln!("File {} doesn't exist", conf.file()); + // std::process::exit(15); + // } + }, + "c"|"create" => { + let f = remote::operation(conf, &remote::Operation::Read, None).unwrap(); + if f.is_none() { + let password = match conf.key() { + Some(v) => Some(v.clone()), + _ => prompt::hidden_confirm(&conf, "New key", "Confirm Key", "Keys don't match"), + }; + if password.is_none() { + std::process::exit(14); + } + cache::write(conf, &password.as_ref().unwrap(), conf.key_cache_time); + let data = Archive::new().to_ref().generate(); + let encdata = crypt::encrypt(&data, &password.unwrap()); + remote::operation(conf, &remote::Operation::Write, Some(encdata)).unwrap(); + Some(()) + } + else { + todo!() + } + }, + _ => None, + } +} + +pub fn process(conf: &Config, args: &[String], archive: &ArchiveRef) -> Result)>,ProcessError> { + + // prepare args + let opargs = if args.len() > 2 { + Vec::from(&args[2..]) + } else { + Vec::new() + }; + let path1_opt = if opargs.len() > 0 { + Some(Path::new(&opargs[0])) + } else { + None + }; + let path1 = path1_opt.unwrap_or(Path::new("")); + + let mut err: Option = None; + let ret = match &args[1][..] { + "t"|"tree" => { + print::print_paths_fancy( archive.tree(path1).unwrap().prune_type(tar::EntryType::Directory).entries(), path1 ); + None + }, + "l"|"ls"|"list" => { + print::print_paths_fancy( archive.list(path1).unwrap().entries(), path1 ); + None + }, + "g"|"get" => { + let mut out = std::io::stdout(); + let mut n=0; + for i in archive.get(opargs.iter().map(|x| Path::new(x).to_path_buf()).collect() ) { + match i { + Some(ref v) => { + out.write_all(&v[..]).unwrap(); + out.flush().unwrap(); + println!(""); + } + None => { + eprintln!("'{}' not found", args[n+2]); + err = Some(ProcessError::PathNotFound); + } + } + n+=1; + } + None + }, + "i"|"info" => { + let t: Vec<&Path> = opargs.iter().map(|x| Path::new(x)).collect(); + let e = archive.filter_paths( &t ); + if e.entries().len() > 0 { + print::print_info( e.entries() ); + } else { + err = Some(ProcessError::PathNotFound); + } + None + } + "x"|"copy" => { + if path1_opt.is_none() { + eprintln!("missing argument"); + return Err(ProcessError::MissingArg); + } + let t = archive.get_one(&path1_opt.unwrap()); + if t.is_none() { + eprintln!("'{}' not found", path1_opt.unwrap().to_str().unwrap()); + return Err(ProcessError::PathNotFound); + } + clipboard::timed_copy(&t.unwrap(), std::time::Duration::from_secs(10)); + None + }, + "s"|"set" => { + let newa = archive.set( &[( path1_opt.unwrap().to_path_buf(), Vec::from(args[3].as_bytes()) )]).unwrap(); + clipboard::cond_copy(conf, args, &newa); + Some( (newa, None) ) + } + "f"|"file" => { + let newa = archive.set( &[( path1_opt.unwrap().to_path_buf(), fs::read(&args[3]).expect("Unable to read file") )] ).unwrap(); + clipboard::cond_copy(conf, args, &newa); + Some( (newa, None) ) + } + "n"|"new" => { + let charset = random::CharSet::new(&conf.rand_set); + let r: Vec<(PathBuf,Vec)> = + Vec::from(&args[2..]).iter().map( + |x| (Path::new(x).to_path_buf(), + random::generate(&charset, conf.rand_len).as_bytes().to_vec() + ) + ).collect(); + let newa = archive.set(&r[..]).unwrap(); + clipboard::cond_copy(conf, args, &newa); + Some( (newa, None) ) + }, + "a"|"add" => { + let r: Vec<(PathBuf,Vec)> = + Vec::from(&args[2..]).iter().map( + |x| (Path::new(x).to_path_buf(), + prompt::hidden(&conf, &(String::from("New value for ")+x)).expect("Abort").as_bytes().to_vec() + ) + ).collect(); + let newa = archive.set(&r[..]).unwrap(); + clipboard::cond_copy(conf, args, &newa); + Some( (newa, None) ) + }, + "ln"|"link" => { + let t = archive.link( path1_opt.unwrap(), Path::new(&args[3]) ); + match t { + Ok(v) => Some((v, None)), + Err(e) => { + return Err(e); + }, + } + }, + "r"|"rm" => { + let args = Vec::from(&args[2..]); + let paths: Vec<&Path> = args.iter().map(|x| Path::new(x)).collect(); + Some( (archive.del( &paths[..] ).clone(), None) ) + }, + "m"|"mv"|"move" => { + let t = archive.mv( path1_opt.unwrap(), Path::new(&args[3]) ); + match t { + Ok(v) => Some((v, None)), + Err(e) => { + return Err(e); + }, + } + }, + "e"|"exec" => { + todo!(); + }, + "c"|"create" => { + let newpass = match conf.key() { + Some(v) => Some(v.clone()), + _ => prompt::hidden_confirm(&conf, "New key", "Confirm Key", "Keys don't match"), + }; + match newpass { + Some (v) => { + cache::write(&conf, &v, conf.key_cache_time); + Some( (archive.clone(), Some(v)) ) + } + None => { + return Err(ProcessError::KeysDontMatch) + }, + } + }, + _ => { + let mut newargs = Vec::from(args); + newargs.insert(1, conf.unk_op_call.clone()); + return process(conf, &newargs[..], &archive); + }, + }; + match err { + Some(v) => Err(v), + _ => Ok(ret), + } +} diff --git a/src/prompt.rs b/src/prompt.rs new file mode 100644 index 0000000..159c44f --- /dev/null +++ b/src/prompt.rs @@ -0,0 +1,108 @@ + +use std::io; +use std::io::{Read,Write,Error}; + +use std::process::{Stdio,Command,ExitStatus}; +use std::env; + +use which::which; + +use crate::config::Config; + +fn cmd_output_status(cmd: &mut Command) -> Result<(Vec,ExitStatus), Error> { + let mut proc = cmd.stdout(Stdio::piped()).spawn().ok().expect("Execution failure"); + + let outstream = proc.stdout.as_mut() + .expect("Pipe failue"); + // Drain it into a &mut [u8]. + let mut output = Vec::new(); + outstream.read_to_end(&mut output) + .expect("Pipe failue"); + match proc.wait() { + Ok(v) => Ok((output, v)), + Err(e) => Err(e), + } +} + +fn hidden_cli(message: &str) -> Option { + Command::new("stty").args(["-echo"]).status().expect("Execution failure"); + + eprint!("{}: ", message); + io::stdout().flush().unwrap(); + + let mut val = String::new(); + io::stdin().read_line(&mut val) + .expect("Error reading input"); + eprintln!(""); + + if &val[val.len()-1..] == "\n" { + val.pop(); + } + + Command::new("stty").args(["echo"]).status().expect("Execution failure"); + Some(val) +} + +fn is_graphical(config: &Config) -> bool { + match env::var("DISPLAY") { + Ok(v) => { + v.len() > 0 && ! ( + config.prioritize_cli && + unsafe {libc::isatty(0)} != 0 + ) + }, + _ => false, + } +} + +pub fn hidden(config: &Config, message: &str) -> Option { + if is_graphical(config) { + let mut cmd = ""; + let mut args = Vec::new(); + if which("kdialog").is_ok() { + cmd = "kdialog"; + args = Vec::from(["--title", "zpass", "--password", message]); + } else if which("zenity").is_ok() { + cmd = "zenity"; + args = Vec::from(["--title", "zpass", "--password"]); + } + if cmd.len() > 0 { + let r = cmd_output_status( + Command::new(cmd).args(&args[..]) + ); + match r { + Ok(v) => if v.1.success() { + let mut val = unsafe { String::from_utf8_unchecked(v.0) }; + if &val[val.len()-1..] == "\n" { + val.pop(); + } + Some( val ) + } else { + None + } + _ => None, + } + } else { + hidden_cli(message) + } + } else { + hidden_cli(message) + } +} + +pub fn hidden_confirm(config: &Config, message1: &str, message2: &str, errmsg: &str) -> Option { + let p1 = hidden(config, message1); + if p1.is_none() { + return None; + } + let p2 = hidden(config, message2); + if p2.is_none() { + return None; + } + if p1.as_ref().unwrap() != p2.as_ref().unwrap() { + eprintln!("{}", errmsg); + None + } else { + p1 + } +} diff --git a/src/random.rs b/src/random.rs new file mode 100644 index 0000000..6c3a274 --- /dev/null +++ b/src/random.rs @@ -0,0 +1,50 @@ +use rand::Rng; + +use std::collections::HashSet; + +pub struct CharSet { + charset: HashSet, +} + +impl CharSet { + pub fn new(s: &str) -> CharSet { + let mut ret = HashSet::new(); + + let mut prevchar: Option = None; + let mut i=0; + while i < s.len() { + let c = s.chars().nth(i).unwrap(); + if i+1 < s.len() && prevchar != None && c == '-' { + for k in prevchar.unwrap()..s.chars().nth(i+1).unwrap() { + ret.insert(k); + } + i+=2; + } + else { + prevchar = Some(c); + ret.insert(c); + i+=1; + } + } + CharSet { charset: ret } + } + + pub fn to_string(&self) -> String { + let mut s = String::new(); + for it in &self.charset { + s.push(*it); + } + s + } +} + +pub fn generate(set: &CharSet, n: u32) -> String { + let mut rng = rand::thread_rng(); + let setstr = set.to_string(); + let mut ret = String::new(); + + for _ in 0..n { + ret.push(setstr.chars().nth(rng.gen_range(0..setstr.len()) ).unwrap() ); + } + ret +} diff --git a/src/remote/ftps.rs b/src/remote/ftps.rs new file mode 100644 index 0000000..5ba92f8 --- /dev/null +++ b/src/remote/ftps.rs @@ -0,0 +1,14 @@ +use crate::process::ProcessError; +use crate::config::Config; + +pub fn download(conf: &Config) -> Result>, ProcessError> { + Err(ProcessError::RemoteError) +} + +pub fn upload(conf: &Config, data: Vec) -> Result>, ProcessError> { + Err(ProcessError::RemoteError) +} + +pub fn delete(conf: &Config) -> Result>, ProcessError> { + Err(ProcessError::RemoteError) +} \ No newline at end of file diff --git a/src/remote/mod.rs b/src/remote/mod.rs new file mode 100644 index 0000000..7aac636 --- /dev/null +++ b/src/remote/mod.rs @@ -0,0 +1,104 @@ +use std::fs; + +use crate::process::ProcessError; +use crate::config::Config; + +pub mod sftp; +pub mod scp; +pub mod ftps; +pub mod webdav; +pub mod ssh; + +pub enum Operation { + Read, + Write, + Delete, +} + +#[derive(Debug)] +pub enum Protocol { + SCP, + SFTP, + FTPS, + WebDAV, +} + +// REMOTE PROTOCOL // +impl Protocol { + pub fn from(s: &str) -> Protocol { + match s { + "SCP"|"scp" => Protocol::SCP, + "SFTP"|"sftp" => Protocol::SFTP, + "FTPS"|"ftps"|"FTPs" => Protocol::FTPS, + "WebDAV"|"WEBDAV"|"webdav"|"WebDav" => Protocol::WebDAV, + _ => panic!("Invalid protocol: {}", s), + } + } +} + +fn remote(proto: &Protocol, op: &Operation, conf: &Config, data: Option>) -> Result>, ProcessError>{ + match proto { + Protocol::SFTP => match op { + Operation::Read => sftp::download(conf), + Operation::Write => sftp::upload(conf, data.unwrap()), + Operation::Delete => sftp::delete(conf), + } + Protocol::SCP => match op { + Operation::Read => scp::download(conf), + Operation::Write => scp::upload(conf, data.unwrap()), + Operation::Delete => scp::delete(conf), + } + Protocol::FTPS => match op { + Operation::Read => ftps::download(conf), + Operation::Write => ftps::upload(conf, data.unwrap()), + Operation::Delete => ftps::delete(conf), + } + Protocol::WebDAV => match op { + Operation::Read => webdav::download(conf), + Operation::Write => webdav::upload(conf, data.unwrap()), + Operation::Delete => webdav::delete(conf), + } + } +} + +pub fn operation(conf: &Config, op: &Operation, data: Option>) -> Result>, ProcessError> { + if conf.remote_addr.is_some() { + remote(&conf.remote_method, op, conf, data) + } else { + let filepath = conf.path.join(conf.file().clone() + &conf.extension); + if filepath.exists() && ! filepath.is_file() { + eprintln!("file {} exists and is not a file", filepath.to_str().unwrap()); + return Err(ProcessError::FileExists); + } + + match + match op { + Operation::Read => { + if ! filepath.exists() { + return Ok(None); + } + match fs::read(&filepath) { + Ok(v) => Ok(Some(v)), + Err(v) => Err(v), + } + }, + Operation::Write => + match fs::write(&filepath, data.unwrap()) { + Ok(_) => Ok(None), + Err(v) => Err(v), + }, + Operation::Delete => + match fs::remove_file(&filepath) { + Ok(_) => Ok(None), + Err(v) => Err(v), + }, + } { + Ok(v) => Ok(v), + Err(_) => match op { + Operation::Read => Err(ProcessError::FileRead), + Operation::Write => Err(ProcessError::FileWrite), + Operation::Delete => Err(ProcessError::FileDelete), + }, + } + } +} diff --git a/src/remote/scp.rs b/src/remote/scp.rs new file mode 100644 index 0000000..8c1e700 --- /dev/null +++ b/src/remote/scp.rs @@ -0,0 +1,16 @@ +use crate::process::ProcessError; +use crate::config::Config; +use crate::remote::ssh; + +pub fn download(conf: &Config) -> Result>, ProcessError> { + ssh::init_session(conf); + Err(ProcessError::RemoteError) +} + +pub fn upload(conf: &Config, data: Vec) -> Result>, ProcessError> { + Err(ProcessError::RemoteError) +} + +pub fn delete(conf: &Config) -> Result>, ProcessError> { + Err(ProcessError::RemoteError) +} \ No newline at end of file diff --git a/src/remote/sftp.rs b/src/remote/sftp.rs new file mode 100644 index 0000000..5ba92f8 --- /dev/null +++ b/src/remote/sftp.rs @@ -0,0 +1,14 @@ +use crate::process::ProcessError; +use crate::config::Config; + +pub fn download(conf: &Config) -> Result>, ProcessError> { + Err(ProcessError::RemoteError) +} + +pub fn upload(conf: &Config, data: Vec) -> Result>, ProcessError> { + Err(ProcessError::RemoteError) +} + +pub fn delete(conf: &Config) -> Result>, ProcessError> { + Err(ProcessError::RemoteError) +} \ No newline at end of file diff --git a/src/remote/ssh.rs b/src/remote/ssh.rs new file mode 100644 index 0000000..08b6c4b --- /dev/null +++ b/src/remote/ssh.rs @@ -0,0 +1,24 @@ +use std::net::TcpStream; +use ssh2; + +use std::path::Path; + +use crate::config::Config; + +pub fn init_session(conf: &Config) -> ssh2::Session { + // Connect to the local SSH server + let s = String::from(conf.remote_addr.as_ref().unwrap()) + ":" + &conf.remote_port.unwrap_or(22).to_string(); + let tcp = TcpStream::connect(&s).unwrap(); + let mut sess = ssh2::Session::new().unwrap(); + sess.set_tcp_stream(tcp); + sess.handshake().unwrap(); + + // Try to authenticate with the first identity in the agent. + // sess.userauth_agent(&conf.remote_user.as_ref().unwrap()).unwrap(); + let key_path = match conf.ssh_id.as_ref() { + Some(v) => v.as_path(), + _ => Path::new("~/.ssh/id_rsa"), + }; + sess.userauth_pubkey_file(&conf.remote_user.as_ref().unwrap(), None, key_path, None ).unwrap(); + sess +} \ No newline at end of file diff --git a/src/remote/webdav.rs b/src/remote/webdav.rs new file mode 100644 index 0000000..5ba92f8 --- /dev/null +++ b/src/remote/webdav.rs @@ -0,0 +1,14 @@ +use crate::process::ProcessError; +use crate::config::Config; + +pub fn download(conf: &Config) -> Result>, ProcessError> { + Err(ProcessError::RemoteError) +} + +pub fn upload(conf: &Config, data: Vec) -> Result>, ProcessError> { + Err(ProcessError::RemoteError) +} + +pub fn delete(conf: &Config) -> Result>, ProcessError> { + Err(ProcessError::RemoteError) +} \ No newline at end of file diff --git a/src/scp.rs b/src/scp.rs new file mode 100644 index 0000000..e69de29