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,32 +108,37 @@ 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) {
if let Ok(password) = check_password(self.password.as_ref().unwrap(), note) { match note {
match password { Note::Encrypted(note) => {
PasswordSystem::V0(password) => { if let Ok(password) = check_password(self.password.as_ref().unwrap(), note) {
info!("migrating from PasswordSystem::V0 to PasswordSystem::V1"); match password {
info!("backing up notes"); PasswordSystem::V0(password) => {
self.saving.backup_notes().expect("failed to back up notes"); info!("migrating from PasswordSystem::V0 to PasswordSystem::V1");
info!("backed up notes successfully!"); info!("backing up notes");
info!("starting migration"); self.saving.backup_notes().expect("failed to back up notes");
self.password = Some( info!("backed up notes successfully!");
migrate_to_password_v1(&password, &self.notes, &self.saving) info!("starting migration");
.expect("failed to migrate notes") self.password = Some(
); migrate_to_password_v1(&password, &self.notes, &self.saving)
self.notes = self.saving.read_notes().expect("failed to update notes after migration"); .expect("failed to migrate notes")
info!("migration was successful!"); );
}, self.notes = self.saving.read_notes().expect("failed to update notes after migration");
PasswordSystem::V1(password) => self.password = Some(password), info!("migration was successful!");
} },
} else { PasswordSystem::V1(password) => self.password = Some(password),
let _ = MessageDialog::new() }
.set_title("invalid password") } else {
.set_text("failed to verify the password against an existing note. please try again") let _ = MessageDialog::new()
.show_alert(); .set_title("invalid password")
.set_text("failed to verify the password against an existing note. please try again")
.show_alert();
self.password = None; self.password = None;
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

@ -12,125 +12,133 @@ pub struct NoteMetadata {
pub arbitrary: Option<HashMap<String, String>>, pub arbitrary: Option<HashMap<String, String>>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptedNote {
pub id: String,
pub title: String,
pub metadata: NoteMetadata,
pub encrypted_text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Note { pub enum Note {
Encrypted { Encrypted(EncryptedNote),
id: String, Decrypted(DecryptedNote),
title: String, Hidden(HiddenNote),
metadata: NoteMetadata, }
encrypted_text: String,
}, impl DecryptedNote {
Decrypted { pub fn encrypt(&self, password: &str) -> anyhow::Result<EncryptedNote> {
id: String, let mut rng = rand::thread_rng();
title: String,
metadata: NoteMetadata, let pwbox = Sodium::build_box(&mut rng)
text: String, .seal(password.to_string().into_bytes(), self.text.clone().into_bytes())?;
},
Hidden { let mut eraser = Eraser::new();
id: String, eraser.add_suite::<Sodium>();
title: String, let erased: ErasedPwBox = eraser.erase(&pwbox)?;
metadata: NoteMetadata,
text: String, Ok(EncryptedNote {
id: self.id.clone(),
title: self.title.clone(),
metadata: self.metadata.clone(),
encrypted_text: serde_json::to_string(&erased)?,
})
}
pub fn hide(self) -> HiddenNote {
HiddenNote(self)
}
}
impl EncryptedNote {
pub fn decrypt(&self, password: &str) -> anyhow::Result<DecryptedNote> {
let mut eraser = Eraser::new();
eraser.add_suite::<Sodium>();
let erased: ErasedPwBox = serde_json::from_str(&self.encrypted_text)?;
let decrypted_bytes = eraser.restore(&erased)?.open(password)?.to_vec();
let decrypted_text = String::from_utf8(decrypted_bytes)?;
Ok(DecryptedNote {
id: self.id.clone(),
title: self.title.clone(),
metadata: self.metadata.clone(),
text: decrypted_text,
})
}
}
impl HiddenNote {
pub fn unhide(self) -> DecryptedNote {
self.0
}
}
impl From<HiddenNote> for Note {
fn from(value: HiddenNote) -> Self {
Self::Hidden(value)
}
}
impl From<DecryptedNote> for Note {
fn from(value: DecryptedNote) -> Self {
Self::Decrypted(value)
}
}
impl From<EncryptedNote> for Note {
fn from(value: EncryptedNote) -> Self {
Self::Encrypted(value)
} }
} }
impl Note { impl Note {
pub fn encrypt(&self, password: &str) -> anyhow::Result<Self> {
let mut rng = rand::thread_rng();
match self {
Note::Decrypted { id, 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 {
id: id.clone(),
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 { id, 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 {
id: id.clone(),
title: title.clone(),
metadata: metadata.clone(),
text: decrypted_text,
})
}
_ => Ok(self.clone()),
}
}
pub fn hide(self) -> Self {
match self {
Note::Decrypted { id, title, metadata, text } => {
Self::Hidden { id, title, metadata, text }
}
_ => unreachable!()
}
}
pub fn unhide(self) -> Self {
match self {
Note::Hidden { id, title, metadata, text } => {
Self::Decrypted { id, title, metadata, text }
}
_ => unreachable!()
}
}
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)?)?;