From 0d4feea7d179b8b6370d030f6f1291b469f061c1 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Sun, 5 May 2024 03:04:41 +0300 Subject: [PATCH] use ChaCha20 for any random generators fix #30 "use fewer digits as nonce_seed when adding dir entries" --- examples/ring_crypto.rs | 2 +- src/crypto.rs | 61 ++++++++++++++++++---- src/crypto/reader.rs | 2 +- src/crypto/writer.rs | 1 - src/encryptedfs.rs | 97 ++++++++++++++++++----------------- src/encryptedfs/moved_test.rs | 4 +- src/main.rs | 2 +- src/stream_util.rs | 2 +- 8 files changed, 105 insertions(+), 66 deletions(-) diff --git a/examples/ring_crypto.rs b/examples/ring_crypto.rs index 13bc8734..f0953de9 100644 --- a/examples/ring_crypto.rs +++ b/examples/ring_crypto.rs @@ -41,7 +41,7 @@ fn main() { // derive key from password let password = SecretString::new("password".to_string()); - let salt = crypto::hash_secret(&password); + let salt = crypto::hash_secret_string(&password); let cipher = Cipher::ChaCha20; let derived_key = crypto::derive_key(&password, &cipher, salt).unwrap(); let path = PathBuf::from("/tmp/key.enc"); diff --git a/src/crypto.rs b/src/crypto.rs index a3762894..0112029e 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -4,12 +4,13 @@ use std::io; use std::num::ParseIntError; use std::path::PathBuf; use std::sync::Arc; -use argon2::Argon2; +use argon2::Argon2; use base64::DecodeError; use hex::FromHexError; use num_format::{Locale, ToFormattedString}; -use rand::thread_rng; +use rand_chacha::ChaCha20Rng; +use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng}; use ring::aead::{AES_256_GCM, CHACHA20_POLY1305}; use secrecy::{ExposeSecret, SecretString, SecretVec}; use serde::{Deserialize, Serialize}; @@ -18,10 +19,10 @@ use strum_macros::{Display, EnumIter, EnumString}; use thiserror::Error; use tracing::{debug, error, instrument}; +use crate::{crypto, stream_util}; use crate::crypto::reader::{CryptoReader, RingCryptoReader}; use crate::crypto::writer::{CryptoWriter, RingCryptoWriter}; use crate::encryptedfs::FsResult; -use crate::stream_util; pub mod reader; pub mod writer; @@ -33,7 +34,7 @@ pub enum Cipher { Aes256Gcm, } -pub const ENCRYPT_FILENAME_OVERHEAD_CHARS: usize = 22; +pub const ENCRYPT_FILENAME_OVERHEAD_CHARS: usize = 4; #[derive(Debug, Error)] pub enum Error { @@ -140,16 +141,34 @@ pub fn create_reader(reader: R, cipher: &Cipher, k // CryptostreamCryptoReader::new(file, get_cipher(cipher), &key.expose_secret(), &iv).unwrap() // } -pub fn encrypt_string_with_nonce_seed(s: &SecretString, cipher: &Cipher, key: Arc>, nonce_seed: u64) -> Result { +/// `nonce_seed`: If we should include the nonce seed in the result so that it can be used when decrypting. +pub fn encrypt_string_with_nonce_seed(s: &SecretString, cipher: &Cipher, key: Arc>, nonce_seed: u64, include_nonce_seed: bool) -> Result { let mut cursor = io::Cursor::new(vec![]); let mut writer = create_writer(cursor, cipher, key, nonce_seed); writer.write_all(s.expose_secret().as_bytes())?; writer.flush()?; cursor = writer.finish()?.unwrap(); let v = cursor.into_inner(); + if include_nonce_seed { + Ok(format!("{}.{}", base64::encode(v), nonce_seed)) + } else { + Ok(base64::encode(v)) + } +} + +/// Encrypt a string with a random nonce seed. It will include the nonce seed in the result so that it can be used when decrypting. +pub fn encrypt_string(s: &SecretString, cipher: &Cipher, key: Arc>) -> Result { + let mut cursor = io::Cursor::new(vec![]); + let nonce_seed = create_rng().next_u64(); + let mut writer = create_writer(cursor, cipher, key, nonce_seed); + writer.write_all(s.expose_secret().as_bytes())?; + writer.flush()?; + cursor = writer.finish()?.unwrap(); + let v = cursor.into_inner(); Ok(format!("{}.{}", base64::encode(v), nonce_seed)) } +/// Decrypt a string that was encrypted with including the nonce seed. pub fn decrypt_string(s: &str, cipher: &Cipher, key: Arc>) -> Result { // extract nonce seed if !s.contains(".") { @@ -167,28 +186,40 @@ pub fn decrypt_string(s: &str, cipher: &Cipher, key: Arc>) -> Resu Ok(SecretString::new(decrypted)) } +/// Decrypt a string that was encrypted with a specific nonce seed. +pub fn decrypt_string_with_nonce_seed(s: &str, cipher: &Cipher, key: Arc>, nonce_seed: u64) -> Result { + let vec = base64::decode(s)?; + let cursor = io::Cursor::new(vec); + + let mut reader = create_reader(cursor, cipher, key, nonce_seed); + let mut decrypted = String::new(); + reader.read_to_string(&mut decrypted)?; + Ok(SecretString::new(decrypted)) +} + pub fn decrypt_file_name(name: &str, cipher: &Cipher, key: Arc>) -> Result { let name = String::from(name).replace("|", "/"); decrypt_string(&name, cipher, key) } #[instrument(skip(password, salt))] -pub fn derive_key(password: &SecretString, cipher: &Cipher, salt: SecretVec) -> Result> { +pub fn derive_key(password: &SecretString, cipher: &Cipher, salt: [u8; 32]) -> Result> { let mut dk = vec![]; let key_len = match cipher { Cipher::ChaCha20 => 32, Cipher::Aes256Gcm => 32, }; dk.resize(key_len, 0); - Argon2::default().hash_password_into(password.expose_secret().as_bytes(), salt.expose_secret(), &mut dk) + Argon2::default().hash_password_into(password.expose_secret().as_bytes(), &salt, &mut dk) .map_err(|err| Error::GenericString(err.to_string()))?; Ok(SecretVec::new(dk)) } -pub fn encrypt_file_name_with_nonce_seed(name: &SecretString, cipher: &Cipher, key: Arc>, nonce_seed: u64) -> FsResult { +/// Encrypt a file name with provided nonce seed. It will **INCLUDE** the nonce seed in the result so that it can be used when decrypting. +pub fn encrypt_file_name(name: &SecretString, cipher: &Cipher, key: Arc>, nonce_seed: u64) -> FsResult { if name.expose_secret() != "$." && name.expose_secret() != "$.." { let normalized_name = SecretString::new(name.expose_secret().replace("/", " ").replace("\\", " ")); - let mut encrypted = encrypt_string_with_nonce_seed(&normalized_name, cipher, key, nonce_seed)?; + let mut encrypted = encrypt_string_with_nonce_seed(&normalized_name, cipher, key, nonce_seed, true)?; encrypted = encrypted.replace("/", "|"); Ok(encrypted) } else { @@ -205,8 +236,12 @@ pub fn hash(data: &[u8]) -> [u8; 32] { hasher.finalize().into() } -pub fn hash_secret(data: &SecretString) -> SecretVec { - SecretVec::new(hash(data.expose_secret().as_bytes()).to_vec()) +pub fn hash_secret_string(data: &SecretString) -> [u8; 32] { + hash(data.expose_secret().as_bytes()) +} + +pub fn hash_secret_vec(data: &SecretVec) -> [u8; 32] { + hash(data.expose_secret()) } /// Copy from `pos` position in file `len` bytes @@ -235,4 +270,8 @@ pub fn extract_nonce_from_encrypted_string(name: &str) -> Result { } let nonce_seed = name.split('.').last().unwrap().parse::()?; Ok(nonce_seed) +} + +pub fn create_rng() -> impl RngCore + CryptoRng { + ChaCha20Rng::from_entropy() } \ No newline at end of file diff --git a/src/crypto/reader.rs b/src/crypto/reader.rs index d7d1e8d4..97110455 100644 --- a/src/crypto/reader.rs +++ b/src/crypto/reader.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use num_format::{Locale, ToFormattedString}; use ring::aead::{Aad, Algorithm, BoundKey, OpeningKey, UnboundKey}; use secrecy::{ExposeSecret, SecretVec}; -use tracing::{debug, error, info, instrument, warn}; +use tracing::{debug, error, instrument, warn}; use crate::crypto::buf_mut::BufMut; use crate::crypto::writer::{BUF_SIZE, RandomNonceSequence}; diff --git a/src/crypto/writer.rs b/src/crypto/writer.rs index f5fc8896..7a9d6808 100644 --- a/src/crypto/writer.rs +++ b/src/crypto/writer.rs @@ -113,7 +113,6 @@ impl RingCryptoWriter { impl CryptoWriter for RingCryptoWriter { fn finish(&mut self) -> io::Result> { - self.flush()?; if self.buf.available() > 0 { // encrypt and write last block, use as many bytes we have self.encrypt_and_write()?; diff --git a/src/encryptedfs.rs b/src/encryptedfs.rs index ab0de011..fc276141 100644 --- a/src/encryptedfs.rs +++ b/src/encryptedfs.rs @@ -16,8 +16,6 @@ use argon2::password_hash::rand_core::RngCore; use futures_util::TryStreamExt; use num_format::{Locale, ToFormattedString}; use rand::thread_rng; -use rand_chacha::ChaCha20Rng; -use rand_chacha::rand_core::SeedableRng; use ring::aead::{AES_256_GCM, CHACHA20_POLY1305}; use secrecy::{ExposeSecret, SecretString, SecretVec}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -370,30 +368,30 @@ impl Iterator for DirectoryEntryIterator { let name = entry.file_name().to_string_lossy().to_string(); let nonce_seed; let name = { - if name.starts_with("$.") { + if name.starts_with("$..") { // extract nonce seed - if name.len() < 3 { + if name.len() < 4 { return Some(Err(FsError::InvalidInput("nonce seed is missing from filename"))); } - let res = name.split(".").last().unwrap().parse::(); + let res = name.split("..").last().unwrap().parse::(); if let Err(err) = res { error!(err = %err, "parsing nonce seed"); return Some(Err(err.into())); } nonce_seed = res.unwrap(); - Some(SecretString::from_str(".").unwrap()) - } else if name.starts_with("$..") { + SecretString::from_str("..").unwrap() + } else if name.starts_with("$.") { // extract nonce seed - if name.len() < 4 { + if name.len() < 3 { return Some(Err(FsError::InvalidInput("nonce seed is missing from filename"))); } - let res = name.split("..").last().unwrap().parse::(); + let res = name.split(".").last().unwrap().parse::(); if let Err(err) = res { error!(err = %err, "parsing nonce seed"); return Some(Err(err.into())); } nonce_seed = res.unwrap(); - Some(SecretString::from_str("..").unwrap()) + SecretString::from_str(".").unwrap() } else { // extract nonce seed let res = extract_nonce_from_encrypted_string(&name); @@ -402,16 +400,16 @@ impl Iterator for DirectoryEntryIterator { return Some(Err(FsError::from(err))); } nonce_seed = res.unwrap(); - crypto::decrypt_file_name(&name, &self.1, self.2.clone()).map_err(|err| { + let name = crypto::decrypt_file_name(&name, &self.1, self.2.clone()).map_err(|err| { error!(err = %err, "decrypting file name"); err - }).ok() + }); + if name.is_err() { + return Some(Err(FsError::InvalidInput("invalid file name"))); + } + name.unwrap() } }; - if name.is_none() { - return Some(Err(FsError::InvalidInput("invalid file name"))); - } - let name = name.unwrap(); let res: bincode::Result<(u64, FileType)> = bincode::deserialize_from(crypto::create_reader(file, &self.1, self.2.clone(), nonce_seed)); if let Err(e) = res { @@ -452,30 +450,30 @@ impl Iterator for DirectoryEntryPlusIterator { let nonce_seed; let name = { - if name.starts_with("$.") { + if name.starts_with("$..") { // extract nonce seed - if name.len() < 3 { + if name.len() < 4 { return Some(Err(FsError::InvalidInput("nonce seed is missing from filename"))); } - let res = name.split(".").last().unwrap().parse::(); + let res = name.split("..").last().unwrap().parse::(); if let Err(err) = res { error!(err = %err, "parsing nonce seed"); return Some(Err(err.into())); } nonce_seed = res.unwrap(); - Some(SecretString::from_str(".").unwrap()) - } else if name.starts_with("$..") { + SecretString::from_str("..").unwrap() + } else if name.starts_with("$.") { // extract nonce seed - if name.len() < 4 { + if name.len() < 3 { return Some(Err(FsError::InvalidInput("nonce seed is missing from filename"))); } - let res = name.split("..").last().unwrap().parse::(); + let res = name.split(".").last().unwrap().parse::(); if let Err(err) = res { error!(err = %err, "parsing nonce seed"); return Some(Err(err.into())); } nonce_seed = res.unwrap(); - Some(SecretString::from_str("..").unwrap()) + SecretString::from_str(".").unwrap() } else { // extract nonce seed let res = extract_nonce_from_encrypted_string(&name); @@ -484,16 +482,17 @@ impl Iterator for DirectoryEntryPlusIterator { return Some(Err(FsError::from(err))); } nonce_seed = res.unwrap(); - crypto::decrypt_file_name(&name, &self.2, self.3.clone()).map_err(|err| { + let name = crypto::decrypt_file_name(&name, &self.2, self.3.clone()).map_err(|err| { error!(err = %err, "decrypting file name"); err - }).ok() + }); + if name.is_err() { + return Some(Err(FsError::InvalidInput("invalid file name"))); + } + name.unwrap() } }; - if name.is_none() { - return Some(Err(FsError::InvalidInput("invalid file name"))); - } - let name = name.unwrap(); + debug!(name = %name.expose_secret(), "decrypted file name"); let res: bincode::Result<(u64, FileType)> = bincode::deserialize_from(crypto::create_reader(file, &self.2, self.3.clone(), nonce_seed)); if let Err(e) = res { @@ -735,12 +734,12 @@ impl EncryptedFs { ino: attr.ino, name: SecretString::from_str("$.").unwrap(), kind: FileType::Directory, - }, self.key.get().await?, parent).await?; + }, self.key.get().await?).await?; self.insert_directory_entry(attr.ino, DirectoryEntry { ino: parent, name: SecretString::from_str("$..").unwrap(), kind: FileType::Directory, - }, self.key.get().await?, parent).await?; + }, self.key.get().await?).await?; } } @@ -749,7 +748,7 @@ impl EncryptedFs { ino: attr.ino, name: SecretString::new(name.expose_secret().to_owned()), kind: attr.kind, - }, self.key.get().await?, parent).await?; + }, self.key.get().await?).await?; self.update_inode(parent, SetFileAttr::default() .with_mtime(SystemTime::now()) .with_ctime(SystemTime::now())).await?; @@ -788,7 +787,7 @@ impl EncryptedFs { SecretString::new(name.expose_secret().to_owned()) } }; - let name = crypto::encrypt_file_name_with_nonce_seed(&name, &self.cipher, self.key.get().await?, parent)?; + let name = crypto::encrypt_file_name(&name, &self.cipher, self.key.get().await?, parent)?; let file = File::open(self.data_dir.join(CONTENTS_DIR).join(parent.to_string()).join(name))?; let (inode, _): (u64, FileType) = bincode::deserialize_from(self.create_crypto_reader(file, parent).await?)?; Ok(Some(self.get_inode(inode).await?)) @@ -887,7 +886,7 @@ impl EncryptedFs { SecretString::new(name.expose_secret().to_owned()) } }; - let name = crypto::encrypt_file_name_with_nonce_seed(&name, &self.cipher, self.key.get().await?, parent)?; + let name = crypto::encrypt_file_name(&name, &self.cipher, self.key.get().await?, parent)?; Ok(self.data_dir.join(CONTENTS_DIR).join(parent.to_string()).join(name).exists()) } @@ -1046,7 +1045,7 @@ impl EncryptedFs { ctx.attr.atime = SystemTime::now(); - Ok(buf.len()) + Ok(len) } pub async fn release(&self, handle: u64) -> FsResult<()> { @@ -1565,7 +1564,7 @@ impl EncryptedFs { ino: attr.ino, name: SecretString::new(new_name.expose_secret().to_owned()), kind: attr.kind, - }, self.key.get().await?, new_parent).await?; + }, self.key.get().await?).await?; let mut parent_attr = self.get_inode(parent).await?; parent_attr.mtime = SystemTime::now(); @@ -1583,7 +1582,7 @@ impl EncryptedFs { ino: new_parent, name: SecretString::from_str("$..").unwrap(), kind: FileType::Directory, - }, self.key.get().await?, parent).await?; + }, self.key.get().await?).await?; } Ok(()) @@ -1611,7 +1610,7 @@ impl EncryptedFs { check_structure(&data_dir, false).await?; // decrypt key - let salt = crypto::hash_secret(&old_password); + let salt = crypto::hash_secret_string(&old_password); let initial_key = crypto::derive_key(&old_password, &cipher, salt)?; let enc_file = data_dir.join(SECURITY_DIR).join(KEY_ENC_FILENAME); let reader = crypto::create_reader(File::open(enc_file.clone())?, &cipher, Arc::new(initial_key), 42_u64); @@ -1622,7 +1621,7 @@ impl EncryptedFs { } // encrypt it with new key derived from new password - let salt = crypto::hash_secret(&new_password); + let salt = crypto::hash_secret_string(&new_password); let new_key = crypto::derive_key(&new_password, &cipher, salt)?; tokio::fs::remove_file(enc_file.clone()).await?; let mut writer = crypto::create_writer(OpenOptions::new().read(true).write(true).create(true).truncate(true).open(enc_file.clone())?, @@ -1771,15 +1770,18 @@ impl EncryptedFs { ino: attr.ino, name: SecretString::from_str("$.").unwrap(), kind: FileType::Directory, - }, self.key.get().await?, ROOT_INODE).await?; + }, self.key.get().await?).await?; } Ok(()) } - async fn insert_directory_entry(&self, parent: u64, entry: DirectoryEntry, key: Arc>, nonce_seed: u64) -> FsResult<()> { - let parent_path = self.data_dir.join(CONTENTS_DIR).join(parent.to_string()); - let name = crypto::encrypt_file_name_with_nonce_seed(&entry.name, &self.cipher, key.clone(), nonce_seed)?; + async fn insert_directory_entry(&self, ino_contents_dir: u64, entry: DirectoryEntry, key: Arc>) -> FsResult<()> { + // in order not to add too much to filename length we keep just 3 digits from nonce seed + let nonce_seed = ino_contents_dir % 1000; + + let parent_path = self.data_dir.join(CONTENTS_DIR).join(ino_contents_dir.to_string()); + let name = crypto::encrypt_file_name(&entry.name, &self.cipher, key.clone(), nonce_seed)?; let file_path = parent_path.join(name); let map = self.serialize_dir_entries_locks.write().unwrap(); @@ -1804,7 +1806,7 @@ impl EncryptedFs { async fn remove_directory_entry(&self, parent: u64, name: &SecretString) -> FsResult<()> { let parent_path = self.data_dir.join(CONTENTS_DIR).join(parent.to_string()); - let name = crypto::encrypt_file_name_with_nonce_seed(name, &self.cipher, self.key.get().await?, parent)?; + let name = crypto::encrypt_file_name(name, &self.cipher, self.key.get().await?, parent)?; let file_path = parent_path.join(name); let map = self.serialize_dir_entries_locks.write().unwrap(); @@ -1832,7 +1834,7 @@ impl EncryptedFs { fn read_or_create_key(path: &PathBuf, password: &SecretString, cipher: &Cipher) -> FsResult> { // derive key from password - let salt = crypto::hash_secret(&password); + let salt = crypto::hash_secret_string(&password); let derived_key = crypto::derive_key(&password, cipher, salt)?; if path.exists() { // read key @@ -1853,8 +1855,7 @@ impl EncryptedFs { Cipher::Aes256Gcm => AES_256_GCM.key_len(), }; key.resize(key_len, 0); - let mut rand = ChaCha20Rng::from_entropy(); - rand.fill_bytes(&mut key); + crypto::create_rng().fill_bytes(&mut key); let key = SecretVec::new(key); let key_store = KeyStore::new(key); let mut writer = crypto::create_writer(OpenOptions::new().read(true).write(true).create(true).open(path)?, diff --git a/src/encryptedfs/moved_test.rs b/src/encryptedfs/moved_test.rs index adb8cd12..1ffcf883 100644 --- a/src/encryptedfs/moved_test.rs +++ b/src/encryptedfs/moved_test.rs @@ -119,8 +119,8 @@ async fn read_exact(fs: &EncryptedFs, ino: u64, offset: u64, buf: &mut [u8], han } #[tokio::test] -async fn test_write_all() { - run_test(TestSetup { data_path: format!("{TESTS_DATA_DIR}test_write_all") }, async { +async fn test_write() { + run_test(TestSetup { data_path: format!("{TESTS_DATA_DIR}test_write") }, async { let fs = SETUP_RESULT.with(|s| Arc::clone(s)); let mut fs = fs.lock().await; let fs = fs.as_mut().unwrap().fs.as_ref().unwrap(); diff --git a/src/main.rs b/src/main.rs index a54a552d..34409d70 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,7 +33,7 @@ async fn main() -> Result<()> { let matches = get_cli_args(); let log_level = if is_debug() { - Level::INFO + Level::DEBUG } else { let log_level_str = matches.get_one::("log-level").unwrap().as_str(); let log_level = Level::from_str(log_level_str); diff --git a/src/stream_util.rs b/src/stream_util.rs index c6d56457..00acadaf 100644 --- a/src/stream_util.rs +++ b/src/stream_util.rs @@ -2,7 +2,7 @@ use std::cmp::min; use std::io; use std::io::{Read, Write}; use num_format::{Locale, ToFormattedString}; -use tracing::{debug, error, info, instrument, warn}; +use tracing::{debug, error, instrument, warn}; use crate::encryptedfs::EncryptedFs; #[cfg(test)]