diff --git a/Cargo.lock b/Cargo.lock index a3f9664b59e..ecfdba90fe3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4993,7 +4993,7 @@ dependencies = [ name = "tari_key_manager" version = "0.38.8" dependencies = [ - "argon2 0.2.4", + "argon2 0.4.1", "arrayvec 0.7.2", "blake2 0.9.2", "chacha20 0.7.3", @@ -5010,6 +5010,7 @@ dependencies = [ "sha2 0.9.9", "strum", "strum_macros", + "subtle", "tari_common_types", "tari_crypto", "tari_utilities", diff --git a/base_layer/key_manager/Cargo.toml b/base_layer/key_manager/Cargo.toml index d4ffa38eb05..85f4ad5ea08 100644 --- a/base_layer/key_manager/Cargo.toml +++ b/base_layer/key_manager/Cargo.toml @@ -17,7 +17,7 @@ tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", tag = " tari_utilities = { git = "https://github.com/tari-project/tari_utilities.git", tag="v0.4.7" } arrayvec = "0.7.1" -argon2 = { version = "0.2", features = ["std"] } +argon2 = { version = "0.4", features = ["std", "alloc"] } blake2 = "0.9.1" chacha20 = "0.7.1" console_error_panic_hook = { version = "0.1.7", optional = true } @@ -35,6 +35,7 @@ strum_macros = "0.22" strum = { version = "0.22", features = ["derive"] } wasm-bindgen = { version = "0.2", features = ["serde-serialize", "nightly"], optional = true } zeroize = "1" +subtle = "2.4.1" [dev-dependencies] sha2 = "0.9.8" diff --git a/base_layer/key_manager/src/cipher_seed.rs b/base_layer/key_manager/src/cipher_seed.rs index ad6591514c9..8b739edbc03 100644 --- a/base_layer/key_manager/src/cipher_seed.rs +++ b/base_layer/key_manager/src/cipher_seed.rs @@ -22,14 +22,7 @@ use std::{convert::TryFrom, mem::size_of}; -use argon2::{ - password_hash::{Salt, SaltString}, - Argon2, - Params, - PasswordHasher, - Version, -}; -use arrayvec::ArrayVec; +use argon2; use chacha20::{ cipher::{NewCipher, StreamCipher}, ChaCha20, @@ -39,9 +32,10 @@ use chacha20::{ use crc32fast::Hasher as CrcHasher; use rand::{rngs::OsRng, RngCore}; use serde::{Deserialize, Serialize}; +use subtle::ConstantTimeEq; use tari_crypto::hash::blake2::Blake256; use tari_utilities::ByteArray; -use zeroize::Zeroize; +use zeroize::{Zeroize, Zeroizing}; use crate::{ error::KeyManagerError, @@ -56,11 +50,14 @@ const CIPHER_SEED_VERSION: u8 = 0u8; // seconds elapsed from unix epoch until '2022-01-01' == 60 * 60 * 24 * 365 * 52 pub const BIRTHDAY_GENESIS_FROM_UNIX_EPOCH: u64 = 1639872000; pub const DEFAULT_CIPHER_SEED_PASSPHRASE: &str = "TARI_CIPHER_SEED"; -const ARGON2_SALT_BYTES: usize = 16; pub const CIPHER_SEED_BIRTHDAY_BYTES: usize = 2; pub const CIPHER_SEED_ENTROPY_BYTES: usize = 16; -pub const CIPHER_SEED_SALT_BYTES: usize = 5; +pub const CIPHER_SEED_MAIN_SALT_BYTES: usize = 5; +pub const ARGON2_SALT_BYTES: usize = 16; pub const CIPHER_SEED_MAC_BYTES: usize = 5; +pub const CIPHER_SEED_ENCRYPTION_KEY_BYTES: usize = 32; +pub const CIPHER_SEED_MAC_KEY_BYTES: usize = 32; +pub const CIPHER_SEED_CHECKSUM_BYTES: usize = 4; /// This is an implementation of a Cipher Seed based on the `aezeed` encoding scheme (https://github.com/lightningnetwork/lnd/tree/master/aezeed) /// The goal of the scheme is produce a wallet seed that is versioned, contains the birthday of the wallet, @@ -113,12 +110,13 @@ pub const CIPHER_SEED_MAC_BYTES: usize = 5; pub struct CipherSeed { version: u8, birthday: u16, - entropy: [u8; CIPHER_SEED_ENTROPY_BYTES], - salt: [u8; CIPHER_SEED_SALT_BYTES], + entropy: Vec, + salt: Vec, } impl CipherSeed { #[cfg(not(target_arch = "wasm32"))] + /// Generate a new seed pub fn new() -> Self { use std::time::{Duration, SystemTime, UNIX_EPOCH}; const SECONDS_PER_DAY: u64 = 24 * 60 * 60; @@ -133,6 +131,7 @@ impl CipherSeed { } #[cfg(target_arch = "wasm32")] + /// Generate a new seed pub fn new() -> Self { const MILLISECONDS_PER_DAY: u64 = 24 * 60 * 60 * 1000; let millis = js_sys::Date::now() as u64; @@ -141,10 +140,11 @@ impl CipherSeed { CipherSeed::new_with_birthday(birthday) } + /// Generate a new seed with a given birthday fn new_with_birthday(birthday: u16) -> Self { - let mut entropy = [0u8; CIPHER_SEED_ENTROPY_BYTES]; - OsRng.fill_bytes(&mut entropy); - let mut salt = [0u8; CIPHER_SEED_SALT_BYTES]; + let mut entropy = vec![0u8; CIPHER_SEED_ENTROPY_BYTES]; + OsRng.fill_bytes(entropy.as_mut()); + let mut salt = vec![0u8; CIPHER_SEED_MAIN_SALT_BYTES]; OsRng.fill_bytes(&mut salt); Self { @@ -155,223 +155,205 @@ impl CipherSeed { } } + /// Generate an encrypted seed from a passphrase pub fn encipher(&self, passphrase: Option) -> Result, KeyManagerError> { - let mut plaintext = self.birthday.to_le_bytes().to_vec(); - plaintext.append(&mut self.entropy().clone().to_vec()); - - let passphrase = passphrase.unwrap_or_else(|| DEFAULT_CIPHER_SEED_PASSPHRASE.to_string()); + // Derive encryption and MAC keys from passphrase and main salt + let passphrase = Zeroizing::new(passphrase.unwrap_or_else(|| DEFAULT_CIPHER_SEED_PASSPHRASE.to_string())); + let (encryption_key, mac_key) = Self::derive_keys(&passphrase, &self.salt)?; - // generate the current MAC + // Generate the MAC let mut mac = Self::generate_mac( &self.birthday.to_le_bytes(), - &self.entropy(), - &[CIPHER_SEED_VERSION], + self.entropy.as_ref(), + CIPHER_SEED_VERSION, &self.salt, - passphrase.as_str(), + mac_key.as_ref(), )?; - plaintext.append(&mut mac); + // Assemble the secret data to be encrypted: birthday, entropy, MAC + let mut secret_data = Zeroizing::new(Vec::::with_capacity( + CIPHER_SEED_BIRTHDAY_BYTES + CIPHER_SEED_ENTROPY_BYTES + CIPHER_SEED_MAC_BYTES, + )); + secret_data.append(&mut self.birthday.to_le_bytes().to_vec()); + secret_data.append(&mut self.entropy.clone()); + secret_data.append(&mut mac); - // apply cipher stream - Self::apply_stream_cipher(&mut plaintext, &passphrase, &self.salt)?; + // Encrypt the secret data + Self::apply_stream_cipher(&mut secret_data, encryption_key.as_ref(), &self.salt)?; - let mut final_seed = vec![CIPHER_SEED_VERSION]; - final_seed.append(&mut plaintext.to_vec()); - final_seed.append(&mut self.salt.to_vec()); + // Assemble the final seed: version, main salt, secret data, checksum + let mut encrypted_seed = + Vec::::with_capacity(1 + CIPHER_SEED_MAIN_SALT_BYTES + secret_data.len() + CIPHER_SEED_CHECKSUM_BYTES); + encrypted_seed.push(CIPHER_SEED_VERSION); + encrypted_seed.append(&mut secret_data.to_vec()); + encrypted_seed.append(&mut self.salt.to_vec()); let mut crc_hasher = CrcHasher::new(); - crc_hasher.update(final_seed.as_slice()); - let checksum = crc_hasher.finalize(); - final_seed.append(&mut checksum.to_le_bytes().to_vec()); - Ok(final_seed) + crc_hasher.update(encrypted_seed.as_slice()); + let mut checksum = crc_hasher.finalize().to_le_bytes().to_vec(); + encrypted_seed.append(&mut checksum); + + Ok(encrypted_seed) } - pub fn from_enciphered_bytes(enciphered_bytes: &[u8], passphrase: Option) -> Result { - // 1 byte Version || 2 byte Birthday || 16 byte Entropy || 5 byte MAC || 5 byte salt || 4 byte CRC32 - if enciphered_bytes.len() != 7 + CIPHER_SEED_ENTROPY_BYTES + CIPHER_SEED_SALT_BYTES + CIPHER_SEED_MAC_BYTES { + /// Recover a seed from encrypted data and a passphrase + pub fn from_enciphered_bytes(encrypted_seed: &[u8], passphrase: Option) -> Result { + // Check the length: version, birthday, entropy, MAC, salt, checksum + if encrypted_seed.len() != + 1 + CIPHER_SEED_BIRTHDAY_BYTES + + CIPHER_SEED_ENTROPY_BYTES + + CIPHER_SEED_MAC_BYTES + + CIPHER_SEED_MAIN_SALT_BYTES + + CIPHER_SEED_CHECKSUM_BYTES + { return Err(KeyManagerError::InvalidData); } - if enciphered_bytes[0] != CIPHER_SEED_VERSION { + // We only support one version right now + let version = encrypted_seed[0]; + if version != CIPHER_SEED_VERSION { return Err(KeyManagerError::VersionMismatch); } - let passphrase = passphrase.unwrap_or_else(|| DEFAULT_CIPHER_SEED_PASSPHRASE.to_string()); - - let mut body = enciphered_bytes.to_owned(); - // extract 32 bit checksum - let checksum_vec = body.split_off(body.len() - 4); + let mut encrypted_seed = encrypted_seed.to_owned(); + // Verify the checksum first, to detect obvious errors + let checksum = encrypted_seed.split_off( + 1 + CIPHER_SEED_BIRTHDAY_BYTES + + CIPHER_SEED_ENTROPY_BYTES + + CIPHER_SEED_MAC_BYTES + + CIPHER_SEED_MAIN_SALT_BYTES, + ); let mut crc_hasher = CrcHasher::new(); - crc_hasher.update(body.as_slice()); - - let calculated_checksum = crc_hasher.finalize(); - - let mut checksum_bytes: [u8; 4] = [0u8; 4]; - checksum_bytes.copy_from_slice(&checksum_vec[..4]); - let checksum = u32::from_le_bytes(checksum_bytes); - - if calculated_checksum != checksum { + crc_hasher.update(encrypted_seed.as_slice()); + let expected_checksum = crc_hasher.finalize().to_le_bytes().to_vec(); + if checksum != expected_checksum { return Err(KeyManagerError::CrcError); } - let salt = body.split_off(body.len() - CIPHER_SEED_SALT_BYTES); - let mut enciphered_seed = body.split_off(1); - let received_version = body[0]; - - // apply cipher stream - Self::apply_stream_cipher(&mut enciphered_seed, &passphrase, salt.as_slice())?; - - let decrypted_mac = enciphered_seed.split_off(enciphered_seed.len() - CIPHER_SEED_MAC_BYTES); + // Derive encryption and MAC keys from passphrase and main salt + let passphrase = Zeroizing::new(passphrase.unwrap_or_else(|| DEFAULT_CIPHER_SEED_PASSPHRASE.to_string())); + let salt = encrypted_seed + .split_off(1 + CIPHER_SEED_BIRTHDAY_BYTES + CIPHER_SEED_ENTROPY_BYTES + CIPHER_SEED_MAC_BYTES); + let (encryption_key, mac_key) = Self::derive_keys(&passphrase, &salt)?; - let decrypted_entropy_vec: ArrayVec<_, CIPHER_SEED_ENTROPY_BYTES> = - enciphered_seed.split_off(2).into_iter().collect(); - let decrypted_entropy = decrypted_entropy_vec - .into_inner() - .map_err(|_| KeyManagerError::InvalidData)?; + // Decrypt the secret data: birthday, entropy, MAC + let mut secret_data = Zeroizing::new(encrypted_seed.split_off(1)); + Self::apply_stream_cipher(&mut secret_data, encryption_key.as_ref(), &salt)?; - let mut birthday_bytes: [u8; CIPHER_SEED_BIRTHDAY_BYTES] = [0u8; CIPHER_SEED_BIRTHDAY_BYTES]; - birthday_bytes.copy_from_slice(&enciphered_seed); - let decrypted_birthday = u16::from_le_bytes(birthday_bytes); + // Parse secret data + let mac = secret_data.split_off(CIPHER_SEED_BIRTHDAY_BYTES + CIPHER_SEED_ENTROPY_BYTES); + let entropy = Zeroizing::new(secret_data.split_off(CIPHER_SEED_BIRTHDAY_BYTES)); + let mut birthday_bytes = [0u8; CIPHER_SEED_BIRTHDAY_BYTES]; + birthday_bytes.copy_from_slice(&secret_data); + let birthday = u16::from_le_bytes(birthday_bytes); - // generate the MAC - let mac = Self::generate_mac( - &decrypted_birthday.to_le_bytes(), - &decrypted_entropy, - &[CIPHER_SEED_VERSION], - salt.as_slice(), - passphrase.as_str(), - )?; + // Generate the MAC + let expected_mac = Self::generate_mac(&birthday_bytes, entropy.as_ref(), version, &salt, mac_key.as_ref())?; - if decrypted_mac != mac { + // Verify the MAC in constant time to avoid leaking data + if mac.ct_eq(&expected_mac).unwrap_u8() == 0 { return Err(KeyManagerError::DecryptionFailed); } - let salt_vec: ArrayVec<_, CIPHER_SEED_SALT_BYTES> = salt.into_iter().collect(); - let salt_bytes = salt_vec.into_inner().map_err(|_| KeyManagerError::InvalidData)?; - Ok(Self { - version: received_version, - birthday: decrypted_birthday, - entropy: decrypted_entropy, - salt: salt_bytes, + version, + birthday, + entropy: (*entropy).clone(), + salt, }) } - fn apply_stream_cipher(data: &mut Vec, passphrase: &str, salt: &[u8]) -> Result<(), KeyManagerError> { - // encryption nonce for ChaCha20 encryption, generated as a domain separated hash of the given salt. Following - // https://libsodium.gitbook.io/doc/advanced/stream_ciphers/chacha20, as of the IEF variant, the produced encryption - // nonce should be 96-bit long + /// Encrypt or decrypt data using ChaCha20 + fn apply_stream_cipher(data: &mut [u8], encryption_key: &[u8], salt: &[u8]) -> Result<(), KeyManagerError> { + // The ChaCha20 nonce is derived from the main salt let encryption_nonce = mac_domain_hasher::(LABEL_CHACHA20_ENCODING) .chain(salt) .finalize(); - let encryption_nonce = &encryption_nonce.as_ref()[..size_of::()]; - let nonce_ga = Nonce::from_slice(encryption_nonce); + let mut key = Key::clone_from_slice(encryption_key); - // we take the last 32 bytes of the generated derived encryption key for ChaCha20 cipher, see documentation - let derived_encryption_key = Self::generate_domain_separated_passphrase_hash(passphrase, salt)?; + // Encrypt/decrypt the data + let mut cipher = ChaCha20::new(&key, Nonce::from_slice(encryption_nonce)); + cipher.apply_keystream(data); - let key = Key::from_slice(&derived_encryption_key[32..]); - let mut cipher = ChaCha20::new(key, nonce_ga); - cipher.apply_keystream(data.as_mut_slice()); + // We need to specifically zeroize the key + key.zeroize(); Ok(()) } - pub fn entropy(&self) -> [u8; CIPHER_SEED_ENTROPY_BYTES] { - self.entropy + /// Get a reference to the seed entropy + pub fn entropy(&self) -> &Vec { + &self.entropy } + /// Get the seed birthday pub fn birthday(&self) -> u16 { self.birthday } -} -impl CipherSeed { + /// Generate a MAC using Blake2b fn generate_mac( birthday: &[u8], entropy: &[u8], - cipher_seed_version: &[u8], + cipher_seed_version: u8, salt: &[u8], - passphrase: &str, + mac_key: &[u8], ) -> Result, KeyManagerError> { - // birthday should be 2 bytes long + // Check all lengths are valid if birthday.len() != CIPHER_SEED_BIRTHDAY_BYTES { return Err(KeyManagerError::InvalidData); } - - // entropy should be 16 bytes long if entropy.len() != CIPHER_SEED_ENTROPY_BYTES { return Err(KeyManagerError::InvalidData); } - - // cipher_seed_version should be 1 byte long - if cipher_seed_version.len() != 1 { + if salt.len() != CIPHER_SEED_MAIN_SALT_BYTES { return Err(KeyManagerError::InvalidData); } - // salt should be 5 bytes long - if salt.len() != CIPHER_SEED_SALT_BYTES { - return Err(KeyManagerError::InvalidData); - } - - // we take the first 32 bytes of the generated derived encryption key for MAC generation, see documentation - let passphrase_key = Self::generate_domain_separated_passphrase_hash(passphrase, salt)?; - Ok(mac_domain_hasher::(LABEL_MAC_GENERATION) .chain(birthday) .chain(entropy) - .chain(cipher_seed_version) + .chain(&[cipher_seed_version]) .chain(salt) - .chain(&passphrase_key[32..]) + .chain(mac_key) .finalize() .as_ref()[..CIPHER_SEED_MAC_BYTES] .to_vec()) } - fn generate_domain_separated_passphrase_hash(passphrase: &str, salt: &[u8]) -> Result, KeyManagerError> { - let argon2 = Argon2::default(); - - // we produce a domain separated hash of the given salt, for Argon2 encryption use. As suggested in - // https://en.wikipedia.org/wiki/Argon2, we shall use a 16-byte length hash salt + /// Use Argon2 to derive encryption and MAC keys from a passphrase and main salt + fn derive_keys(passphrase: &str, salt: &[u8]) -> Result<(Zeroizing>, Zeroizing>), KeyManagerError> { + // The Argon2 salt is derived from the main salt let argon2_salt = mac_domain_hasher::(LABEL_ARGON_ENCODING) .chain(salt) .finalize(); let argon2_salt = &argon2_salt.as_ref()[..ARGON2_SALT_BYTES]; - // produce a base64 salt string - let argon2_salt = SaltString::b64_encode(argon2_salt)?; - - // to generate two 32-byte keys, we produce a 64-byte argon2 output, as the default output size - // for argon is 32, we have to update its parameters accordingly - - // the following choice of parameters is based on + // Run Argon2 with enough output to accommodate both keys, so we only run it once + // We use the recommended OWASP parameters for this: // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id - let params = Params { - m_cost: 37 * 1024, // m-cost should be 37 Mib = 37 * 1024 Kib - t_cost: 1, // t-cost - p_cost: 1, // p-cost - output_size: 64, // 64 bytes output size, - version: Version::V0x13, // version - }; - - // Argon2id algorithm: https://docs.rs/argon2/0.2.4/argon2/enum.Algorithm.html#variant.Argon2id - let algorithm = argon2::Algorithm::Argon2id; - - // generate the given derived encryption key - let derived_encryption_key = argon2 - .hash_password( - passphrase.as_bytes(), - Some(algorithm.ident()), - params, - Salt::try_from(argon2_salt.as_str())?, - )? - .hash - .ok_or_else(|| KeyManagerError::CryptographicError("Problem generating encryption key hash".to_string()))?; - - Ok(derived_encryption_key.as_bytes().into()) + let params = argon2::Params::new( + 37 * 1024, // m-cost should be 37 Mib = 37 * 1024 Kib + 1, // t-cost + 1, // p-cost + Some(CIPHER_SEED_ENCRYPTION_KEY_BYTES + CIPHER_SEED_MAC_KEY_BYTES), + ) + .map_err(|_| KeyManagerError::CryptographicError("Problem generating Argon2 parameters".to_string()))?; + + // Derive the main key from the password in place + let mut main_key = Zeroizing::new([0u8; CIPHER_SEED_ENCRYPTION_KEY_BYTES + CIPHER_SEED_MAC_KEY_BYTES]); + let hasher = argon2::Argon2::new(argon2::Algorithm::Argon2d, argon2::Version::V0x13, params); + hasher + .hash_password_into(passphrase.as_bytes(), argon2_salt, main_key.as_mut()) + .map_err(|_| KeyManagerError::CryptographicError("Problem generating Argon2 password hash".to_string()))?; + + // Split off the keys + let encryption_key = Zeroizing::new(main_key.as_ref()[..CIPHER_SEED_ENCRYPTION_KEY_BYTES].to_vec()); + let mac_key = Zeroizing::new(main_key.as_ref()[CIPHER_SEED_ENCRYPTION_KEY_BYTES..].to_vec()); + Ok((encryption_key, mac_key)) } } diff --git a/base_layer/key_manager/src/wasm.rs b/base_layer/key_manager/src/wasm.rs index 834896319a3..d1688f53f0f 100644 --- a/base_layer/key_manager/src/wasm.rs +++ b/base_layer/key_manager/src/wasm.rs @@ -181,8 +181,8 @@ mod test { #[wasm_bindgen_test] fn it_creates_key_manager_from() { let bytes = [ - 0, 2, 116, 75, 54, 160, 21, 1, 43, 55, 107, 155, 189, 230, 182, 215, 17, 191, 94, 156, 114, 136, 40, 175, - 144, 166, 93, 233, 179, 11, 8, 49, 139, + 0, 51, 51, 135, 119, 105, 176, 118, 226, 143, 252, 180, 103, 0, 79, 167, 147, 131, 44, 90, 219, 96, 251, + 87, 70, 56, 131, 169, 240, 208, 18, 43, 134, ]; let seed = CipherSeed::from_enciphered_bytes(&bytes, None).unwrap(); let seed = JsValue::from_serde(&seed).unwrap(); @@ -194,7 +194,7 @@ mod test { let next_key = response.key_manager.next_key().unwrap(); assert_eq!( next_key.k.to_hex(), - "84feaddf54f1b4321db67f7aae382c338d03c56280a417651c4e0cde3363d00a".to_string() + "861236415198eda054744b04c9f41f620d96c36d1affb85b3b80b2afe7cc5d05".to_string() ) } diff --git a/base_layer/wallet/tests/output_manager_service_tests/service.rs b/base_layer/wallet/tests/output_manager_service_tests/service.rs index bb2d5e5e30e..9f792ab8bd2 100644 --- a/base_layer/wallet/tests/output_manager_service_tests/service.rs +++ b/base_layer/wallet/tests/output_manager_service_tests/service.rs @@ -182,9 +182,9 @@ async fn setup_output_manager_service = [ - "octavo", "joroba", "aplicar", "lamina", "semilla", "tiempo", "codigo", "contar", "maniqui", "guiso", - "imponer", "barba", "torpedo", "mejilla", "fijo", "grave", "caer", "libertad", "sol", "sordo", "alacran", - "bucle", "diente", "vereda", + "lince", "nectar", "donar", "panuelo", "neon", "azafran", "atun", "koala", "peine", "sesion", "nino", "bulto", + "ternura", "buey", "ganar", "senal", "pereza", "koala", "santo", "yerno", "mueble", "caballo", "ironia", + "fiel", ] .iter() .map(|w| w.to_string()) diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index a062e6fa68d..6bf913a25f0 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -9228,9 +9228,9 @@ mod test { // println!("{:?}", mnemonic_seq); let mnemonic = vec![ - "scale", "poem", "sorry", "language", "gorilla", "despair", "alarm", "jungle", "invite", "orient", - "blast", "try", "jump", "escape", "estate", "reward", "race", "taxi", "pitch", "soccer", "matter", - "team", "parrot", "enter", + "parade", "foam", "dirt", "easily", "coyote", "hurry", "category", "hawk", "other", "patient", "gain", + "certain", "diet", "hurry", "prepare", "print", "chair", "blur", "humor", "amount", "raise", "guard", + "crouch", "noise", ]; let seed_words = seed_words_create();