A small tool for storing passwords locally with git sync
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

253 lines
6.9 KiB

use crate::paths;
use crate::encoder;
use crate::git;
use encoder::Encoder;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::fs;
use std::path::Path;
use std::io::{self, Write, BufRead};
use std::fmt;
use std::cmp::{PartialEq, Ordering};
#[derive(Clone)]
pub struct Item {
pub id: String,
pub content: String
}
impl Item {
pub fn from(s: String, c: String) -> Item {
Item { id: s, content: c }
}
// used only to search in HashSet
pub fn from_empty(s: String) -> Item {
Item { id: s, content: String::from("") }
}
}
impl PartialEq for Item {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for Item {}
impl Hash for Item {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl fmt::Display for Item {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "{}", self.id)?;
writeln!(f, "---------")?;
writeln!(f, "{}", self.content)
}
}
impl PartialOrd for Item {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.id.partial_cmp(&other.id)
}
}
impl Ord for Item {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
pub struct Storage {
items: HashSet::<Item>,
encoder: Encoder
}
impl Storage {
pub fn new(passphrase: String) -> Storage {
Storage {
items: HashSet::<Item>::new(),
encoder: Encoder::from(&passphrase)
}
}
pub fn from_db(passphrase: String) -> io::Result<Storage> {
if !Storage::is_inited()? {
return Err(io::Error::new(
io::ErrorKind::Other,
"Storage is not initialized"
));
}
let encoder = Encoder::from(&passphrase);
let file = fs::File::open(paths::get_db_path()?)?;
let reader = io::BufReader::new(file);
let mut items = HashSet::<Item>::new();
let mut id: Option<String> = None;
let mut lines = reader.lines();
let passtest = match lines.next() {
Some(line) => line?,
None => return Err(
io::Error::new(io::ErrorKind::InvalidData,
"Bad storage db format: no passphrase in the beginnning"
)),
};
// TODO: only in debug mode
//println!("passphrase ok");
if !encoder.test_encoded_passphrase(passtest)? {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Wrong passphrase"));
}
for line in lines {
match line {
Ok(line) => {
if id.is_none() {
let line = encoder.decrypt(line)?;
// TODO: only in debug mode
//println!("{}", line);
id = Some(line);
} else {
let content = encoder.decrypt(line)?;
// TODO: only in debug
//println!("{}", content);
items.insert(Item::from(id.unwrap(), content));
id = None;
}
},
Err(e) => {
eprintln!("Error reading line, {}", e);
}
}
}
Ok(Storage {
items,
encoder
})
}
pub fn init(passphrase: String) -> io::Result<()> {
//Storage::check_installed()?;
let sp = paths::get_storage_path()?;
fs::create_dir(sp)?;
let st = Storage::new(passphrase);
st.dump_db()?;
println!("Storage db created");
Ok(())
}
pub fn check_installed() -> io::Result<()> {
let sp = paths::get_storage_path()?;
let storage_path = Path::new(&sp);
// Check if the folder exists and is a directory
if !storage_path.exists() || !storage_path.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("{} does not exist or not a dir", sp)
));
}
let git_path = storage_path.join(".git");
if !git_path.exists() || !git_path.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("{} not under git", sp)
));
}
Ok(())
}
pub fn is_inited() -> io::Result<bool> {
//Storage::check_installed()?;
let db = paths::get_db_path()?;
let db_path = Path::new(&db);
Ok(db_path.exists())
}
pub fn ids(&self) -> Vec<String> {
let mut result = Vec::new();
for item in self.items.iter() {
result.push(item.id.clone());
}
result.sort();
result
}
pub fn contains(&self, id: &String) -> bool {
let item = Item::from_empty(id.clone());
self.items.contains(&item)
}
// TODO: return Result<Item>
pub fn get(&self, id: &String) -> &Item {
let item = Item::from_empty(id.clone());
self.items.get(&item).unwrap()
}
/// Counting starts from 1, according to UI
pub fn get_id_by_number(&self, number: u32) -> io::Result<String> {
let number = number as usize;
if number == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Items numbering starts from 1"
));
}
let ids = self.ids();
if number > ids.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("There are only {} items, but asked with number {}", ids.len(), number)
));
}
Ok(ids[number - 1].clone())
}
pub fn add(&mut self, item: Item) {
self.items.insert(item);
}
pub fn update(&mut self, item: Item) {
self.items.remove(&item);
self.items.insert(item);
}
pub fn remove(&mut self, id: &String) {
let item = Item::from_empty(id.clone());
self.items.remove(&item);
}
pub fn dump(&self) -> io::Result<()> {
self.dump_db()?;
git::Git::sync()?;
Ok(())
}
fn dump_db(&self) -> io::Result<()> {
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true) // Clear the file content before writing
.create(true)
.open(paths::get_db_path()?)?;
writeln!(file, "{}", self.encoder.get_encoded_test_passphrase()?)?;
for item in self.items.iter() {
writeln!(file, "{}", self.encoder.encrypt(&item.id)?)?;
let content = self.encoder.encrypt(&item.content)?;
writeln!(file, "{}", content)?;
}
Ok(())
}
pub fn new_from_passphrase(&self, passphrase: &String) -> Storage {
return Storage {
encoder: Encoder::from(passphrase),
items: self.items.clone()
}
}
}