From 2078c82f4512fdfb100caa3d8de2c9c16de3ccf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sof=C3=ADa=20Aritz?= Date: Mon, 6 Mar 2023 18:23:37 +0100 Subject: [PATCH] Add a migration system for the new password system Added an automatic migration system for notes before the #1 redesign. Ths system works as follows: 1. Checks the password system used. 2. If it's the old system, the migration is started. 3. The data directory is backed up. 4. The notes are decrypted using the old password. 5. The notes are encrypted and saved using the `KDF(password)` This commit also adds documentation related to future migrations of the "password system" and which migrations will be supported by each future version. This documents also showcases that when v1 is released, support for `PasswordSystem::V0` will be completely removed. --- Cargo.toml | 3 ++- docs/migrations/passwords.md | 27 +++++++++++++++++++++++++++ src/main.rs | 21 ++++++++++++++++++--- src/password/mod.rs | 22 +++++++++++++++++++--- src/saving/mod.rs | 18 ++++++++++++++++-- 5 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 docs/migrations/passwords.md 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));