Skip to content

Commit 88ee0b8

Browse files
committed
Feat: Auto-migrate legacy vaults to Argon2id on login
- Try legacy decrypt first (fast, no KDF) on login - If legacy succeeds, re-encrypt with Argon2id automatically - Only one Argon2id derivation per login regardless of format - No manual re-creation needed for existing users
1 parent 8514515 commit 88ee0b8

2 files changed

Lines changed: 58 additions & 7 deletions

File tree

src-tauri/src/lib.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,26 @@ fn register(password: &str) -> Value {
179179

180180
#[tauri::command]
181181
fn login(password: &str) -> bool {
182-
let container = driver::read(password);
182+
let container_path = vault::get_container_path();
183+
let encrypted_data = match std::fs::read(&container_path) {
184+
Ok(data) => data,
185+
Err(_) => return false,
186+
};
183187

188+
// Try legacy format first (fast, no KDF)
189+
if let Ok(plaintext) = vault::decrypt_legacy_public(password, &encrypted_data) {
190+
if let Ok(container) = serde_json::from_str::<serde_json::Value>(&plaintext) {
191+
if container.is_array() {
192+
// Re-encrypt with Argon2id
193+
driver::write(password, container);
194+
return true;
195+
}
196+
}
197+
return false;
198+
}
199+
200+
// Try new Argon2id format
201+
let container = driver::read(password);
184202
container.is_array()
185203
}
186204

src-tauri/src/vault.rs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,20 @@ fn derive_key(password: &str, salt: &[u8]) -> Vec<u8> {
5454
key
5555
}
5656

57-
pub fn decrypt(mut password: String, encrypted_data: Vec<u8>) -> Result<String, Error> {
58-
if encrypted_data.len() < SALT_LEN + NONCE_LEN + 16 {
57+
fn decrypt_legacy(password: &str, encrypted_data: &[u8]) -> Result<String, Error> {
58+
if encrypted_data.len() < NONCE_LEN + 16 {
5959
return Err(aes_gcm::Error);
6060
}
6161

62-
let (salt, rest) = encrypted_data.split_at(SALT_LEN);
63-
let (nonce_bytes, ciphertext) = rest.split_at(NONCE_LEN);
62+
let (nonce_bytes, ciphertext) = encrypted_data.split_at(NONCE_LEN);
6463

65-
let mut key_bytes = derive_key(&password, salt);
66-
password.zeroize();
64+
let mut padded = password.to_string();
65+
if padded.len() < 32 {
66+
padded += &"!".repeat(32 - padded.len());
67+
}
68+
69+
let mut key_bytes = padded.as_bytes().to_vec();
70+
padded.zeroize();
6771
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
6872
let nonce = Nonce::from_slice(nonce_bytes);
6973
let cipher = Aes256Gcm::new(key);
@@ -75,6 +79,35 @@ pub fn decrypt(mut password: String, encrypted_data: Vec<u8>) -> Result<String,
7579
String::from_utf8(plaintext).map_err(|_| aes_gcm::Error)
7680
}
7781

82+
pub fn decrypt(mut password: String, encrypted_data: Vec<u8>) -> Result<String, Error> {
83+
// Try new format first (salt + nonce + ciphertext)
84+
if encrypted_data.len() >= SALT_LEN + NONCE_LEN + 16 {
85+
let (salt, rest) = encrypted_data.split_at(SALT_LEN);
86+
let (nonce_bytes, ciphertext) = rest.split_at(NONCE_LEN);
87+
88+
let mut key_bytes = derive_key(&password, salt);
89+
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
90+
let nonce = Nonce::from_slice(nonce_bytes);
91+
let cipher = Aes256Gcm::new(key);
92+
93+
if let Ok(plaintext) = cipher.decrypt(nonce, ciphertext) {
94+
key_bytes.zeroize();
95+
password.zeroize();
96+
return String::from_utf8(plaintext).map_err(|_| aes_gcm::Error);
97+
}
98+
key_bytes.zeroize();
99+
}
100+
101+
// Fallback to legacy format (nonce + ciphertext, password padded with !)
102+
let result = decrypt_legacy(&password, &encrypted_data);
103+
password.zeroize();
104+
result
105+
}
106+
107+
pub fn decrypt_legacy_public(password: &str, encrypted_data: &[u8]) -> Result<String, Error> {
108+
decrypt_legacy(password, encrypted_data)
109+
}
110+
78111
pub fn encrypt(mut password: String, plaintext: String) -> Vec<u8> {
79112
let mut salt = [0u8; SALT_LEN];
80113
aes_gcm::aead::OsRng.fill_bytes(&mut salt);

0 commit comments

Comments
 (0)