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.
This commit is contained in:
Sofía Aritz 2023-03-06 18:23:37 +01:00
parent fab26f8c73
commit 2078c82f45
5 changed files with 82 additions and 9 deletions

View file

@ -18,4 +18,5 @@ directories = "4.0"
native-dialog = "0.6"
hex = "0.4"
sha2 = "0.10"
argon2 = "0.5"
argon2 = "0.5"
fs_extra = "1.3"

View file

@ -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`

View file

@ -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);

View file

@ -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<f64> {
Some(length * unique_characters.log2())
}
pub fn check_password(password: &str, note: &Note) -> anyhow::Result<String> {
pub enum PasswordSystem {
V0(String),
V1(String),
}
pub fn check_password(password: &str, note: &Note) -> anyhow::Result<PasswordSystem> {
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<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)?;
}
Ok(new_password)
}

View file

@ -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));