This commit is contained in:
mateoferon 2023-02-16 17:16:28 +01:00
commit 9b560174c1
25 changed files with 2112 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/target
/zpasspath
/conf
Zmakefile
Cargo.lock
TODO

21
Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "zrpass"
version = "0.1.0"
authors = ["zawz <zawz@zawz.net>"]
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"

13
build-musl.sh Normal file
View file

@ -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 "$@"

109
completion/zrpass.bash Normal file
View file

@ -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

450
src/archive.rs Normal file
View file

@ -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<u8>,
}
#[derive(Debug)]
pub struct Archive {
entries: Vec<Entry>,
}
#[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<Entry> = 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<Entry> = Vec::new();
for it in &self.entries {
e.push((*it).clone());
}
Archive { entries: e }
}
pub fn tree<'a>(&'a self, path: &Path) -> Option<ArchiveRef<'a>> {
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<ArchiveRef<'a>> {
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<Vec<u8>> {
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<PathBuf>) -> Vec<Option<Vec<u8>>> {
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<B, F>(&self, mut f: F) -> Vec<B>
where
Self: Sized,
F: FnMut(&Entry) -> B,
{
let mut newvec: Vec<B> = Vec::new();
for entry in &self.entries {
newvec.push(f(entry));
}
newvec
}
pub fn export_paths(&self) -> Vec<PathBuf> {
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<Vec<u8>> {
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<u8> {
self.build().into_inner().unwrap()
}
pub fn set(&self, values: &[(PathBuf, Vec<u8>)]) -> Result<Archive,ProcessError> {
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<Archive,ProcessError> {
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<Archive,ProcessError> {
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<T: std::cmp::PartialEq>(val: T, paths: &[T]) -> bool {
for it in paths {
if *it == val {
return true;
}
}
false
}

64
src/cache/file.rs vendored Normal file
View file

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

83
src/cache/mod.rs vendored Normal file
View file

@ -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<u8> {
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<String> {
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)
}
}

48
src/cache/redis.rs vendored Normal file
View file

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

44
src/clipboard.rs Normal file
View file

@ -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"),
}
}

244
src/config.rs Normal file
View file

@ -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<String,String>,
}
#[derive(Debug)]
pub struct Config {
pub cache_path: PathBuf,
pub cache_sock: PathBuf,
pub path: PathBuf,
pub ssh_id: Option<PathBuf>,
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<u16>,
pub file: String,
pub extension: String,
pub key: Option<String>,
pub rand_set: String,
pub remote_addr: Option<String>,
pub remote_method: remote::Protocol,
pub remote_password: Option<String>,
pub remote_user: Option<String>,
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<String> {
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<PathBuf> {
match self.env.get(&String::from(key)) {
Some(v) => Some(Path::new(v).to_path_buf()),
_ => None,
}
}
pub fn get_parse<T: std::str::FromStr>(&self, key: &str) -> T {
match self.env.get(&String::from(key)).unwrap().parse::<T>() {
Ok(v) => v,
_ => panic!("Invalid format for {}", key),
}
}
pub fn get_parse_opt<T: std::str::FromStr>(&self, key: &str) -> Option<T> {
match self.env.get(&String::from(key)) {
Some(s) => match s.parse::<T>() {
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::<u64>("ZPASS_CLIPBOARD_TIME"),
key_cache_time: envconf.get_parse::<u64>("ZPASS_KEY_CACHE_TIME"),
rand_len: envconf.get_parse::<u32>("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::<u16>("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,
}
}

21
src/crypt/compress.rs Normal file
View file

@ -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<Vec<u8>, 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<u8> {
let mut d = GzEncoder::new(data, Compression::new(level));
let mut s = Vec::new();
d.read_to_end(&mut s).unwrap();
s
}

24
src/crypt/mod.rs Normal file
View file

@ -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<Vec<u8>, 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<u8> {
let compressed = compress::compress(&data, 6);
let result = ssl::ssl_encrypt(&compressed, &password.as_bytes());
result
}

58
src/crypt/ssl.rs Normal file
View file

@ -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<Vec<u8>, 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<u8> {
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
}

109
src/main.rs Normal file
View file

@ -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<String> = 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 => (),
};
}

154
src/print.rs Normal file
View file

@ -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<PathBuf>) {
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!("{} <operation>", 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 <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 <path...> Get values of targets");
println!(" copy <path> Copy the target value to clipboard. Shortcut 'x'");
println!(" set <path> <value> Set directly the value of target");
println!(" file <path> <file> Set the targetted value from file content");
println!(" add <path...> Prompt for input value on paths");
println!(" new <path...> Generate a random password at target");
println!(" rm <path...> Delete targets");
println!(" mv <path...> Move targets");
println!(" link <path> <target> Create a link to target");
println!(" exec <cmd> 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!(" {:<varlen$} {:<vallen$} {}", "*Variable*", "*Default value*", "*Description*");
println!("-{:-<varlen$}---{:-<vallen$}---{:-<desclen$}", "", "", "");
let emptystring = &String::new();
for it in ENVVARS {
println!(" {:<varlen$} {:<vallen$} {}", it.0, defaultconf.get(it.0).unwrap_or(emptystring), it.1 );
}
}

320
src/process.rs Normal file
View file

@ -0,0 +1,320 @@
use std::fs;
use std::path::{Path,PathBuf};
use std::io::Write;
use crate::config::Config;
use crate::cache;
use crate::crypt;
use crate::print;
use crate::clipboard;
use crate::random;
use crate::crypt::DecryptError;
use crate::archive::{Archive,ArchiveRef};
use crate::prompt;
use crate::remote;
#[derive(Debug)]
pub enum ProcessError {
BadFile,
BadDecrypt,
Abort,
KeysDontMatch,
PathNotFound,
MissingArg,
PathAlreadyExists,
PathConflict,
FileNotFound,
FileExists,
FileRead,
FileWrite,
FileDelete,
RemoteError,
}
fn remove_suffix<'a>(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<u8>,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<Option<(Archive,Option<String>)>,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<ProcessError> = 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<u8>)> =
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<u8>)> =
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),
}
}

108
src/prompt.rs Normal file
View file

@ -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<u8>,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<String> {
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<String> {
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<String> {
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
}
}

50
src/random.rs Normal file
View file

@ -0,0 +1,50 @@
use rand::Rng;
use std::collections::HashSet;
pub struct CharSet {
charset: HashSet<char>,
}
impl CharSet {
pub fn new(s: &str) -> CharSet {
let mut ret = HashSet::new();
let mut prevchar: Option<char> = 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
}

14
src/remote/ftps.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::process::ProcessError;
use crate::config::Config;
pub fn download(conf: &Config) -> Result<Option<Vec<u8>>, ProcessError> {
Err(ProcessError::RemoteError)
}
pub fn upload(conf: &Config, data: Vec<u8>) -> Result<Option<Vec<u8>>, ProcessError> {
Err(ProcessError::RemoteError)
}
pub fn delete(conf: &Config) -> Result<Option<Vec<u8>>, ProcessError> {
Err(ProcessError::RemoteError)
}

104
src/remote/mod.rs Normal file
View file

@ -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<Vec<u8>>) -> Result<Option<Vec<u8>>, 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<Vec<u8>>) -> Result<Option<Vec<u8>>, 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),
},
}
}
}

16
src/remote/scp.rs Normal file
View file

@ -0,0 +1,16 @@
use crate::process::ProcessError;
use crate::config::Config;
use crate::remote::ssh;
pub fn download(conf: &Config) -> Result<Option<Vec<u8>>, ProcessError> {
ssh::init_session(conf);
Err(ProcessError::RemoteError)
}
pub fn upload(conf: &Config, data: Vec<u8>) -> Result<Option<Vec<u8>>, ProcessError> {
Err(ProcessError::RemoteError)
}
pub fn delete(conf: &Config) -> Result<Option<Vec<u8>>, ProcessError> {
Err(ProcessError::RemoteError)
}

14
src/remote/sftp.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::process::ProcessError;
use crate::config::Config;
pub fn download(conf: &Config) -> Result<Option<Vec<u8>>, ProcessError> {
Err(ProcessError::RemoteError)
}
pub fn upload(conf: &Config, data: Vec<u8>) -> Result<Option<Vec<u8>>, ProcessError> {
Err(ProcessError::RemoteError)
}
pub fn delete(conf: &Config) -> Result<Option<Vec<u8>>, ProcessError> {
Err(ProcessError::RemoteError)
}

24
src/remote/ssh.rs Normal file
View file

@ -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
}

14
src/remote/webdav.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::process::ProcessError;
use crate::config::Config;
pub fn download(conf: &Config) -> Result<Option<Vec<u8>>, ProcessError> {
Err(ProcessError::RemoteError)
}
pub fn upload(conf: &Config, data: Vec<u8>) -> Result<Option<Vec<u8>>, ProcessError> {
Err(ProcessError::RemoteError)
}
pub fn delete(conf: &Config) -> Result<Option<Vec<u8>>, ProcessError> {
Err(ProcessError::RemoteError)
}

0
src/scp.rs Normal file
View file