Add structs DecryptedNote, EncryptedNote and HiddenNote

This makes some invalid states unrepresentable, which is something that is desired in this application to reduce mental complexity and to reduce the risk of undesired behaviour of aplying functions like `encrypt` on already encrypted notes.

`Note` is now an enum that holds the structs. This approach is similar to the one used in some parts of `std`, though in this case other approaches could be used, but they would be way too much verbose for this use case.

This change was done because having functions like `hide` on already hidden functions is not a good idea (even if was they do is "just" cloning the structure...).

There are some rough edges, but I'll improve them in another moment.
This commit is contained in:
Sofía Aritz 2023-03-16 22:28:17 +01:00
parent 237f343b33
commit c97700b29a
4 changed files with 148 additions and 136 deletions

View file

@ -15,7 +15,7 @@ use egui::{Color32, Context, RichText, TextEdit, Widget, WidgetText};
use log::info; use log::info;
use native_dialog::MessageDialog; use native_dialog::MessageDialog;
use simple_logger::SimpleLogger; use simple_logger::SimpleLogger;
use crate::notes::{Note, NoteMetadata}; use crate::notes::{DecryptedNote, Note, NoteMetadata};
use crate::password::{check_password, entropy, generate_new_password, migrate_to_password_v1, PasswordSystem}; use crate::password::{check_password, entropy, generate_new_password, migrate_to_password_v1, PasswordSystem};
use crate::saving::Saving; use crate::saving::Saving;
@ -108,6 +108,8 @@ impl eframe::App for App {
if !self.tested_password && self.password.is_some() { if !self.tested_password && self.password.is_some() {
if let Some(note) = self.notes.get(0) { if let Some(note) = self.notes.get(0) {
match note {
Note::Encrypted(note) => {
if let Ok(password) = check_password(self.password.as_ref().unwrap(), note) { if let Ok(password) = check_password(self.password.as_ref().unwrap(), note) {
match password { match password {
PasswordSystem::V0(password) => { PasswordSystem::V0(password) => {
@ -135,6 +137,9 @@ impl eframe::App for App {
self.tested_password = false; self.tested_password = false;
return; return;
} }
}
_ => unreachable!()
}
} else { } else {
self.password = Some(generate_new_password(self.password.as_ref().unwrap())) self.password = Some(generate_new_password(self.password.as_ref().unwrap()))
} }
@ -245,7 +250,7 @@ impl eframe::App for App {
.then_some(metadata) .then_some(metadata)
}); });
let note = Note::Decrypted { let note = Note::Decrypted(DecryptedNote {
id: SystemTime::now() id: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.unwrap() .unwrap()
@ -257,9 +262,9 @@ impl eframe::App for App {
arbitrary: metadata, arbitrary: metadata,
}, },
text: self.text_buffer.clone(), text: self.text_buffer.clone(),
}; });
self.saving.save_note(note.clone(), password).unwrap(); self.saving.save_note(&note.clone(), password).unwrap();
let mut vec_deque = VecDeque::from(self.notes.clone()); let mut vec_deque = VecDeque::from(self.notes.clone());
vec_deque.push_front(note); vec_deque.push_front(note);
@ -353,7 +358,10 @@ impl eframe::App for App {
if let Some(save_path) = save_path { if let Some(save_path) = save_path {
let notes: Vec<Note> = self.notes.clone() let notes: Vec<Note> = self.notes.clone()
.iter() .iter()
.map(|note| note.decrypt(password).expect("failed to decrypt note")) .map(|note| match note {
Note::Encrypted(note) => note.decrypt(password).expect("failed to decrypt note").into(),
_ => note.clone(),
})
.collect(); .collect();
let json_notes = serde_json::to_string(&notes).unwrap(); let json_notes = serde_json::to_string(&notes).unwrap();

View file

@ -13,124 +13,132 @@ pub struct NoteMetadata {
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Note { pub struct EncryptedNote {
Encrypted { pub id: String,
id: String, pub title: String,
title: String, pub metadata: NoteMetadata,
metadata: NoteMetadata, pub encrypted_text: String,
encrypted_text: String,
},
Decrypted {
id: String,
title: String,
metadata: NoteMetadata,
text: String,
},
Hidden {
id: String,
title: String,
metadata: NoteMetadata,
text: String,
}
} }
impl Note { #[derive(Debug, Clone, Serialize, Deserialize)]
pub fn encrypt(&self, password: &str) -> anyhow::Result<Self> { pub struct DecryptedNote {
pub id: String,
pub title: String,
pub metadata: NoteMetadata,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HiddenNote(pub DecryptedNote);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Note {
Encrypted(EncryptedNote),
Decrypted(DecryptedNote),
Hidden(HiddenNote),
}
impl DecryptedNote {
pub fn encrypt(&self, password: &str) -> anyhow::Result<EncryptedNote> {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
match self {
Note::Decrypted { id, text, metadata, title } => {
let pwbox = Sodium::build_box(&mut rng) let pwbox = Sodium::build_box(&mut rng)
.seal(password.to_string().into_bytes(), text.clone().into_bytes())?; .seal(password.to_string().into_bytes(), self.text.clone().into_bytes())?;
let mut eraser = Eraser::new(); let mut eraser = Eraser::new();
eraser.add_suite::<Sodium>(); eraser.add_suite::<Sodium>();
let erased: ErasedPwBox = eraser.erase(&pwbox)?; let erased: ErasedPwBox = eraser.erase(&pwbox)?;
Ok(Self::Encrypted { Ok(EncryptedNote {
id: id.clone(), id: self.id.clone(),
title: title.clone(), title: self.title.clone(),
metadata: metadata.clone(), metadata: self.metadata.clone(),
encrypted_text: serde_json::to_string(&erased)?, encrypted_text: serde_json::to_string(&erased)?,
}) })
} }
_ => Ok(self.clone()),
pub fn hide(self) -> HiddenNote {
HiddenNote(self)
} }
} }
pub fn decrypt(&self, password: &str) -> anyhow::Result<Self> { impl EncryptedNote {
match self { pub fn decrypt(&self, password: &str) -> anyhow::Result<DecryptedNote> {
Note::Encrypted { id, title, metadata, encrypted_text } => {
let mut eraser = Eraser::new(); let mut eraser = Eraser::new();
eraser.add_suite::<Sodium>(); eraser.add_suite::<Sodium>();
let erased: ErasedPwBox = serde_json::from_str(encrypted_text)?; let erased: ErasedPwBox = serde_json::from_str(&self.encrypted_text)?;
let decrypted_bytes = eraser.restore(&erased)?.open(password)?.to_vec(); let decrypted_bytes = eraser.restore(&erased)?.open(password)?.to_vec();
let decrypted_text = String::from_utf8(decrypted_bytes)?; let decrypted_text = String::from_utf8(decrypted_bytes)?;
Ok(Self::Decrypted { Ok(DecryptedNote {
id: id.clone(), id: self.id.clone(),
title: title.clone(), title: self.title.clone(),
metadata: metadata.clone(), metadata: self.metadata.clone(),
text: decrypted_text, text: decrypted_text,
}) })
} }
_ => Ok(self.clone()), }
impl HiddenNote {
pub fn unhide(self) -> DecryptedNote {
self.0
} }
} }
pub fn hide(self) -> Self { impl From<HiddenNote> for Note {
match self { fn from(value: HiddenNote) -> Self {
Note::Decrypted { id, title, metadata, text } => { Self::Hidden(value)
Self::Hidden { id, title, metadata, text }
}
_ => unreachable!()
} }
} }
pub fn unhide(self) -> Self { impl From<DecryptedNote> for Note {
match self { fn from(value: DecryptedNote) -> Self {
Note::Hidden { id, title, metadata, text } => { Self::Decrypted(value)
Self::Decrypted { id, title, metadata, text }
}
_ => unreachable!()
} }
} }
impl From<EncryptedNote> for Note {
fn from(value: EncryptedNote) -> Self {
Self::Encrypted(value)
}
}
impl Note {
pub fn render(&self, ui: &mut Ui, password: &str, cb: impl FnOnce(String)) -> Option<Self> { pub fn render(&self, ui: &mut Ui, password: &str, cb: impl FnOnce(String)) -> Option<Self> {
let mut value = None; let mut value = None;
ui.group(|ui| { ui.group(|ui| {
match self { match self {
Note::Encrypted { id, title, metadata, .. } => { Note::Encrypted(note) => {
let result = render_title_and_metadata(ui, title, metadata, false, None); let result = render_title_and_metadata(ui, &note.title, &note.metadata, false, None);
if let Some(action) = result { if let Some(action) = result {
match action { match action {
NoteRenderAction::Delete => cb(id.to_string()), NoteRenderAction::Delete => cb(note.id.to_string()),
_ => unreachable!() _ => unreachable!()
} }
} }
if ui.button("decrypt note").clicked() { if ui.button("decrypt note").clicked() {
value = Some(self.decrypt(password).unwrap()); value = Some(note.decrypt(password).unwrap().into());
} }
} }
Note::Decrypted { id, title, metadata, text, .. } => { Note::Decrypted(note) => {
let result = render_title_and_metadata(ui, title, metadata, true, Some(ButtonType::Hide)); let result = render_title_and_metadata(ui, &note.title, &note.metadata, true, Some(ButtonType::Hide));
if let Some(action) = result { if let Some(action) = result {
match action { match action {
NoteRenderAction::Delete => cb(id.to_string()), NoteRenderAction::Delete => cb(note.id.to_string()),
NoteRenderAction::Hide => value = Some(self.clone().hide()), NoteRenderAction::Hide => value = Some(note.clone().hide().into()),
_ => unreachable!() _ => unreachable!()
} }
} }
ui.label(text); ui.label(&note.text);
} }
Note::Hidden { id, title, metadata, .. } => { Note::Hidden(note) => {
let result = render_title_and_metadata(ui, title, metadata, true, Some(ButtonType::Unhide)); let result = render_title_and_metadata(ui, &note.0.title, &note.0.metadata, true, Some(ButtonType::Unhide));
if let Some(action) = result { if let Some(action) = result {
match action { match action {
NoteRenderAction::Delete => cb(id.to_string()), NoteRenderAction::Delete => cb(note.0.id.to_string()),
NoteRenderAction::Unhide => value = Some(self.clone().unhide()), NoteRenderAction::Unhide => value = Some(note.clone().unhide().into()),
_ => unreachable!() _ => unreachable!()
} }
} }
@ -142,14 +150,6 @@ impl Note {
value value
} }
pub fn id(&self) -> String {
match self {
Note::Encrypted { id, .. } => id.clone(),
Note::Decrypted { id, .. } => id.clone(),
Note::Hidden { id, .. } => id.clone(),
}
}
} }
enum NoteRenderAction { enum NoteRenderAction {

View file

@ -1,7 +1,7 @@
use anyhow::bail; use anyhow::bail;
use argon2::Argon2; use argon2::Argon2;
use sha2::{Sha256, Digest}; use sha2::{Sha256, Digest};
use crate::notes::Note; use crate::notes::{EncryptedNote, Note};
use crate::saving::Saving; use crate::saving::Saving;
pub fn generate_new_password(password: &str) -> String { pub fn generate_new_password(password: &str) -> String {
@ -35,7 +35,7 @@ pub enum PasswordSystem {
V1(String), V1(String),
} }
pub fn check_password(password: &str, note: &Note) -> anyhow::Result<PasswordSystem> { pub fn check_password(password: &str, note: &EncryptedNote) -> anyhow::Result<PasswordSystem> {
if note.decrypt(password).is_err() { if note.decrypt(password).is_err() {
let new_password = generate_new_password(password); let new_password = generate_new_password(password);
if note.decrypt(&new_password).is_err() { if note.decrypt(&new_password).is_err() {
@ -59,7 +59,6 @@ fn hash_password(password: &[u8], output: &mut [u8]) {
pub fn migrate_to_password_v1(password: &str, notes: &Vec<Note>, saving: &Saving) -> anyhow::Result<String> { pub fn migrate_to_password_v1(password: &str, notes: &Vec<Note>, saving: &Saving) -> anyhow::Result<String> {
let new_password = generate_new_password(password); let new_password = generate_new_password(password);
for note in notes { for note in notes {
let note = note.decrypt(password)?;
saving.save_note(note, &new_password)?; saving.save_note(note, &new_password)?;
} }

View file

@ -30,10 +30,15 @@ impl Saving {
Ok(notes.into()) Ok(notes.into())
} }
pub fn save_note(&self, note: Note, password: &str) -> anyhow::Result<()> { pub fn save_note(&self, note: &Note, password: &str) -> anyhow::Result<()> {
let note = note.encrypt(password)?; let note = match note {
Note::Encrypted(note) => note.clone(),
Note::Decrypted(note) => note.encrypt(password)?,
Note::Hidden(note) => note.0.encrypt(password)?,
};
let mut path = self.path.clone().join("_"); let mut path = self.path.clone().join("_");
path.set_file_name(format!("{}.json", note.id())); path.set_file_name(format!("{}.json", note.id));
fs::write(path, serde_json::to_string(&note)?)?; fs::write(path, serde_json::to_string(&note)?)?;