diff --git a/mm2src/crypto/src/encrypt.rs b/mm2src/crypto/src/encrypt.rs index 30c5246aa1..8ad537e7cf 100644 --- a/mm2src/crypto/src/encrypt.rs +++ b/mm2src/crypto/src/encrypt.rs @@ -9,6 +9,8 @@ use hmac::{Hmac, Mac}; use mm2_err_handle::prelude::*; use sha2::Sha256; +const ENCRYPTED_DATA_VERSION: u8 = 1; + type Aes256CbcEnc = cbc::Encryptor; #[derive(Debug, Display, PartialEq)] @@ -41,6 +43,10 @@ pub enum EncryptionAlgorithm { /// providing a robust and comprehensive approach to securing sensitive mnemonic data. #[derive(Serialize, Deserialize, Debug)] pub struct EncryptedData { + /// Version of the encrypted data format. + /// This version value allows future changes to this struct while maintaining backward compatibility. + pub version: u8, + /// The encryption algorithm used to encrypt the mnemonic. /// Example: "AES-256-CBC". pub encryption_algorithm: EncryptionAlgorithm, @@ -107,6 +113,7 @@ pub fn encrypt_data( let tag = mac.finalize().into_bytes(); let encrypted_data = EncryptedData { + version: ENCRYPTED_DATA_VERSION, encryption_algorithm: EncryptionAlgorithm::AES256CBC, key_derivation_details, iv: STANDARD.encode(iv), diff --git a/mm2src/crypto/src/key_derivation.rs b/mm2src/crypto/src/key_derivation.rs index 876852ff75..69cd1a4d66 100644 --- a/mm2src/crypto/src/key_derivation.rs +++ b/mm2src/crypto/src/key_derivation.rs @@ -6,13 +6,14 @@ use hmac::{Hmac, Mac}; use mm2_err_handle::mm_error::MmResult; use mm2_err_handle::prelude::*; use sha2::Sha512; -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; -const ARGON2_ALGORITHM: &str = "Argon2id"; -const ARGON2ID_VERSION: &str = "0x13"; +const ARGON2_ALGORITHM: &str = "argon2id"; +const ARGON2ID_VERSION: u32 = 19; const ARGON2ID_M_COST: u32 = 65536; const ARGON2ID_T_COST: u32 = 2; const ARGON2ID_P_COST: u32 = 1; +const ARGON2ID_OUTPUT_LEN: usize = 32; #[allow(dead_code)] type HmacSha512 = Hmac; @@ -25,6 +26,8 @@ pub enum KeyDerivationError { HmacInitialization, #[display(fmt = "Invalid key length")] InvalidKeyLength, + #[display(fmt = "Not supported: {}", _0)] + NotSupported(String), } impl From for KeyDerivationError { @@ -41,8 +44,8 @@ pub struct Argon2Params { /// The specific variant of the Argon2 algorithm used (e.g., Argon2id). algorithm: String, - /// The version of the Argon2 algorithm (e.g., 0x13 for the latest version). - version: String, + /// The version of the Argon2 algorithm (e.g., 0x13 19 in u32) for the latest version). + version: u32, /// The memory cost parameter defining the memory usage of the algorithm. /// Expressed in kibibytes (KiB). @@ -54,16 +57,20 @@ pub struct Argon2Params { /// The parallelism cost parameter defining the number of parallel threads. p_cost: u32, + + /// The size of the output hash in bytes. + output_len: usize, } impl Default for Argon2Params { fn default() -> Self { Argon2Params { algorithm: ARGON2_ALGORITHM.to_string(), - version: ARGON2ID_VERSION.to_string(), + version: ARGON2ID_VERSION, m_cost: ARGON2ID_M_COST, t_cost: ARGON2ID_T_COST, p_cost: ARGON2ID_P_COST, + output_len: ARGON2ID_OUTPUT_LEN, } } } @@ -95,40 +102,66 @@ pub enum KeyDerivationDetails { // Placeholder for future algorithms. } +fn build_argon2_instance(params: &Argon2Params) -> Result, KeyDerivationError> { + let argon2_params = argon2::Params::new(params.m_cost, params.t_cost, params.p_cost, Some(params.output_len)) + .map_err(|e| KeyDerivationError::PasswordHashingFailed(format!("Invalid Argon2 parameters: {}", e)))?; + + let algorithm = argon2::Algorithm::new(¶ms.algorithm) + .map_err(|e| KeyDerivationError::PasswordHashingFailed(format!("Unknown Argon2 algorithm: {}", e)))?; + + let version = argon2::Version::try_from(params.version) + .map_err(|e| KeyDerivationError::PasswordHashingFailed(format!("Unknown Argon2 version: {}", e)))?; + + Ok(Argon2::new(algorithm, version, argon2_params)) +} + /// Derives AES and HMAC keys from a given password and salts for mnemonic encryption/decryption. /// /// # Returns /// A tuple containing the AES key and HMAC key as byte arrays, or a `MnemonicError` in case of failure. -#[allow(dead_code)] pub(crate) fn derive_keys_for_mnemonic( password: &str, - salt_aes: &SaltString, - salt_hmac: &SaltString, + key_details: &KeyDerivationDetails, ) -> MmResult<([u8; 32], [u8; 32]), KeyDerivationError> { - let argon2 = Argon2::default(); - - // Derive AES Key - let aes_password_hash = argon2.hash_password(password.as_bytes(), salt_aes)?; - let key_aes_output = aes_password_hash - .serialize() - .hash() - .ok_or_else(|| KeyDerivationError::PasswordHashingFailed("Error finding AES key hashing output".to_string()))?; - let key_aes = key_aes_output - .as_bytes() - .try_into() - .map_err(|_| KeyDerivationError::PasswordHashingFailed("Invalid AES key length".to_string()))?; - - // Derive HMAC Key - let hmac_password_hash = argon2.hash_password(password.as_bytes(), salt_hmac)?; - let key_hmac_output = hmac_password_hash.serialize().hash().ok_or_else(|| { - KeyDerivationError::PasswordHashingFailed("Error finding HMAC key hashing output".to_string()) - })?; - let key_hmac = key_hmac_output - .as_bytes() - .try_into() - .map_err(|_| KeyDerivationError::PasswordHashingFailed("Invalid HMAC key length".to_string()))?; + match key_details { + KeyDerivationDetails::Argon2 { + params, + salt_aes, + salt_hmac, + } => { + let argon2 = build_argon2_instance(params)?; + + let salt_aes = SaltString::from_b64(salt_aes) + .map_err(|e| KeyDerivationError::PasswordHashingFailed(format!("Invalid AES salt: {}", e)))?; + let salt_hmac = SaltString::from_b64(salt_hmac) + .map_err(|e| KeyDerivationError::PasswordHashingFailed(format!("Invalid HMAC salt: {}", e)))?; - Ok((key_aes, key_hmac)) + // Derive AES Key + let aes_password_hash = argon2.hash_password(password.as_bytes(), &salt_aes)?; + let key_aes_output = aes_password_hash.serialize().hash().ok_or_else(|| { + KeyDerivationError::PasswordHashingFailed("Error finding AES key hashing output".to_string()) + })?; + let key_aes = key_aes_output + .as_bytes() + .try_into() + .map_err(|_| KeyDerivationError::PasswordHashingFailed("Invalid AES key length".to_string()))?; + + // Derive HMAC Key + let hmac_password_hash = argon2.hash_password(password.as_bytes(), &salt_hmac)?; + let key_hmac_output = hmac_password_hash.serialize().hash().ok_or_else(|| { + KeyDerivationError::PasswordHashingFailed("Error finding HMAC key hashing output".to_string()) + })?; + let key_hmac = key_hmac_output + .as_bytes() + .try_into() + .map_err(|_| KeyDerivationError::PasswordHashingFailed("Invalid HMAC key length".to_string()))?; + + Ok((key_aes, key_hmac)) + }, + KeyDerivationDetails::SLIP0021 { .. } => MmError::err(KeyDerivationError::NotSupported( + "SLIP-0021 key derivation is not supported for mnemonic keys".to_string(), + )), + } } /// Splits a path into its components and derives a key for each component. diff --git a/mm2src/crypto/src/mnemonic.rs b/mm2src/crypto/src/mnemonic.rs index 922b292da0..9d89b25fce 100644 --- a/mm2src/crypto/src/mnemonic.rs +++ b/mm2src/crypto/src/mnemonic.rs @@ -29,10 +29,6 @@ impl From for MnemonicError { fn from(e: bip39::Error) -> Self { MnemonicError::BIP39Error(e.to_string()) } } -impl From for MnemonicError { - fn from(e: argon2::password_hash::Error) -> Self { MnemonicError::KeyDerivationError(e.to_string()) } -} - impl From for MnemonicError { fn from(e: KeyDerivationError) -> Self { MnemonicError::KeyDerivationError(e.to_string()) } } @@ -84,7 +80,7 @@ pub fn encrypt_mnemonic(mnemonic: &str, password: &str) -> MmResult MmResult MmResult { - // Re-create the salts from Base64-encoded strings - let (salt_aes, salt_hmac) = match &encrypted_data.key_derivation_details { - KeyDerivationDetails::Argon2 { - salt_aes, salt_hmac, .. - } => (SaltString::from_b64(salt_aes)?, SaltString::from_b64(salt_hmac)?), - _ => { - return MmError::err(MnemonicError::KeyDerivationError( - "Key derivation details should be Argon2!".to_string(), - )) - }, - }; - // Re-create the keys from the password and salts - let (key_aes, key_hmac) = derive_keys_for_mnemonic(password, &salt_aes, &salt_hmac)?; + let (key_aes, key_hmac) = derive_keys_for_mnemonic(password, &encrypted_data.key_derivation_details)?; // Decrypt the ciphertext let decrypted_data =