Initial release

This is an initial release, I'm aware that this doesn't have the best structure and that there's lots of copying, but performance isn't relevant right now
This commit is contained in:
Sofía Aritz 2023-02-27 15:20:01 +01:00
commit 4abecc7971
5 changed files with 356 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
Cargo.lock
.idea/

19
Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "note-taking"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
egui = "0.21"
eframe = "0.21"
anyhow = "1"
log = "0.4"
simple_logger = "4.0"
chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rand = "0.8"
pwbox = "0.5"
directories = "4.0"

184
src/main.rs Normal file
View file

@ -0,0 +1,184 @@
use std::collections::{VecDeque};
use std::fs;
use std::time::SystemTime;
use directories::ProjectDirs;
use eframe::Frame;
use egui::{Context, RichText};
use log::info;
use simple_logger::SimpleLogger;
use crate::notes::{Note, NoteMetadata};
use crate::saving::Saving;
mod notes;
mod saving;
#[derive(PartialEq)]
enum CurrentMode {
View,
Compose,
PasswordInput,
}
struct App {
notes: Vec<Note>,
password: Option<String>,
tested_password: bool,
mode: CurrentMode,
saving: Saving,
password_buffer: String,
title_buffer: String,
text_buffer: String,
}
fn main() {
SimpleLogger::new().init().expect("failed to initialize logger");
info!("starting the UI");
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(640.0, 640.0)),
..Default::default()
};
let project_directories = ProjectDirs::from("com", "sofiaritz", "notes")
.unwrap();
let data_directory = project_directories
.data_dir();
info!("data directory: {:?}", data_directory);
fs::create_dir_all(data_directory).unwrap();
info!("created all intermediate directories");
let saving = Saving::new(data_directory);
let notes = saving.read_notes().unwrap();
info!("successfully read initial notes");
let app = App {
password: None,
tested_password: false,
notes,
mode: CurrentMode::PasswordInput,
saving,
password_buffer: String::new(),
title_buffer: String::new(),
text_buffer: String::new(),
};
eframe::run_native(
"notes",
options,
Box::new(move |_cc| Box::new(app))
).expect("failed to run native");
info!("shutdown");
}
impl eframe::App for App {
fn update(&mut self, ctx: &Context, _frame: &mut Frame) {
if self.password.is_none() {
self.mode = CurrentMode::PasswordInput;
}
if !self.tested_password && self.password.is_some() {
if let Some(note) = self.notes.get(0) {
note.decrypt(self.password.as_ref().unwrap()).unwrap();
self.tested_password = true;
}
}
match self.mode {
CurrentMode::View => {
let password = self.password.as_ref().unwrap();
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("notes");
ui.horizontal(|ui| {
ui.label("the simple note taking app that uses encryption by default");
ui.add_space(7.5);
if ui.button("want to add one note?").clicked() {
self.mode = CurrentMode::Compose;
}
});
ui.separator();
for note in &mut self.notes {
if let Some(new_note) = note.render(ui, password) {
*note = new_note;
}
ui.add_space(10.0);
}
});
}
CurrentMode::Compose => {
let password = self.password.as_ref().unwrap();
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("notes");
ui.horizontal(|ui| {
ui.label("the simple note taking app that uses encryption by default");
ui.add_space(7.5);
if ui.button("return to view mode?").clicked() {
self.title_buffer = String::new();
self.text_buffer = String::new();
self.mode = CurrentMode::View;
}
});
ui.separator();
ui.label("title:");
ui.text_edit_singleline(&mut self.title_buffer);
ui.label("text:");
ui.text_edit_multiline(&mut self.text_buffer);
ui.add_space(10.0);
if ui.button("add note").clicked() {
let note = Note::Decrypted {
title: self.title_buffer.clone(),
metadata: NoteMetadata {
date: SystemTime::now(),
arbitrary: None,
},
text: self.text_buffer.clone(),
};
self.saving.save_note(note.clone(), password.to_string()).unwrap();
let mut vec_deque = VecDeque::from(self.notes.clone());
vec_deque.push_front(note);
self.notes = Vec::from(vec_deque);
self.title_buffer = String::new();
self.text_buffer = String::new();
self.mode = CurrentMode::View;
}
});
}
CurrentMode::PasswordInput => {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("notes");
ui.label("the simple note taking app that uses encryption by default");
ui.separator();
ui.label("you need to put your password to access the notes");
ui.label(RichText::new("after starting the app, the password will be tested against the latest note").italics());
ui.add_space(10.0);
ui.text_edit_singleline(&mut self.password_buffer);
if ui.button("start app").clicked() {
self.password = Some(self.password_buffer.clone());
self.password_buffer = String::new();
self.mode = CurrentMode::View;
}
});
}
}
}
}

109
src/notes/mod.rs Normal file
View file

@ -0,0 +1,109 @@
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use std::time::SystemTime;
use chrono::{DateTime, Utc};
use egui::{RichText, Ui};
use pwbox::sodium::Sodium;
use pwbox::{ErasedPwBox, Eraser, Suite};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoteMetadata {
pub date: SystemTime,
pub arbitrary: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Note {
Encrypted {
title: String,
metadata: NoteMetadata,
encrypted_text: String,
},
Decrypted {
title: String,
metadata: NoteMetadata,
text: String,
}
}
impl Note {
pub fn encrypt(&self, password: &str) -> anyhow::Result<Self> {
let mut rng = rand::thread_rng();
match self {
Note::Decrypted { text, metadata, title } => {
let pwbox = Sodium::build_box(&mut rng)
.seal(password.to_string().into_bytes(), text.clone().into_bytes())?;
let mut eraser = Eraser::new();
eraser.add_suite::<Sodium>();
let erased: ErasedPwBox = eraser.erase(&pwbox)?;
Ok(Self::Encrypted {
title: title.clone(),
metadata: metadata.clone(),
encrypted_text: serde_json::to_string(&erased)?,
})
}
_ => Ok(self.clone()),
}
}
pub fn decrypt(&self, password: &str) -> anyhow::Result<Self> {
match self {
Note::Encrypted { title, metadata, encrypted_text } => {
let mut eraser = Eraser::new();
eraser.add_suite::<Sodium>();
let erased: ErasedPwBox = serde_json::from_str(encrypted_text)?;
let decrypted_bytes = eraser.restore(&erased)?.open(password)?.to_vec();
let decrypted_text = String::from_utf8(decrypted_bytes)?;
Ok(Self::Decrypted {
title: title.clone(),
metadata: metadata.clone(),
text: decrypted_text,
})
}
_ => Ok(self.clone()),
}
}
pub fn render(&self, ui: &mut Ui, password: &str) -> Option<Self> {
let mut value = None;
ui.group(|ui| {
match self {
Note::Encrypted { title, metadata, .. } => {
render_title_and_metadata(ui, title, metadata);
if ui.button("decrypt note").clicked() {
value = Some(self.decrypt(password).unwrap());
}
}
Note::Decrypted { title, metadata, text } => {
render_title_and_metadata(ui, title, metadata);
ui.label(text);
}
};
});
value
}
}
fn render_title_and_metadata(ui: &mut Ui, title: impl Into<String>, metadata: &NoteMetadata) {
ui.horizontal(|ui| {
ui.label(RichText::new(title).size(14.0));
let date: DateTime<Utc> = metadata.date.into();
ui.separator();
ui.label(date.format("%d-%m-%y").to_string());
if let Some(arbitrary) = &metadata.arbitrary {
for (k, v) in arbitrary.iter() {
ui.separator();
ui.label(RichText::new(format!("{}: {}", k, v)).monospace());
}
}
});
ui.separator();
}

41
src/saving/mod.rs Normal file
View file

@ -0,0 +1,41 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::notes::Note;
pub struct Saving {
path: PathBuf,
}
impl Saving {
pub fn new(path: &Path) -> Self {
Self {
path: path.into(),
}
}
pub fn read_notes(&self) -> anyhow::Result<Vec<Note>> {
let mut notes = vec![];
let paths = fs::read_dir(&self.path)?;
for path in paths {
let path = path?.path();
if path.is_file() {
notes.push(serde_json::from_str(&fs::read_to_string(path)?)?);
}
}
Ok(notes)
}
pub fn save_note(&self, note: Note, password: String) -> anyhow::Result<()> {
let note = note.encrypt(&password)?;
let mut path = self.path.clone().join("_");
path.set_file_name(format!("{}.json", SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_millis()));
fs::write(path, serde_json::to_string(&note)?)?;
Ok(())
}
}