From 4abecc797178ab97b3da22b70d04c2de0a571d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sof=C3=ADa=20Aritz?= Date: Mon, 27 Feb 2023 15:20:01 +0100 Subject: [PATCH] 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 --- .gitignore | 3 + Cargo.toml | 19 +++++ src/main.rs | 184 ++++++++++++++++++++++++++++++++++++++++++++++ src/notes/mod.rs | 109 +++++++++++++++++++++++++++ src/saving/mod.rs | 41 +++++++++++ 5 files changed, 356 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/main.rs create mode 100644 src/notes/mod.rs create mode 100644 src/saving/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e04901 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +.idea/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..86feb95 --- /dev/null +++ b/Cargo.toml @@ -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" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6f8da90 --- /dev/null +++ b/src/main.rs @@ -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, + password: Option, + 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; + } + }); + } + } + } +} \ No newline at end of file diff --git a/src/notes/mod.rs b/src/notes/mod.rs new file mode 100644 index 0000000..071db55 --- /dev/null +++ b/src/notes/mod.rs @@ -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>, +} + +#[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 { + 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::(); + 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 { + match self { + Note::Encrypted { title, metadata, encrypted_text } => { + let mut eraser = Eraser::new(); + eraser.add_suite::(); + 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 { + 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, metadata: &NoteMetadata) { + ui.horizontal(|ui| { + ui.label(RichText::new(title).size(14.0)); + + let date: DateTime = 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(); +} \ No newline at end of file diff --git a/src/saving/mod.rs b/src/saving/mod.rs new file mode 100644 index 0000000..7f5854f --- /dev/null +++ b/src/saving/mod.rs @@ -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> { + 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(¬e)?)?; + + Ok(()) + } +} \ No newline at end of file