diff --git a/Cargo.toml b/Cargo.toml index 0be7408..46ac351 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,5 @@ directories = "4.0" native-dialog = "0.6" hex = "0.4" sha2 = "0.10" -argon2 = "0.5" \ No newline at end of file +argon2 = "0.5" +fs_extra = "1.3" \ No newline at end of file diff --git a/docs/migrations/passwords.md b/docs/migrations/passwords.md new file mode 100644 index 0000000..8bb770a --- /dev/null +++ b/docs/migrations/passwords.md @@ -0,0 +1,27 @@ +# Password migrations + +After [issue #1](https://git.sofiaritz.com/sofia/note-taking/issues/1), the migration of old "databases" was required. +Right now, this isn't very useful because the only user is (probably) me, but this serves as an exercise for the future. + +## Password systems +Currently, there are two password systems _on-the-wild_: +1. `PasswordSystem::V0`. The system used before [issue #1](https://git.sofiaritz.com/sofia/note-taking/issues/1) +2. `PasswordSystem::V1`. The system used after [issue #1](https://git.sofiaritz.com/sofia/note-taking/issues/1) +(with the KDF) + +## Support +Only the latest and previous password system are going to be supported at any point in time. + +### Example +1. `v1` uses `PasswordSystem::V1` and supports migration from `PasswordSystem::V0` +2. `v2` is released with the `PasswordSystem::V2` and migration from `PasswordSystem::V1`, those who are still on the +`PasswordSystem::V0` are redirected to the latest `v1` release. + +## Currently supported systems +- `v0.*`: `PasswordSystem::V1` and migration from `PasswordSystem::V0` +- `v1.*`: + - If a new system is used: `PasswordSystem::V2` and migration from `PasswordSystem::V1` + - If no new system is used: `PasswordSystem::V1` +- `v2.*`: + - If a new system is used: `PasswordSystem::V3` and migration from `PasswordSystem::V2` + - If no new system is used: `PasswordSystem::V2` and migration from `PasswordSystem::V1` \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index dc7b8f4..288c267 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use log::info; use native_dialog::MessageDialog; use simple_logger::SimpleLogger; use crate::notes::{Note, NoteMetadata}; -use crate::password::{check_password, entropy, generate_new_password}; +use crate::password::{check_password, entropy, generate_new_password, migrate_to_password_v1, PasswordSystem}; use crate::saving::Saving; mod notes; @@ -104,7 +104,22 @@ 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) { - self.password = Some(password); + 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") @@ -199,7 +214,7 @@ impl eframe::App for App { text: self.text_buffer.clone(), }; - self.saving.save_note(note.clone(), password.to_string()).unwrap(); + self.saving.save_note(note.clone(), password).unwrap(); let mut vec_deque = VecDeque::from(self.notes.clone()); vec_deque.push_front(note); diff --git a/src/password/mod.rs b/src/password/mod.rs index dc05018..da8be87 100644 --- a/src/password/mod.rs +++ b/src/password/mod.rs @@ -2,6 +2,7 @@ use anyhow::bail; use argon2::Argon2; use sha2::{Sha256, Digest}; use crate::notes::Note; +use crate::saving::Saving; pub fn generate_new_password(password: &str) -> String { let mut output = [0_u8; 32]; @@ -29,16 +30,21 @@ pub fn entropy(password: &str) -> Option { Some(length * unique_characters.log2()) } -pub fn check_password(password: &str, note: &Note) -> anyhow::Result { +pub enum PasswordSystem { + V0(String), + V1(String), +} + +pub fn check_password(password: &str, note: &Note) -> anyhow::Result { if note.decrypt(password).is_err() { let new_password = generate_new_password(password); if note.decrypt(&new_password).is_err() { bail!("invalid password"); } else { - Ok(new_password) + Ok(PasswordSystem::V1(new_password)) } } else { - Ok(password.into()) + Ok(PasswordSystem::V0(password.into())) } } @@ -48,4 +54,14 @@ fn hash_password(password: &[u8], output: &mut [u8]) { let result = hasher.finalize(); output.clone_from_slice(&result[..]) +} + +pub fn migrate_to_password_v1(password: &str, notes: &Vec, saving: &Saving) -> anyhow::Result { + let new_password = generate_new_password(password); + for note in notes { + let note = note.decrypt(password)?; + saving.save_note(note, &new_password)?; + } + + Ok(new_password) } \ No newline at end of file diff --git a/src/saving/mod.rs b/src/saving/mod.rs index 7ed51db..8fbbd26 100644 --- a/src/saving/mod.rs +++ b/src/saving/mod.rs @@ -1,6 +1,7 @@ use std::collections::VecDeque; use std::fs; use std::path::{Path, PathBuf}; +use fs_extra::dir::{copy, CopyOptions}; use crate::notes::Note; pub struct Saving { @@ -29,8 +30,8 @@ impl Saving { Ok(notes.into()) } - pub fn save_note(&self, note: Note, password: String) -> anyhow::Result<()> { - let note = note.encrypt(&password)?; + pub fn save_note(&self, note: Note, password: &str) -> anyhow::Result<()> { + let note = note.encrypt(password)?; let mut path = self.path.clone().join("_"); path.set_file_name(format!("{}.json", note.id())); @@ -39,6 +40,19 @@ impl Saving { Ok(()) } + pub fn backup_notes(&self) -> anyhow::Result<()> { + let path = { + let mut path = self.path.clone(); + path.pop(); + path.join("data_bak") + }; + + fs::create_dir_all(&path)?; + copy(&self.path, path, &CopyOptions::new())?; + + Ok(()) + } + pub fn delete_note(&self, id: &str) -> anyhow::Result<()> { let mut path = self.path.clone().join("_"); path.set_file_name(format!("{}.json", id));