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 native_dialog::MessageDialog;
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::saving::Saving;
@ -108,32 +108,37 @@ impl eframe::App for App {
if !self.tested_password && self.password.is_some() {
if let Some(note) = self.notes.get(0) {
if let Ok(password) = check_password(self.password.as_ref().unwrap(), note) {
match password {
PasswordSystem::V0(password) => {
info!("migrating from PasswordSystem::V0 to PasswordSystem::V1");
info!("backing up notes");
self.saving.backup_notes().expect("failed to back up notes");
info!("backed up notes successfully!");
info!("starting migration");
self.password = Some(
migrate_to_password_v1(&password, &self.notes, &self.saving)
.expect("failed to migrate notes")
);
self.notes = self.saving.read_notes().expect("failed to update notes after migration");
info!("migration was successful!");
},
PasswordSystem::V1(password) => self.password = Some(password),
}
} else {
let _ = MessageDialog::new()
.set_title("invalid password")
.set_text("failed to verify the password against an existing note. please try again")
.show_alert();
match note {
Note::Encrypted(note) => {
if let Ok(password) = check_password(self.password.as_ref().unwrap(), note) {
match password {
PasswordSystem::V0(password) => {
info!("migrating from PasswordSystem::V0 to PasswordSystem::V1");
info!("backing up notes");
self.saving.backup_notes().expect("failed to back up notes");
info!("backed up notes successfully!");
info!("starting migration");
self.password = Some(
migrate_to_password_v1(&password, &self.notes, &self.saving)
.expect("failed to migrate notes")
);
self.notes = self.saving.read_notes().expect("failed to update notes after migration");
info!("migration was successful!");
},
PasswordSystem::V1(password) => self.password = Some(password),
}
} else {
let _ = MessageDialog::new()
.set_title("invalid password")
.set_text("failed to verify the password against an existing note. please try again")
.show_alert();
self.password = None;
self.tested_password = false;
return;
self.password = None;
self.tested_password = false;
return;
}
}
_ => unreachable!()
}
} else {
self.password = Some(generate_new_password(self.password.as_ref().unwrap()))
@ -245,7 +250,7 @@ impl eframe::App for App {
.then_some(metadata)
});
let note = Note::Decrypted {
let note = Note::Decrypted(DecryptedNote {
id: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
@ -257,9 +262,9 @@ impl eframe::App for App {
arbitrary: metadata,
},
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());
vec_deque.push_front(note);
@ -353,7 +358,10 @@ impl eframe::App for App {
if let Some(save_path) = save_path {
let notes: Vec<Note> = self.notes.clone()
.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();
let json_notes = serde_json::to_string(&notes).unwrap();

View File

@ -12,125 +12,133 @@ pub struct NoteMetadata {
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)]
pub enum Note {
Encrypted {
id: String,
title: String,
metadata: NoteMetadata,
encrypted_text: String,
},
Decrypted {
id: String,
title: String,
metadata: NoteMetadata,
text: String,
},
Hidden {
id: String,
title: String,
metadata: NoteMetadata,
text: String,
Encrypted(EncryptedNote),
Decrypted(DecryptedNote),
Hidden(HiddenNote),
}
impl DecryptedNote {
pub fn encrypt(&self, password: &str) -> anyhow::Result<EncryptedNote> {
let mut rng = rand::thread_rng();
let pwbox = Sodium::build_box(&mut rng)
.seal(password.to_string().into_bytes(), self.text.clone().into_bytes())?;
let mut eraser = Eraser::new();
eraser.add_suite::<Sodium>();
let erased: ErasedPwBox = eraser.erase(&pwbox)?;
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 {
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> {
let mut value = None;
ui.group(|ui| {
match self {
Note::Encrypted { id, title, metadata, .. } => {
let result = render_title_and_metadata(ui, title, metadata, false, None);
Note::Encrypted(note) => {
let result = render_title_and_metadata(ui, &note.title, &note.metadata, false, None);
if let Some(action) = result {
match action {
NoteRenderAction::Delete => cb(id.to_string()),
NoteRenderAction::Delete => cb(note.id.to_string()),
_ => unreachable!()
}
}
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, .. } => {
let result = render_title_and_metadata(ui, title, metadata, true, Some(ButtonType::Hide));
Note::Decrypted(note) => {
let result = render_title_and_metadata(ui, &note.title, &note.metadata, true, Some(ButtonType::Hide));
if let Some(action) = result {
match action {
NoteRenderAction::Delete => cb(id.to_string()),
NoteRenderAction::Hide => value = Some(self.clone().hide()),
NoteRenderAction::Delete => cb(note.id.to_string()),
NoteRenderAction::Hide => value = Some(note.clone().hide().into()),
_ => unreachable!()
}
}
ui.label(text);
ui.label(&note.text);
}
Note::Hidden { id, title, metadata, .. } => {
let result = render_title_and_metadata(ui, title, metadata, true, Some(ButtonType::Unhide));
Note::Hidden(note) => {
let result = render_title_and_metadata(ui, &note.0.title, &note.0.metadata, true, Some(ButtonType::Unhide));
if let Some(action) = result {
match action {
NoteRenderAction::Delete => cb(id.to_string()),
NoteRenderAction::Unhide => value = Some(self.clone().unhide()),
NoteRenderAction::Delete => cb(note.0.id.to_string()),
NoteRenderAction::Unhide => value = Some(note.clone().unhide().into()),
_ => unreachable!()
}
}
@ -142,14 +150,6 @@ impl Note {
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 {

View File

@ -1,7 +1,7 @@
use anyhow::bail;
use argon2::Argon2;
use sha2::{Sha256, Digest};
use crate::notes::Note;
use crate::notes::{EncryptedNote, Note};
use crate::saving::Saving;
pub fn generate_new_password(password: &str) -> String {
@ -35,7 +35,7 @@ pub enum PasswordSystem {
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() {
let new_password = generate_new_password(password);
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> {
let new_password = generate_new_password(password);
for note in notes {
let note = note.decrypt(password)?;
saving.save_note(note, &new_password)?;
}

View File

@ -30,10 +30,15 @@ impl Saving {
Ok(notes.into())
}
pub fn save_note(&self, note: Note, password: &str) -> anyhow::Result<()> {
let note = note.encrypt(password)?;
pub fn save_note(&self, note: &Note, password: &str) -> anyhow::Result<()> {
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("_");
path.set_file_name(format!("{}.json", note.id()));
path.set_file_name(format!("{}.json", note.id));
fs::write(path, serde_json::to_string(&note)?)?;