init
This commit is contained in:
commit
9b560174c1
25 changed files with 2112 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/target
|
||||
/zpasspath
|
||||
/conf
|
||||
Zmakefile
|
||||
Cargo.lock
|
||||
TODO
|
||||
21
Cargo.toml
Normal file
21
Cargo.toml
Normal 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
13
build-musl.sh
Normal 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
109
completion/zrpass.bash
Normal 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
450
src/archive.rs
Normal 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
64
src/cache/file.rs
vendored
Normal 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
83
src/cache/mod.rs
vendored
Normal 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
48
src/cache/redis.rs
vendored
Normal 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
44
src/clipboard.rs
Normal 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
244
src/config.rs
Normal 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
21
src/crypt/compress.rs
Normal 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
24
src/crypt/mod.rs
Normal 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
58
src/crypt/ssl.rs
Normal 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
109
src/main.rs
Normal 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
154
src/print.rs
Normal 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
320
src/process.rs
Normal 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
108
src/prompt.rs
Normal 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
50
src/random.rs
Normal 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
14
src/remote/ftps.rs
Normal 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
104
src/remote/mod.rs
Normal 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
16
src/remote/scp.rs
Normal 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
14
src/remote/sftp.rs
Normal 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
24
src/remote/ssh.rs
Normal 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
14
src/remote/webdav.rs
Normal 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
0
src/scp.rs
Normal file
Loading…
Reference in a new issue