Use AES-XTS for encryption

This commit is contained in:
Jeremy Soller
2025-07-02 10:20:17 -06:00
parent 291ce5edc0
commit aecd042afc
7 changed files with 141 additions and 110 deletions
Generated
+11
View File
@@ -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"
+6 -5
View File
@@ -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"]
+37 -61
View File
@@ -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<D: Disk> {
//TODO: make private
pub header: Header,
pub(crate) allocator: Allocator,
pub(crate) aes_opt: Option<Aes128>,
aes_blocks: Vec<aes::Block>,
pub(crate) cipher_opt: Option<Xts128<Aes128>>,
}
impl<D: Disk> FileSystem<D> {
@@ -52,14 +52,14 @@ impl<D: Disk> FileSystem<D> {
}
}
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<D: Disk> FileSystem<D> {
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<D: Disk> FileSystem<D> {
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<D: Disk> FileSystem<D> {
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<D: Disk> FileSystem<D> {
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
}
}
+16 -14
View File
@@ -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<Aes128>>) -> [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<Aes128> {
pub fn cipher(&self, password: &[u8]) -> Option<Xts128<Aes128>> {
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<Aes128>>) {
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<Aes128>>) -> 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::<Tree>::default(),
alloc: BlockPtr::<AllocList>::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(),
}
+25 -16
View File
@@ -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<Self, argon2::Error> {
pub fn new(password: &[u8], salt: Salt, keys: (Key, Key)) -> Result<Self, argon2::Error> {
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<Key, argon2::Error> {
/// Get the encryption cipher from this key slot
pub fn cipher(&self, password: &[u8]) -> Result<Xts128<Aes128>, 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(),
))
}
}
+43 -11
View File
@@ -88,9 +88,15 @@ impl<D: Disk> FileScheme<D> {
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<D: Disk> FileScheme<D> {
Err(Error::new(ELOOP))
}
fn open_internal(&mut self, start_ptr: TreePtr<Node>, url: &str, flags: usize, ctx: &CallerCtx) -> Result<OpenResult> {
fn open_internal(
&mut self,
start_ptr: TreePtr<Node>,
url: &str,
flags: usize,
ctx: &CallerCtx,
) -> Result<OpenResult> {
let CallerCtx { uid, gid, .. } = *ctx;
let path = url.trim_matches('/');
@@ -307,7 +319,6 @@ impl<D: Disk> FileScheme<D> {
number: id,
flags: NewFdFlags::POSITIONED,
})
}
fn path_nodes(
@@ -377,8 +388,15 @@ impl<D: Disk> SchemeSync for FileScheme<D> {
self.open_internal(TreePtr::root(), url, flags, ctx)
}
fn openat(&mut self, dirfd: usize, path: &str, flags: usize, _fcntl_flags: u32, ctx: &CallerCtx) -> Result<OpenResult> {
// If pathname is absolute, then dirfd is ignored.
fn openat(
&mut self,
dirfd: usize,
path: &str,
flags: usize,
_fcntl_flags: u32,
ctx: &CallerCtx,
) -> Result<OpenResult> {
// 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<D: Disk> SchemeSync for FileScheme<D> {
}
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<D: Disk> SchemeSync for FileScheme<D> {
}
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) {
+3 -3
View File
@@ -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);