From aecd042afc2648b8e43693d43333314ece778532 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 2 Jul 2025 10:20:17 -0600 Subject: [PATCH] Use AES-XTS for encryption --- Cargo.lock | 11 +++++ Cargo.toml | 11 +++-- src/filesystem.rs | 98 +++++++++++++++------------------------ src/header.rs | 30 ++++++------ src/key.rs | 41 +++++++++------- src/mount/redox/scheme.rs | 54 ++++++++++++++++----- src/transaction.rs | 6 +-- 7 files changed, 141 insertions(+), 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a4cd9c81b..bbfa0775fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -459,6 +459,7 @@ dependencies = [ "termion", "time", "uuid", + "xts-mode", ] [[package]] @@ -722,6 +723,16 @@ dependencies = [ "bitflags", ] +[[package]] +name = "xts-mode" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a099a2f21d48275314733f85bc43b6c6213b66394233aaea573fc7a520dcd9" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 9eebacc4dc..e26f6b75bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,19 +34,20 @@ required-features = ["std"] aes = { version = "=0.7.5", default-features = false } argon2 = { version = "0.4", default-features = false, features = ["alloc"] } base64ct = { version = "1", default-features = false } -env_logger = { version = "0.11", optional = true } endian-num = "0.1" +env_logger = { version = "0.11", optional = true } getrandom = { version = "0.2.5", optional = true } libc = "0.2" +libredox = { version = "0.1.3", optional = true } log = { version = "0.4.14", default-features = false, optional = true} -redox_syscall = { version = "0.5.13" } range-tree = { version = "0.1", optional = true } +redox-path = "0.3.0" +redox-scheme = { version = "0.6.2", optional = true } +redox_syscall = { version = "0.5.13" } seahash = { version = "4.1.0", default-features = false } termion = { version = "4", optional = true } uuid = { version = "1.4", default-features = false } -redox-path = "0.3.0" -libredox = { version = "0.1.3", optional = true } -redox-scheme = { version = "0.6.2", optional = true } +xts-mode = { version = "=0.4.1", default-features = false } [features] default = ["std", "log"] diff --git a/src/filesystem.rs b/src/filesystem.rs index 87f023c7dc..15b86665e3 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -1,6 +1,7 @@ -use aes::{Aes128, BlockDecrypt, BlockEncrypt}; -use alloc::{collections::VecDeque, vec::Vec}; +use aes::Aes128; +use alloc::collections::VecDeque; use syscall::error::{Error, Result, EKEYREJECTED, ENOENT, ENOKEY}; +use xts_mode::{get_tweak_default, Xts128}; #[cfg(feature = "std")] use crate::{AllocEntry, AllocList, BlockData, BlockTrait, Key, KeySlot, Node, Salt, TreeList}; @@ -15,8 +16,7 @@ pub struct FileSystem { //TODO: make private pub header: Header, pub(crate) allocator: Allocator, - pub(crate) aes_opt: Option, - aes_blocks: Vec, + pub(crate) cipher_opt: Option>, } impl FileSystem { @@ -52,14 +52,14 @@ impl FileSystem { } } - let aes_opt = match password_opt { + let cipher_opt = match password_opt { Some(password) => { if !header.encrypted() { // Header not encrypted but password provided return Err(Error::new(EKEYREJECTED)); } - match header.aes(password) { - Some(aes) => Some(aes), + match header.cipher(password) { + Some(cipher) => Some(cipher), None => { // Header encrypted with a different password return Err(Error::new(ENOKEY)); @@ -80,8 +80,7 @@ impl FileSystem { block, header, allocator: Allocator::default(), - aes_opt, - aes_blocks: Vec::with_capacity(BLOCK_SIZE as usize / aes::BLOCK_SIZE), + cipher_opt, }; unsafe { fs.reset_allocator()? }; @@ -141,12 +140,16 @@ impl FileSystem { let mut header = Header::new(size); - let aes_opt = match password_opt { + let cipher_opt = match password_opt { Some(password) => { //TODO: handle errors - header.key_slots[0] = - KeySlot::new(password, Salt::new().unwrap(), Key::new().unwrap()).unwrap(); - Some(header.key_slots[0].key(password).unwrap().into_aes()) + header.key_slots[0] = KeySlot::new( + password, + Salt::new().unwrap(), + (Key::new().unwrap(), Key::new().unwrap()), + ) + .unwrap(); + Some(header.key_slots[0].cipher(password).unwrap()) } None => None, }; @@ -156,8 +159,7 @@ impl FileSystem { block: block_offset, header, allocator: Allocator::default(), - aes_opt, - aes_blocks: Vec::with_capacity(BLOCK_SIZE as usize / aes::BLOCK_SIZE), + cipher_opt, }; // Write header generation zero @@ -267,59 +269,33 @@ impl FileSystem { Ok(()) } - pub(crate) fn decrypt(&mut self, data: &mut [u8]) -> bool { - let aes = if let Some(ref aes) = self.aes_opt { - aes + pub(crate) fn decrypt(&mut self, data: &mut [u8], addr: BlockAddr) -> bool { + if let Some(ref cipher) = self.cipher_opt { + cipher.decrypt_area( + data, + BLOCK_SIZE as usize, + addr.index().into(), + get_tweak_default, + ); + true } else { // Do nothing if encryption is disabled - return false; - }; - - assert_eq!(data.len() % aes::BLOCK_SIZE, 0); - - self.aes_blocks.clear(); - for i in 0..data.len() / aes::BLOCK_SIZE { - self.aes_blocks.push(aes::Block::clone_from_slice( - &data[i * aes::BLOCK_SIZE..(i + 1) * aes::BLOCK_SIZE], - )); + false } - - aes.decrypt_blocks(&mut self.aes_blocks); - - for i in 0..data.len() / aes::BLOCK_SIZE { - data[i * aes::BLOCK_SIZE..(i + 1) * aes::BLOCK_SIZE] - .copy_from_slice(&self.aes_blocks[i]); - } - self.aes_blocks.clear(); - - true } - pub(crate) fn encrypt(&mut self, data: &mut [u8]) -> bool { - let aes = if let Some(ref aes) = self.aes_opt { - aes + pub(crate) fn encrypt(&mut self, data: &mut [u8], addr: BlockAddr) -> bool { + if let Some(ref cipher) = self.cipher_opt { + cipher.encrypt_area( + data, + BLOCK_SIZE as usize, + addr.index().into(), + get_tweak_default, + ); + true } else { // Do nothing if encryption is disabled - return false; - }; - - assert_eq!(data.len() % aes::BLOCK_SIZE, 0); - - self.aes_blocks.clear(); - for i in 0..data.len() / aes::BLOCK_SIZE { - self.aes_blocks.push(aes::Block::clone_from_slice( - &data[i * aes::BLOCK_SIZE..(i + 1) * aes::BLOCK_SIZE], - )); + false } - - aes.encrypt_blocks(&mut self.aes_blocks); - - for i in 0..data.len() / aes::BLOCK_SIZE { - data[i * aes::BLOCK_SIZE..(i + 1) * aes::BLOCK_SIZE] - .copy_from_slice(&self.aes_blocks[i]); - } - self.aes_blocks.clear(); - - true } } diff --git a/src/header.rs b/src/header.rs index ecc501e456..0211964a5d 100644 --- a/src/header.rs +++ b/src/header.rs @@ -2,7 +2,8 @@ use core::ops::{Deref, DerefMut}; use core::{fmt, mem, slice}; use endian_num::Le; -use aes::{Aes128, BlockDecrypt, BlockEncrypt}; +use aes::Aes128; +use xts_mode::{get_tweak_default, Xts128}; use crate::{AllocList, BlockPtr, KeySlot, Tree, BLOCK_SIZE, SIGNATURE, VERSION}; @@ -29,7 +30,7 @@ pub struct Header { /// Key slots pub key_slots: [KeySlot; 64], /// Padding - pub padding: [u8; BLOCK_SIZE as usize - 2152], + pub padding: [u8; BLOCK_SIZE as usize - 3176], /// encrypted hash of header data without hash, set to hash and padded if disk is not encrypted pub encrypted_hash: [u8; 16], /// hash of header data without hash @@ -91,14 +92,14 @@ impl Header { seahash::hash(&self[..end]) } - fn create_encrypted_hash(&self, aes_opt: Option<&Aes128>) -> [u8; 16] { + fn create_encrypted_hash(&self, cipher_opt: Option<&Xts128>) -> [u8; 16] { let mut encrypted_hash = [0; 16]; for (i, b) in self.hash.to_le_bytes().iter().enumerate() { encrypted_hash[i] = *b; } - if let Some(aes) = aes_opt { + if let Some(cipher) = cipher_opt { let mut block = aes::Block::from(encrypted_hash); - aes.encrypt_block(&mut block); + cipher.encrypt_area(&mut block, BLOCK_SIZE as usize, self.generation().into(), get_tweak_default); encrypted_hash = block.into(); } encrypted_hash @@ -108,30 +109,31 @@ impl Header { (self.encrypted_hash) != self.create_encrypted_hash(None) } - pub fn aes(&self, password: &[u8]) -> Option { + pub fn cipher(&self, password: &[u8]) -> Option> { let hash = self.create_encrypted_hash(None); for slot in self.key_slots.iter() { //TODO: handle errors - let aes = slot.key(password).unwrap().into_aes(); + let cipher = slot.cipher(password).unwrap(); let mut block = aes::Block::from(self.encrypted_hash); - aes.decrypt_block(&mut block); + cipher.decrypt_area(&mut block, BLOCK_SIZE as usize, self.generation().into(), get_tweak_default); if block == aes::Block::from(hash) { - return Some(aes); + return Some(cipher); } } None } - fn update_hash(&mut self, aes_opt: Option<&Aes128>) { + + fn update_hash(&mut self, cipher_opt: Option<&Xts128>) { self.hash = self.create_hash().into(); // Make sure to do this second, it relies on the hash being up to date - self.encrypted_hash = self.create_encrypted_hash(aes_opt); + self.encrypted_hash = self.create_encrypted_hash(cipher_opt); } - pub fn update(&mut self, aes_opt: Option<&Aes128>) -> u64 { + pub fn update(&mut self, cipher_opt: Option<&Xts128>) -> u64 { let mut generation = self.generation(); generation += 1; self.generation = generation.into(); - self.update_hash(aes_opt); + self.update_hash(cipher_opt); generation } } @@ -147,7 +149,7 @@ impl Default for Header { tree: BlockPtr::::default(), alloc: BlockPtr::::default(), key_slots: [KeySlot::default(); 64], - padding: [0; BLOCK_SIZE as usize - 2152], + padding: [0; BLOCK_SIZE as usize - 3176], encrypted_hash: [0; 16], hash: 0.into(), } diff --git a/src/key.rs b/src/key.rs index 7a0eb2fecf..4873d71a1a 100644 --- a/src/key.rs +++ b/src/key.rs @@ -1,4 +1,5 @@ use aes::{Aes128, BlockDecrypt, BlockEncrypt, NewBlockCipher}; +use xts_mode::Xts128; // The raw key, keep secret! #[repr(transparent)] @@ -13,6 +14,12 @@ impl Key { Ok(Self(bytes)) } + pub fn encrypt(&self, password_aes: &Aes128) -> EncryptedKey { + let mut block = aes::Block::from(self.0); + password_aes.encrypt_block(&mut block); + EncryptedKey(block.into()) + } + pub fn into_aes(self) -> Aes128 { Aes128::new(&aes::Block::from(self.0)) } @@ -23,6 +30,14 @@ impl Key { #[repr(transparent)] pub struct EncryptedKey([u8; 16]); +impl EncryptedKey { + pub fn decrypt(&self, password_aes: &Aes128) -> Key { + let mut block = aes::Block::from(self.0); + password_aes.decrypt_block(&mut block); + Key(block.into()) + } +} + /// Salt used to prevent rainbow table attacks on the encryption password #[derive(Clone, Copy, Default)] #[repr(transparent)] @@ -43,7 +58,8 @@ impl Salt { #[repr(C, packed)] pub struct KeySlot { salt: Salt, - encrypted_key: EncryptedKey, + // Two keys for AES XTS 128 + encrypted_keys: (EncryptedKey, EncryptedKey), } impl KeySlot { @@ -66,27 +82,20 @@ impl KeySlot { } /// Create a new key slot from a password, salt, and encryption key - pub fn new(password: &[u8], salt: Salt, key: Key) -> Result { + pub fn new(password: &[u8], salt: Salt, keys: (Key, Key)) -> Result { let password_aes = Self::password_aes(password, &salt)?; - - // Encrypt the real AES key - let mut block = aes::Block::from(key.0); - password_aes.encrypt_block(&mut block); - Ok(Self { salt, - encrypted_key: EncryptedKey(block.into()), + encrypted_keys: (keys.0.encrypt(&password_aes), keys.1.encrypt(&password_aes)), }) } - /// Get the encryption key from this key slot - pub fn key(&self, password: &[u8]) -> Result { + /// Get the encryption cipher from this key slot + pub fn cipher(&self, password: &[u8]) -> Result, argon2::Error> { let password_aes = Self::password_aes(password, &self.salt)?; - - // Decrypt the real AES key - let mut block = aes::Block::from(self.encrypted_key.0); - password_aes.decrypt_block(&mut block); - - Ok(Key(block.into())) + Ok(Xts128::new( + self.encrypted_keys.0.decrypt(&password_aes).into_aes(), + self.encrypted_keys.1.decrypt(&password_aes).into_aes(), + )) } } diff --git a/src/mount/redox/scheme.rs b/src/mount/redox/scheme.rs index f91c69a19d..62a72f53ae 100644 --- a/src/mount/redox/scheme.rs +++ b/src/mount/redox/scheme.rs @@ -88,9 +88,15 @@ impl FileScheme { let target_reference = reference.to_string(); nodes.clear(); - if let Some((next_node, next_node_name)) = - Self::path_nodes(scheme_name, tx, TreePtr::root(), &target_reference, uid, gid, nodes)? - { + if let Some((next_node, next_node_name)) = Self::path_nodes( + scheme_name, + tx, + TreePtr::root(), + &target_reference, + uid, + gid, + nodes, + )? { if !next_node.data().is_symlink() { nodes.push((next_node, next_node_name)); return Ok(target_reference); @@ -104,7 +110,13 @@ impl FileScheme { Err(Error::new(ELOOP)) } - fn open_internal(&mut self, start_ptr: TreePtr, url: &str, flags: usize, ctx: &CallerCtx) -> Result { + fn open_internal( + &mut self, + start_ptr: TreePtr, + url: &str, + flags: usize, + ctx: &CallerCtx, + ) -> Result { let CallerCtx { uid, gid, .. } = *ctx; let path = url.trim_matches('/'); @@ -307,7 +319,6 @@ impl FileScheme { number: id, flags: NewFdFlags::POSITIONED, }) - } fn path_nodes( @@ -377,8 +388,15 @@ impl SchemeSync for FileScheme { self.open_internal(TreePtr::root(), url, flags, ctx) } - fn openat(&mut self, dirfd: usize, path: &str, flags: usize, _fcntl_flags: u32, ctx: &CallerCtx) -> Result { - // If pathname is absolute, then dirfd is ignored. + fn openat( + &mut self, + dirfd: usize, + path: &str, + flags: usize, + _fcntl_flags: u32, + ctx: &CallerCtx, + ) -> Result { + // If pathname is absolute, then dirfd is ignored. if path.starts_with('/') { return self.open_internal(TreePtr::root(), path, flags, ctx); } @@ -623,8 +641,15 @@ impl SchemeSync for FileScheme { } let mut new_nodes = Vec::new(); - let new_node_opt = - Self::path_nodes(scheme_name, tx, TreePtr::root(), new_path, uid, gid, &mut new_nodes)?; + let new_node_opt = Self::path_nodes( + scheme_name, + tx, + TreePtr::root(), + new_path, + uid, + gid, + &mut new_nodes, + )?; if let Some((ref new_parent, _)) = new_nodes.last() { if !new_parent.data().owner(uid) { @@ -723,8 +748,15 @@ impl SchemeSync for FileScheme { } let mut new_nodes = Vec::new(); - let new_node_opt = - Self::path_nodes(scheme_name, tx, TreePtr::root(), new_path, uid, gid, &mut new_nodes)?; + let new_node_opt = Self::path_nodes( + scheme_name, + tx, + TreePtr::root(), + new_path, + uid, + gid, + &mut new_nodes, + )?; if let Some((ref new_parent, _)) = new_nodes.last() { if !new_parent.data().owner(uid) { diff --git a/src/transaction.rs b/src/transaction.rs index 471decc52a..c48d9b0208 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -220,7 +220,7 @@ impl<'a, D: Disk> Transaction<'a, D> { // if we have any blocks to write assert!(self.header_changed); - self.fs.encrypt(raw); + self.fs.encrypt(raw, *addr); let count = unsafe { self.fs.disk.write_at(self.fs.block + addr.index(), raw)? }; if count != raw.len() { // Read wrong number of bytes @@ -240,7 +240,7 @@ impl<'a, D: Disk> Transaction<'a, D> { } // Update header to next generation - let gen = self.header.update(self.fs.aes_opt.as_ref()); + let gen = self.header.update(self.fs.cipher_opt.as_ref()); let gen_block = gen % HEADER_RING; // Write header @@ -293,7 +293,7 @@ impl<'a, D: Disk> Transaction<'a, D> { log::error!("READ_BLOCK: WRONG NUMBER OF BYTES"); return Err(Error::new(EIO)); } - self.fs.decrypt(&mut data); + self.fs.decrypt(&mut data, ptr.addr()); } let block = BlockData::new(ptr.addr(), data);