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:
parent
fab26f8c73
commit
2078c82f45
|
@ -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"
|
27
docs/migrations/passwords.md
Normal file
27
docs/migrations/passwords.md
Normal 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`
|
21
src/main.rs
21
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);
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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));
|
||||
|
|
Loading…
Reference in a new issue