From 3020a9cfc969e8ace69471457a50b8e0957de0cc Mon Sep 17 00:00:00 2001 From: escouade-bot Date: Thu, 9 Apr 2026 00:02:49 -0400 Subject: [PATCH] fix: migrate PIN hashing from SHA-256 to Argon2id (#54) Replace SHA-256 with Argon2id (m=64MiB, t=3, p=1) for PIN hashing. Existing SHA-256 hashes are verified transparently via format detection (argon2id: prefix). New PINs are always hashed with Argon2id. Addresses CWE-916: Use of Password Hash With Insufficient Computational Effort. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.fr.md | 3 ++ CHANGELOG.md | 3 ++ docs/architecture.md | 4 +- src-tauri/src/commands/profile_commands.rs | 61 +++++++++++++++++++--- 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 67cc154..bf20901 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -2,6 +2,9 @@ ## [Non publié] +### Modifié +- Hachage du PIN migré de SHA-256 vers Argon2id pour résistance au brute-force (CWE-916). Les PINs SHA-256 existants sont vérifiés de façon transparente ; les nouveaux PINs utilisent Argon2id (#54) + ## [0.6.7] - 2026-03-29 ### Modifié diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ff4be2..bff96f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Changed +- PIN hashing migrated from SHA-256 to Argon2id for brute-force resistance (CWE-916). Existing SHA-256 PINs are verified transparently; new PINs use Argon2id (#54) + ## [0.6.7] - 2026-03-29 ### Changed diff --git a/docs/architecture.md b/docs/architecture.md index f1d2ae8..40da47b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -171,8 +171,8 @@ Chaque hook encapsule la logique d'état via `useReducer` : - `save_profiles` — Sauvegarde de la configuration - `delete_profile_db` — Suppression du fichier de base de données - `get_new_profile_init_sql` — Récupération du schéma consolidé -- `hash_pin` — Hachage Argon2 du PIN -- `verify_pin` — Vérification du PIN +- `hash_pin` — Hachage Argon2id du PIN (format `argon2id:salt:hash`) +- `verify_pin` — Vérification du PIN (supporte Argon2id et legacy SHA-256 pour rétrocompatibilité) - `repair_migrations` — Réparation des checksums de migration (rusqlite) ## Pages et routing diff --git a/src-tauri/src/commands/profile_commands.rs b/src-tauri/src/commands/profile_commands.rs index 91f63dc..0661ab6 100644 --- a/src-tauri/src/commands/profile_commands.rs +++ b/src-tauri/src/commands/profile_commands.rs @@ -1,3 +1,4 @@ +use argon2::{Algorithm, Argon2, Params, Version}; use rand::RngCore; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256, Sha384}; @@ -118,24 +119,55 @@ pub fn get_new_profile_init_sql() -> Result, String> { ]) } +// Argon2id parameters for PIN hashing (same as export_import_commands.rs) +const ARGON2_M_COST: u32 = 65536; // 64 MiB +const ARGON2_T_COST: u32 = 3; +const ARGON2_P_COST: u32 = 1; +const ARGON2_OUTPUT_LEN: usize = 32; +const ARGON2_SALT_LEN: usize = 16; + +fn argon2_hash(pin: &str, salt: &[u8]) -> Result, String> { + let params = Params::new(ARGON2_M_COST, ARGON2_T_COST, ARGON2_P_COST, Some(ARGON2_OUTPUT_LEN)) + .map_err(|e| format!("Argon2 params error: {}", e))?; + let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + let mut hash = vec![0u8; ARGON2_OUTPUT_LEN]; + argon2 + .hash_password_into(pin.as_bytes(), salt, &mut hash) + .map_err(|e| format!("Argon2 hash error: {}", e))?; + Ok(hash) +} + #[tauri::command] pub fn hash_pin(pin: String) -> Result { - let mut salt = [0u8; 16]; + let mut salt = [0u8; ARGON2_SALT_LEN]; rand::rngs::OsRng.fill_bytes(&mut salt); let salt_hex = hex_encode(&salt); - let mut hasher = Sha256::new(); - hasher.update(salt_hex.as_bytes()); - hasher.update(pin.as_bytes()); - let result = hasher.finalize(); - let hash_hex = hex_encode(&result); + let hash = argon2_hash(&pin, &salt)?; + let hash_hex = hex_encode(&hash); - // Store as "salt:hash" - Ok(format!("{}:{}", salt_hex, hash_hex)) + // Store as "argon2id:salt:hash" to distinguish from legacy SHA-256 "salt:hash" + Ok(format!("argon2id:{}:{}", salt_hex, hash_hex)) } #[tauri::command] pub fn verify_pin(pin: String, stored_hash: String) -> Result { + // Argon2id format: "argon2id:salt_hex:hash_hex" + if let Some(rest) = stored_hash.strip_prefix("argon2id:") { + let parts: Vec<&str> = rest.split(':').collect(); + if parts.len() != 2 { + return Err("Invalid Argon2id hash format".to_string()); + } + let salt = hex_decode(parts[0])?; + let expected_hash = parts[1]; + + let computed = argon2_hash(&pin, &salt)?; + let computed_hex = hex_encode(&computed); + + return Ok(computed_hex == expected_hash); + } + + // Legacy SHA-256 format: "salt_hex:hash_hex" let parts: Vec<&str> = stored_hash.split(':').collect(); if parts.len() != 2 { return Err("Invalid stored hash format".to_string()); @@ -156,6 +188,19 @@ fn hex_encode(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{:02x}", b)).collect() } +fn hex_decode(hex: &str) -> Result, String> { + if hex.len() % 2 != 0 { + return Err("Invalid hex string length".to_string()); + } + (0..hex.len()) + .step_by(2) + .map(|i| { + u8::from_str_radix(&hex[i..i + 2], 16) + .map_err(|e| format!("Invalid hex character: {}", e)) + }) + .collect() +} + /// Repair migration checksums for a profile database. /// Updates stored checksums to match current migration SQL, avoiding re-application /// of destructive migrations (e.g., migration 2 which DELETEs categories/keywords).