Repository: Avarok-Cybersecurity/Citadel-Protocol (language: rust)

Issue:
Extend methods used to protect data-in-transit for protecting data-at-rest
The for-profit company American Binary recently announced the release of data-at-rest protection using Kyber encryption (as opposed to common symmetric ciphers like AES/CHA-CHA). Considering The Citadel Protocol already offers Kyber encryption combined with OTP for protecting data-in-transit, using the same methods for data-at-rest should be a no-brainer. People should not pay for basic software, it should be free and open source.


Discussion / hints:

Fix patch:
diff --git a/citadel_crypt/src/entropy_bank.rs b/citadel_crypt/src/entropy_bank.rs
index a5b10874c..d4d564327 100644
--- a/citadel_crypt/src/entropy_bank.rs
+++ b/citadel_crypt/src/entropy_bank.rs
@@ -54,7 +54,7 @@ impl EntropyBank {
     #[inline]
     fn get_nonce(&self, nonce_version: usize) -> ArrayVec<u8, LARGEST_NONCE_LEN> {
         let mut base = Default::default();
-        let nonce_len = self.algorithm.nonce_len();
+        let nonce_len = LARGEST_NONCE_LEN; // all ciphers now potentially use Kyber for local encryption/decryption
         let f64s_needed = num_integer::Integer::div_ceil(&nonce_len, &8);
         let mut outer_idx = 0;
 
@@ -151,6 +151,28 @@ impl EntropyBank {
             .map_err(|err| CryptError::Encrypt(err.to_string()))
     }
 
+    pub fn local_encrypt<T: AsRef<[u8]>>(
+        &self,
+        quantum_container: &PostQuantumContainer,
+        payload: T,
+    ) -> Result<Vec<u8>, CryptError<String>> {
+        let nonce = &self.get_nonce(0);
+        quantum_container
+            .local_encrypt(payload, nonce)
+            .map_err(|err| CryptError::Encrypt(err.to_string()))
+    }
+
+    pub fn local_decrypt<T: AsRef<[u8]>>(
+        &self,
+        quantum_container: &PostQuantumContainer,
+        payload: T,
+    ) -> Result<Vec<u8>, CryptError<String>> {
+        let nonce = &self.get_nonce(0);
+        quantum_container
+            .local_decrypt(payload, nonce)
+            .map_err(|err| CryptError::Encrypt(err.to_string()))
+    }
+
     /// Returns the multiport width
     pub fn get_multiport_width(&self) -> usize {
         self.scramble_mappings.len()
diff --git a/citadel_crypt/src/fcm/fcm_ratchet.rs b/citadel_crypt/src/fcm/fcm_ratchet.rs
index d9547109b..9ab6b5934 100644
--- a/citadel_crypt/src/fcm/fcm_ratchet.rs
+++ b/citadel_crypt/src/fcm/fcm_ratchet.rs
@@ -12,6 +12,7 @@ use citadel_pqcrypto::wire::{AliceToBobTransferParameters, BobToAliceTransferPar
 use citadel_pqcrypto::PostQuantumContainer;
 use citadel_pqcrypto::LARGEST_NONCE_LEN;
 use serde::{Deserialize, Serialize};
+use std::borrow::Cow;
 use std::convert::TryFrom;
 use std::sync::Arc;
 
@@ -117,6 +118,24 @@ impl Ratchet for ThinRatchet {
         let (pqc, drill) = self.message_pqc_drill(None);
         drill.validate_packet_in_place_split(pqc, header, packet)
     }
+
+    fn local_encrypt<'a, T: Into<Cow<'a, [u8]>>>(
+        &self,
+        contents: T,
+        _security_level: SecurityLevel,
+    ) -> Result<Vec<u8>, CryptError> {
+        let (pqc, drill) = self.message_pqc_drill(None);
+        drill.local_encrypt(pqc, contents.into())
+    }
+
+    fn local_decrypt<'a, T: Into<Cow<'a, [u8]>>>(
+        &self,
+        contents: T,
+        _security_level: SecurityLevel,
+    ) -> Result<Vec<u8>, CryptError> {
+        let (pqc, drill) = self.message_pqc_drill(None);
+        drill.local_decrypt(pqc, contents.into())
+    }
 }
 
 /// Used for constructing the ratchet
diff --git a/citadel_crypt/src/misc.rs b/citadel_crypt/src/misc.rs
index f4a87a31c..5fa499e8f 100644
--- a/citadel_crypt/src/misc.rs
+++ b/citadel_crypt/src/misc.rs
@@ -1,9 +1,12 @@
 use crate::entropy_bank::PORT_RANGE;
+use crate::prelude::SecurityLevel;
 use rand::prelude::SliceRandom;
 use rand::thread_rng;
+use serde::{Deserialize, Serialize};
 use std::fmt::{Display, Formatter};
 #[cfg(target_family = "unix")]
 use std::os::raw::c_void;
+use std::path::PathBuf;
 
 /// Default Error type for this crate
 pub enum CryptError<T = String> {
@@ -164,3 +167,12 @@ unsafe fn volatile_set<T: Copy + Sized>(dst: *mut T, src: T, count: usize) {
         std::ptr::write_volatile(ptr, src);
     }
 }
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum TransferType {
+    FileTransfer,
+    RemoteEncryptedVirtualFilesystem {
+        virtual_path: PathBuf,
+        security_level: SecurityLevel,
+    },
+}
diff --git a/citadel_crypt/src/scramble/crypt_splitter.rs b/citadel_crypt/src/scramble/crypt_splitter.rs
index 54a2df0db..2608bd31f 100644
--- a/citadel_crypt/src/scramble/crypt_splitter.rs
+++ b/citadel_crypt/src/scramble/crypt_splitter.rs
@@ -1,3 +1,4 @@
+use std::borrow::Cow;
 use std::collections::HashMap;
 use std::ops::Range;
 use std::time::{Duration, Instant};
@@ -68,6 +69,7 @@ pub fn calculate_nonce_version(a: usize, b: u64) -> usize {
     }
 }
 
+#[allow(clippy::too_many_arguments)]
 pub fn generate_scrambler_metadata<T: AsRef<[u8]>>(
     msg_drill: &EntropyBank,
     plain_text: T,
@@ -76,6 +78,7 @@ pub fn generate_scrambler_metadata<T: AsRef<[u8]>>(
     group_id: u64,
     enx: EncryptionAlgorithm,
     sig_alg: SigAlgorithm,
+    transfer_type: &TransferType,
 ) -> Result<GroupReceiverConfig, CryptError<String>> {
     let plain_text = plain_text.as_ref();
 
@@ -127,7 +130,7 @@ pub fn generate_scrambler_metadata<T: AsRef<[u8]>>(
     let cfg = GroupReceiverConfig::new_refresh(
         group_id,
         header_size_bytes,
-        plain_text,
+        plain_text.len(),
         max_packet_payload_size,
         number_of_full_waves,
         number_of_partial_waves,
@@ -135,6 +138,7 @@ pub fn generate_scrambler_metadata<T: AsRef<[u8]>>(
         bytes_in_last_wave,
         max_packets_per_wave,
         ciphertext_len_last_wave,
+        transfer_type,
     );
 
     Ok(cfg)
@@ -146,6 +150,7 @@ fn get_scramble_encrypt_config<'a, R: Ratchet>(
     header_size_bytes: usize,
     security_level: SecurityLevel,
     group_id: u64,
+    transfer_type: &TransferType,
 ) -> Result<
     (
         GroupReceiverConfig,
@@ -165,6 +170,7 @@ fn get_scramble_encrypt_config<'a, R: Ratchet>(
         group_id,
         msg_pqc.params.encryption_algorithm,
         msg_pqc.params.sig_algorithm,
+        transfer_type,
     )?;
     Ok((cfg, msg_drill, msg_pqc, scramble_drill))
 }
@@ -186,22 +192,34 @@ pub fn par_scramble_encrypt_group<T: AsRef<[u8]>, R: Ratchet, F, const N: usize>
     plain_text: T,
     security_level: SecurityLevel,
     hyper_ratchet: &R,
+    static_aux_ratchet: &R,
     header_size_bytes: usize,
     target_cid: u64,
     object_id: u32,
     group_id: u64,
+    transfer_type: TransferType,
     header_inscriber: F,
 ) -> Result<GroupSenderDevice<N>, CryptError<String>>
 where
     F: Fn(&PacketVector, &EntropyBank, u32, u64, &mut BytesMut) + Send + Sync,
 {
-    let plain_text = plain_text.as_ref();
+    let mut plain_text = Cow::Borrowed(plain_text.as_ref());
+    if let TransferType::RemoteEncryptedVirtualFilesystem { security_level, .. } = &transfer_type {
+        // pre-encrypt
+        let local_encrypted = static_aux_ratchet
+            .local_encrypt(plain_text, *security_level)
+            .unwrap();
+
+        plain_text = Cow::Owned(local_encrypted);
+    }
+
     let (mut cfg, msg_drill, msg_pqc, scramble_drill) = get_scramble_encrypt_config(
         hyper_ratchet,
-        plain_text,
+        &plain_text,
         header_size_bytes,
         security_level,
         group_id,
+        &transfer_type,
     )?;
 
     #[cfg(not(target_family = "wasm"))]
@@ -230,11 +248,13 @@ where
 
     debug_assert_ne!(cfg.last_plaintext_wave_length, 0);
 
-    if msg_pqc.params.encryption_algorithm != EncryptionAlgorithm::Kyber {
+    if msg_pqc.params.encryption_algorithm != EncryptionAlgorithm::Kyber
+        && matches!(&transfer_type, TransferType::FileTransfer)
+    {
         debug_assert_eq!(cfg.packets_needed, packets.len());
     } else {
         let last_wave_idx = cfg.wave_count as u32 - 1;
-        // Kyber encryptions have a non-deterministic output length sometimes. Update the cfg
+        // Kyber encryptions and remote_encrypted types have a non-deterministic output length sometimes. Update the cfg
         let ciphertext_len: usize = packets
             .values()
             .filter_map(|r| {
@@ -248,7 +268,7 @@ where
         cfg = GroupReceiverConfig::new_refresh(
             cfg.group_id as u64,
             cfg.header_size_bytes,
-            plain_text,
+            plain_text.len(),
             cfg.max_payload_size,
             cfg.number_of_full_waves,
             cfg.number_of_partial_waves,
@@ -256,6 +276,7 @@ where
             cfg.last_plaintext_wave_length,
             cfg.max_packets_per_wave,
             ciphertext_len,
+            &transfer_type,
         );
     }
 
@@ -300,13 +321,6 @@ fn scramble_encrypt_wave(
         .collect::<Vec<(usize, PacketCoordinate)>>();
     packets.shuffle(&mut ThreadRng::default());
 
-    /*
-    if cfg.wave_count - 1 == wave_idx {
-        debug_assert_eq!(packets.len(), cfg.packets_in_last_wave);
-    } else {
-        debug_assert_eq!(packets.len(), cfg.max_packets_per_wave);
-    }*/
-
     packets
 }
 
@@ -331,6 +345,7 @@ pub fn oneshot_unencrypted_group_unified<const N: usize>(
         last_plaintext_wave_length: len,
         max_packets_per_wave: 1,
         packets_in_last_wave: 1,
+        transfer_type: None,
     };
 
     Ok(GroupSenderDevice::<N>::new_oneshot(
@@ -383,6 +398,7 @@ pub struct GroupReceiver {
     wave_timeout: Duration,
 }
 
+use crate::misc::TransferType;
 use crate::secure_buffer::sec_packet::SecureMessagePacket;
 use citadel_pqcrypto::algorithm_dictionary::{EncryptionAlgorithm, SigAlgorithm};
 use citadel_pqcrypto::PostQuantumContainer;
@@ -390,7 +406,6 @@ use serde::{Deserialize, Serialize};
 
 /// For containing the data needed to receive a corresponding group
 #[derive(Clone, Debug, Serialize, Deserialize)]
-#[repr(C)]
 #[allow(missing_docs)]
 pub struct GroupReceiverConfig {
     pub packets_needed: usize,
@@ -407,6 +422,10 @@ pub struct GroupReceiverConfig {
     // this is NOT inscribed; only for transmission
     pub header_size_bytes: usize,
     pub group_id: usize,
+    // only relevant for files. Note: if transfer type is RemoteVirtualFileystem, then,
+    // the receiving endpoint won't decrypt the first level of encryption since the goal
+    // is to keep the file remotely encrypted
+    pub transfer_type: Option<TransferType>,
 }
 
 /// Used in citadel_proto
@@ -417,7 +436,7 @@ impl GroupReceiverConfig {
     pub fn new_refresh(
         group_id: u64,
         header_size_bytes: usize,
-        plain_text: &[u8],
+        plaintext_length: usize,
         max_packet_payload_size: usize,
         number_of_full_waves: usize,
         number_of_partial_waves: usize,
@@ -425,6 +444,7 @@ impl GroupReceiverConfig {
         bytes_in_last_wave: usize,
         max_packets_per_wave: usize,
         ciphertext_len_last_wave: usize,
+        transfer_type: &TransferType,
     ) -> Self {
         let number_of_waves = number_of_full_waves + number_of_partial_waves;
         let packets_in_last_wave =
@@ -441,25 +461,11 @@ impl GroupReceiverConfig {
 
         let packets_needed = (number_of_full_waves * max_packets_per_wave) + packets_in_last_wave;
 
-        //    pub packets_needed: usize,
-        //     pub max_packets_per_wave: usize,
-        //     pub plaintext_length: usize,
-        //     pub max_payload_size: usize,
-        //     pub last_payload_size: usize,
-        //     pub number_of_full_waves: usize,
-        //     pub number_of_partial_waves: usize,
-        //     pub wave_count: usize,
-        //     pub max_plaintext_wave_length: usize,
-        //     pub last_plaintext_wave_length: usize,
-        //     pub packets_in_last_wave: usize,
-        //     // this is NOT inscribed; only for transmission
-        //     pub header_size_bytes: usize,
-        //     pub group_id: usize,
         GroupReceiverConfig {
             group_id: group_id as usize,
             packets_needed,
             header_size_bytes,
-            plaintext_length: plain_text.len(),
+            plaintext_length,
             max_payload_size: max_packet_payload_size,
             last_payload_size: debug_last_payload_size,
             number_of_full_waves,
@@ -469,6 +475,7 @@ impl GroupReceiverConfig {
             last_plaintext_wave_length: bytes_in_last_wave,
             max_packets_per_wave,
             packets_in_last_wave,
+            transfer_type: Some(transfer_type.clone()),
         }
     }
 
diff --git a/citadel_crypt/src/stacked_ratchet.rs b/citadel_crypt/src/stacked_ratchet.rs
index e03536440..ec8e2e3fd 100644
--- a/citadel_crypt/src/stacked_ratchet.rs
+++ b/citadel_crypt/src/stacked_ratchet.rs
@@ -10,6 +10,7 @@ use citadel_pqcrypto::constructor_opts::{ConstructorOpts, RecursiveChain};
 use citadel_pqcrypto::PostQuantumContainer;
 use serde::{Deserialize, Serialize};
 use sha3::Digest;
+use std::borrow::Cow;
 use std::convert::TryFrom;
 use std::sync::Arc;
 
@@ -56,6 +57,17 @@ pub trait Ratchet: Serialize + for<'a> Deserialize<'a> + Clone + Send + Sync + '
             Some(self.get_default_security_level()),
         )
     }
+
+    fn local_encrypt<'a, T: Into<Cow<'a, [u8]>>>(
+        &self,
+        contents: T,
+        security_level: SecurityLevel,
+    ) -> Result<Vec<u8>, CryptError>;
+    fn local_decrypt<'a, T: Into<Cow<'a, [u8]>>>(
+        &self,
+        contents: T,
+        security_level: SecurityLevel,
+    ) -> Result<Vec<u8>, CryptError>;
 }
 
 /// For returning a variable hyper ratchet from a function
@@ -155,6 +167,36 @@ impl Ratchet for StackedRatchet {
     ) -> Result<(), CryptError<String>> {
         self.validate_message_packet(security_level, header, packet)
     }
+
+    fn local_encrypt<'a, T: Into<Cow<'a, [u8]>>>(
+        &self,
+        contents: T,
+        security_level: SecurityLevel,
+    ) -> Result<Vec<u8>, CryptError> {
+        let idx = self.verify_level(Some(security_level))?;
+        let mut data = contents.into();
+        for n in 0..=idx {
+            let (pqc, drill) = self.message_pqc_drill(Some(n));
+            data = Cow::Owned(drill.local_encrypt(pqc, &data)?);
+        }
+
+        Ok(data.into_owned())
+    }
+
+    fn local_decrypt<'a, T: Into<Cow<'a, [u8]>>>(
+        &self,
+        contents: T,
+        security_level: SecurityLevel,
+    ) -> Result<Vec<u8>, CryptError> {
+        let mut data = contents.into();
+        let idx = self.verify_level(Some(security_level))?;
+        for n in (0..=idx).rev() {
+            let (pqc, drill) = self.message_pqc_drill(Some(n));
+            data = Cow::Owned(drill.local_decrypt(pqc, &data)?);
+        }
+
+        Ok(data.into_owned())
+    }
 }
 
 impl StackedRatchet {
diff --git a/citadel_crypt/src/streaming_crypt_scrambler.rs b/citadel_crypt/src/streaming_crypt_scrambler.rs
index fcbd47e4e..eae4f4e9f 100644
--- a/citadel_crypt/src/streaming_crypt_scrambler.rs
+++ b/citadel_crypt/src/streaming_crypt_scrambler.rs
@@ -1,6 +1,7 @@
 use bytes::BytesMut;
 use futures::task::Context;
 use std::io::{BufReader, Read};
+use std::path::PathBuf;
 use tokio::macros::support::Pin;
 use tokio::sync::mpsc::Sender as GroupChanneler;
 use tokio::sync::oneshot::Receiver;
@@ -9,7 +10,7 @@ use crate::entropy_bank::{EntropyBank, SecurityLevel};
 use crate::packet_vector::PacketVector;
 use crate::scramble::crypt_splitter::{par_scramble_encrypt_group, GroupSenderDevice};
 
-use crate::misc::CryptError;
+use crate::misc::{CryptError, TransferType};
 use crate::stacked_ratchet::StackedRatchet;
 use citadel_io::Mutex;
 use citadel_io::{BlockingSpawn, BlockingSpawnError};
@@ -24,12 +25,12 @@ pub const MAX_BYTES_PER_GROUP: usize = crate::scramble::crypt_splitter::MAX_BYTE
 const DEFAULT_BYTES_PER_GROUP: usize = 1024 * 1024 * 3;
 
 /// Used for streaming sources of a fixed size
-pub trait FixedSizedStream: Read + Send + 'static {
+pub trait FixedSizedSource: Read + Send + 'static {
     fn length(&self) -> std::io::Result<u64>;
 }
 
 #[cfg(feature = "filesystem")]
-impl FixedSizedStream for std::fs::File {
+impl FixedSizedSource for std::fs::File {
     fn length(&self) -> std::io::Result<u64> {
         self.metadata().map(|r| r.len())
     }
@@ -51,24 +52,96 @@ impl<
 
 #[auto_impl::auto_impl(Box)]
 pub trait ObjectSource: Send + Sync + 'static {
-    fn try_get_stream(&mut self) -> Result<Box<dyn FixedSizedStream>, CryptError>;
+    fn try_get_stream(&mut self) -> Result<Box<dyn FixedSizedSource>, CryptError>;
     fn get_source_name(&self) -> Result<String, CryptError>;
+    fn delete_path(&self) -> Option<PathBuf> {
+        None
+    }
 }
 
-#[cfg(feature = "filesystem")]
-impl ObjectSource for std::path::PathBuf {
-    fn try_get_stream(&mut self) -> Result<Box<dyn FixedSizedStream>, CryptError> {
-        std::fs::File::open(self)
-            .map_err(|err| CryptError::Encrypt(err.to_string()))
-            .map(|r| Box::new(r) as Box<dyn FixedSizedStream>)
+macro_rules! impl_file_src {
+    ($value:ty) => {
+        #[cfg(feature = "filesystem")]
+        impl ObjectSource for $value {
+            fn try_get_stream(&mut self) -> Result<Box<dyn FixedSizedSource>, CryptError> {
+                std::fs::File::open(self)
+                    .map_err(|err| CryptError::Encrypt(err.to_string()))
+                    .map(|r| Box::new(r) as Box<dyn FixedSizedSource>)
+            }
+
+            fn get_source_name(&self) -> Result<String, CryptError> {
+                let name = std::path::Path::new(self);
+                name.file_name()
+                    .ok_or_else(|| CryptError::Encrypt("Unable to get filename".to_string()))?
+                    .to_str()
+                    .map(|r| r.to_string())
+                    .ok_or_else(|| CryptError::Encrypt("Unable to get filename/2".to_string()))
+            }
+
+            fn delete_path(&self) -> Option<PathBuf> {
+                let path = std::path::PathBuf::from(self);
+                Some(path)
+            }
+        }
+    };
+}
+
+impl_file_src!(std::path::PathBuf);
+impl_file_src!(&'static str);
+impl_file_src!(String);
+
+pub struct BytesSource {
+    pub inner: Option<Vec<u8>>,
+}
+
+// The only time this is cloned is for a post-file-transfer hook,
+// wherein the delete() function is called. As such, we don't need
+// the inner device
+impl Clone for BytesSource {
+    fn clone(&self) -> Self {
+        Self { inner: None }
+    }
+}
+
+impl ObjectSource for BytesSource {
+    fn try_get_stream(&mut self) -> Result<Box<dyn FixedSizedSource>, CryptError> {
+        struct VecReader {
+            len: usize,
+            cursor: std::io::Cursor<Vec<u8>>,
+        }
+
+        impl std::io::Read for VecReader {
+            fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+                self.cursor.read(buf)
+            }
+        }
+
+        impl FixedSizedSource for VecReader {
+            fn length(&self) -> std::io::Result<u64> {
+                Ok(self.len as u64)
+            }
+        }
+
+        let inner = self
+            .inner
+            .take()
+            .ok_or_else(|| CryptError::Encrypt("Source has already been exhausted".into()))?;
+        let len = inner.len();
+        let cursor = std::io::Cursor::new(inner);
+        Ok(Box::new(VecReader { len, cursor }))
     }
 
     fn get_source_name(&self) -> Result<String, CryptError> {
-        self.file_name()
-            .ok_or_else(|| CryptError::Encrypt("Unable to get filename".to_string()))?
-            .to_str()
-            .map(|r| r.to_string())
-            .ok_or_else(|| CryptError::Encrypt("Unable to get filename/2".to_string()))
+        let rand_id = rand::random::<u128>();
+        Ok(format!("{rand_id}.bin"))
+    }
+}
+
+impl<T: Into<Vec<u8>>> From<T> for BytesSource {
+    fn from(value: T) -> Self {
+        Self {
+            inner: Some(value.into()),
+        }
     }
 }
 
@@ -89,11 +162,13 @@ pub fn scramble_encrypt_source<S: ObjectSource, F: HeaderInscriberFn, const N: u
     stop: Receiver<()>,
     security_level: SecurityLevel,
     hyper_ratchet: StackedRatchet,
+    static_aux_ratchet: StackedRatchet,
     header_size_bytes: usize,
     target_cid: u64,
     group_id: u64,
+    transfer_type: TransferType,
     header_inscriber: F,
-) -> Result<(usize, usize), CryptError> {
+) -> Result<(usize, usize, usize), CryptError> {
     let source = source.try_get_stream()?;
     let object_len = source
         .length()
@@ -128,7 +203,9 @@ pub fn scramble_encrypt_source<S: ObjectSource, F: HeaderInscriberFn, const N: u
         group_id,
         security_level,
         hyper_ratchet,
+        static_aux_ratchet,
         reader,
+        transfer_type,
         file_len: object_len,
         max_bytes_per_group,
         read_cursor: 0,
@@ -151,7 +228,7 @@ pub fn scramble_encrypt_source<S: ObjectSource, F: HeaderInscriberFn, const N: u
     // drop the handle, we will not be using it
     std::mem::drop(handle);
 
-    Ok((object_len, total_groups))
+    Ok((object_len, total_groups, max_bytes_per_group))
 }
 
 async fn stopper(stop: Receiver<()>) -> Result<(), CryptError> {
@@ -177,7 +254,9 @@ async fn file_streamer<F: HeaderInscriberFn, R: Read, const N: usize>(
 struct AsyncCryptScrambler<F: HeaderInscriberFn, R: Read, const N: usize> {
     reader: BufReader<R>,
     hyper_ratchet: StackedRatchet,
+    static_aux_ratchet: StackedRatchet,
     security_level: SecurityLevel,
+    transfer_type: TransferType,
     file_len: usize,
     read_cursor: usize,
     object_id: u32,
@@ -224,6 +303,7 @@ impl<F: HeaderInscriberFn, R: Read, const N: usize> AsyncCryptScrambler<F, R, N>
     ) -> Poll<Option<GroupSenderDevice<N>>> {
         let Self {
             hyper_ratchet,
+            static_aux_ratchet,
             file_len,
             read_cursor,
             buffer,
@@ -237,6 +317,7 @@ impl<F: HeaderInscriberFn, R: Read, const N: usize> AsyncCryptScrambler<F, R, N>
             security_level,
             max_bytes_per_group,
             cur_task,
+            transfer_type,
             poll_amt,
             ..
         } = &mut *self;
@@ -253,26 +334,27 @@ impl<F: HeaderInscriberFn, R: Read, const N: usize> AsyncCryptScrambler<F, R, N>
             if reader.read_exact(bytes).is_ok() {
                 let group_id_input = *group_id + (*groups_rendered as u64);
                 std::mem::drop(lock);
-                // let mut compressed = Vec::new();
-                // flate3::Compressor::new().deflate(bytes as &[u8])
-                // let len = flate2::bufread::DeflateEncoder::new(bytes as &[u8], flate2::Compression::fast()).read_to_end(&mut compressed).unwrap();
                 let header_inscriber = header_inscriber.clone();
                 let buffer = buffer.clone();
                 let security_level = *security_level;
                 let hyper_ratchet = hyper_ratchet.clone();
+                let static_aux_ratchet = static_aux_ratchet.clone();
                 let header_size_bytes = *header_size_bytes;
                 let target_cid = *target_cid;
                 let object_id = *object_id;
+                let transfer_type = transfer_type.clone();
 
                 let task = citadel_io::spawn_blocking(move || {
                     par_scramble_encrypt_group(
                         &buffer.lock()[..poll_len],
                         security_level,
                         &hyper_ratchet,
+                        &static_aux_ratchet,
                         header_size_bytes,
                         target_cid,
                         object_id,
                         group_id_input,
+                        transfer_type,
                         |a, b, c, d, e| (header_inscriber)(a, b, c, d, e),
                     )
                 });
diff --git a/citadel_pqcrypto/src/encryption.rs b/citadel_pqcrypto/src/encryption.rs
index 9d829555f..fb81e84b1 100644
--- a/citadel_pqcrypto/src/encryption.rs
+++ b/citadel_pqcrypto/src/encryption.rs
@@ -24,87 +24,65 @@ pub trait AeadModule: Send + Sync {
         self.decrypt_in_place(nonce, &[], &mut ret)?;
         Ok(ret)
     }
+    /// Encrypts data such that it ensures only the local user may
+    /// see the contents, not even the endpoints
+    fn local_user_encrypt_in_place(
+        &self,
+        nonce: &[u8],
+        ad: &[u8],
+        input: &mut dyn Buffer,
+    ) -> Result<(), Error>;
+    fn local_user_decrypt_in_place(
+        &self,
+        nonce: &[u8],
+        ad: &[u8],
+        input: &mut dyn Buffer,
+    ) -> Result<(), Error>;
+
+    fn local_user_decrypt(&self, nonce: &[u8], input: &[u8]) -> Result<Vec<u8>, Error> {
+        let mut ret = Vec::from(input);
+        self.local_user_decrypt_in_place(nonce, &[], &mut ret)?;
+        Ok(ret)
+    }
+
+    fn local_user_encrypt(&self, nonce: &[u8], input: &[u8]) -> Result<Vec<u8>, Error> {
+        let mut ret = Vec::from(input);
+        self.local_user_encrypt_in_place(nonce, &[], &mut ret)?;
+        Ok(ret)
+    }
 }
 
 pub(crate) mod aes_impl {
     use crate::encryption::AeadModule;
     use crate::ez_error::Error;
+    use crate::PostQuantumMetaKex;
     use aes_gcm_siv::aead::generic_array::GenericArray;
     use aes_gcm_siv::aead::{AeadInPlace, Buffer};
     use aes_gcm_siv::Aes256GcmSiv;
 
-    impl AeadModule for Aes256GcmSiv {
-        fn encrypt_in_place(
-            &self,
-            nonce: &[u8],
-            ad: &[u8],
-            input: &mut dyn Buffer,
-        ) -> Result<(), Error> {
-            <Self as AeadInPlace>::encrypt_in_place(
-                self,
-                GenericArray::from_slice(nonce),
-                ad,
-                input,
-            )
-            .map_err(|_| Error::EncryptionFailure)
-        }
-
-        fn decrypt_in_place(
-            &self,
-            nonce: &[u8],
-            ad: &[u8],
-            input: &mut dyn Buffer,
-        ) -> Result<(), Error> {
-            <Self as AeadInPlace>::decrypt_in_place(
-                self,
-                GenericArray::from_slice(nonce),
-                ad,
-                input,
-            )
-            .map_err(|_| Error::EncryptionFailure)
-        }
+    pub struct AesModule {
+        pub aead: Aes256GcmSiv,
+        pub kex: PostQuantumMetaKex,
     }
+
+    crate::impl_basic_aead_module!(AesModule, crate::AES_GCM_NONCE_LENGTH_BYTES);
 }
 
 pub(crate) mod chacha_impl {
     use crate::encryption::AeadModule;
     use crate::ez_error::Error;
+    use crate::PostQuantumMetaKex;
     use aes_gcm_siv::aead::Buffer;
     use chacha20poly1305::aead::generic_array::GenericArray;
     use chacha20poly1305::aead::AeadInPlace;
     use chacha20poly1305::XChaCha20Poly1305;
 
-    impl AeadModule for XChaCha20Poly1305 {
-        fn encrypt_in_place(
-            &self,
-            nonce: &[u8],
-            ad: &[u8],
-            input: &mut dyn Buffer,
-        ) -> Result<(), Error> {
-            <Self as AeadInPlace>::encrypt_in_place(
-                self,
-                GenericArray::from_slice(nonce),
-                ad,
-                input,
-            )
-            .map_err(|_| Error::EncryptionFailure)
-        }
-
-        fn decrypt_in_place(
-            &self,
-            nonce: &[u8],
-            ad: &[u8],
-            input: &mut dyn Buffer,
-        ) -> Result<(), Error> {
-            <Self as AeadInPlace>::decrypt_in_place(
-                self,
-                GenericArray::from_slice(nonce),
-                ad,
-                input,
-            )
-            .map_err(|_| Error::EncryptionFailure)
-        }
+    pub struct ChaChaModule {
+        pub aead: XChaCha20Poly1305,
+        pub kex: PostQuantumMetaKex,
     }
+
+    crate::impl_basic_aead_module!(ChaChaModule, crate::CHA_CHA_NONCE_LENGTH_BYTES);
 }
 
 pub(crate) mod kyber_module {
@@ -113,7 +91,6 @@ pub(crate) mod kyber_module {
     use crate::wire::ScramCryptDictionary;
     use crate::{
         AeadModule, Error, KemAlgorithm, PostQuantumMetaKex, PostQuantumMetaSig, SigAlgorithm,
-        AES_GCM_NONCE_LENGTH_BYTES,
     };
     use aes_gcm_siv::aead::Buffer;
 
@@ -137,7 +114,7 @@ pub(crate) mod kyber_module {
             // signing the header ensures header does not change
             // encrypting the input ciphertext + the signature ensures ciphertext works
 
-            let aes_nonce = &nonce[..AES_GCM_NONCE_LENGTH_BYTES];
+            //let aes_nonce = &nonce[..AES_GCM_NONCE_LENGTH_BYTES];
             let signature = crate::functions::signature_sign(
                 sha3_256_with_ad(ad, input.as_ref()),
                 self.sig.sig_private_key.as_slice(),
@@ -148,35 +125,17 @@ pub(crate) mod kyber_module {
                 .map_err(|err| Error::Other(err.to_string()))?;
             encode_length_be_bytes(signature.as_slice().len(), input)?;
 
-            // encrypt everything so far with AES GCM
-            self.symmetric_key_local
-                .encrypt_in_place(aes_nonce, ad, input)?;
-
-            let pre_scramble_len = input.len();
-            // scramble the AES GCM encrypted ciphertext
-            // use N=32 bytes to ensure that we get only a single output ciphertext block from kyber (~1100 bytes)
-            let scram_crypt_dict = ScramCryptDictionary::<32>::new().unwrap();
-            scram_crypt_dict.scramble_in_place(input)?;
-            // encode the pre-scramble length
-            encode_length_be_bytes(pre_scramble_len, input)?;
-            // encrypt the 32-byte scramble dict using post-quantum pke
-            let remote_public_key = self.kex.remote_public_key.as_ref().unwrap();
-
-            let scram_crypt_ser = bincode2::serialize(&scram_crypt_dict)
-                .map_err(|err| Error::Other(err.to_string()))?;
-
-            let encrypted_scramble_dict =
-                encrypt_pke(self.kem_alg, &**remote_public_key, scram_crypt_ser, nonce)?;
-            input
-                .extend_from_slice(encrypted_scramble_dict.as_slice())
-                .map_err(|err| Error::Other(err.to_string()))?;
-            encode_length_be_bytes(encrypted_scramble_dict.len(), input)?;
+            // encrypt the data using the remote's public key
+            let remote_public_key = self.kex.remote_public_key.as_deref().unwrap();
 
-            let sha = sha3_256(input.as_ref());
-            input
-                .extend_from_slice(&sha)
-                .map_err(|err| Error::Other(err.to_string()))?;
-            Ok(())
+            core_kyber_otp_encrypt(
+                &*self.symmetric_key_local,
+                remote_public_key,
+                self.kem_alg,
+                nonce,
+                ad,
+                input,
+            )
         }
 
         fn decrypt_in_place(
@@ -185,42 +144,16 @@ pub(crate) mod kyber_module {
             ad: &[u8],
             input: &mut dyn Buffer,
         ) -> Result<(), Error> {
-            let local_sk = self.kex.secret_key.as_deref().unwrap();
             let sig_remote_pk = self.sig.remote_sig_public_key.as_ref().unwrap();
-
-            let (ciphertext, sha_required) =
-                input.as_ref().split_at(input.len().saturating_sub(32));
-            let sha_ciphertext = sha3_256(ciphertext);
-            if sha_ciphertext != sha_required {
-                return Err(Error::Other(format!(
-                    "Invalid ciphertext checksum. {sha_ciphertext:?} != {sha_required:?}",
-                )));
-            }
-
-            input.truncate(input.len().saturating_sub(32));
-
-            let encrypted_scramble_dict_len = decode_length(input)?;
-            let split_pt = input.len().saturating_sub(encrypted_scramble_dict_len);
-            let (_, encrypted_scramble_dict) = input.as_ref().split_at(split_pt);
-            let decrypted_scramble_dict =
-                decrypt_pke(self.kem_alg, local_sk, encrypted_scramble_dict)?;
-            //let scram_crypt_dict = ScramCryptDictionary::<32>::try_from(decrypted_scramble_dict)?;
-            let scram_crypt_dict: ScramCryptDictionary<32> =
-                bincode2::deserialize(&decrypted_scramble_dict)
-                    .map_err(|err| Error::Other(err.to_string()))?;
-            // remove the encrypted scramble data from the input buf
-            let truncate_point = input.len().saturating_sub(encrypted_scramble_dict_len);
-            input.truncate(truncate_point);
-            // get the pre-scramble length
-            let pre_scramble_length = decode_length(input)?;
-            // descramble
-            scram_crypt_dict.descramble_in_place(input)?;
-            // truncate
-            input.truncate(pre_scramble_length);
-            // with the AES-GCM encrypted ciphertext descrambled, now, decrypt it
-            let aes_nonce = &nonce[..AES_GCM_NONCE_LENGTH_BYTES];
-            self.symmetric_key_remote
-                .decrypt_in_place(aes_nonce, ad, input)?;
+            let secret_key = self.kex.secret_key.as_deref().unwrap();
+            core_kyber_otp_decrypt(
+                &*self.symmetric_key_remote,
+                secret_key,
+                self.kem_alg,
+                nonce,
+                ad,
+                input,
+            )?;
             // get the signature
             let signature_len = decode_length(input)?;
             let split_pt = input.len().saturating_sub(signature_len);
@@ -236,6 +169,26 @@ pub(crate) mod kyber_module {
 
             Ok(())
         }
+
+        fn local_user_encrypt_in_place(
+            &self,
+            nonce: &[u8],
+            ad: &[u8],
+            input: &mut dyn Buffer,
+        ) -> Result<(), Error> {
+            self.symmetric_key_local
+                .local_user_encrypt_in_place(nonce, ad, input)
+        }
+
+        fn local_user_decrypt_in_place(
+            &self,
+            nonce: &[u8],
+            ad: &[u8],
+            input: &mut dyn Buffer,
+        ) -> Result<(), Error> {
+            self.symmetric_key_local
+                .local_user_decrypt_in_place(nonce, ad, input)
+        }
     }
 
     pub fn encrypt_pke<T: AsRef<[u8]>, R: AsRef<[u8]>, V: AsRef<[u8]>>(
@@ -244,7 +197,8 @@ pub(crate) mod kyber_module {
         plaintext: R,
         nonce: V,
     ) -> Result<Vec<u8>, Error> {
-        kyber_pke::encrypt(local_pk, plaintext, nonce).map_err(|_| Error::EncryptionFailure)
+        kyber_pke::encrypt(local_pk, plaintext, nonce)
+            .map_err(|err| Error::Other(format!("{err:?}")))
     }
 
     pub fn decrypt_pke<T: AsRef<[u8]>, R: AsRef<[u8]>>(
@@ -288,6 +242,82 @@ pub(crate) mod kyber_module {
         Ok(object_len)
     }
 
+    pub fn core_kyber_otp_encrypt(
+        symmetric_cipher: &dyn AeadModule,
+        public_key: impl AsRef<[u8]>,
+        kem_alg: KemAlgorithm,
+        nonce: &[u8],
+        ad: &[u8],
+        input: &mut dyn Buffer,
+    ) -> Result<(), Error> {
+        // encrypt everything so far with AES GCM
+        symmetric_cipher.encrypt_in_place(nonce, ad, input)?;
+
+        let pre_scramble_len = input.len();
+        // scramble the AES GCM encrypted ciphertext
+        // use N=32 bytes to ensure that we get only a single output ciphertext block from kyber (~1100 bytes)
+        let scram_crypt_dict = ScramCryptDictionary::<32>::new().unwrap();
+        scram_crypt_dict.scramble_in_place(input)?;
+        // encode the pre-scramble length
+        encode_length_be_bytes(pre_scramble_len, input)?;
+        // encrypt the 32-byte scramble dict using post-quantum pke
+
+        let scram_crypt_ser =
+            bincode2::serialize(&scram_crypt_dict).map_err(|err| Error::Other(err.to_string()))?;
+
+        let encrypted_scramble_dict = encrypt_pke(kem_alg, public_key, scram_crypt_ser, nonce)?;
+        input
+            .extend_from_slice(encrypted_scramble_dict.as_slice())
+            .map_err(|err| Error::Other(err.to_string()))?;
+        encode_length_be_bytes(encrypted_scramble_dict.len(), input)?;
+
+        let sha = sha3_256(input.as_ref());
+        input
+            .extend_from_slice(&sha)
+            .map_err(|err| Error::Other(err.to_string()))?;
+        Ok(())
+    }
+
+    pub fn core_kyber_otp_decrypt(
+        symmetric_cipher: &dyn AeadModule,
+        local_sk: impl AsRef<[u8]>,
+        kem_alg: KemAlgorithm,
+        nonce: &[u8],
+        ad: &[u8],
+        input: &mut dyn Buffer,
+    ) -> Result<(), Error> {
+        let (ciphertext, sha_required) = input.as_ref().split_at(input.len().saturating_sub(32));
+        let sha_ciphertext = sha3_256(ciphertext);
+        if sha_ciphertext != sha_required {
+            return Err(Error::Other(format!(
+                "Invalid ciphertext checksum. {sha_ciphertext:?} != {sha_required:?}",
+            )));
+        }
+
+        input.truncate(input.len().saturating_sub(32));
+
+        let encrypted_scramble_dict_len = decode_length(input)?;
+        let split_pt = input.len().saturating_sub(encrypted_scramble_dict_len);
+        let (_, encrypted_scramble_dict) = input.as_ref().split_at(split_pt);
+        let decrypted_scramble_dict = decrypt_pke(kem_alg, local_sk, encrypted_scramble_dict)?;
+        let scram_crypt_dict: ScramCryptDictionary<32> =
+            bincode2::deserialize(&decrypted_scramble_dict)
+                .map_err(|err| Error::Other(err.to_string()))?;
+        // remove the encrypted scramble data from the input buf
+        let truncate_point = input.len().saturating_sub(encrypted_scramble_dict_len);
+        input.truncate(truncate_point);
+        // get the pre-scramble length
+        let pre_scramble_length = decode_length(input)?;
+        // descramble
+        scram_crypt_dict.descramble_in_place(input)?;
+        // truncate
+        input.truncate(pre_scramble_length);
+        // with the AES-GCM encrypted ciphertext descrambled, now, decrypt it
+        symmetric_cipher.decrypt_in_place(nonce, ad, input)?;
+
+        Ok(())
+    }
+
     fn sha3_256(input: &[u8]) -> [u8; 32] {
         sha3_256_with_ad(&[], input)
     }
@@ -304,3 +334,66 @@ pub(crate) mod kyber_module {
         digest.finalize().into()
     }
 }
+
+#[macro_export]
+macro_rules! impl_basic_aead_module {
+    ($val:ty, $nonce_len:expr) => {
+        impl AeadModule for $val {
+            fn encrypt_in_place(
+                &self,
+                nonce: &[u8],
+                ad: &[u8],
+                input: &mut dyn Buffer,
+            ) -> Result<(), Error> {
+                self.aead
+                    .encrypt_in_place(GenericArray::from_slice(&nonce[..$nonce_len]), ad, input)
+                    .map_err(|_| Error::EncryptionFailure)
+            }
+
+            fn decrypt_in_place(
+                &self,
+                nonce: &[u8],
+                ad: &[u8],
+                input: &mut dyn Buffer,
+            ) -> Result<(), Error> {
+                self.aead
+                    .decrypt_in_place(GenericArray::from_slice(&nonce[..$nonce_len]), ad, input)
+                    .map_err(|_| Error::EncryptionFailure)
+            }
+
+            fn local_user_encrypt_in_place(
+                &self,
+                nonce: &[u8],
+                ad: &[u8],
+                input: &mut dyn Buffer,
+            ) -> Result<(), Error> {
+                let public_key = &*self.kex.public_key;
+                super::kyber_module::core_kyber_otp_encrypt(
+                    self,
+                    public_key,
+                    self.kex.kem_alg,
+                    nonce,
+                    ad,
+                    input,
+                )
+            }
+
+            fn local_user_decrypt_in_place(
+                &self,
+                nonce: &[u8],
+                ad: &[u8],
+                input: &mut dyn Buffer,
+            ) -> Result<(), Error> {
+                let private_key = self.kex.secret_key.as_deref().unwrap();
+                super::kyber_module::core_kyber_otp_decrypt(
+                    self,
+                    private_key,
+                    self.kex.kem_alg,
+                    nonce,
+                    ad,
+                    input,
+                )
+            }
+        }
+    };
+}
diff --git a/citadel_pqcrypto/src/export.rs b/citadel_pqcrypto/src/export.rs
index 860e69294..8f74b6fb3 100644
--- a/citadel_pqcrypto/src/export.rs
+++ b/citadel_pqcrypto/src/export.rs
@@ -1,4 +1,6 @@
 use crate::algorithm_dictionary::EncryptionAlgorithm;
+use crate::encryption::aes_impl::AesModule;
+use crate::encryption::chacha_impl::ChaChaModule;
 use crate::encryption::kyber_module::KyberModule;
 use crate::encryption::AeadModule;
 use crate::{CryptoParameters, KeyStore, PQNode, PostQuantumMetaKex, PostQuantumMetaSig};
@@ -87,29 +89,36 @@ pub(crate) fn keys_to_aead_store(
     pq_node: PQNode,
 ) -> AeadStore {
     match params.encryption_algorithm {
-        EncryptionAlgorithm::AES_GCM_256_SIV => (
-            Some(Box::new(aes_gcm_siv::Aes256GcmSiv::new(alice))),
-            Some(Box::new(aes_gcm_siv::Aes256GcmSiv::new(bob))),
-        ),
+        EncryptionAlgorithm::AES_GCM_256_SIV => {
+            let symmetric_key_local = Box::new(AesModule {
+                aead: aes_gcm_siv::Aes256GcmSiv::new(alice),
+                kex: kex.clone(),
+            });
 
+            let symmetric_key_remote = Box::new(AesModule {
+                aead: aes_gcm_siv::Aes256GcmSiv::new(bob),
+                kex: kex.clone(),
+            });
+
+            (Some(symmetric_key_local), Some(symmetric_key_remote))
+        }
         EncryptionAlgorithm::Xchacha20Poly_1305 => (
-            Some(Box::new(chacha20poly1305::XChaCha20Poly1305::new(alice))),
-            Some(Box::new(chacha20poly1305::XChaCha20Poly1305::new(bob))),
+            Some(Box::new(ChaChaModule {
+                aead: chacha20poly1305::XChaCha20Poly1305::new(alice),
+                kex: kex.clone(),
+            })),
+            Some(Box::new(ChaChaModule {
+                aead: chacha20poly1305::XChaCha20Poly1305::new(bob),
+                kex: kex.clone(),
+            })),
         ),
 
         EncryptionAlgorithm::Kyber => {
             let kem_alg = params.kem_algorithm;
             let sig_alg = params.sig_algorithm;
 
-            let symmetric_key_local = match pq_node {
-                PQNode::Alice => Box::new(aes_gcm_siv::Aes256GcmSiv::new(alice)),
-                PQNode::Bob => Box::new(aes_gcm_siv::Aes256GcmSiv::new(bob)),
-            };
-
-            let symmetric_key_remote = match pq_node {
-                PQNode::Alice => Box::new(aes_gcm_siv::Aes256GcmSiv::new(bob)),
-                PQNode::Bob => Box::new(aes_gcm_siv::Aes256GcmSiv::new(alice)),
-            };
+            let (symmetric_key_local, symmetric_key_remote) =
+                generate_symmetric_aes_module(pq_node, alice, bob, kex);
 
             let keys = Box::new(KyberModule {
                 kem_alg,
@@ -120,7 +129,6 @@ pub(crate) fn keys_to_aead_store(
                 symmetric_key_remote,
             }) as Box<dyn AeadModule>;
 
-            // TODO: multi-modal ratcheted encryption
             match pq_node {
                 PQNode::Alice => (Some(keys), None),
                 PQNode::Bob => (None, Some(keys)),
@@ -128,3 +136,34 @@ pub(crate) fn keys_to_aead_store(
         }
     }
 }
+
+fn generate_symmetric_aes_module(
+    pq_node: PQNode,
+    alice: &GenericArray<u8, generic_array::typenum::U32>,
+    bob: &GenericArray<u8, generic_array::typenum::U32>,
+    kex: &PostQuantumMetaKex,
+) -> (Box<dyn AeadModule>, Box<dyn AeadModule>) {
+    let symmetric_key_local = match pq_node {
+        PQNode::Alice => Box::new(AesModule {
+            aead: aes_gcm_siv::Aes256GcmSiv::new(alice),
+            kex: kex.clone(),
+        }),
+        PQNode::Bob => Box::new(AesModule {
+            aead: aes_gcm_siv::Aes256GcmSiv::new(bob),
+            kex: kex.clone(),
+        }),
+    };
+
+    let symmetric_key_remote = match pq_node {
+        PQNode::Alice => Box::new(AesModule {
+            aead: aes_gcm_siv::Aes256GcmSiv::new(bob),
+            kex: kex.clone(),
+        }),
+        PQNode::Bob => Box::new(AesModule {
+            aead: aes_gcm_siv::Aes256GcmSiv::new(alice),
+            kex: kex.clone(),
+        }),
+    };
+
+    (symmetric_key_local, symmetric_key_remote)
+}
diff --git a/citadel_pqcrypto/src/lib.rs b/citadel_pqcrypto/src/lib.rs
index 42b3d83b9..942a81094 100644
--- a/citadel_pqcrypto/src/lib.rs
+++ b/citadel_pqcrypto/src/lib.rs
@@ -241,14 +241,14 @@ impl PostQuantumContainer {
         let previous_symmetric_key = opts.chain;
         let data = Self::create_new_alice(params.kem_algorithm, params.sig_algorithm)
             .map_err(|err| Error::Other(err.to_string()))?;
-        let aes_gcm_key = None;
+        let key_store = None;
         log::trace!(target: "citadel", "Success creating new ALICE container");
 
         Ok(Self {
             params,
             data,
             chain: previous_symmetric_key,
-            key_store: aes_gcm_key,
+            key_store,
             anti_replay_attack: AntiReplayAttackContainer::default(),
             node: PQNode::Alice,
         })
@@ -437,7 +437,7 @@ impl PostQuantumContainer {
     fn get_decryption_key(&self) -> Option<&dyn AeadModule> {
         if let EncryptionAlgorithm::Kyber = self.params.encryption_algorithm {
             // use multi-modal asymmetric + symmetric ratcheted encryption
-            // alices key is in alice, bob's key is in bob. Thus, use encryption key
+            // alice's key is in alice, bob's key is in bob. Thus, use encryption key
             self.get_encryption_key()
         } else {
             // use symmetric encryption only (NOT post quantum, only quantum-resistant, but faster)
@@ -546,8 +546,46 @@ impl PostQuantumContainer {
 
         // if the shared secret is loaded, the AES GCM abstraction should too.
 
-        if let Some(aes_gcm_key) = self.get_encryption_key() {
-            aes_gcm_key.encrypt(nonce, input)
+        if let Some(symmetric_key) = self.get_encryption_key() {
+            symmetric_key.encrypt(nonce, input)
+        } else {
+            Err(Error::SharedSecretNotLoaded)
+        }
+    }
+
+    /// Encrypts the data using the local public key. This implies
+    /// only local's private key may be used for decryption.
+    pub fn local_encrypt<T: AsRef<[u8]>, R: AsRef<[u8]>>(
+        &self,
+        input: T,
+        nonce: R,
+    ) -> Result<Vec<u8>, Error> {
+        let input = input.as_ref();
+        let nonce = nonce.as_ref();
+
+        // if the shared secret is loaded, the AES GCM abstraction should too.
+
+        if let Some(symmetric_key) = self.get_encryption_key() {
+            symmetric_key.local_user_encrypt(nonce, input)
+        } else {
+            Err(Error::SharedSecretNotLoaded)
+        }
+    }
+
+    /// Encrypts the data using the local public key. This implies
+    /// only local's private key may be used for decryption.
+    pub fn local_decrypt<T: AsRef<[u8]>, R: AsRef<[u8]>>(
+        &self,
+        input: T,
+        nonce: R,
+    ) -> Result<Vec<u8>, Error> {
+        let input = input.as_ref();
+        let nonce = nonce.as_ref();
+
+        // if the shared secret is loaded, the AES GCM abstraction should too.
+
+        if let Some(symmetric_key) = self.get_encryption_key() {
+            symmetric_key.local_user_decrypt(nonce, input)
         } else {
             Err(Error::SharedSecretNotLoaded)
         }
@@ -569,8 +607,8 @@ impl PostQuantumContainer {
 
         let mut in_place_payload = InPlaceBuffer::new(&mut payload, 0..payload_len)
             .ok_or(Error::Generic("Bad window range"))?;
-        if let Some(aes_gcm_key) = self.get_encryption_key() {
-            aes_gcm_key
+        if let Some(symmetric_key) = self.get_encryption_key() {
+            symmetric_key
                 .encrypt_in_place(nonce, header.subset(0..header_len), &mut in_place_payload)
                 .map_err(|_| Error::EncryptionFailure)?;
             header.unsplit(payload);
@@ -593,8 +631,8 @@ impl PostQuantumContainer {
 
         let mut in_place_payload = InPlaceBuffer::new(payload, 0..payload_len)
             .ok_or(Error::Generic("Bad window range"))?;
-        if let Some(aes_gcm_key) = self.get_decryption_key() {
-            aes_gcm_key
+        if let Some(symmetric_key) = self.get_decryption_key() {
+            symmetric_key
                 .decrypt_in_place(nonce, header, &mut in_place_payload)
                 .and_then(|_| {
                     // get the last 8 bytes of the payload
@@ -637,8 +675,8 @@ impl PostQuantumContainer {
         let nonce = nonce.as_ref();
         // if the shared secret is loaded, the AES GCM abstraction should too.
 
-        if let Some(aes_gcm_key) = self.get_decryption_key() {
-            aes_gcm_key.decrypt(nonce, input)
+        if let Some(symmetric_key) = self.get_decryption_key() {
+            symmetric_key.decrypt(nonce, input)
         } else {
             Err(Error::SharedSecretNotLoaded)
         }
@@ -914,7 +952,7 @@ pub struct PostQuantumMetaKex {
     remote_public_key: Option<Arc<Vec<u8>>>,
     /// The public key. Both Alice and Bob get this
     public_key: Arc<Vec<u8>>,
-    /// Only Alice gets this one
+    /// secret key pair of the public key
     secret_key: Option<Arc<Vec<u8>>>,
     /// Both Bob and Alice get this one
     ciphertext: Option<Arc<Vec<u8>>>,
diff --git a/citadel_proto/src/lib.rs b/citadel_proto/src/lib.rs
index a90af4eb3..10e008e21 100644
--- a/citadel_proto/src/lib.rs
+++ b/citadel_proto/src/lib.rs
@@ -399,6 +399,11 @@ pub mod prelude {
     pub use crate::proto::node_request::*;
     pub use crate::proto::node_result::*;
     pub use crate::proto::remote::*;
+
+    pub use citadel_crypt::misc::TransferType;
+    pub use citadel_crypt::prelude::SecurityLevel;
+    pub use citadel_crypt::streaming_crypt_scrambler::{BytesSource, ObjectSource};
+    pub use citadel_user::misc::{prepare_virtual_path, validate_virtual_path};
 }
 
 pub mod auth;
diff --git a/citadel_proto/src/proto/mod.rs b/citadel_proto/src/proto/mod.rs
index 81ff563b8..28645b5bb 100644
--- a/citadel_proto/src/proto/mod.rs
+++ b/citadel_proto/src/proto/mod.rs
@@ -1,3 +1,9 @@
+use crate::proto::outbound_sender::OutboundPrimaryStreamSender;
+use crate::proto::packet::HdpHeader;
+use crate::proto::session::HdpSession;
+use crate::proto::state_container::StateContainerInner;
+use bytes::BytesMut;
+
 /// For the custom BytesCodec that doesn't overflow
 pub(crate) mod codec;
 ///
@@ -31,3 +37,26 @@ pub(crate) mod state_subcontainers;
 pub(crate) mod transfer_stats;
 /// Packet validations. This is not the same as encryption
 pub(crate) mod validation;
+
+/// Returns the preferred primary stream for returning a response
+pub(crate) fn get_preferred_primary_stream(
+    header: &HdpHeader,
+    session: &HdpSession,
+    state_container: &StateContainerInner,
+) -> Option<OutboundPrimaryStreamSender> {
+    if header.target_cid.get() != 0 {
+        Some(
+            state_container
+                .get_preferred_stream(header.session_cid.get())
+                .clone(),
+        )
+    } else {
+        session.to_primary_stream.clone()
+    }
+}
+
+pub(crate) fn send_with_error_logging(stream: &OutboundPrimaryStreamSender, packet: BytesMut) {
+    if let Err(err) = stream.unbounded_send(packet) {
+        log::error!(target: "citadel", "Error while sending packet outbound: {:?}", err)
+    }
+}
diff --git a/citadel_proto/src/proto/node.rs b/citadel_proto/src/proto/node.rs
index e8a5e7f75..99da0b958 100644
--- a/citadel_proto/src/proto/node.rs
+++ b/citadel_proto/src/proto/node.rs
@@ -21,6 +21,7 @@ use crate::error::NetworkError;
 use crate::functional::PairMap;
 use crate::kernel::kernel_communicator::KernelAsyncCallbackHandler;
 use crate::kernel::RuntimeFuture;
+use crate::prelude::{DeleteObject, PullObject};
 use crate::proto::misc::net::{
     DualListener, FirstPacket, GenericNetworkListener, GenericNetworkStream, TlsListener,
 };
@@ -840,6 +841,7 @@ impl HdpServer {
                     chunk_size,
                     implicated_cid,
                     v_conn_type: virtual_target,
+                    transfer_type,
                 }) => {
                     if let Err(err) = session_manager.process_outbound_file(
                         ticket_id,
@@ -848,6 +850,41 @@ impl HdpServer {
                         implicated_cid,
                         virtual_target,
                         SecurityLevel::Standard,
+                        transfer_type,
+                    ) {
+                        send_error(ticket_id, err)?;
+                    }
+                }
+
+                NodeRequest::PullObject(PullObject {
+                    v_conn,
+                    virtual_dir,
+                    delete_on_pull,
+                    transfer_security_level,
+                }) => {
+                    if let Err(err) = session_manager.revfs_pull(
+                        ticket_id,
+                        v_conn.get_implicated_cid(),
+                        v_conn,
+                        virtual_dir,
+                        delete_on_pull,
+                        transfer_security_level,
+                    ) {
+                        send_error(ticket_id, err)?;
+                    }
+                }
+
+                NodeRequest::DeleteObject(DeleteObject {
+                    v_conn,
+                    virtual_dir,
+                    security_level,
+                }) => {
+                    if let Err(err) = session_manager.revfs_delete(
+                        ticket_id,
+                        v_conn.get_implicated_cid(),
+                        v_conn,
+                        virtual_dir,
+                        security_level,
                     ) {
                         send_error(ticket_id, err)?;
                     }
diff --git a/citadel_proto/src/proto/node_request.rs b/citadel_proto/src/proto/node_request.rs
index 8e7e0151c..57ad7726b 100644
--- a/citadel_proto/src/proto/node_request.rs
+++ b/citadel_proto/src/proto/node_request.rs
@@ -3,9 +3,12 @@ use crate::prelude::{
     ConnectMode, GroupBroadcast, PeerSignal, SessionSecuritySettings, UdpMode, VirtualTargetType,
 };
 use crate::proto::state_container::VirtualConnectionType;
+use citadel_crypt::misc::TransferType;
+use citadel_crypt::prelude::SecurityLevel;
 use citadel_crypt::streaming_crypt_scrambler::ObjectSource;
 use citadel_user::auth::proposed_credentials::ProposedCredentials;
 use std::net::SocketAddr;
+use std::path::PathBuf;
 
 pub struct RegisterToHypernode {
     pub remote_addr: SocketAddr,
@@ -35,11 +38,26 @@ pub struct ReKey {
     pub v_conn_type: VirtualTargetType,
 }
 
+// Also used for updating objects
 pub struct SendObject {
     pub source: Box<dyn ObjectSource>,
     pub chunk_size: Option<usize>,
     pub implicated_cid: u64,
     pub v_conn_type: VirtualTargetType,
+    pub transfer_type: TransferType,
+}
+
+pub struct PullObject {
+    pub v_conn: VirtualConnectionType,
+    pub virtual_dir: PathBuf,
+    pub delete_on_pull: bool,
+    pub transfer_security_level: SecurityLevel,
+}
+
+pub struct DeleteObject {
+    pub v_conn: VirtualConnectionType,
+    pub virtual_dir: PathBuf,
+    pub security_level: SecurityLevel,
 }
 
 pub struct GroupBroadcastCommand {
@@ -66,8 +84,12 @@ pub enum NodeRequest {
     ConnectToHypernode(ConnectToHypernode),
     /// Updates the drill for the given CID
     ReKey(ReKey),
-    /// Send a file
+    /// Sends or updates a file
     SendObject(SendObject),
+    /// Pulls a file from the remote virtual encrypted filesystem
+    PullObject(PullObject),
+    /// Deletes a file from the remote virtual encrypted filesystem
+    DeleteObject(DeleteObject),
     /// A group-message related command
     GroupBroadcastCommand(GroupBroadcastCommand),
     /// Tells the server to disconnect a session (implicated cid, target_cid)
diff --git a/citadel_proto/src/proto/node_result.rs b/citadel_proto/src/proto/node_result.rs
index e71f64a38..2d3128fab 100644
--- a/citadel_proto/src/proto/node_result.rs
+++ b/citadel_proto/src/proto/node_result.rs
@@ -2,9 +2,11 @@ use crate::prelude::{GroupBroadcast, GroupChannel, PeerChannel, PeerSignal, UdpC
 use crate::proto::peer::peer_layer::MailboxTransfer;
 use crate::proto::remote::Ticket;
 use crate::proto::state_container::VirtualConnectionType;
+
 use citadel_user::backend::utils::ObjectTransferHandler;
 use citadel_user::client_account::ClientNetworkAccount;
 use std::net::SocketAddr;
+use std::path::PathBuf;
 
 #[derive(Debug)]
 pub struct RegisterOkay {
@@ -125,8 +127,14 @@ pub struct SessionList {
     pub sessions: Vec<u64>,
 }
 
-/// This type is for relaying results between the lower-level server and the higher-level kernel
-/// TODO: Convert to enum structs
+#[derive(Debug)]
+pub struct ReVFSResult {
+    pub error_message: Option<String>,
+    pub data: Option<PathBuf>,
+    pub ticket: Ticket,
+}
+
+/// This type is for relaying results between the lower-level protocol and the higher-level kernel
 #[derive(Debug)]
 pub enum NodeResult {
     /// Returns the CNAC which was created during the registration process
@@ -140,6 +148,7 @@ pub enum NodeResult {
     /// The connection was a failure
     ConnectFail(ConnectFail),
     ReKeyResult(ReKeyResult),
+    ReVFS(ReVFSResult),
     /// The outbound request was rejected
     OutboundRequestRejected(OutboundRequestRejected),
     /// For file transfers. Implicated CID, Peer/Target CID, object ID
@@ -232,6 +241,7 @@ impl NodeResult {
             }) => Some(*t),
             NodeResult::Shutdown => None,
             NodeResult::ReKeyResult(ReKeyResult { ticket, .. }) => Some(*ticket),
+            NodeResult::ReVFS(ReVFSResult { ticket, .. }) => Some(*ticket),
         }
     }
 }
diff --git a/citadel_proto/src/proto/packet.rs b/citadel_proto/src/proto/packet.rs
index 95b7a457f..c9a8990fe 100644
--- a/citadel_proto/src/proto/packet.rs
+++ b/citadel_proto/src/proto/packet.rs
@@ -107,6 +107,10 @@ pub(crate) mod packet_flags {
             pub(crate) mod file {
                 pub(crate) const FILE_HEADER: u8 = 0;
                 pub(crate) const FILE_HEADER_ACK: u8 = 1;
+                pub(crate) const REVFS_PULL: u8 = 2;
+                pub(crate) const REVFS_DELETE: u8 = 3;
+                pub(crate) const REVFS_ACK: u8 = 4;
+                pub(crate) const REVFS_PULL_ACK: u8 = 5;
             }
 
             pub(crate) mod udp {
diff --git a/citadel_proto/src/proto/packet_crafter.rs b/citadel_proto/src/proto/packet_crafter.rs
index 0c8ed172a..8fa583529 100644
--- a/citadel_proto/src/proto/packet_crafter.rs
+++ b/citadel_proto/src/proto/packet_crafter.rs
@@ -1582,12 +1582,22 @@ pub(crate) mod file {
     use crate::proto::packet_processor::includes::{packet_flags, HdpHeader, SecurityLevel};
     use crate::proto::remote::Ticket;
     use crate::proto::state_container::VirtualTargetType;
-    use bytes::{BufMut, BytesMut};
-    use citadel_crypt::scramble::crypt_splitter::AES_GCM_GHASH_OVERHEAD;
+    use bytes::BytesMut;
     use citadel_crypt::stacked_ratchet::StackedRatchet;
     use citadel_user::backend::utils::VirtualObjectMetadata;
+    use citadel_user::serialization::SyncIO;
+    use serde::{Deserialize, Serialize};
+    use std::path::PathBuf;
     use zerocopy::{I64, U128, U32, U64};
 
+    #[derive(Serialize, Deserialize, Debug)]
+    pub struct FileHeaderPacket {
+        pub file_metadata: VirtualObjectMetadata,
+        pub virtual_target: VirtualTargetType,
+        pub local_encryption_level: Option<SecurityLevel>,
+    }
+
+    #[allow(clippy::too_many_arguments)]
     pub(crate) fn craft_file_header_packet(
         hyper_ratchet: &StackedRatchet,
         group_start: u64,
@@ -1596,9 +1606,8 @@ pub(crate) mod file {
         virtual_target: VirtualTargetType,
         file_metadata: VirtualObjectMetadata,
         timestamp: i64,
+        local_encryption_level: Option<SecurityLevel>,
     ) -> BytesMut {
-        let metadata_serialized = file_metadata.serialize();
-        let serialized_vt = virtual_target.serialize();
         let header = HdpHeader {
             protocol_version: (*crate::constants::PROTOCOL_VERSION).into(),
             cmd_primary: packet_flags::cmd::primary::FILE,
@@ -1607,23 +1616,23 @@ pub(crate) mod file {
             security_level: security_level.value(),
             context_info: U128::new(ticket.0),
             group: U64::new(group_start),
-            wave_id: U32::new(serialized_vt.len() as u32),
+            wave_id: U32::new(0),
             session_cid: U64::new(hyper_ratchet.get_cid()),
             drill_version: U32::new(hyper_ratchet.version()),
             timestamp: I64::new(timestamp),
             target_cid: U64::new(virtual_target.get_target_cid()),
         };
 
-        let mut packet = BytesMut::with_capacity(
-            HDP_HEADER_BYTE_LEN
-                + serialized_vt.len()
-                + metadata_serialized.len()
-                + AES_GCM_GHASH_OVERHEAD,
-        );
+        let mut packet = BytesMut::with_capacity(HDP_HEADER_BYTE_LEN);
         header.inscribe_into(&mut packet);
-        //processor.group_config.inscribe_into(&mut packet);
-        packet.put(serialized_vt.as_slice());
-        packet.put(metadata_serialized.as_slice());
+
+        let payload = FileHeaderPacket {
+            file_metadata,
+            virtual_target,
+            local_encryption_level,
+        };
+
+        payload.serialize_into_buf(&mut packet).unwrap();
 
         hyper_ratchet
             .protect_message_packet(Some(security_level), HDP_HEADER_BYTE_LEN, &mut packet)
@@ -1632,6 +1641,13 @@ pub(crate) mod file {
         packet
     }
 
+    #[derive(Serialize, Deserialize, Debug)]
+    pub struct FileHeaderAckPacket {
+        pub success: bool,
+        pub virtual_target: VirtualTargetType,
+        pub object_id: u32,
+    }
+
     #[allow(clippy::too_many_arguments)]
     pub(crate) fn craft_file_header_ack_packet(
         hyper_ratchet: &StackedRatchet,
@@ -1643,8 +1659,6 @@ pub(crate) mod file {
         virtual_target: VirtualTargetType,
         timestamp: i64,
     ) -> BytesMut {
-        let success: u64 = u64::from(success);
-        let serialized_vt = virtual_target.serialize();
         let header = HdpHeader {
             protocol_version: (*crate::constants::PROTOCOL_VERSION).into(),
             cmd_primary: packet_flags::cmd::primary::FILE,
@@ -1652,20 +1666,199 @@ pub(crate) mod file {
             algorithm: 0,
             security_level: security_level.value(),
             context_info: U128::new(ticket.0),
-            group: U64::new(success),
-            wave_id: U32::new(object_id),
+            group: U64::new(0),
+            wave_id: U32::new(0),
             session_cid: U64::new(hyper_ratchet.get_cid()),
             drill_version: U32::new(hyper_ratchet.version()),
             timestamp: I64::new(timestamp),
             target_cid: U64::new(target_cid),
         };
 
-        let mut packet = BytesMut::with_capacity(
-            HDP_HEADER_BYTE_LEN + serialized_vt.len() + AES_GCM_GHASH_OVERHEAD,
-        );
+        let mut packet = BytesMut::with_capacity(HDP_HEADER_BYTE_LEN);
+        header.inscribe_into(&mut packet);
+
+        let payload = FileHeaderAckPacket {
+            success,
+            virtual_target,
+            object_id,
+        };
+
+        payload.serialize_into_buf(&mut packet).unwrap();
+
+        hyper_ratchet
+            .protect_message_packet(Some(security_level), HDP_HEADER_BYTE_LEN, &mut packet)
+            .unwrap();
+
+        packet
+    }
+
+    #[derive(Serialize, Deserialize, Debug)]
+    pub struct ReVFSPullPacket {
+        pub virtual_path: PathBuf,
+        pub delete_on_pull: bool,
+        pub security_level: SecurityLevel,
+    }
+
+    /// This packet will essentially cause the receiving endpoint to emulate
+    /// a FILE_HEADER with auto-accept on
+    pub fn craft_revfs_pull(
+        hyper_ratchet: &StackedRatchet,
+        security_level: SecurityLevel,
+        ticket: Ticket,
+        timestamp: i64,
+        target_cid: u64,
+        virtual_path: PathBuf,
+        delete_on_pull: bool,
+    ) -> BytesMut {
+        let header = HdpHeader {
+            protocol_version: (*crate::constants::PROTOCOL_VERSION).into(),
+            cmd_primary: packet_flags::cmd::primary::FILE,
+            cmd_aux: packet_flags::cmd::aux::file::REVFS_PULL,
+            algorithm: 0,
+            security_level: security_level.value(),
+            context_info: U128::new(ticket.0),
+            group: U64::new(0),
+            wave_id: U32::new(0),
+            session_cid: U64::new(hyper_ratchet.get_cid()),
+            drill_version: U32::new(hyper_ratchet.version()),
+            timestamp: I64::new(timestamp),
+            target_cid: U64::new(target_cid),
+        };
+
+        let mut packet = BytesMut::with_capacity(HDP_HEADER_BYTE_LEN);
+        header.inscribe_into(&mut packet);
+
+        let payload = ReVFSPullPacket {
+            virtual_path,
+            delete_on_pull,
+            security_level,
+        };
+
+        payload.serialize_into_buf(&mut packet).unwrap();
+        hyper_ratchet
+            .protect_message_packet(Some(security_level), HDP_HEADER_BYTE_LEN, &mut packet)
+            .unwrap();
+
+        packet
+    }
+
+    #[derive(Serialize, Deserialize, Debug)]
+    pub struct ReVFSDeletePacket {
+        pub virtual_path: PathBuf,
+    }
+
+    pub fn craft_revfs_delete(
+        hyper_ratchet: &StackedRatchet,
+        security_level: SecurityLevel,
+        ticket: Ticket,
+        timestamp: i64,
+        target_cid: u64,
+        virtual_path: PathBuf,
+    ) -> BytesMut {
+        let header = HdpHeader {
+            protocol_version: (*crate::constants::PROTOCOL_VERSION).into(),
+            cmd_primary: packet_flags::cmd::primary::FILE,
+            cmd_aux: packet_flags::cmd::aux::file::REVFS_DELETE,
+            algorithm: 0,
+            security_level: security_level.value(),
+            context_info: U128::new(ticket.0),
+            group: U64::new(0),
+            wave_id: U32::new(0),
+            session_cid: U64::new(hyper_ratchet.get_cid()),
+            drill_version: U32::new(hyper_ratchet.version()),
+            timestamp: I64::new(timestamp),
+            target_cid: U64::new(target_cid),
+        };
+
+        let mut packet = BytesMut::with_capacity(HDP_HEADER_BYTE_LEN);
         header.inscribe_into(&mut packet);
-        packet.put(serialized_vt.as_slice());
 
+        let payload = ReVFSDeletePacket { virtual_path };
+
+        payload.serialize_into_buf(&mut packet).unwrap();
+        hyper_ratchet
+            .protect_message_packet(Some(security_level), HDP_HEADER_BYTE_LEN, &mut packet)
+            .unwrap();
+
+        packet
+    }
+
+    #[derive(Serialize, Deserialize, Debug)]
+    pub struct ReVFSAckPacket {
+        pub success: bool,
+        pub error_msg: Option<String>,
+    }
+
+    pub fn craft_revfs_ack(
+        hyper_ratchet: &StackedRatchet,
+        security_level: SecurityLevel,
+        ticket: Ticket,
+        timestamp: i64,
+        target_cid: u64,
+        error_msg: Option<String>,
+    ) -> BytesMut {
+        let header = HdpHeader {
+            protocol_version: (*crate::constants::PROTOCOL_VERSION).into(),
+            cmd_primary: packet_flags::cmd::primary::FILE,
+            cmd_aux: packet_flags::cmd::aux::file::REVFS_ACK,
+            algorithm: 0,
+            security_level: security_level.value(),
+            context_info: U128::new(ticket.0),
+            group: U64::new(0),
+            wave_id: U32::new(0),
+            session_cid: U64::new(hyper_ratchet.get_cid()),
+            drill_version: U32::new(hyper_ratchet.version()),
+            timestamp: I64::new(timestamp),
+            target_cid: U64::new(target_cid),
+        };
+
+        let mut packet = BytesMut::with_capacity(HDP_HEADER_BYTE_LEN);
+        header.inscribe_into(&mut packet);
+
+        let success = error_msg.is_none();
+        let payload = ReVFSAckPacket { success, error_msg };
+
+        payload.serialize_into_buf(&mut packet).unwrap();
+        hyper_ratchet
+            .protect_message_packet(Some(security_level), HDP_HEADER_BYTE_LEN, &mut packet)
+            .unwrap();
+
+        packet
+    }
+
+    #[derive(Serialize, Deserialize, Debug)]
+    pub enum ReVFSPullAckPacket {
+        Success,
+        Error { error: String },
+    }
+
+    pub fn craft_revfs_pull_ack(
+        hyper_ratchet: &StackedRatchet,
+        security_level: SecurityLevel,
+        ticket: Ticket,
+        timestamp: i64,
+        target_cid: u64,
+        payload: ReVFSPullAckPacket,
+    ) -> BytesMut {
+        let header = HdpHeader {
+            protocol_version: (*crate::constants::PROTOCOL_VERSION).into(),
+            cmd_primary: packet_flags::cmd::primary::FILE,
+            cmd_aux: packet_flags::cmd::aux::file::REVFS_PULL_ACK,
+            algorithm: 0,
+            security_level: security_level.value(),
+            context_info: U128::new(ticket.0),
+            group: U64::new(0),
+            wave_id: U32::new(0),
+            session_cid: U64::new(hyper_ratchet.get_cid()),
+            drill_version: U32::new(hyper_ratchet.version()),
+            timestamp: I64::new(timestamp),
+            target_cid: U64::new(target_cid),
+        };
+
+        let mut packet = BytesMut::with_capacity(HDP_HEADER_BYTE_LEN);
+        header.inscribe_into(&mut packet);
+
+        payload.serialize_into_buf(&mut packet).unwrap();
         hyper_ratchet
             .protect_message_packet(Some(security_level), HDP_HEADER_BYTE_LEN, &mut packet)
             .unwrap();
diff --git a/citadel_proto/src/proto/packet_processor/file_packet.rs b/citadel_proto/src/proto/packet_processor/file_packet.rs
index 4d9727022..7ed734e62 100644
--- a/citadel_proto/src/proto/packet_processor/file_packet.rs
+++ b/citadel_proto/src/proto/packet_processor/file_packet.rs
@@ -1,6 +1,13 @@
 use super::includes::*;
 use crate::error::NetworkError;
-use crate::proto::packet_processor::primary_group_packet::get_proper_hyper_ratchet;
+use crate::prelude::{ReVFSResult, Ticket};
+use crate::proto::packet_crafter::file::ReVFSPullAckPacket;
+use crate::proto::packet_processor::header_to_response_vconn_type;
+use crate::proto::packet_processor::primary_group_packet::{
+    get_proper_hyper_ratchet, get_resp_target_cid_from_header,
+};
+use crate::proto::{get_preferred_primary_stream, send_with_error_logging};
+use citadel_crypt::misc::TransferType;
 use std::sync::atomic::Ordering;
 
 #[cfg_attr(feature = "localhost-testing", tracing::instrument(target = "citadel", skip_all, ret, err, fields(is_server = session.is_server, src = packet.parse().unwrap().0.session_cid.get(), target = packet.parse().unwrap().0.target_cid.get())))]
@@ -27,6 +34,9 @@ pub fn process_file_packet(
         "Unable to get proper HR"
     );
     let security_level = header.security_level.into();
+    let ticket: Ticket = header.context_info.get().into();
+    let ts = session.time_tracker.get_global_time_ns();
+
     // ALL FILE packets must be authenticated
     match validation::group::validate(&hyper_ratchet, security_level, header_bytes, payload) {
         Some(payload) => {
@@ -34,7 +44,11 @@ pub fn process_file_packet(
                 packet_flags::cmd::aux::file::FILE_HEADER => {
                     log::trace!(target: "citadel", "RECV FILE HEADER");
                     match validation::file::validate_file_header(&header, &payload[..]) {
-                        Some((v_target, vfm)) => {
+                        Some(payload) => {
+                            let v_target = payload.virtual_target;
+                            let vfm = payload.file_metadata;
+                            let local_encryption_level = payload.local_encryption_level;
+                            log::trace!(target: "citadel", "Declared local encryption level on file header: {local_encryption_level:?}");
                             let (target_cid, v_target_flipped) = match v_target {
                                 VirtualConnectionType::LocalGroupPeer(
                                     implicated_cid,
@@ -57,13 +71,9 @@ pub fn process_file_packet(
                                 }
                             };
 
-                            let preferred_primary_stream = if header.target_cid.get() != 0 {
-                                state_container
-                                    .get_preferred_stream(header.session_cid.get())
-                                    .clone()
-                            } else {
-                                return_if_none!(session.to_primary_stream.clone())
-                            };
+                            let preferred_primary_stream = return_if_none!(
+                                get_preferred_primary_stream(&header, session, &state_container)
+                            );
 
                             if !state_container.on_file_header_received(
                                 &header,
@@ -75,6 +85,7 @@ pub fn process_file_packet(
                                 target_cid,
                                 v_target_flipped,
                                 preferred_primary_stream,
+                                local_encryption_level,
                             ) {
                                 log::warn!(target: "citadel", "Failed to run on_file_header_received");
                             }
@@ -94,14 +105,17 @@ pub fn process_file_packet(
                 packet_flags::cmd::aux::file::FILE_HEADER_ACK => {
                     log::trace!(target: "citadel", "RECV FILE HEADER ACK");
                     match validation::file::validate_file_header_ack(&header, &payload[..]) {
-                        Some((success, object_id, v_target)) => {
+                        Some(payload) => {
+                            let success = payload.success;
+                            let object_id = payload.object_id;
+                            let v_target = payload.virtual_target;
                             // the target is the implicated cid of THIS receiving node
-                            let implicated_cid = header.target_cid.get();
+                            let original_implicated_cid = header.target_cid.get();
                             // conclude by passing this data into the state container
                             if state_container
                                 .on_file_header_ack_received(
                                     success,
-                                    implicated_cid,
+                                    original_implicated_cid,
                                     header.context_info.get().into(),
                                     object_id,
                                     v_target,
@@ -121,8 +135,184 @@ pub fn process_file_packet(
                     }
                 }
 
+                packet_flags::cmd::aux::file::REVFS_PULL => {
+                    // Let A be the sender, and B be this node, the receiver.
+                    // A is asking to pull its own file from B. To do this,
+                    // we will send a file_header back to A with auto-accept on.
+                    // This will cause the standard file transfer protocol to occur.
+                    // The only extra information we need to give the adjacent endpoint
+                    // is the metadata pertaining to the encryption strength used on
+                    // the data.
+                    match validation::file::validate_revfs_pull(&header, &payload) {
+                        Some(packet) => {
+                            let session = session.clone();
+                            let preferred_primary_stream = return_if_none!(
+                                get_preferred_primary_stream(&header, &session, &state_container)
+                            );
+                            let virtual_target = header_to_response_vconn_type(&header);
+                            let revfs_cid = header.session_cid.get();
+                            let resp_target_cid = get_resp_target_cid_from_header(&header);
+                            let delete_on_pull = packet.delete_on_pull;
+
+                            // get the real_path and security level used from the backend
+                            let task = async move {
+                                let response_payload = match session
+                                    .account_manager
+                                    .get_persistence_handler()
+                                    .revfs_get_file_info(revfs_cid, packet.virtual_path)
+                                    .await
+                                {
+                                    Ok((source, local_encryption_level)) => {
+                                        let transfer_type = TransferType::FileTransfer; // use a basic file transfer since we don't need to data to be locally encrypted when sending it back
+                                        match session.process_outbound_file(
+                                            ticket,
+                                            None,
+                                            source,
+                                            virtual_target,
+                                            packet.security_level,
+                                            transfer_type,
+                                            Some(local_encryption_level),
+                                            move |source| {
+                                                if delete_on_pull {
+                                                    spawn!(tokio::fs::remove_file(source));
+                                                }
+                                            },
+                                        ) {
+                                            Ok(_) => ReVFSPullAckPacket::Success,
+
+                                            Err(err) => ReVFSPullAckPacket::Error {
+                                                error: err.into_string(),
+                                            },
+                                        }
+                                    }
+
+                                    Err(err) => ReVFSPullAckPacket::Error {
+                                        error: err.into_string(),
+                                    },
+                                };
+
+                                // on top of spawning the file transfer subroutine prior to this,
+                                // we will also send a REVFS pull ack
+                                let response_packet = packet_crafter::file::craft_revfs_pull_ack(
+                                    &hyper_ratchet,
+                                    security_level,
+                                    ticket,
+                                    ts,
+                                    resp_target_cid,
+                                    response_payload,
+                                );
+                                send_with_error_logging(&preferred_primary_stream, response_packet);
+                            };
+
+                            spawn!(task);
+
+                            Ok(PrimaryProcessorResult::Void)
+                        }
+
+                        None => {
+                            log::error!(target: "citadel", "Unable to validate REVFS PULL packet");
+                            Ok(PrimaryProcessorResult::Void)
+                        }
+                    }
+                }
+
+                packet_flags::cmd::aux::file::REVFS_DELETE => {
+                    log::trace!(target: "citadel", "RECV REVFS DELETE");
+                    match validation::file::validate_revfs_delete(&header, &payload) {
+                        Some(payload) => {
+                            let virtual_path = payload.virtual_path;
+                            // we use the cid of the sender, because, they are requesting to alter data here
+                            let re_vfs_cid = header.session_cid.get();
+                            let resp_target_cid = get_resp_target_cid_from_header(&header);
+                            let pers = session.account_manager.get_persistence_handler().clone();
+
+                            let preferred_primary_stream = return_if_none!(
+                                get_preferred_primary_stream(&header, session, &state_container)
+                            );
+
+                            let task = async move {
+                                let err_opt = pers
+                                    .revfs_delete(re_vfs_cid, virtual_path)
+                                    .await
+                                    .err()
+                                    .map(|e| e.into_string());
+                                let response_packet = packet_crafter::file::craft_revfs_ack(
+                                    &hyper_ratchet,
+                                    security_level,
+                                    ticket,
+                                    ts,
+                                    resp_target_cid,
+                                    err_opt,
+                                );
+                                send_with_error_logging(&preferred_primary_stream, response_packet);
+                            };
+
+                            spawn!(task);
+
+                            Ok(PrimaryProcessorResult::Void)
+                        }
+
+                        None => {
+                            log::error!(target: "citadel", "Unable to validate REVFS DELETE packet");
+                            Ok(PrimaryProcessorResult::Void)
+                        }
+                    }
+                }
+
+                packet_flags::cmd::aux::file::REVFS_ACK => {
+                    log::trace!(target: "citadel", "RECV REVFS ACK");
+                    match validation::file::validate_revfs_ack(&header, &payload) {
+                        Some(payload) => {
+                            let response = NodeResult::ReVFS(ReVFSResult {
+                                error_message: payload.error_msg,
+                                data: None,
+                                ticket,
+                            });
+
+                            session.send_to_kernel(response)?;
+
+                            Ok(PrimaryProcessorResult::Void)
+                        }
+
+                        None => {
+                            log::error!(target: "citadel", "Unable to validate REVFS ACK packet");
+                            Ok(PrimaryProcessorResult::Void)
+                        }
+                    }
+                }
+
+                packet_flags::cmd::aux::file::REVFS_PULL_ACK => {
+                    log::trace!(target: "citadel", "RECV REVFS PULL ACK");
+                    match validation::file::validate_revfs_pull_ack(&header, &payload) {
+                        Some(payload) => match payload {
+                            ReVFSPullAckPacket::Success => {
+                                // Iwe will not send an ReVFSResult with data quite yet.
+                                // Instead, we will send it once the inbound file transfer
+                                // is complete
+                                Ok(PrimaryProcessorResult::Void)
+                            }
+                            ReVFSPullAckPacket::Error { error } => {
+                                let error_signal = NodeResult::ReVFS(ReVFSResult {
+                                    error_message: Some(error),
+                                    data: None,
+                                    ticket,
+                                });
+
+                                session.send_to_kernel(error_signal)?;
+
+                                Ok(PrimaryProcessorResult::Void)
+                            }
+                        },
+
+                        None => {
+                            log::error!(target: "citadel", "Invalid REVFS PULL ACK command received");
+                            Ok(PrimaryProcessorResult::Void)
+                        }
+                    }
+                }
+
                 _ => {
-                    log::error!(target: "citadel", "Invalid FILE auxiliary command received");
+                    log::error!(target: "citadel", "Invalid REVFS ACK command received");
                     Ok(PrimaryProcessorResult::Void)
                 }
             }
diff --git a/citadel_proto/src/proto/packet_processor/mod.rs b/citadel_proto/src/proto/packet_processor/mod.rs
index 1484dc94f..c69f8645a 100644
--- a/citadel_proto/src/proto/packet_processor/mod.rs
+++ b/citadel_proto/src/proto/packet_processor/mod.rs
@@ -93,7 +93,7 @@ impl std::fmt::Debug for PrimaryProcessorResult {
 }
 
 /// should only be called by the receiver of a packet
-pub(crate) fn header_to_vconn_type(header: &HdpHeader) -> VirtualConnectionType {
+pub(crate) fn header_to_response_vconn_type(header: &HdpHeader) -> VirtualConnectionType {
     let session_cid = header.session_cid.get();
     let target_cid = header.target_cid.get();
     if target_cid != C2S_ENCRYPTION_ONLY {
diff --git a/citadel_proto/src/proto/packet_processor/rekey_packet.rs b/citadel_proto/src/proto/packet_processor/rekey_packet.rs
index 9c420e52f..5c7b9ef1c 100644
--- a/citadel_proto/src/proto/packet_processor/rekey_packet.rs
+++ b/citadel_proto/src/proto/packet_processor/rekey_packet.rs
@@ -3,7 +3,7 @@ use crate::error::NetworkError;
 use crate::prelude::ReKeyReturnType;
 use crate::proto::node::SecrecyMode;
 use crate::proto::packet_crafter::peer_cmd::C2S_ENCRYPTION_ONLY;
-use crate::proto::packet_processor::header_to_vconn_type;
+use crate::proto::packet_processor::header_to_response_vconn_type;
 use crate::proto::packet_processor::primary_group_packet::{
     attempt_kem_as_alice_finish, attempt_kem_as_bob, get_proper_hyper_ratchet,
     get_resp_target_cid_from_header, ToolsetUpdate,
@@ -135,7 +135,7 @@ pub fn process_rekey(
                     if needs_truncate.is_none() || needs_early_kernel_alert {
                         // we only alert the user once truncate_ack received
                         state_container.ratchet_update_state.on_complete(
-                            header_to_vconn_type(&header),
+                            header_to_response_vconn_type(&header),
                             &session.kernel_tx,
                             ReKeyReturnType::Success {
                                 version: latest_hr.version(),
@@ -277,7 +277,7 @@ pub fn process_rekey(
             }
 
             state_container.ratchet_update_state.on_complete(
-                header_to_vconn_type(&header),
+                header_to_response_vconn_type(&header),
                 &session.kernel_tx,
                 ReKeyReturnType::Success {
                     version: hyper_ratchet.version(),
diff --git a/citadel_proto/src/proto/session.rs b/citadel_proto/src/proto/session.rs
index 01278ead9..da0e1eb53 100644
--- a/citadel_proto/src/proto/session.rs
+++ b/citadel_proto/src/proto/session.rs
@@ -69,6 +69,7 @@ use crate::proto::state_subcontainers::preconnect_state_container::UdpChannelSen
 use crate::proto::state_subcontainers::rekey_container::calculate_update_frequency;
 use crate::proto::transfer_stats::TransferStats;
 use atomic::Atomic;
+use citadel_crypt::misc::TransferType;
 use citadel_crypt::prelude::ConstructorOpts;
 use citadel_crypt::streaming_crypt_scrambler::{scramble_encrypt_source, ObjectSource};
 use citadel_user::backend::PersistenceHandler;
@@ -76,6 +77,7 @@ use citadel_wire::exports::tokio_rustls::rustls;
 use citadel_wire::exports::Connection;
 use citadel_wire::nat_identification::NatType;
 use std::ops::Deref;
+use std::path::PathBuf;
 use std::pin::Pin;
 //use futures_codec::Framed;
 use crate::proto::node_result::{Disconnect, InternalServerError, NodeResult};
@@ -1157,8 +1159,143 @@ impl HdpSession {
         queue_worker.await
     }
 
+    pub fn revfs_pull(
+        &self,
+        ticket: Ticket,
+        v_conn: VirtualConnectionType,
+        virtual_path: PathBuf,
+        delete_on_pull: bool,
+        security_level: SecurityLevel,
+    ) -> Result<(), NetworkError> {
+        self.ensure_connected(&ticket)?;
+
+        let mut state_container = inner_mut_state!(self.state_container);
+        let ts = self.time_tracker.get_global_time_ns();
+
+        match v_conn {
+            VirtualConnectionType::LocalGroupServer(_implicated_cid) => {
+                let crypt_container = &mut state_container
+                    .c2s_channel_container
+                    .as_mut()
+                    .unwrap()
+                    .peer_session_crypto;
+
+                let latest_hr = crypt_container.get_hyper_ratchet(None).unwrap();
+                let packet = packet_crafter::file::craft_revfs_pull(
+                    latest_hr,
+                    security_level,
+                    ticket,
+                    ts,
+                    C2S_ENCRYPTION_ONLY,
+                    virtual_path,
+                    delete_on_pull,
+                );
+                self.send_to_primary_stream(Some(ticket), packet)
+            }
+            VirtualConnectionType::LocalGroupPeer(_, target_cid) => {
+                let endpoint_container =
+                    state_container.get_peer_endpoint_container_mut(target_cid)?;
+                let latest_hr = endpoint_container
+                    .endpoint_crypto
+                    .get_hyper_ratchet(None)
+                    .unwrap();
+                let packet = packet_crafter::file::craft_revfs_pull(
+                    latest_hr,
+                    security_level,
+                    ticket,
+                    ts,
+                    target_cid,
+                    virtual_path,
+                    delete_on_pull,
+                );
+                let primary_stream = endpoint_container
+                    .get_direct_p2p_primary_stream()
+                    .unwrap_or_else(|| self.to_primary_stream.as_ref().unwrap());
+                primary_stream
+                    .unbounded_send(packet)
+                    .map_err(|err| NetworkError::Generic(err.to_string()))
+            }
+
+            ty => Err(NetworkError::msg(format!(
+                "REVFS is not yet enabled for virtual connections of type {ty:?}"
+            ))),
+        }
+    }
+
+    pub fn revfs_delete(
+        &self,
+        ticket: Ticket,
+        v_conn: VirtualConnectionType,
+        virtual_path: PathBuf,
+        security_level: SecurityLevel,
+    ) -> Result<(), NetworkError> {
+        self.ensure_connected(&ticket)?;
+
+        let mut state_container = inner_mut_state!(self.state_container);
+        let ts = self.time_tracker.get_global_time_ns();
+
+        match v_conn {
+            VirtualConnectionType::LocalGroupServer(_implicated_cid) => {
+                let crypt_container = &mut state_container
+                    .c2s_channel_container
+                    .as_mut()
+                    .unwrap()
+                    .peer_session_crypto;
+
+                let latest_hr = crypt_container.get_hyper_ratchet(None).unwrap();
+                let packet = packet_crafter::file::craft_revfs_delete(
+                    latest_hr,
+                    security_level,
+                    ticket,
+                    ts,
+                    C2S_ENCRYPTION_ONLY,
+                    virtual_path,
+                );
+                self.send_to_primary_stream(Some(ticket), packet)
+            }
+            VirtualConnectionType::LocalGroupPeer(_, target_cid) => {
+                let endpoint_container =
+                    state_container.get_peer_endpoint_container_mut(target_cid)?;
+                let latest_hr = endpoint_container
+                    .endpoint_crypto
+                    .get_hyper_ratchet(None)
+                    .unwrap();
+                let packet = packet_crafter::file::craft_revfs_delete(
+                    latest_hr,
+                    security_level,
+                    ticket,
+                    ts,
+                    target_cid,
+                    virtual_path,
+                );
+                let primary_stream = endpoint_container
+                    .get_direct_p2p_primary_stream()
+                    .unwrap_or_else(|| self.to_primary_stream.as_ref().unwrap());
+                primary_stream
+                    .unbounded_send(packet)
+                    .map_err(|err| NetworkError::Generic(err.to_string()))
+            }
+
+            ty => Err(NetworkError::msg(format!(
+                "REVFS is not yet enabled for virtual connections of type {ty:?}"
+            ))),
+        }
+    }
+
+    fn ensure_connected(&self, ticket: &Ticket) -> Result<(), NetworkError> {
+        if self.state.load(Ordering::Relaxed) != SessionState::Connected {
+            Err(NetworkError::Generic(format!("Attempted to send a request (ticket: {ticket}) outbound, but the session is not connected")))
+        } else {
+            Ok(())
+        }
+    }
+
     /// Similar to process_outbound_packet, but optimized to handle files
+    /// The `local_encryption_level` refers to the security applied to the file that already exists,
+    /// note, the desired applied encryption level. The desired applied encryption level is inside the
+    /// `transfer_type` parameter
     // TODO: Reduce cognitive complexity
+    #[allow(clippy::too_many_arguments)]
     pub fn process_outbound_file(
         &self,
         ticket: Ticket,
@@ -1166,417 +1303,426 @@ impl HdpSession {
         source: Box<dyn ObjectSource>,
         virtual_target: VirtualTargetType,
         security_level: SecurityLevel,
+        transfer_type: TransferType,
+        local_encryption_level: Option<SecurityLevel>,
+        post_close_hook: impl for<'a> FnOnce(PathBuf) + Send + 'static,
     ) -> Result<(), NetworkError> {
         let this = self;
+        let source_path_location_opt = source.delete_path();
+        self.ensure_connected(&ticket)?;
 
-        if this.state.load(Ordering::Relaxed) != SessionState::Connected {
-            Err(NetworkError::Generic(format!("Attempted to send data (ticket: {ticket}, src: {:?}) outbound, but the session is not connected", source.get_source_name())))
-        } else {
-            let file_name = source
-                .get_source_name()
-                .map_err(|err| NetworkError::msg(err.into_string()))?;
+        let file_name = source
+            .get_source_name()
+            .map_err(|err| NetworkError::msg(err.into_string()))?;
 
-            let time_tracker = this.time_tracker;
-            let timestamp = this.time_tracker.get_global_time_ns();
-            let (group_sender, group_sender_rx) = channel(5);
-            let mut group_sender_rx = tokio_stream::wrappers::ReceiverStream::new(group_sender_rx);
-            let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
-            // the above are the same for all vtarget types. Now, we need to get the proper drill and pqc
-
-            let mut state_container = inner_mut_state!(this.state_container);
-
-            log::trace!(target: "citadel", "Transmit file name: {}", &file_name);
-            // the key cid must be differentiated from the target cid because the target_cid needs to be zero if
-            // there is no proxying. the key cid cannot be zero; if client -> server, key uses implicated cid
-            let (to_primary_stream, file_header, object_id, target_cid, key_cid, groups_needed) =
-                match virtual_target {
-                    VirtualTargetType::LocalGroupServer(implicated_cid) => {
-                        // if we are sending this just to the HyperLAN server (in the case of file uploads),
-                        // then, we use this session's pqc, the cnac's latest drill, and 0 for target_cid
-                        let crypt_container = &mut state_container
-                            .c2s_channel_container
-                            .as_mut()
-                            .unwrap()
-                            .peer_session_crypto;
-                        let object_id = crypt_container.get_and_increment_object_id();
-                        let group_id_start = crypt_container.get_and_increment_group_id();
-                        let latest_hr = crypt_container.get_hyper_ratchet(None).cloned().unwrap();
-
-                        let to_primary_stream = this.to_primary_stream.clone().unwrap();
-                        let target_cid = 0;
-                        let (file_size, groups_needed) = scramble_encrypt_source(
-                            source,
-                            max_group_size,
-                            object_id,
-                            group_sender,
-                            stop_rx,
-                            security_level,
-                            latest_hr.clone(),
-                            HDP_HEADER_BYTE_LEN,
-                            target_cid,
-                            group_id_start,
-                            packet_crafter::group::craft_wave_payload_packet_into,
-                        )
-                        .map_err(|err| NetworkError::Generic(err.to_string()))?;
+        let time_tracker = this.time_tracker;
+        let timestamp = this.time_tracker.get_global_time_ns();
+        let (group_sender, group_sender_rx) = channel(5);
+        let mut group_sender_rx = tokio_stream::wrappers::ReceiverStream::new(group_sender_rx);
+        let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
+        // the above are the same for all vtarget types. Now, we need to get the proper drill and pqc
 
-                        let file_metadata = VirtualObjectMetadata {
-                            object_id,
-                            name: file_name,
-                            date_created: "".to_string(),
-                            author: "N/A".to_string(),
-                            plaintext_length: file_size,
-                            group_count: groups_needed,
-                        };
+        let mut state_container = inner_mut_state!(this.state_container);
 
-                        // if 1 group, we don't need to reserve any more group IDs. If 2, then we reserve just one. 3, then 2
-                        let amt_to_reserve = groups_needed - 1;
-                        crypt_container.rolling_group_id += amt_to_reserve as u64;
-                        let file_header = packet_crafter::file::craft_file_header_packet(
-                            &latest_hr,
-                            group_id_start,
-                            ticket,
-                            security_level,
-                            virtual_target,
-                            file_metadata,
-                            timestamp,
-                        );
-                        (
-                            to_primary_stream,
-                            file_header,
-                            object_id,
-                            target_cid,
-                            implicated_cid,
-                            groups_needed,
-                        )
-                    }
+        log::trace!(target: "citadel", "Transmit file name: {}", &file_name);
+        // the key cid must be differentiated from the target cid because the target_cid needs to be zero if
+        // there is no proxying. the key cid cannot be zero; if client -> server, key uses implicated cid
+        let (to_primary_stream, file_header, object_id, target_cid, key_cid, groups_needed) =
+            match virtual_target {
+                VirtualTargetType::LocalGroupServer(implicated_cid) => {
+                    // if we are sending this just to the HyperLAN server (in the case of file uploads),
+                    // then, we use this session's pqc, the cnac's latest drill, and 0 for target_cid
+                    let crypt_container = &mut state_container
+                        .c2s_channel_container
+                        .as_mut()
+                        .unwrap()
+                        .peer_session_crypto;
+                    let object_id = crypt_container.get_and_increment_object_id();
+                    let group_id_start = crypt_container.get_and_increment_group_id();
+                    let latest_hr = crypt_container.get_hyper_ratchet(None).cloned().unwrap();
+                    let static_aux_ratchet = crypt_container
+                        .toolset
+                        .get_static_auxiliary_ratchet()
+                        .clone();
+
+                    let to_primary_stream = this.to_primary_stream.clone().unwrap();
+                    let target_cid = 0;
+                    let (file_size, groups_needed, _max_bytes_per_group) = scramble_encrypt_source(
+                        source,
+                        max_group_size,
+                        object_id,
+                        group_sender,
+                        stop_rx,
+                        security_level,
+                        latest_hr.clone(),
+                        static_aux_ratchet,
+                        HDP_HEADER_BYTE_LEN,
+                        target_cid,
+                        group_id_start,
+                        transfer_type.clone(),
+                        packet_crafter::group::craft_wave_payload_packet_into,
+                    )
+                    .map_err(|err| NetworkError::Generic(err.to_string()))?;
 
-                    VirtualConnectionType::LocalGroupPeer(implicated_cid, target_cid) => {
-                        log::trace!(target: "citadel", "Sending HyperLAN peer ({}) <-> HyperLAN Peer ({})", implicated_cid, target_cid);
-                        // here, we don't use the base session's PQC. Instead, we use the vconn's pqc and
-                        if let Some(vconn) = state_container
-                            .active_virtual_connections
-                            .get_mut(&target_cid)
-                        {
-                            if let Some(endpoint_container) = vconn.endpoint_container.as_mut() {
-                                let object_id = endpoint_container
-                                    .endpoint_crypto
-                                    .get_and_increment_object_id();
-                                // reserve group ids
-                                let start_group_id = endpoint_container
-                                    .endpoint_crypto
-                                    .get_and_increment_group_id();
-
-                                let latest_usable_ratchet = endpoint_container
-                                    .endpoint_crypto
-                                    .get_hyper_ratchet(None)
-                                    .unwrap();
-
-                                let preferred_primary_stream = endpoint_container
-                                    .get_direct_p2p_primary_stream()
-                                    .cloned()
-                                    .unwrap_or_else(|| this.to_primary_stream.clone().unwrap());
-
-                                let (file_size, groups_needed) = scramble_encrypt_source(
-                                    source,
-                                    max_group_size,
-                                    object_id,
-                                    group_sender,
-                                    stop_rx,
-                                    security_level,
-                                    latest_usable_ratchet.clone(),
-                                    HDP_HEADER_BYTE_LEN,
-                                    target_cid,
-                                    start_group_id,
-                                    packet_crafter::group::craft_wave_payload_packet_into,
-                                )
-                                .map_err(|err| NetworkError::Generic(err.to_string()))?;
-
-                                let file_metadata = VirtualObjectMetadata {
-                                    object_id,
-                                    name: file_name,
-                                    date_created: "".to_string(),
-                                    author: "".to_string(),
-                                    plaintext_length: file_size,
-                                    group_count: groups_needed,
-                                };
-
-                                let file_header = packet_crafter::file::craft_file_header_packet(
-                                    latest_usable_ratchet,
-                                    start_group_id,
-                                    ticket,
-                                    security_level,
-                                    virtual_target,
-                                    file_metadata,
-                                    timestamp,
-                                );
-
-                                // if 1 group, we don't need to reserve any more group IDs. If 2, then we reserve just one. 3, then 2
-                                let amt_to_reserve = groups_needed - 1;
-                                endpoint_container.endpoint_crypto.rolling_group_id +=
-                                    amt_to_reserve as u64;
+                    let file_metadata = VirtualObjectMetadata {
+                        object_id,
+                        name: file_name,
+                        // TODO: update metadata using the local file handle metadata
+                        date_created: "".to_string(),
+                        author: "N/A".to_string(),
+                        plaintext_length: file_size,
+                        group_count: groups_needed,
+                        cid: implicated_cid,
+                        transfer_type,
+                    };
+
+                    // if 1 group, we don't need to reserve any more group IDs. If 2, then we reserve just one. 3, then 2
+                    let amt_to_reserve = groups_needed - 1;
+                    crypt_container.rolling_group_id += amt_to_reserve as u64;
+                    let file_header = packet_crafter::file::craft_file_header_packet(
+                        &latest_hr,
+                        group_id_start,
+                        ticket,
+                        security_level,
+                        virtual_target,
+                        file_metadata,
+                        timestamp,
+                        local_encryption_level,
+                    );
+                    (
+                        to_primary_stream,
+                        file_header,
+                        object_id,
+                        target_cid,
+                        implicated_cid,
+                        groups_needed,
+                    )
+                }
 
-                                (
-                                    preferred_primary_stream,
-                                    file_header,
-                                    object_id,
-                                    target_cid,
-                                    target_cid,
-                                    groups_needed,
-                                )
-                            } else {
-                                log::error!(target: "citadel", "Endpoint container not found");
-                                return Err(NetworkError::InternalError(
-                                    "Endpoint container not found",
-                                ));
-                            }
-                        } else {
-                            log::error!(target: "citadel", "Unable to find active vconn for the channel");
-                            return Err(NetworkError::InternalError(
-                                "Virtual connection not found for channel",
-                            ));
-                        }
-                    }
+                VirtualConnectionType::LocalGroupPeer(implicated_cid, target_cid) => {
+                    log::trace!(target: "citadel", "Sending HyperLAN peer ({}) <-> HyperLAN Peer ({})", implicated_cid, target_cid);
+                    // here, we don't use the base session's PQC. Instead, we use the vconn's pqc and
+                    let endpoint_container =
+                        state_container.get_peer_endpoint_container_mut(target_cid)?;
+
+                    let object_id = endpoint_container
+                        .endpoint_crypto
+                        .get_and_increment_object_id();
+                    // reserve group ids
+                    let start_group_id = endpoint_container
+                        .endpoint_crypto
+                        .get_and_increment_group_id();
+
+                    let latest_usable_ratchet = endpoint_container
+                        .endpoint_crypto
+                        .get_hyper_ratchet(None)
+                        .unwrap();
+
+                    let static_aux_ratchet = endpoint_container
+                        .endpoint_crypto
+                        .toolset
+                        .get_static_auxiliary_ratchet()
+                        .clone();
+
+                    let preferred_primary_stream = endpoint_container
+                        .get_direct_p2p_primary_stream()
+                        .cloned()
+                        .unwrap_or_else(|| this.to_primary_stream.clone().unwrap());
+
+                    let (file_size, groups_needed, _max_bytes_per_group) = scramble_encrypt_source(
+                        source,
+                        max_group_size,
+                        object_id,
+                        group_sender,
+                        stop_rx,
+                        security_level,
+                        latest_usable_ratchet.clone(),
+                        static_aux_ratchet,
+                        HDP_HEADER_BYTE_LEN,
+                        target_cid,
+                        start_group_id,
+                        transfer_type.clone(),
+                        packet_crafter::group::craft_wave_payload_packet_into,
+                    )
+                    .map_err(|err| NetworkError::Generic(err.to_string()))?;
 
-                    _ => {
-                        log::error!(target: "citadel", "HyperWAN functionality not yet implemented");
-                        return Err(NetworkError::InternalError(
-                            "HyperWAN functionality not yet implemented",
-                        ));
-                    }
-                };
+                    let file_metadata = VirtualObjectMetadata {
+                        object_id,
+                        name: file_name,
+                        date_created: "".to_string(),
+                        author: "".to_string(),
+                        plaintext_length: file_size,
+                        group_count: groups_needed,
+                        cid: implicated_cid,
+                        transfer_type,
+                    };
+
+                    let file_header = packet_crafter::file::craft_file_header_packet(
+                        latest_usable_ratchet,
+                        start_group_id,
+                        ticket,
+                        security_level,
+                        virtual_target,
+                        file_metadata,
+                        timestamp,
+                        local_encryption_level,
+                    );
 
-            // now that the async cryptscrambler tasks have been spawned on the threadpool, we need to also
-            // spawn tasks that read the [GroupSenders] from there. We also need to store an [OutboundFileMetadataTransmitter]
-            // to store the stopper. After spawning them, the rest is under control. Note: for the async task that spawns here
-            // should be given a Rc<RefCell<StateContainer>>. Finally, since two vpeers may send to the source we are sending
-            // to, the GROUP HEADER ACK needs to return the group start idx. It is expected the adjacent node reserve enough groups
-            // on its end to take into account
+                    // if 1 group, we don't need to reserve any more group IDs. If 2, then we reserve just one. 3, then 2
+                    let amt_to_reserve = groups_needed - 1;
+                    endpoint_container.endpoint_crypto.rolling_group_id += amt_to_reserve as u64;
+
+                    (
+                        preferred_primary_stream,
+                        file_header,
+                        object_id,
+                        target_cid,
+                        target_cid,
+                        groups_needed,
+                    )
+                }
 
-            // send the FILE_HEADER
-            to_primary_stream
-                .unbounded_send(file_header)
-                .map_err(|_| NetworkError::InternalError("Primary stream disconnected"))?;
-            // create the outbound file container
-            let kernel_tx = state_container.kernel_tx.clone();
-            let (next_gs_alerter, next_gs_alerter_rx) = unbounded();
-            let mut next_gs_alerter_rx =
-                tokio_stream::wrappers::UnboundedReceiverStream::new(next_gs_alerter_rx);
-            let (start, start_rx) = tokio::sync::oneshot::channel();
-            let outbound_file_transfer_container = OutboundFileTransfer {
-                stop_tx: Some(stop_tx),
-                object_id,
-                ticket,
-                next_gs_alerter: next_gs_alerter.clone(),
-                start: Some(start),
+                _ => {
+                    log::error!(target: "citadel", "HyperWAN functionality not yet implemented");
+                    return Err(NetworkError::InternalError(
+                        "HyperWAN functionality not yet implemented",
+                    ));
+                }
             };
-            let file_key = FileKey::new(key_cid, object_id);
-            let _ = state_container
-                .outbound_files
-                .insert(file_key, outbound_file_transfer_container);
-            // spawn the task that takes GroupSenders from the threadpool cryptscrambler
-            std::mem::drop(state_container);
-
-            let this = self.clone();
-            let future = async move {
-                let this = &this;
-                let next_gs_alerter = &next_gs_alerter;
-                // this future will resolve when the sender drops in the file_crypt_scrambler
-                match start_rx.await {
-                    Ok(false) => {
-                        log::warn!(target: "citadel", "start_rx signalled to NOT begin streaming process. Ending async subroutine");
-                        return;
-                    }
-                    Err(err) => {
-                        log::error!(target: "citadel", "start_rx error occurred: {:?}", err);
-                        return;
-                    }
 
-                    _ => {
-                        log::trace!(target: "citadel", "Outbound file transfer async subroutine signalled to begin!");
-                    }
+        // now that the async cryptscrambler tasks have been spawned on the threadpool, we need to also
+        // spawn tasks that read the [GroupSenders] from there. We also need to store an [OutboundFileMetadataTransmitter]
+        // to store the stopper. After spawning them, the rest is under control. Note: for the async task that spawns here
+        // should be given a Rc<RefCell<StateContainer>>. Finally, since two vpeers may send to the source we are sending
+        // to, the GROUP HEADER ACK needs to return the group start idx. It is expected the adjacent node reserve enough groups
+        // on its end to take into account
+
+        // send the FILE_HEADER
+        to_primary_stream
+            .unbounded_send(file_header)
+            .map_err(|_| NetworkError::InternalError("Primary stream disconnected"))?;
+        // create the outbound file container
+        let kernel_tx = state_container.kernel_tx.clone();
+        let (next_gs_alerter, next_gs_alerter_rx) = unbounded();
+        let mut next_gs_alerter_rx =
+            tokio_stream::wrappers::UnboundedReceiverStream::new(next_gs_alerter_rx);
+        let (start, start_rx) = tokio::sync::oneshot::channel();
+        let outbound_file_transfer_container = OutboundFileTransfer {
+            stop_tx: Some(stop_tx),
+            object_id,
+            ticket,
+            next_gs_alerter: next_gs_alerter.clone(),
+            start: Some(start),
+        };
+        let file_key = FileKey::new(key_cid, object_id);
+        let _ = state_container
+            .outbound_files
+            .insert(file_key, outbound_file_transfer_container);
+        // spawn the task that takes GroupSenders from the threadpool cryptscrambler
+        std::mem::drop(state_container);
+
+        let this = self.clone();
+        let future = async move {
+            let this = &this;
+            let next_gs_alerter = &next_gs_alerter;
+            // this future will resolve when the sender drops in the file_crypt_scrambler
+            match start_rx.await {
+                Ok(false) => {
+                    log::warn!(target: "citadel", "start_rx signalled to NOT begin streaming process. Ending async subroutine");
+                    return;
+                }
+                Err(err) => {
+                    log::error!(target: "citadel", "start_rx error occurred: {:?}", err);
+                    return;
                 }
 
-                // TODO: planning/overhaul of file transmission process
-                // By now, the file container has been created remotely and locally
-                // We have been signalled to begin polling the group sender
-                // NOTE: polling the group_sender_rx (eventually) stops polling the
-                // async crypt scrambler. Up to 5 groups can be enqueued before stopping
-                // Once 5 groups have enqueued, the only way to continue is if the receiving
-                // end tells us it finished that group, and, we poll the next() group sender below.
-                //
-
-                let mut relative_group_id = 0;
-                // while waiting, we likely have a set of GroupSenders to process
-                while let Some(sender) = group_sender_rx.next().await {
-                    match sender {
-                        Ok(sender) => {
-                            let (group_id, key) = {
-                                // construct the OutboundTransmitters
-                                let sess = this;
-                                if sess.state.load(Ordering::Relaxed) != SessionState::Connected {
-                                    log::warn!(target: "citadel", "Since transmitting the file, the session ended");
-                                    return;
-                                }
+                _ => {}
+            }
 
-                                let mut state_container = inner_mut_state!(sess.state_container);
-
-                                let proper_latest_hyper_ratchet = match virtual_target {
-                                    VirtualConnectionType::LocalGroupServer(_) => state_container
-                                        .c2s_channel_container
-                                        .as_ref()
-                                        .unwrap()
-                                        .peer_session_crypto
-                                        .get_hyper_ratchet(None),
-                                    VirtualConnectionType::LocalGroupPeer(_, peer_cid) => {
-                                        match state_container.get_peer_session_crypto(peer_cid) {
-                                            Some(peer_sess_crypt) => {
-                                                peer_sess_crypt.get_hyper_ratchet(None)
-                                            }
+            log::trace!(target: "citadel", "Outbound file transfer async subroutine signalled to begin!");
+
+            // TODO: planning/overhaul of file transmission process
+            // By now, the file container has been created remotely and locally
+            // We have been signalled to begin polling the group sender
+            // NOTE: polling the group_sender_rx (eventually) stops polling the
+            // async crypt scrambler. Up to 5 groups can be enqueued before stopping
+            // Once 5 groups have enqueued, the only way to continue is if the receiving
+            // end tells us it finished that group, and, we poll the next() group sender below.
+            //
+
+            let mut relative_group_id = 0;
+            // while waiting, we likely have a set of GroupSenders to process
+            while let Some(sender) = group_sender_rx.next().await {
+                match sender {
+                    Ok(sender) => {
+                        let (group_id, key) = {
+                            // construct the OutboundTransmitters
+                            let sess = this;
+                            if sess.state.load(Ordering::Relaxed) != SessionState::Connected {
+                                log::warn!(target: "citadel", "Since transmitting the file, the session ended");
+                                return;
+                            }
 
-                                            None => {
-                                                log::warn!(target: "citadel", "Since transmitting the file, the peer session ended");
-                                                return;
-                                            }
+                            let mut state_container = inner_mut_state!(sess.state_container);
+
+                            let proper_latest_hyper_ratchet = match virtual_target {
+                                VirtualConnectionType::LocalGroupServer(_) => state_container
+                                    .c2s_channel_container
+                                    .as_ref()
+                                    .unwrap()
+                                    .peer_session_crypto
+                                    .get_hyper_ratchet(None),
+                                VirtualConnectionType::LocalGroupPeer(_, peer_cid) => {
+                                    match state_container.get_peer_session_crypto(peer_cid) {
+                                        Some(peer_sess_crypt) => {
+                                            peer_sess_crypt.get_hyper_ratchet(None)
                                         }
-                                    }
 
-                                    _ => {
-                                        log::error!(target: "citadel", "HyperWAN Functionality not implemented");
-                                        return;
+                                        None => {
+                                            log::warn!(target: "citadel", "Since transmitting the file, the peer session ended");
+                                            return;
+                                        }
                                     }
-                                };
-
-                                if proper_latest_hyper_ratchet.is_none() {
-                                    log::error!(target: "citadel", "Unable to unwrap StackedRatchet (X-05)");
-                                    return;
                                 }
 
-                                let hyper_ratchet = proper_latest_hyper_ratchet.unwrap();
-
-                                let mut transmitter = GroupTransmitter::new_from_group_sender(
-                                    to_primary_stream.clone(),
-                                    sender,
-                                    RatchetPacketCrafterContainer::new(hyper_ratchet.clone(), None),
-                                    object_id,
-                                    ticket,
-                                    security_level,
-                                    time_tracker,
-                                );
-                                // group_id is unique per session
-                                let group_id = transmitter.group_id;
-
-                                // We manually send the header. The tails get sent automatically
-                                log::trace!(target: "citadel", "Sending GROUP HEADER through primary stream for group {}", group_id);
-                                if let Err(err) = sess.try_action(Some(ticket), || {
-                                    transmitter.transmit_group_header(virtual_target)
-                                }) {
-                                    log::error!(target: "citadel", "Unable to send through primary stream: {}", err.to_string());
+                                _ => {
+                                    log::error!(target: "citadel", "HyperWAN Functionality not implemented");
                                     return;
                                 }
-                                let group_byte_len = transmitter.get_total_plaintext_bytes();
-
-                                let outbound_container = OutboundTransmitterContainer::new(
-                                    Some(next_gs_alerter.clone()),
-                                    transmitter,
-                                    group_byte_len,
-                                    groups_needed,
-                                    relative_group_id,
-                                    ticket,
-                                );
-                                relative_group_id += 1;
-                                // The payload packets won't be sent until a GROUP_HEADER_ACK is received
-                                // the key is the target_cid coupled with the group id
-                                let key = GroupKey::new(key_cid, group_id);
-
-                                assert!(state_container
-                                    .outbound_transmitters
-                                    .insert(key, outbound_container)
-                                    .is_none());
-                                // We can't just add the outbound container. We need to wait til we get the signal to. When the > 50% WAVE_ACKs
-                                // are received, the OutboundFileContainer (which should have a group_notifier) should send a signal which we await for
-                                // here. Also: DROP `sess`!
-                                std::mem::drop(state_container);
-                                //sess.transfer_stats += TransferStats::new(timestamp, group_byte_len as isize);
-                                (group_id, key)
                             };
 
-                            let kernel_tx2 = kernel_tx.clone();
-
-                            this.queue_handle.insert_ordinary(group_id as usize, target_cid, GROUP_EXPIRE_TIME_MS, move |state_container| {
-                                if let Some(transmitter) = state_container.outbound_transmitters.get(&key) {
-                                    // as long as a wave ACK has been received, proceed with the timeout check
-                                    // The reason why is because this group may be loaded, but the previous one isn't done
-                                    if transmitter.has_begun {
-                                        let transmitter = &transmitter.burst_transmitter.group_transmitter;
-                                        if transmitter.has_expired(GROUP_EXPIRE_TIME_MS) {
-                                            if state_container.meta_expiry_state.expired() {
-                                                log::error!(target: "citadel", "Outbound group {} has expired; dropping entire transfer", group_id);
-                                                //std::mem::drop(transmitter);
-                                                if let Some(mut outbound_container) = state_container.outbound_files.remove(&file_key) {
-                                                    if let Some(stop) = outbound_container.stop_tx.take() {
-                                                        if stop.send(()).is_err() {
-                                                            log::error!(target: "citadel", "Unable to send stop signal");
-                                                        }
+                            if proper_latest_hyper_ratchet.is_none() {
+                                log::error!(target: "citadel", "Unable to unwrap StackedRatchet (X-05)");
+                                return;
+                            }
+
+                            let hyper_ratchet = proper_latest_hyper_ratchet.unwrap();
+
+                            let mut transmitter = GroupTransmitter::new_from_group_sender(
+                                to_primary_stream.clone(),
+                                sender,
+                                RatchetPacketCrafterContainer::new(hyper_ratchet.clone(), None),
+                                object_id,
+                                ticket,
+                                security_level,
+                                time_tracker,
+                            );
+                            // group_id is unique per session
+                            let group_id = transmitter.group_id;
+
+                            // We manually send the header. The tails get sent automatically
+                            log::trace!(target: "citadel", "Sending GROUP HEADER through primary stream for group {}", group_id);
+                            if let Err(err) = sess.try_action(Some(ticket), || {
+                                transmitter.transmit_group_header(virtual_target)
+                            }) {
+                                log::error!(target: "citadel", "Unable to send through primary stream: {}", err.to_string());
+                                return;
+                            }
+                            let group_byte_len = transmitter.get_total_plaintext_bytes();
+
+                            let outbound_container = OutboundTransmitterContainer::new(
+                                Some(next_gs_alerter.clone()),
+                                transmitter,
+                                group_byte_len,
+                                groups_needed,
+                                relative_group_id,
+                                ticket,
+                            );
+                            relative_group_id += 1;
+                            // The payload packets won't be sent until a GROUP_HEADER_ACK is received
+                            // the key is the target_cid coupled with the group id
+                            let key = GroupKey::new(key_cid, group_id);
+
+                            assert!(state_container
+                                .outbound_transmitters
+                                .insert(key, outbound_container)
+                                .is_none());
+                            // We can't just add the outbound container. We need to wait til we get the signal to. When the > 50% WAVE_ACKs
+                            // are received, the OutboundFileContainer (which should have a group_notifier) should send a signal which we await for
+                            // here. Also: DROP `sess`!
+                            std::mem::drop(state_container);
+                            //sess.transfer_stats += TransferStats::new(timestamp, group_byte_len as isize);
+                            (group_id, key)
+                        };
+
+                        let kernel_tx2 = kernel_tx.clone();
+
+                        this.queue_handle.insert_ordinary(group_id as usize, target_cid, GROUP_EXPIRE_TIME_MS, move |state_container| {
+                            if let Some(transmitter) = state_container.outbound_transmitters.get(&key) {
+                                // as long as a wave ACK has been received, proceed with the timeout check
+                                // The reason why is because this group may be loaded, but the previous one isn't done
+                                if transmitter.has_begun {
+                                    let transmitter = &transmitter.burst_transmitter.group_transmitter;
+                                    if transmitter.has_expired(GROUP_EXPIRE_TIME_MS) {
+                                        if state_container.meta_expiry_state.expired() {
+                                            log::error!(target: "citadel", "Outbound group {} has expired; dropping entire transfer", group_id);
+                                            //std::mem::drop(transmitter);
+                                            if let Some(mut outbound_container) = state_container.outbound_files.remove(&file_key) {
+                                                if let Some(stop) = outbound_container.stop_tx.take() {
+                                                    if stop.send(()).is_err() {
+                                                        log::error!(target: "citadel", "Unable to send stop signal");
                                                     }
-                                                } else {
-                                                    log::warn!(target: "citadel", "Attempted to remove {:?}, but was already absent from map", &file_key);
                                                 }
+                                            } else {
+                                                log::warn!(target: "citadel", "Attempted to remove {:?}, but was already absent from map", &file_key);
+                                            }
 
-                                                if kernel_tx2.unbounded_send(NodeResult::InternalServerError(InternalServerError {
-                                                    ticket_opt: Some(ticket),
-                                                    message: format!("Timeout on ticket {ticket}")
-                                                })).is_err() {
-                                                    log::error!(target: "citadel", "[File] Unable to send kernel error signal. Ending session");
-                                                    QueueWorkerResult::EndSession
-                                                } else {
-                                                    QueueWorkerResult::Complete
-                                                }
+                                            if kernel_tx2.unbounded_send(NodeResult::InternalServerError(InternalServerError {
+                                                ticket_opt: Some(ticket),
+                                                message: format!("Timeout on ticket {ticket}")
+                                            })).is_err() {
+                                                log::error!(target: "citadel", "[File] Unable to send kernel error signal. Ending session");
+                                                QueueWorkerResult::EndSession
                                             } else {
-                                                log::trace!(target: "citadel", "[X-04] Other outbound groups being processed; patiently awaiting group {}", group_id);
-                                                QueueWorkerResult::Incomplete
+                                                QueueWorkerResult::Complete
                                             }
                                         } else {
-                                            // it hasn't expired yet, and is still transmitting
+                                            log::trace!(target: "citadel", "[X-04] Other outbound groups being processed; patiently awaiting group {}", group_id);
                                             QueueWorkerResult::Incomplete
                                         }
                                     } else {
-                                        // WAVE_ACK hasn't been received yet; try again later
+                                        // it hasn't expired yet, and is still transmitting
                                         QueueWorkerResult::Incomplete
                                     }
                                 } else {
-                                    // it finished
-                                    QueueWorkerResult::Complete
+                                    // WAVE_ACK hasn't been received yet; try again later
+                                    QueueWorkerResult::Incomplete
                                 }
-                            });
+                            } else {
+                                // it finished
+                                QueueWorkerResult::Complete
+                            }
+                        });
 
-                            // When a wave ACK in the previous group comes, if the group is 50% or more done, the group_sender_rx will
-                            // received a signal here
+                        // When a wave ACK in the previous group comes, if the group is 50% or more done, the group_sender_rx will
+                        // received a signal here
 
-                            if (next_gs_alerter_rx.next().await).is_none() {
-                                log::warn!(target: "citadel", "next_gs_alerter: steam ended");
-                                return;
-                            }
+                        if (next_gs_alerter_rx.next().await).is_none() {
+                            log::warn!(target: "citadel", "next_gs_alerter: steam ended");
+                            return;
                         }
+                    }
 
-                        Err(err) => {
-                            let _ =
-                                kernel_tx
-                                    .clone()
-                                    .unbounded_send(NodeResult::InternalServerError(
-                                        InternalServerError {
-                                            ticket_opt: Some(ticket),
-                                            message: err.to_string(),
-                                        },
-                                    ));
-                        }
+                    Err(err) => {
+                        let _ = kernel_tx
+                            .clone()
+                            .unbounded_send(NodeResult::InternalServerError(InternalServerError {
+                                ticket_opt: Some(ticket),
+                                message: err.to_string(),
+                            }));
                     }
                 }
-            };
+            }
 
-            spawn!(future);
+            // we finished pulling. Now, execute the hook if present
+            if let Some(path) = source_path_location_opt {
+                post_close_hook(path);
+            }
+        };
 
-            Ok(())
-        }
+        spawn!(future);
+
+        Ok(())
     }
 
     // TODO: Make a generic version to allow requests the ability to bypass the session manager
diff --git a/citadel_proto/src/proto/session_manager.rs b/citadel_proto/src/proto/session_manager.rs
index b5021b263..067c2be8a 100644
--- a/citadel_proto/src/proto/session_manager.rs
+++ b/citadel_proto/src/proto/session_manager.rs
@@ -1,5 +1,6 @@
 use std::collections::HashMap;
 use std::net::SocketAddr;
+use std::path::PathBuf;
 use std::pin::Pin;
 use std::sync::atomic::Ordering;
 
@@ -42,6 +43,7 @@ use crate::proto::session::{
     ClientOnlySessionInitSettings, HdpSession, HdpSessionInitMode, SessionInitParams,
 };
 use crate::proto::state_container::{VirtualConnectionType, VirtualTargetType};
+use citadel_crypt::misc::TransferType;
 use citadel_crypt::streaming_crypt_scrambler::ObjectSource;
 use citadel_wire::exports::tokio_rustls::rustls;
 use citadel_wire::exports::tokio_rustls::rustls::ClientConfig;
@@ -522,6 +524,7 @@ impl HdpSessionManager {
     }
 
     /// When the [HdpServer] receives an outbound request, the request flows here. It returns where the packet must be sent to
+    #[allow(clippy::too_many_arguments)]
     pub fn process_outbound_file(
         &self,
         ticket: Ticket,
@@ -530,6 +533,7 @@ impl HdpSessionManager {
         implicated_cid: u64,
         virtual_target: VirtualTargetType,
         security_level: SecurityLevel,
+        transfer_type: TransferType,
     ) -> Result<(), NetworkError> {
         let this = inner!(self);
         if let Some(existing_session) = this.sessions.get(&implicated_cid) {
@@ -539,6 +543,9 @@ impl HdpSessionManager {
                 source,
                 virtual_target,
                 security_level,
+                transfer_type,
+                None,
+                |_| {},
             )
         } else {
             Err(NetworkError::Generic(format!(
@@ -547,6 +554,43 @@ impl HdpSessionManager {
         }
     }
 
+    pub fn revfs_pull(
+        &self,
+        ticket: Ticket,
+        implicated_cid: u64,
+        v_conn: VirtualConnectionType,
+        virtual_path: PathBuf,
+        delete_on_pull: bool,
+        security_level: SecurityLevel,
+    ) -> Result<(), NetworkError> {
+        let lock = inner!(self);
+        if let Some((_, sess)) = lock.sessions.get(&implicated_cid) {
+            sess.revfs_pull(ticket, v_conn, virtual_path, delete_on_pull, security_level)
+        } else {
+            Err(NetworkError::Generic(format!(
+                "Hypernode session for {implicated_cid} does not exist! Not going to process request ..."
+            )))
+        }
+    }
+
+    pub fn revfs_delete(
+        &self,
+        ticket: Ticket,
+        implicated_cid: u64,
+        v_conn: VirtualConnectionType,
+        virtual_path: PathBuf,
+        security_level: SecurityLevel,
+    ) -> Result<(), NetworkError> {
+        let lock = inner!(self);
+        if let Some((_, sess)) = lock.sessions.get(&implicated_cid) {
+            sess.revfs_delete(ticket, v_conn, virtual_path, security_level)
+        } else {
+            Err(NetworkError::Generic(format!(
+                "Hypernode session for {implicated_cid} does not exist! Not going to process request ..."
+            )))
+        }
+    }
+
     /// Returns true if the process continued successfully
     pub fn initiate_update_drill_subroutine(
         &self,
diff --git a/citadel_proto/src/proto/state_container.rs b/citadel_proto/src/proto/state_container.rs
index aa5ee8b1d..80d4c0198 100644
--- a/citadel_proto/src/proto/state_container.rs
+++ b/citadel_proto/src/proto/state_container.rs
@@ -24,7 +24,7 @@ use crate::constants::{
 };
 use crate::error::NetworkError;
 use crate::functional::IfEqConditional;
-use crate::prelude::{MessageGroupKey, ReKeyResult, ReKeyReturnType};
+use crate::prelude::{InternalServerError, MessageGroupKey, ReKeyResult, ReKeyReturnType};
 use crate::proto::misc::dual_late_init::DualLateInit;
 use crate::proto::misc::ordered_channel::OrderedChannel;
 use crate::proto::misc::session_security_settings::SessionSecuritySettings;
@@ -33,7 +33,6 @@ use crate::proto::node_result::{NodeResult, ObjectTransferHandle};
 use crate::proto::outbound_sender::{OutboundPrimaryStreamSender, OutboundUdpSender};
 use crate::proto::packet::packet_flags;
 use crate::proto::packet::HdpHeader;
-use crate::proto::packet_crafter;
 use crate::proto::packet_crafter::peer_cmd::C2S_ENCRYPTION_ONLY;
 use crate::proto::packet_crafter::{
     GroupTransmitter, RatchetPacketCrafterContainer, SecureProtocolPacket,
@@ -56,6 +55,7 @@ use crate::proto::state_subcontainers::preconnect_state_container::PreConnectSta
 use crate::proto::state_subcontainers::register_state_container::RegisterState;
 use crate::proto::state_subcontainers::rekey_container::RatchetUpdateState;
 use crate::proto::transfer_stats::TransferStats;
+use crate::proto::{packet_crafter, send_with_error_logging};
 use atomic::Atomic;
 use bytes::Bytes;
 use citadel_crypt::endpoint_crypto_container::{KemTransferStatus, PeerSessionCrypto};
@@ -153,7 +153,8 @@ pub(crate) struct InboundFileTransfer {
     pub virtual_target: VirtualTargetType,
     pub metadata: VirtualObjectMetadata,
     pub stream_to_hd: UnboundedSender<Vec<u8>>,
-    pub reception_complete_tx: tokio::sync::oneshot::Sender<()>,
+    pub reception_complete_tx: tokio::sync::oneshot::Sender<HdpHeader>,
+    pub local_encryption_level: Option<SecurityLevel>,
 }
 
 #[allow(dead_code)]
@@ -863,6 +864,25 @@ impl StateContainerInner {
         )
     }
 
+    pub fn get_peer_endpoint_container_mut(
+        &mut self,
+        target_cid: u64,
+    ) -> Result<&mut EndpointChannelContainer, NetworkError> {
+        if let Some(vconn) = self.active_virtual_connections.get_mut(&target_cid) {
+            if let Some(endpoint_container) = vconn.endpoint_container.as_mut() {
+                Ok(endpoint_container)
+            } else {
+                Err(NetworkError::msg(format!(
+                    "Unabel to access endpoint container to peer {target_cid}"
+                )))
+            }
+        } else {
+            Err(NetworkError::msg(format!(
+                "Unabel to find virtual connection to peer {target_cid}"
+            )))
+        }
+    }
+
     pub fn get_c2s_crypto(&self) -> Option<&PeerSessionCrypto> {
         Some(&self.c2s_channel_container.as_ref()?.peer_session_crypto)
     }
@@ -997,11 +1017,12 @@ impl StateContainerInner {
         target_cid: u64,
         v_target_flipped: VirtualTargetType,
         preferred_primary_stream: OutboundPrimaryStreamSender,
+        local_encryption_level: Option<SecurityLevel>,
     ) -> bool {
         let key = FileKey::new(header.session_cid.get(), metadata_orig.object_id);
         let ticket = header.context_info.get().into();
+        let is_revfs_pull = local_encryption_level.is_some();
 
-        // TODO: Add file transfer accept request here. Once local accepts, then begin this subroutine
         if let std::collections::hash_map::Entry::Vacant(e) = self.inbound_files.entry(key) {
             let (stream_to_hd, stream_to_hd_rx) = unbounded::<Vec<u8>>();
             let (start_recv_tx, start_recv_rx) = tokio::sync::oneshot::channel::<bool>();
@@ -1011,8 +1032,8 @@ impl StateContainerInner {
             let object_id = metadata_orig.object_id;
             let pers = pers.clone();
             let metadata = metadata_orig.clone();
-            let (reception_complete_tx, success_receiving_rx) =
-                tokio::sync::oneshot::channel::<()>();
+            let tt = self.time_tracker;
+            let (reception_complete_tx, success_receiving_rx) = tokio::sync::oneshot::channel();
             let entry = InboundFileTransfer {
                 last_group_finish_time: Instant::now(),
                 last_group_window_len: 0,
@@ -1024,6 +1045,7 @@ impl StateContainerInner {
                 metadata: metadata.clone(),
                 reception_complete_tx,
                 stream_to_hd,
+                local_encryption_level,
             };
 
             e.insert(entry);
@@ -1032,6 +1054,7 @@ impl StateContainerInner {
                 header.target_cid.get(),
                 ObjectTransferOrientation::Receiver,
                 Some(start_recv_tx),
+                is_revfs_pull,
             );
             self.file_transfer_handles.insert(
                 key,
@@ -1046,7 +1069,15 @@ impl StateContainerInner {
                 }));
 
             let task = async move {
-                let res = start_recv_rx.await;
+                let res = if is_revfs_pull {
+                    // auto-accept for revfs pull requests
+                    log::trace!(target: "citadel", "Auto-accepting for REVFS pull request");
+                    Ok(true)
+                } else {
+                    log::trace!(target: "citadel", "Will not auto-accept");
+                    start_recv_rx.await
+                };
+
                 let accepted = res.as_ref().map(|r| *r).unwrap_or(false);
                 // first, send a rebound signal immediately to the sender
                 // to ensure the sender knows if the user accepted or not
@@ -1074,15 +1105,34 @@ impl StateContainerInner {
                             match pers
                                 .stream_object_to_backend(
                                     stream_to_hd_rx,
-                                    Arc::new(metadata),
+                                    Arc::new(metadata.clone()),
                                     tx_status.clone(),
                                 )
                                 .await
                             {
                                 Ok(()) => {
-                                    log::trace!(target: "citadel", "Successfully synced file to backend");
+                                    log::info!(target: "citadel", "Successfully synced file to backend | {is_revfs_pull}");
                                     let status = match success_receiving_rx.await {
-                                        Ok(_) => ObjectTransferStatus::ReceptionComplete,
+                                        Ok(header) => {
+                                            // write the header
+                                            let wave_ack = packet_crafter::group::craft_wave_ack(
+                                                &hyper_ratchet,
+                                                header.context_info.get() as u32,
+                                                get_resp_target_cid_from_header(&header),
+                                                header.group.get(),
+                                                header.wave_id.get(),
+                                                tt.get_global_time_ns(),
+                                                None,
+                                                header.security_level.into(),
+                                            );
+
+                                            send_with_error_logging(
+                                                &preferred_primary_stream,
+                                                wave_ack,
+                                            );
+
+                                            ObjectTransferStatus::ReceptionComplete
+                                        }
 
                                         Err(_) => ObjectTransferStatus::Fail(
                                             "An unknown error occurred while receiving file"
@@ -1106,6 +1156,17 @@ impl StateContainerInner {
 
                     Err(err) => {
                         log::error!(target: "citadel", "Start_recv_rx failed: {:?}", err);
+                        let err_packet = packet_crafter::file::craft_file_header_ack_packet(
+                            &hyper_ratchet,
+                            false,
+                            object_id,
+                            target_cid,
+                            ticket,
+                            security_level_rebound,
+                            virtual_target,
+                            timestamp,
+                        );
+                        let _ = preferred_primary_stream.unbounded_send(err_packet);
                     }
                 }
             };
@@ -1152,6 +1213,7 @@ impl StateContainerInner {
                     receiver_cid,
                     ObjectTransferOrientation::Sender,
                     None,
+                    true, //this value does not matter here since start_recv_tx is false. TODO: refactor
                 );
                 tx.send(ObjectTransferStatus::TransferBeginning).ok()?;
                 let _ = self
@@ -1174,6 +1236,13 @@ impl StateContainerInner {
                 file_transfer.stop_tx?.send(()).ok()?;
                 // stop the async task pulling from the async cryptscrambler
                 file_transfer.start?.send(false).ok()?;
+                let _ = self
+                    .kernel_tx
+                    .unbounded_send(NodeResult::InternalServerError(InternalServerError {
+                        message: "The adjacent node did not accept the file transfer request"
+                            .into(),
+                        ticket_opt: Some(ticket),
+                    }));
             } else {
                 log::error!(target: "citadel", "Attempted to remove OutboundFileTransfer for {:?}, but it didn't exist", key);
             }
@@ -1299,12 +1368,25 @@ impl StateContainerInner {
         ) {
             GroupReceiverStatus::GROUP_COMPLETE(_last_wid) => {
                 log::trace!(target: "citadel", "GROUP {} COMPLETE. Total groups: {}", group_id, file_container.total_groups);
-                let chunk = self
+                let mut chunk = self
                     .inbound_groups
                     .remove(&group_key)
                     .unwrap()
                     .receiver
                     .finalize();
+
+                if let Some(local_encryption_level) = file_container.local_encryption_level {
+                    // which static hr do we need? Since we are receiving this chunk, always our local account's
+                    let static_aux_hr = self
+                        .cnac
+                        .as_ref()
+                        .unwrap()
+                        .get_static_auxiliary_hyper_ratchet();
+                    chunk = static_aux_hr
+                        .local_decrypt(chunk, local_encryption_level)
+                        .map_err(|err| NetworkError::msg(err.into_string()))?;
+                }
+
                 file_container
                     .stream_to_hd
                     .unbounded_send(chunk)
@@ -1317,9 +1399,10 @@ impl StateContainerInner {
                     let file_container = self.inbound_files.remove(&file_key).unwrap();
                     // status of reception complete now located where the streaming to HD completes
                     // we need only take the sender and send a signal to prove that we finished correctly here
+                    // TODO: it seems to be sending the file before the backend streamer even gets a chance to finish
                     file_container
                         .reception_complete_tx
-                        .send(())
+                        .send(header.clone())
                         .map_err(|_| NetworkError::msg("reception_complete_tx err"))?;
                 } else {
                     file_container.last_group_finish_time = Instant::now();
@@ -1356,17 +1439,20 @@ impl StateContainerInner {
         }
 
         if send_wave_ack {
-            let wave_ack = packet_crafter::group::craft_wave_ack(
-                hr,
-                header.context_info.get() as u32,
-                get_resp_target_cid_from_header(header),
-                header.group.get(),
-                header.wave_id.get(),
-                ts,
-                None,
-                header.security_level.into(),
-            );
-            return Ok(PrimaryProcessorResult::ReplyToSender(wave_ack));
+            // only send a wave ack if incomplete, since the backend sync will send it
+            if !complete {
+                let wave_ack = packet_crafter::group::craft_wave_ack(
+                    hr,
+                    header.context_info.get() as u32,
+                    get_resp_target_cid_from_header(header),
+                    header.group.get(),
+                    header.wave_id.get(),
+                    ts,
+                    None,
+                    header.security_level.into(),
+                );
+                return Ok(PrimaryProcessorResult::ReplyToSender(wave_ack));
+            }
         }
 
         Ok(PrimaryProcessorResult::Void)
@@ -1429,7 +1515,9 @@ impl StateContainerInner {
                     };
 
                     if let Err(err) = tx.unbounded_send(status.clone()) {
-                        log::error!(target: "citadel", "FileTransfer receiver handle cannot be reached {:?}", err);
+                        // if the server is using an accept-only policy with no further responses, this branch
+                        // will be reached
+                        log::warn!(target: "citadel", "FileTransfer receiver handle cannot be reached {:?}", err);
                         // drop local async sending subroutines
                         let _ = self.file_transfer_handles.remove(&file_key);
                     }
diff --git a/citadel_proto/src/proto/validation.rs b/citadel_proto/src/proto/validation.rs
index 353e5e100..43b1ed072 100644
--- a/citadel_proto/src/proto/validation.rs
+++ b/citadel_proto/src/proto/validation.rs
@@ -351,39 +351,54 @@ pub(crate) mod pre_connect {
 
 pub(crate) mod file {
     use crate::proto::packet::HdpHeader;
+    use crate::proto::packet_crafter::file::{
+        FileHeaderAckPacket, FileHeaderPacket, ReVFSAckPacket, ReVFSDeletePacket,
+        ReVFSPullAckPacket, ReVFSPullPacket,
+    };
     use crate::proto::packet_processor::includes::LayoutVerified;
-    use crate::proto::state_container::VirtualTargetType;
-    use citadel_user::backend::utils::VirtualObjectMetadata;
+    use citadel_user::serialization::SyncIO;
 
     pub fn validate_file_header(
-        header: &LayoutVerified<&[u8], HdpHeader>,
+        _header: &LayoutVerified<&[u8], HdpHeader>,
         payload: &[u8],
-    ) -> Option<(VirtualTargetType, VirtualObjectMetadata)> {
-        let split_idx = header.wave_id.get() as usize;
-        if payload.len() < split_idx {
-            None
-        } else {
-            let (vtarget_bytes, vfm_bytes) = payload.split_at(split_idx);
-            let vtarget = VirtualTargetType::deserialize_from(vtarget_bytes)?;
-            let vfm = VirtualObjectMetadata::deserialize_from(vfm_bytes)?;
-            Some((vtarget, vfm))
-        }
+    ) -> Option<FileHeaderPacket> {
+        FileHeaderPacket::deserialize_from_vector(payload).ok()
     }
 
     /// return Some(success, object_id) if valid, or None if invalid
     pub fn validate_file_header_ack(
-        header: &LayoutVerified<&[u8], HdpHeader>,
+        _header: &LayoutVerified<&[u8], HdpHeader>,
         payload: &[u8],
-    ) -> Option<(bool, u32, VirtualTargetType)> {
-        // 16 bytes for the signature
-        if !payload.is_empty() {
-            let object_id = header.wave_id.get();
-            let success = header.group.get() != 0;
-            let v_target = VirtualTargetType::deserialize_from(payload)?;
-            Some((success, object_id, v_target))
-        } else {
-            None
-        }
+    ) -> Option<FileHeaderAckPacket> {
+        FileHeaderAckPacket::deserialize_from_vector(payload).ok()
+    }
+
+    pub fn validate_revfs_delete(
+        _header: &LayoutVerified<&[u8], HdpHeader>,
+        payload: &[u8],
+    ) -> Option<ReVFSDeletePacket> {
+        ReVFSDeletePacket::deserialize_from_vector(payload).ok()
+    }
+
+    pub fn validate_revfs_pull(
+        _header: &LayoutVerified<&[u8], HdpHeader>,
+        payload: &[u8],
+    ) -> Option<ReVFSPullPacket> {
+        ReVFSPullPacket::deserialize_from_vector(payload).ok()
+    }
+
+    pub fn validate_revfs_ack(
+        _header: &LayoutVerified<&[u8], HdpHeader>,
+        payload: &[u8],
+    ) -> Option<ReVFSAckPacket> {
+        ReVFSAckPacket::deserialize_from_vector(payload).ok()
+    }
+
+    pub fn validate_revfs_pull_ack(
+        _header: &LayoutVerified<&[u8], HdpHeader>,
+        payload: &[u8],
+    ) -> Option<ReVFSPullAckPacket> {
+        ReVFSPullAckPacket::deserialize_from_vector(payload).ok()
     }
 }
 
diff --git a/citadel_sdk/Cargo.toml b/citadel_sdk/Cargo.toml
index 4e38df225..3baec1a49 100644
--- a/citadel_sdk/Cargo.toml
+++ b/citadel_sdk/Cargo.toml
@@ -44,6 +44,7 @@ dirs2 = { version = "3.0.1", default-features = false, optional = true }
 tracing = { version = "0.1.37", default-features = false, optional = true }
 citadel_logging = { path = "../citadel_logging", version = "0.3.0", optional = true }
 anyhow = { version = "1", default-features = false }
+bytes = "1.4.0"
 
 [dev-dependencies]
 tokio = { version = "1.24", default-features = false, features = ["rt"] }
diff --git a/citadel_sdk/src/builder/node_builder.rs b/citadel_sdk/src/builder/node_builder.rs
index c32db9216..8767709df 100644
--- a/citadel_sdk/src/builder/node_builder.rs
+++ b/citadel_sdk/src/builder/node_builder.rs
@@ -313,7 +313,7 @@ mod tests {
             builder.kernel_executor_settings.clone().unwrap()
         );
 
-        #[allow(clippy::let_underscore_future)]
+        #[allow(clippy::let_underscore_must_use)]
         let _ = builder.build(EmptyKernel::default()).unwrap();
     }
 }
diff --git a/citadel_sdk/src/fs.rs b/citadel_sdk/src/fs.rs
new file mode 100644
index 000000000..fdc563604
--- /dev/null
+++ b/citadel_sdk/src/fs.rs
@@ -0,0 +1,321 @@
+use crate::prelude::{ObjectSource, ProtocolRemoteTargetExt, SecurityLevel, TargetLockedRemote};
+
+use citadel_proto::prelude::NetworkError;
+use std::path::PathBuf;
+
+/// Writes a file or BytesSource to the Remote Encrypted Virtual Filesystem
+pub async fn write<T: ObjectSource, R: Into<PathBuf> + Send>(
+    remote: &mut impl TargetLockedRemote,
+    source: T,
+    virtual_path: R,
+) -> Result<(), NetworkError> {
+    write_with_security_level(remote, source, Default::default(), virtual_path).await
+}
+
+/// Writes a file or BytesSource to the Remote Encrypted Virtual Filesystem with a custom security level.
+pub async fn write_with_security_level<T: ObjectSource, R: Into<PathBuf> + Send>(
+    remote: &mut impl TargetLockedRemote,
+    source: T,
+    security_level: SecurityLevel,
+    virtual_path: R,
+) -> Result<(), NetworkError> {
+    remote
+        .remote_encrypted_virtual_filesystem_push(source, virtual_path, security_level)
+        .await
+}
+
+/// Reads a file from the Remote Encrypted Virtual Filesystem
+pub async fn read<R: Into<PathBuf> + Send>(
+    remote: &mut impl TargetLockedRemote,
+    virtual_path: R,
+) -> Result<PathBuf, NetworkError> {
+    read_with_security_level(remote, Default::default(), virtual_path).await
+}
+
+/// Reads a file from the Remote Encrypted Virtual Filesystem with a custom transport security level
+pub async fn read_with_security_level<R: Into<PathBuf> + Send>(
+    remote: &mut impl TargetLockedRemote,
+    transfer_security_level: SecurityLevel,
+    virtual_path: R,
+) -> Result<PathBuf, NetworkError> {
+    remote
+        .remote_encrypted_virtual_filesystem_pull(virtual_path, transfer_security_level, false)
+        .await
+}
+
+/// Takes a file from the Remote Encrypted Virtual Filesystem
+pub async fn take<R: Into<PathBuf> + Send>(
+    remote: &mut impl TargetLockedRemote,
+    virtual_path: R,
+) -> Result<PathBuf, NetworkError> {
+    remote
+        .remote_encrypted_virtual_filesystem_pull(virtual_path, Default::default(), true)
+        .await
+}
+
+/// Takes a file from the Remote Encrypted Virtual Filesystem with a custom security level.
+pub async fn take_with_security_level<R: Into<PathBuf> + Send>(
+    remote: &mut impl TargetLockedRemote,
+    transfer_security_level: SecurityLevel,
+    virtual_path: R,
+) -> Result<PathBuf, NetworkError> {
+    remote
+        .remote_encrypted_virtual_filesystem_pull(virtual_path, transfer_security_level, true)
+        .await
+}
+
+/// Deletes a file from the Remote Encrypted Virtual Filesystem
+pub async fn delete<R: Into<PathBuf> + Send>(
+    remote: &mut impl TargetLockedRemote,
+    virtual_path: R,
+) -> Result<(), NetworkError> {
+    remote
+        .remote_encrypted_virtual_filesystem_delete(virtual_path)
+        .await
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::builder::node_builder::{NodeBuilder, NodeFuture};
+    use crate::prefabs::client::single_connection::SingleClientServerConnectionKernel;
+    use crate::prefabs::server::accept_file_transfer_kernel::AcceptFileTransferKernel;
+
+    use crate::prelude::*;
+    use rstest::rstest;
+    use std::net::SocketAddr;
+    use std::path::PathBuf;
+    use std::str::FromStr;
+    use std::sync::atomic::{AtomicBool, Ordering};
+    use uuid::Uuid;
+
+    pub fn server_info<'a>() -> (NodeFuture<'a, AcceptFileTransferKernel>, SocketAddr) {
+        let port = crate::test_common::get_unused_tcp_port();
+        let bind_addr = SocketAddr::from_str(&format!("127.0.0.1:{port}")).unwrap();
+        let server = crate::test_common::server_test_node(
+            bind_addr,
+            AcceptFileTransferKernel::default(),
+            |_| {},
+        );
+        (server, bind_addr)
+    }
+
+    #[rstest]
+    #[timeout(std::time::Duration::from_secs(90))]
+    #[case(
+        EncryptionAlgorithm::AES_GCM_256_SIV,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
+    #[case(
+        EncryptionAlgorithm::Kyber,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::Falcon1024
+    )]
+    #[tokio::test]
+    async fn test_c2s_file_transfer_revfs(
+        #[case] enx: EncryptionAlgorithm,
+        #[case] kem: KemAlgorithm,
+        #[case] sig: SigAlgorithm,
+        #[values(SecurityLevel::Standard, SecurityLevel::Reinforced)] security_level: SecurityLevel,
+    ) {
+        citadel_logging::setup_log();
+        let client_success = &AtomicBool::new(false);
+        let (server, server_addr) = server_info();
+        let uuid = Uuid::new_v4();
+
+        let source_dir = PathBuf::from("../resources/TheBridge.pdf");
+
+        let session_security_settings = SessionSecuritySettingsBuilder::default()
+            .with_crypto_params(enx + kem + sig)
+            .with_security_level(security_level)
+            .build()
+            .unwrap();
+
+        let client_kernel = SingleClientServerConnectionKernel::new_passwordless(
+            uuid,
+            server_addr,
+            UdpMode::Disabled,
+            session_security_settings,
+            |_channel, mut remote| async move {
+                log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
+                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
+                // write to file to the RE-VFS
+                crate::fs::write_with_security_level(
+                    &mut remote,
+                    source_dir.clone(),
+                    security_level,
+                    &virtual_path,
+                )
+                .await?;
+                log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
+                // now, pull it
+                let save_dir = crate::fs::read(&mut remote, virtual_path).await?;
+                // now, compare bytes
+                log::trace!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
+                let original_bytes = tokio::fs::read(&source_dir).await.unwrap();
+                let revfs_pulled_bytes = tokio::fs::read(&save_dir).await.unwrap();
+                assert_eq!(original_bytes, revfs_pulled_bytes);
+                log::trace!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
+                client_success.store(true, Ordering::Relaxed);
+                remote.shutdown_kernel().await
+            },
+        )
+        .unwrap();
+
+        let client = NodeBuilder::default().build(client_kernel).unwrap();
+
+        let result = tokio::select! {
+            res0 = client => res0.map(|_| ()),
+            res1 = server => res1.map(|_| ())
+        };
+
+        result.unwrap();
+
+        assert!(client_success.load(Ordering::Relaxed));
+    }
+
+    #[rstest]
+    #[timeout(std::time::Duration::from_secs(90))]
+    #[case(
+        EncryptionAlgorithm::AES_GCM_256_SIV,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
+    #[tokio::test]
+    async fn test_c2s_file_transfer_revfs_take(
+        #[case] enx: EncryptionAlgorithm,
+        #[case] kem: KemAlgorithm,
+        #[case] sig: SigAlgorithm,
+        #[values(SecurityLevel::Standard)] security_level: SecurityLevel,
+    ) {
+        citadel_logging::setup_log();
+        let client_success = &AtomicBool::new(false);
+        let (server, server_addr) = server_info();
+        let uuid = Uuid::new_v4();
+
+        let source_dir = PathBuf::from("../resources/TheBridge.pdf");
+
+        let session_security_settings = SessionSecuritySettingsBuilder::default()
+            .with_crypto_params(enx + kem + sig)
+            .with_security_level(security_level)
+            .build()
+            .unwrap();
+
+        let client_kernel = SingleClientServerConnectionKernel::new_passwordless(
+            uuid,
+            server_addr,
+            UdpMode::Disabled,
+            session_security_settings,
+            |_channel, mut remote| async move {
+                log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
+                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
+                // write to file to the RE-VFS
+                crate::fs::write_with_security_level(
+                    &mut remote,
+                    source_dir.clone(),
+                    security_level,
+                    &virtual_path,
+                )
+                .await?;
+                log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
+                // now, pull it
+                let save_dir = crate::fs::take(&mut remote, &virtual_path).await?;
+                // now, compare bytes
+                log::trace!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
+                let original_bytes = tokio::fs::read(&source_dir).await.unwrap();
+                let revfs_pulled_bytes = tokio::fs::read(&save_dir).await.unwrap();
+                assert_eq!(original_bytes, revfs_pulled_bytes);
+                log::trace!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
+                // prove we can no longer read from this virtual file
+                assert!(crate::fs::read(&mut remote, &virtual_path).await.is_err());
+                client_success.store(true, Ordering::Relaxed);
+                remote.shutdown_kernel().await
+            },
+        )
+        .unwrap();
+
+        let client = NodeBuilder::default().build(client_kernel).unwrap();
+
+        let result = tokio::select! {
+            res0 = client => res0.map(|_| ()),
+            res1 = server => res1.map(|_| ())
+        };
+
+        result.unwrap();
+
+        assert!(client_success.load(Ordering::Relaxed));
+    }
+
+    #[rstest]
+    #[timeout(std::time::Duration::from_secs(90))]
+    #[case(
+        EncryptionAlgorithm::AES_GCM_256_SIV,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
+    #[tokio::test]
+    async fn test_c2s_file_transfer_revfs_delete(
+        #[case] enx: EncryptionAlgorithm,
+        #[case] kem: KemAlgorithm,
+        #[case] sig: SigAlgorithm,
+        #[values(SecurityLevel::Standard)] security_level: SecurityLevel,
+    ) {
+        citadel_logging::setup_log();
+        let client_success = &AtomicBool::new(false);
+        let (server, server_addr) = server_info();
+        let uuid = Uuid::new_v4();
+
+        let source_dir = PathBuf::from("../resources/TheBridge.pdf");
+
+        let session_security_settings = SessionSecuritySettingsBuilder::default()
+            .with_crypto_params(enx + kem + sig)
+            .with_security_level(security_level)
+            .build()
+            .unwrap();
+
+        let client_kernel = SingleClientServerConnectionKernel::new_passwordless(
+            uuid,
+            server_addr,
+            UdpMode::Disabled,
+            session_security_settings,
+            |_channel, mut remote| async move {
+                log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
+                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
+                // write to file to the RE-VFS
+                crate::fs::write_with_security_level(
+                    &mut remote,
+                    source_dir.clone(),
+                    security_level,
+                    &virtual_path,
+                )
+                .await?;
+                log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
+                // now, pull it
+                let save_dir = crate::fs::read(&mut remote, &virtual_path).await?;
+                // now, compare bytes
+                log::trace!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
+                let original_bytes = tokio::fs::read(&source_dir).await.unwrap();
+                let revfs_pulled_bytes = tokio::fs::read(&save_dir).await.unwrap();
+                assert_eq!(original_bytes, revfs_pulled_bytes);
+                log::trace!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
+                crate::fs::delete(&mut remote, &virtual_path).await?;
+                // prove we can no longer read from this virtual file since it was just deleted
+                assert!(crate::fs::read(&mut remote, &virtual_path).await.is_err());
+                client_success.store(true, Ordering::Relaxed);
+                remote.shutdown_kernel().await
+            },
+        )
+        .unwrap();
+
+        let client = NodeBuilder::default().build(client_kernel).unwrap();
+
+        let result = tokio::select! {
+            res0 = client => res0.map(|_| ()),
+            res1 = server => res1.map(|_| ())
+        };
+
+        result.unwrap();
+
+        assert!(client_success.load(Ordering::Relaxed));
+    }
+}
diff --git a/citadel_sdk/src/lib.rs b/citadel_sdk/src/lib.rs
index 0dd37f482..2ff265810 100644
--- a/citadel_sdk/src/lib.rs
+++ b/citadel_sdk/src/lib.rs
@@ -147,6 +147,61 @@
 //! # Ok::<(), Box<dyn std::error::Error>>(())
 //! ```
 //!
+//! ## Remote Encrypted Virtual Filesystem (RE-VFS)
+//! The RE-VFS allows clients, servers, and peers to treat each other as remote endpoints for encrypted file storage.
+//! Since encrypting data locally using a symmetric key poses a vulnerability if the local node is compromised, The
+//! Citadel Protocol solves this issue by using a local 1024-Kyber public key to encrypt the data (via Kyber scramcryption for
+//! keeping the data size to a minimum), then, sending the contents to the adjacent endpoint. By doing this, the private decryption
+//! key and the contents are kept separate, forcing the hacker to compromise both endpoints.
+//!
+//! In order to use the RE-VFS, both endpoints must use the Filesystem backend. Second, the endpoint serving as a storage point
+//! must accept the inbound file transfer requests, otherwise, the transfer will fail. The example below for the receiving endpoint
+//! shows how to auto-accept inbound file transfer requests
+//!
+//! # Examples
+//!
+//! ## Receiving endpoint
+//! ```
+//! use citadel_sdk::prelude::*;
+//! use citadel_sdk::prefabs::server::accept_file_transfer_kernel::AcceptFileTransferKernel;
+//!
+//! // this server will listen on 127.0.0.1:25021, and will use the built-in defaults with a kernel
+//! // that auto-accepts inbound file transfer requests
+//! let server = NodeBuilder::default()
+//! .with_node_type(NodeType::server("127.0.0.1:25021")?)
+//! .build(AcceptFileTransferKernel::default())?;
+//!
+//! // await the server to execute
+//! # async move {
+//! let result = server.await;
+//! # };
+//! # Ok::<(), Box<dyn std::error::Error>>(())
+//! ```
+//!
+//! ## Sending endpoint
+//! ```
+//! use citadel_sdk::prefabs::client::single_connection::SingleClientServerConnectionKernel;
+//! use futures::StreamExt;
+//! use citadel_proto::prelude::*;
+//! use citadel_sdk::prelude::NodeBuilder;
+//!
+//! let client_kernel = SingleClientServerConnectionKernel::new_register_defaults("John Doe", "john.doe", "password", "127.0.0.1:25021", |connect_success, remote| async move {
+//!     let virtual_path = "/home/virtual_user/output.pdf";
+//!     // write the contents with reinforced security.
+//!     citadel_sdk::fs::write_with_security_level(&mut remote, "../path/to/input.pdf", SecurityLevel::Reinforced, virtual_path).await?;
+//!     // read the contents. Reading downloads the file to a local path
+//!     let stored_local_path = citadel_sdk::fs::read(&mut remote, virtual_path).await?;
+//!  
+//!     Ok(())
+//! })?;
+//!
+//! let client = NodeBuilder::default().build(client_kernel)?;
+//! # async move {
+//! let result = client.await;
+//! # };
+//! # Ok::<(), Box<dyn std::error::Error>>(())
+//! ```
+//!
 //! [`UdpChannel`]: crate::prelude::UdpChannel
 //! [`NetKernel`]: crate::prelude::NetKernel
 //! [`NetKernel::load_remote`]: crate::prelude::NetKernel::load_remote
@@ -185,6 +240,8 @@ pub mod prelude {
 /// Store data to the backend using this library
 pub mod backend_kv_store;
 mod builder;
+/// Convenience functions for interacting with the remote encrypted virtual filesystem (RE-VFS)
+pub mod fs;
 /// A list of prefabricated kernels designed for common use cases. If a greater degree of control is required for an application, a custom implementation of [NetKernel](crate::prelude::NetKernel) is desirable
 pub mod prefabs;
 /// Extension implementations endowed upon the [NodeRemote](crate::prelude::NodeRemote)
diff --git a/citadel_sdk/src/prefabs/client/peer_connection.rs b/citadel_sdk/src/prefabs/client/peer_connection.rs
index a22044674..d3cf0495a 100644
--- a/citadel_sdk/src/prefabs/client/peer_connection.rs
+++ b/citadel_sdk/src/prefabs/client/peer_connection.rs
@@ -593,9 +593,10 @@ mod tests {
                         // one user will send the file, the other will receive the file
                         if idx == 0 {
                             conn.remote
-                                .send_file_with_custom_chunking(
+                                .send_file_with_custom_opts(
                                     "../resources/TheBridge.pdf",
                                     32 * 1024,
+                                    TransferType::FileTransfer
                                 )
                                 .await?;
 
diff --git a/citadel_sdk/src/prefabs/client/single_connection.rs b/citadel_sdk/src/prefabs/client/single_connection.rs
index 6bbe42c62..fe62d0ad6 100644
--- a/citadel_sdk/src/prefabs/client/single_connection.rs
+++ b/citadel_sdk/src/prefabs/client/single_connection.rs
@@ -185,7 +185,7 @@ where
 
     #[cfg_attr(
         feature = "localhost-testing",
-        tracing::instrument(target = "citadel", skip_all, ret, err(Debug))
+        tracing::instrument(target = "citadel", skip_all, err(Debug))
     )]
     async fn on_start(&self) -> Result<(), NetworkError> {
         let mut remote = self.remote.clone().unwrap();
diff --git a/citadel_sdk/src/prefabs/server/accept_file_transfer_kernel.rs b/citadel_sdk/src/prefabs/server/accept_file_transfer_kernel.rs
new file mode 100644
index 000000000..49fc0b47e
--- /dev/null
+++ b/citadel_sdk/src/prefabs/server/accept_file_transfer_kernel.rs
@@ -0,0 +1,31 @@
+use crate::prelude::*;
+use citadel_proto::prelude::NetKernel;
+
+#[derive(Default)]
+pub struct AcceptFileTransferKernel;
+
+#[async_trait]
+impl NetKernel for AcceptFileTransferKernel {
+    fn load_remote(&mut self, _node_remote: NodeRemote) -> Result<(), NetworkError> {
+        Ok(())
+    }
+
+    async fn on_start(&self) -> Result<(), NetworkError> {
+        Ok(())
+    }
+
+    async fn on_node_event_received(&self, message: NodeResult) -> Result<(), NetworkError> {
+        if let NodeResult::ObjectTransferHandle(mut handle) = message {
+            handle
+                .handle
+                .accept()
+                .map_err(|err| NetworkError::Generic(err.into_string()))?;
+        }
+
+        Ok(())
+    }
+
+    async fn on_stop(&mut self) -> Result<(), NetworkError> {
+        Ok(())
+    }
+}
diff --git a/citadel_sdk/src/prefabs/server/mod.rs b/citadel_sdk/src/prefabs/server/mod.rs
index bc21893cd..53b6a1e74 100644
--- a/citadel_sdk/src/prefabs/server/mod.rs
+++ b/citadel_sdk/src/prefabs/server/mod.rs
@@ -1,3 +1,6 @@
+/// A kernel that accepts all inbound file transfer requests for basic file transfers
+/// AND RE-VFS transfers
+pub mod accept_file_transfer_kernel;
 /// A kernel that reacts to new channels created, allowing communication with new clients.
 /// Useful for when a server needs to send messages to clients
 pub mod client_connect_listener;
diff --git a/citadel_sdk/src/remote_ext.rs b/citadel_sdk/src/remote_ext.rs
index 0f040ee61..95d649db5 100644
--- a/citadel_sdk/src/remote_ext.rs
+++ b/citadel_sdk/src/remote_ext.rs
@@ -4,6 +4,7 @@ use crate::prelude::*;
 use crate::remote_ext::remote_specialization::PeerRemote;
 use crate::remote_ext::results::HyperlanPeer;
 use crate::remote_ext::user_ids::{SymmetricIdentifierHandleRef, TargetLockedRemote};
+
 use citadel_proto::auth::AuthenticationRequest;
 use futures::StreamExt;
 use std::path::PathBuf;
@@ -39,7 +40,7 @@ pub(crate) mod user_ids {
         target_username: Option<String>,
     }
 
-    pub trait TargetLockedRemote {
+    pub trait TargetLockedRemote: Send {
         fn user(&self) -> &VirtualTargetType;
         fn remote(&mut self) -> &mut NodeRemote;
         fn target_username(&self) -> Option<&String>;
@@ -432,10 +433,11 @@ impl ProtocolRemoteExt for ClientServerRemote {
 /// Some functions require that a target exists
 pub trait ProtocolRemoteTargetExt: TargetLockedRemote {
     /// Sends a file with a custom size. The smaller the chunks, the higher the degree of scrambling, but the higher the performance cost. A chunk size of zero will use the default
-    async fn send_file_with_custom_chunking<T: Into<PathBuf> + Send>(
+    async fn send_file_with_custom_opts<T: ObjectSource>(
         &mut self,
-        path: T,
+        source: T,
         chunk_size: usize,
+        transfer_type: TransferType,
     ) -> Result<(), NetworkError> {
         let chunk_size = if chunk_size == 0 {
             None
@@ -448,10 +450,11 @@ pub trait ProtocolRemoteTargetExt: TargetLockedRemote {
 
         let result = remote
             .send_callback(NodeRequest::SendObject(SendObject {
-                source: Box::new(path.into()),
+                source: Box::new(source),
                 chunk_size,
                 implicated_cid,
                 v_conn_type: user,
+                transfer_type,
             }))
             .await?;
         match map_errors(result)? {
@@ -476,8 +479,122 @@ pub trait ProtocolRemoteTargetExt: TargetLockedRemote {
     }
 
     /// Sends a file to the provided target using the default chunking size
-    async fn send_file<T: Into<PathBuf> + Send>(&mut self, path: T) -> Result<(), NetworkError> {
-        self.send_file_with_custom_chunking(path, 0).await
+    async fn send_file<T: ObjectSource>(&mut self, source: T) -> Result<(), NetworkError> {
+        self.send_file_with_custom_opts(source, 0, TransferType::FileTransfer)
+            .await
+    }
+
+    /// Sends a file to the provided target using custom chunking size with local encryption.
+    /// Only this local node may decrypt the information send to the adjacent node.
+    async fn remote_encrypted_virtual_filesystem_push_custom_chunking<
+        T: ObjectSource,
+        R: Into<PathBuf> + Send,
+    >(
+        &mut self,
+        source: T,
+        virtual_directory: R,
+        chunk_size: usize,
+        security_level: SecurityLevel,
+    ) -> Result<(), NetworkError> {
+        let mut virtual_path = virtual_directory.into();
+        virtual_path = prepare_virtual_path(virtual_path);
+        validate_virtual_path(&virtual_path)
+            .map_err(|err| NetworkError::Generic(err.into_string()))?;
+        let tx_type = TransferType::RemoteEncryptedVirtualFilesystem {
+            virtual_path,
+            security_level,
+        };
+        self.send_file_with_custom_opts(source, chunk_size, tx_type)
+            .await
+    }
+
+    /// Sends a file to the provided target using the default chunking size with local encryption.
+    /// Only this local node may decrypt the information send to the adjacent node.
+    async fn remote_encrypted_virtual_filesystem_push<T: ObjectSource, R: Into<PathBuf> + Send>(
+        &mut self,
+        source: T,
+        virtual_directory: R,
+        security_level: SecurityLevel,
+    ) -> Result<(), NetworkError> {
+        self.remote_encrypted_virtual_filesystem_push_custom_chunking(
+            source,
+            virtual_directory,
+            0,
+            security_level,
+        )
+        .await
+    }
+
+    /// Pulls a virtual file from the RE-VFS. If `delete_on_pull` is true, then, the virtual file
+    /// will be taken from the RE-VFS
+    async fn remote_encrypted_virtual_filesystem_pull<R: Into<PathBuf> + Send>(
+        &mut self,
+        virtual_directory: R,
+        transfer_security_level: SecurityLevel,
+        delete_on_pull: bool,
+    ) -> Result<PathBuf, NetworkError> {
+        let request = NodeRequest::PullObject(PullObject {
+            v_conn: *self.user(),
+            virtual_dir: virtual_directory.into(),
+            delete_on_pull,
+            transfer_security_level,
+        });
+
+        match map_errors(self.remote().send_callback(request).await?)? {
+            NodeResult::ObjectTransferHandle(ObjectTransferHandle {
+                ticket: _ticket,
+                mut handle,
+            }) => {
+                let mut local_path = None;
+                while let Some(res) = handle.next().await {
+                    log::trace!(target: "citadel", "Client received RES {:?}", res);
+                    match res {
+                        ObjectTransferStatus::ReceptionBeginning(path, _) => {
+                            local_path = Some(path)
+                        }
+                        ObjectTransferStatus::TransferComplete => {
+                            break;
+                        }
+
+                        _ => {}
+                    }
+                }
+
+                Ok(local_path.ok_or(NetworkError::InternalError("Local path never loaded"))?)
+            }
+
+            res => {
+                log::error!(target: "citadel", "Invalid NodeResult for FileTransfer request received: {:?}", res);
+                Err(NetworkError::InternalError(
+                    "Received invalid response from protocol",
+                ))
+            }
+        }
+    }
+
+    /// Deletes the file from the RE-VFS. If the contents are desired on delete,
+    /// consider calling `Self::remote_encrypted_virtual_filesystem_pull` with the delete
+    /// parameter set to true
+    async fn remote_encrypted_virtual_filesystem_delete<R: Into<PathBuf> + Send>(
+        &mut self,
+        virtual_directory: R,
+    ) -> Result<(), NetworkError> {
+        let request = NodeRequest::DeleteObject(DeleteObject {
+            v_conn: *self.user(),
+            virtual_dir: virtual_directory.into(),
+            security_level: Default::default(),
+        });
+
+        let response = map_errors(self.remote().send_callback(request).await?)?;
+        if let NodeResult::ReVFS(result) = response {
+            if let Some(error) = result.error_message {
+                Err(NetworkError::Generic(error))
+            } else {
+                Ok(())
+            }
+        } else {
+            Err(NetworkError::InternalError("Invalid NodeRequest response"))
+        }
     }
 
     /// Connects to the peer with custom settings
@@ -975,7 +1092,11 @@ mod tests {
             |_channel, mut remote| async move {
                 log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
                 remote
-                    .send_file_with_custom_chunking("../resources/TheBridge.pdf", 32 * 1024)
+                    .send_file_with_custom_opts(
+                        "../resources/TheBridge.pdf",
+                        32 * 1024,
+                        TransferType::FileTransfer,
+                    )
                     .await
                     .unwrap();
                 log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
diff --git a/citadel_user/src/account_loader.rs b/citadel_user/src/account_loader.rs
index 589069c6d..2a4560ad9 100644
--- a/citadel_user/src/account_loader.rs
+++ b/citadel_user/src/account_loader.rs
@@ -11,8 +11,8 @@ use std::collections::HashMap;
 pub fn load_cnac_files<R: Ratchet, Fcm: Ratchet>(
     ds: &DirectoryStore,
 ) -> Result<HashMap<u64, ClientNetworkAccount<R, Fcm>>, AccountError> {
-    let hyxe_nac_dir_impersonal = ds.hyxe_nac_dir_impersonal.as_str();
-    let hyxe_nac_dir_personal = ds.hyxe_nac_dir_personal.as_str();
+    let hyxe_nac_dir_impersonal = ds.nac_dir_impersonal.as_str();
+    let hyxe_nac_dir_personal = ds.nac_dir_personal.as_str();
 
     let cnacs_impersonal = load_file_types_by_ext::<ClientNetworkAccountInner<R, Fcm>, _>(
         CNAC_SERIALIZED_EXTENSION,
diff --git a/citadel_user/src/backend/filesystem_backend.rs b/citadel_user/src/backend/filesystem_backend.rs
index 226c2fbf4..205020a5d 100644
--- a/citadel_user/src/backend/filesystem_backend.rs
+++ b/citadel_user/src/backend/filesystem_backend.rs
@@ -1,18 +1,21 @@
 use super::utils::StreamableTargetInformation;
 use crate::account_loader::load_cnac_files;
 use crate::backend::memory::MemoryBackend;
-use crate::backend::utils::ObjectTransferStatus;
+use crate::backend::utils::{ObjectTransferStatus, VirtualObjectMetadata};
 use crate::backend::BackendConnection;
 use crate::client_account::{ClientNetworkAccount, MutualPeer};
 use crate::directory_store::DirectoryStore;
 use crate::misc::{AccountError, CNACMetadata};
 use crate::prelude::CNAC_SERIALIZED_EXTENSION;
+use crate::serialization::SyncIO;
 use async_trait::async_trait;
+use citadel_crypt::misc::TransferType;
+use citadel_crypt::prelude::SecurityLevel;
 use citadel_crypt::stacked_ratchet::Ratchet;
+use citadel_crypt::streaming_crypt_scrambler::ObjectSource;
 use std::collections::HashMap;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 use std::sync::Arc;
-use tokio::io::AsyncWriteExt;
 use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
 use tokio_stream::StreamExt;
 
@@ -92,7 +95,7 @@ impl<R: Ratchet, Fcm: Ratchet> BackendConnection<R, Fcm> for FilesystemBackend<R
         }
 
         // delete the home directory
-        let home_dir = self.directory_store.as_ref().unwrap().hyxe_home.as_str();
+        let home_dir = self.directory_store.as_ref().unwrap().home.as_str();
         tokio::fs::remove_dir_all(home_dir)
             .await
             .map_err(|err| AccountError::Generic(err.to_string()))?;
@@ -338,36 +341,109 @@ impl<R: Ratchet, Fcm: Ratchet> BackendConnection<R, Fcm> for FilesystemBackend<R
         status_tx: UnboundedSender<ObjectTransferStatus>,
     ) -> Result<(), AccountError> {
         let directory_store = self.directory_store.as_ref().unwrap();
-        let name = sink_metadata.get_target_name();
-        let save_path = directory_store.hyxe_virtual_dir.as_str();
-        let save_location = format!("{save_path}{name}");
-        let save_location = PathBuf::from(save_location);
-        log::info!(target: "citadel", "Will stream object to {:?}", save_location);
-        let file = tokio::fs::File::create(&save_location)
+        let is_virtual_file = matches!(
+            sink_metadata.get_transfer_type(),
+            TransferType::RemoteEncryptedVirtualFilesystem { .. }
+        );
+        let metadata = sink_metadata.get_metadata_file().clone();
+        let file_path = get_file_path(
+            sink_metadata.get_cid(),
+            sink_metadata.get_transfer_type(),
+            directory_store,
+            Some(metadata.name.as_str()),
+        )
+        .await?;
+
+        log::info!(target: "citadel", "Will stream object to {file_path:?}");
+        let file = tokio::fs::File::create(&file_path)
             .await
             .map_err(|err| AccountError::IoError(err.to_string()))?;
 
+        let _ = status_tx.send(ObjectTransferStatus::ReceptionBeginning(
+            file_path.clone(),
+            sink_metadata,
+        ));
+
+        let mut size = 0;
         let mut writer = tokio::io::BufWriter::new(file);
         let mut reader = tokio_util::io::StreamReader::new(
             tokio_stream::wrappers::UnboundedReceiverStream::new(source).map(|r| {
+                size += r.len();
                 Ok(std::io::Cursor::new(r)) as Result<std::io::Cursor<Vec<u8>>, std::io::Error>
             }),
         );
 
-        let _ = status_tx.send(ObjectTransferStatus::ReceptionBeginning(
-            save_location,
-            sink_metadata,
-        ));
+        if is_virtual_file {
+            // start by writing the metadata file next to it
+            let metadata_path = get_revfs_file_metadata_path(&file_path);
+            let serialized = metadata.serialize_to_vector()?;
+            tokio::fs::write(metadata_path, serialized)
+                .await
+                .map_err(|err| AccountError::IoError(err.to_string()))?
+        }
 
         if let Err(err) = tokio::io::copy(&mut reader, &mut writer).await {
             log::error!(target: "citadel", "Error while copying from reader to writer: {}", err);
         }
 
         writer
-            .shutdown()
+            .into_inner()
+            .sync_all()
             .await
             .map_err(|err| AccountError::IoError(err.to_string()))
     }
+
+    async fn revfs_get_file_info(
+        &self,
+        cid: u64,
+        virtual_path: std::path::PathBuf,
+    ) -> Result<(Box<dyn ObjectSource>, SecurityLevel), AccountError> {
+        let directory_store = self.directory_store.as_ref().unwrap();
+        let file_path = get_file_path(
+            cid,
+            &TransferType::RemoteEncryptedVirtualFilesystem {
+                virtual_path,
+                security_level: Default::default(),
+            },
+            directory_store,
+            None,
+        )
+        .await?;
+
+        let metadata_path = get_revfs_file_metadata_path(&file_path);
+        // first, figure out what security level it was encrypted at. This data should be passed back to the client pulling
+        // this file
+        let raw_metadata = tokio::fs::read(&metadata_path)
+            .await
+            .map_err(|err| AccountError::IoError(err.to_string()))?;
+        let metadata: VirtualObjectMetadata =
+            VirtualObjectMetadata::deserialize_from_owned_vector(raw_metadata)?;
+
+        let security_level = metadata.get_security_level().ok_or_else(|| AccountError::IoError("The requested file was not designated as a RE-VFS type, yet, a metadata file existed for it".into()))?;
+
+        Ok((Box::new(file_path), security_level))
+    }
+
+    async fn revfs_delete(
+        &self,
+        cid: u64,
+        virtual_path: std::path::PathBuf,
+    ) -> Result<(), AccountError> {
+        let directory_store = self.directory_store.as_ref().unwrap();
+        let file_path = get_file_path(
+            cid,
+            &TransferType::RemoteEncryptedVirtualFilesystem {
+                virtual_path,
+                security_level: Default::default(),
+            },
+            directory_store,
+            None,
+        )
+        .await?;
+        let metadata_path = get_revfs_file_metadata_path(&file_path);
+
+        delete_paths(&[metadata_path, file_path]).await
+    }
 }
 
 impl<R: Ratchet, Fcm: Ratchet> FilesystemBackend<R, Fcm> {
@@ -387,14 +463,14 @@ impl<R: Ratchet, Fcm: Ratchet> FilesystemBackend<R, Fcm> {
         if is_personal {
             PathBuf::from(format!(
                 "{}{}.{}",
-                dirs.hyxe_nac_dir_personal.as_str(),
+                dirs.nac_dir_personal.as_str(),
                 cid,
                 CNAC_SERIALIZED_EXTENSION
             ))
         } else {
             PathBuf::from(format!(
                 "{}{}.{}",
-                dirs.hyxe_nac_dir_impersonal.as_str(),
+                dirs.nac_dir_impersonal.as_str(),
                 cid,
                 CNAC_SERIALIZED_EXTENSION
             ))
@@ -411,3 +487,68 @@ impl<R: Ratchet, Fcm: Ratchet> From<String> for FilesystemBackend<R, Fcm> {
         }
     }
 }
+
+// works for RE-FVS and standard file transfers
+async fn get_file_path(
+    source_cid: u64,
+    transfer_type: &TransferType,
+    directory_store: &DirectoryStore,
+    target_name: Option<&str>,
+) -> Result<PathBuf, AccountError> {
+    match transfer_type {
+        TransferType::FileTransfer => {
+            // TODO: ensure for sources that come from bytes, the name is randomly generated
+            // to prevent collisions
+            let name = target_name.ok_or_else(|| {
+                AccountError::IoError(
+                    "File transfer type specified, yet, no target name given".into(),
+                )
+            })?;
+            let save_path = directory_store.file_transfer_dir.as_str();
+            let mut base_path = PathBuf::from(format!("{save_path}{source_cid}"));
+
+            // create the directory in case it doesn't exist
+            tokio::fs::create_dir_all(&base_path)
+                .await
+                .map_err(|err| AccountError::IoError(err.to_string()))?;
+
+            // finally, add the file name
+            base_path.push(name);
+
+            Ok(base_path)
+        }
+        TransferType::RemoteEncryptedVirtualFilesystem { virtual_path, .. } => {
+            let virtual_dir = &crate::misc::prepare_virtual_path(virtual_path);
+            crate::misc::validate_virtual_path(virtual_dir)?;
+            let save_path = directory_store.virtual_dir.as_str();
+            let file_path =
+                PathBuf::from(format!("{save_path}{source_cid}{}", virtual_dir.display()));
+            // create the directory for the file if it doesn't exist
+            let mut file_path_dir = file_path.clone();
+            let _ = file_path_dir.pop();
+
+            // create the directory in case it doesn't exist
+            tokio::fs::create_dir_all(&file_path_dir)
+                .await
+                .map_err(|err| AccountError::IoError(err.to_string()))?;
+            Ok(file_path)
+        }
+    }
+}
+
+fn get_revfs_file_metadata_path<P: AsRef<Path>>(path: P) -> PathBuf {
+    let mut metadata_path = format!("{}", path.as_ref().display());
+    metadata_path.push_str(crate::misc::VIRTUAL_FILE_METADATA_EXT);
+    crate::misc::prepare_virtual_path(metadata_path)
+}
+
+async fn delete_paths<T: AsRef<Path>, R: AsRef<[T]>>(paths: R) -> Result<(), AccountError> {
+    let paths = paths.as_ref();
+    for path in paths {
+        tokio::fs::remove_file(path)
+            .await
+            .map_err(|err| AccountError::IoError(err.to_string()))?;
+    }
+
+    Ok(())
+}
diff --git a/citadel_user/src/backend/memory.rs b/citadel_user/src/backend/memory.rs
index 297cc7946..d98492db7 100644
--- a/citadel_user/src/backend/memory.rs
+++ b/citadel_user/src/backend/memory.rs
@@ -387,6 +387,7 @@ pub(crate) async fn no_backend_streaming(
     _sink_metadata: Arc<dyn StreamableTargetInformation>,
     _status_tx: UnboundedSender<ObjectTransferStatus>,
 ) -> Result<(), AccountError> {
+    // TODO: on client-side, immediately block client file upload requests
     log::warn!(target: "citadel", "Attempted to stream object to backend, but, streaming is not enabled for this backend");
 
     while source.recv().await.is_some() {
diff --git a/citadel_user/src/backend/mod.rs b/citadel_user/src/backend/mod.rs
index f4748aa9e..2e546d7a4 100644
--- a/citadel_user/src/backend/mod.rs
+++ b/citadel_user/src/backend/mod.rs
@@ -16,6 +16,8 @@ use crate::backend::utils::misc::StreamableTargetInformation;
 use crate::backend::utils::ObjectTransferStatus;
 use crate::client_account::{ClientNetworkAccount, MutualPeer};
 use crate::misc::{AccountError, CNACMetadata};
+use citadel_crypt::prelude::SecurityLevel;
+use citadel_crypt::streaming_crypt_scrambler::ObjectSource;
 use tokio::sync::mpsc::UnboundedSender;
 
 /// Implementation for the default filesystem backend
@@ -289,6 +291,29 @@ pub trait BackendConnection<R: Ratchet, Fcm: Ratchet>: Send + Sync {
         sink_metadata: Arc<dyn StreamableTargetInformation>,
         status_tx: UnboundedSender<ObjectTransferStatus>,
     ) -> Result<(), AccountError>;
+    /// Returns the encrypted file from the virtual filesystem into the provided buffer.
+    /// The security level used to encrypt the data is also returned
+    #[allow(unused_variables)]
+    async fn revfs_get_file_info(
+        &self,
+        cid: u64,
+        virtual_path: std::path::PathBuf,
+    ) -> Result<(Box<dyn ObjectSource>, SecurityLevel), AccountError> {
+        Err(AccountError::Generic(
+            "The target does not support the RE-VFS protocol".into(),
+        ))
+    }
+    /// Deletes the encrypted file from the virtual filesystem
+    #[allow(unused_variables)]
+    async fn revfs_delete(
+        &self,
+        cid: u64,
+        virtual_path: std::path::PathBuf,
+    ) -> Result<(), AccountError> {
+        Err(AccountError::Generic(
+            "The target does not support the RE-VFS protocol".into(),
+        ))
+    }
 }
 
 /// This is what every C/NAC gets. This gets called before making I/O operations
diff --git a/citadel_user/src/backend/utils/misc.rs b/citadel_user/src/backend/utils/misc.rs
index e848c2d29..9cffb60db 100644
--- a/citadel_user/src/backend/utils/misc.rs
+++ b/citadel_user/src/backend/utils/misc.rs
@@ -1,3 +1,5 @@
+use crate::backend::utils::VirtualObjectMetadata;
+use citadel_crypt::misc::TransferType;
 use std::fmt::Debug;
 
 /// Used for determining location
@@ -5,4 +7,9 @@ pub trait StreamableTargetInformation: Debug + Send + Sync + 'static {
     /// Returns the target name. Should not include the full path,
     /// as this is determined by the backend
     fn get_target_name(&self) -> &String;
+    /// Returns the CID.
+    fn get_cid(&self) -> u64;
+    /// Returns the
+    fn get_transfer_type(&self) -> &TransferType;
+    fn get_metadata_file(&self) -> &VirtualObjectMetadata;
 }
diff --git a/citadel_user/src/backend/utils/mod.rs b/citadel_user/src/backend/utils/mod.rs
index 6f54b6189..d2b8be25b 100644
--- a/citadel_user/src/backend/utils/mod.rs
+++ b/citadel_user/src/backend/utils/mod.rs
@@ -8,6 +8,8 @@ use std::pin::Pin;
 use std::task::{Context, Poll};
 
 use crate::misc::AccountError;
+use citadel_crypt::misc::TransferType;
+use citadel_crypt::prelude::SecurityLevel;
 use std::sync::Arc;
 use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
 
@@ -22,6 +24,8 @@ pub struct VirtualObjectMetadata {
     pub plaintext_length: usize,
     pub group_count: usize,
     pub object_id: u32,
+    pub cid: u64,
+    pub transfer_type: TransferType,
 }
 
 impl VirtualObjectMetadata {
@@ -32,12 +36,30 @@ impl VirtualObjectMetadata {
     pub fn deserialize_from<'a, T: AsRef<[u8]> + 'a>(input: T) -> Option<Self> {
         Self::deserialize_from_vector(input.as_ref()).ok()
     }
+
+    pub fn get_security_level(&self) -> Option<SecurityLevel> {
+        match &self.transfer_type {
+            TransferType::FileTransfer => None,
+            TransferType::RemoteEncryptedVirtualFilesystem { security_level, .. } => {
+                Some(*security_level)
+            }
+        }
+    }
 }
 
 impl StreamableTargetInformation for VirtualObjectMetadata {
     fn get_target_name(&self) -> &String {
         &self.name
     }
+    fn get_cid(&self) -> u64 {
+        self.cid
+    }
+    fn get_transfer_type(&self) -> &TransferType {
+        &self.transfer_type
+    }
+    fn get_metadata_file(&self) -> &VirtualObjectMetadata {
+        self
+    }
 }
 
 /// Used to keep track of file transfer progress for either
@@ -47,6 +69,7 @@ pub struct ObjectTransferHandler {
     inner: UnboundedReceiver<ObjectTransferStatus>,
     pub source: u64,
     pub receiver: u64,
+    pub is_revfs_pull: bool,
     pub orientation: ObjectTransferOrientation,
     start_recv_tx: Option<tokio::sync::oneshot::Sender<bool>>,
 }
@@ -65,6 +88,7 @@ impl ObjectTransferHandler {
         receiver: u64,
         orientation: ObjectTransferOrientation,
         start_recv_tx: Option<tokio::sync::oneshot::Sender<bool>>,
+        is_revfs_pull: bool,
     ) -> (Self, UnboundedSender<ObjectTransferStatus>) {
         let (tx, inner) = unbounded_channel();
 
@@ -74,6 +98,7 @@ impl ObjectTransferHandler {
             receiver,
             orientation,
             start_recv_tx,
+            is_revfs_pull,
         };
 
         (this, tx)
@@ -93,6 +118,10 @@ impl ObjectTransferHandler {
     }
 
     fn respond(&mut self, accept: bool) -> Result<(), AccountError> {
+        if self.is_revfs_pull {
+            return Ok(());
+        }
+
         if matches!(self.orientation, ObjectTransferOrientation::Receiver) {
             self.start_recv_tx
                 .take()
diff --git a/citadel_user/src/directory_store.rs b/citadel_user/src/directory_store.rs
index f807d6a06..5e95bcbc9 100644
--- a/citadel_user/src/directory_store.rs
+++ b/citadel_user/src/directory_store.rs
@@ -1,4 +1,4 @@
-use crate::misc::AccountError;
+use crate::misc::{format_path, AccountError};
 use std::fs::create_dir_all as mkdir;
 use std::path::PathBuf;
 
@@ -17,38 +17,42 @@ pub enum BasePath {
     ServerDir,
     ConfigDir,
     VirtualDir,
+    FileTransferDir,
 }
 
 #[derive(Clone)]
 /// Stores important information for the filesystem
 pub struct DirectoryStore {
-    /// The hyxe home-directory ~/
-    pub hyxe_home: String,
+    /// The program home-directory ~/
+    pub home: String,
     /// base for the nac dir
-    pub hyxe_nac_dir_base: String,
+    pub nac_dir_base: String,
     /// directory for impersonal accounts
-    pub hyxe_nac_dir_impersonal: String,
+    pub nac_dir_impersonal: String,
     /// Directory for personal accounts
-    pub hyxe_nac_dir_personal: String,
+    pub nac_dir_personal: String,
     /// For server-only files
-    pub hyxe_server_dir: String,
+    pub server_dir: String,
     /// Configuration files for either server or client
-    pub hyxe_config_dir: String,
-    /// Directory for the virtual file-sharing platform
-    pub hyxe_virtual_dir: String,
+    pub config_dir: String,
+    /// Directory for the virtual encrypted filesystem
+    pub virtual_dir: String,
+    /// Directory for basic file transfer
+    pub file_transfer_dir: String,
 }
 
 impl DirectoryStore {
     /// Creates a properly formatted path given the `base` value (the base value should come from self)
     pub fn make_path<T: AsRef<str>>(&self, base: BasePath, file: T) -> PathBuf {
         let base = match base {
-            BasePath::Home => &self.hyxe_home,
-            BasePath::NacDirBase => &self.hyxe_nac_dir_base,
-            BasePath::NacDirImpersonal => &self.hyxe_nac_dir_impersonal,
-            BasePath::NacDirPersonal => &self.hyxe_nac_dir_personal,
-            BasePath::ServerDir => &self.hyxe_server_dir,
-            BasePath::ConfigDir => &self.hyxe_config_dir,
-            BasePath::VirtualDir => &self.hyxe_virtual_dir,
+            BasePath::Home => &self.home,
+            BasePath::NacDirBase => &self.nac_dir_base,
+            BasePath::NacDirImpersonal => &self.nac_dir_impersonal,
+            BasePath::NacDirPersonal => &self.nac_dir_personal,
+            BasePath::ServerDir => &self.server_dir,
+            BasePath::ConfigDir => &self.config_dir,
+            BasePath::VirtualDir => &self.virtual_dir,
+            BasePath::FileTransferDir => &self.file_transfer_dir,
         };
 
         PathBuf::from(append_to_path(base.clone(), file.as_ref()))
@@ -76,30 +80,19 @@ fn setup_directory(mut home_dir: String) -> Result<DirectoryStore, AccountError>
     let hyxe_server_dir = append_to_path(home.clone(), "server/");
 
     let dirs = DirectoryStore {
-        hyxe_home: home.clone(),
-        hyxe_nac_dir_base: append_to_path(home.clone(), "accounts/"),
-        hyxe_nac_dir_impersonal: append_to_path(home.clone(), "accounts/impersonal/"),
-        hyxe_nac_dir_personal: append_to_path(home.clone(), "accounts/personal/"),
-        hyxe_server_dir,
-        hyxe_config_dir: append_to_path(home.clone(), "config/"),
-        hyxe_virtual_dir: append_to_path(home, "virtual/"),
+        home: home.clone(),
+        nac_dir_base: append_to_path(home.clone(), "accounts/"),
+        nac_dir_impersonal: append_to_path(home.clone(), "accounts/impersonal/"),
+        nac_dir_personal: append_to_path(home.clone(), "accounts/personal/"),
+        server_dir: hyxe_server_dir,
+        config_dir: append_to_path(home.clone(), "config/"),
+        virtual_dir: append_to_path(home.clone(), "virtual/"),
+        file_transfer_dir: append_to_path(home, "transfers/"),
     };
 
     Ok(dirs)
 }
 
-#[cfg(not(target_os = "windows"))]
-/// #
-pub fn format_path(input: String) -> String {
-    input.replace('\\', "/")
-}
-
-#[cfg(any(target_os = "windows"))]
-/// #
-pub fn format_path(input: String) -> String {
-    input.replace("/", "\\")
-}
-
 fn append_to_path(base: String, addition: &str) -> String {
     format_path(base + addition)
 }
@@ -107,14 +100,15 @@ fn append_to_path(base: String, addition: &str) -> String {
 /// Sets up local directories that are pre-requisite to launching either client or server application
 pub fn setup_directories(home_dir: String) -> Result<DirectoryStore, AccountError> {
     let store = setup_directory(home_dir)?;
-    let base = mkdir(store.hyxe_home.as_str());
-
-    base.and(mkdir(store.hyxe_nac_dir_base.as_str()))
-        .and(mkdir(store.hyxe_nac_dir_impersonal.as_str()))
-        .and(mkdir(store.hyxe_nac_dir_personal.as_str()))
-        .and(mkdir(store.hyxe_server_dir.as_str()))
-        .and(mkdir(store.hyxe_config_dir.as_str()))
-        .and(mkdir(store.hyxe_virtual_dir.as_str()))
+    let base = mkdir(store.home.as_str());
+
+    base.and(mkdir(store.nac_dir_base.as_str()))
+        .and(mkdir(store.nac_dir_impersonal.as_str()))
+        .and(mkdir(store.nac_dir_personal.as_str()))
+        .and(mkdir(store.server_dir.as_str()))
+        .and(mkdir(store.config_dir.as_str()))
+        .and(mkdir(store.virtual_dir.as_str()))
+        .and(mkdir(store.file_transfer_dir.as_str()))
         .map_err(|err| AccountError::IoError(err.to_string()))?;
 
     Ok(store)
diff --git a/citadel_user/src/misc.rs b/citadel_user/src/misc.rs
index 149aa51ce..d8c93a236 100644
--- a/citadel_user/src/misc.rs
+++ b/citadel_user/src/misc.rs
@@ -1,4 +1,5 @@
 use chrono::Utc;
+use std::path::{Path, PathBuf};
 
 /// Default Error type for this crate
 #[derive(Debug)]
@@ -163,3 +164,81 @@ pub mod base64_string {
 pub fn get_present_formatted_timestamp() -> String {
     Utc::now().to_rfc3339()
 }
+
+pub fn validate_virtual_path<R: AsRef<Path>>(virtual_path: R) -> Result<(), AccountError> {
+    let virtual_path = virtual_path.as_ref();
+    #[cfg(not(target_os = "windows"))]
+    const REQUIRED_BEGINNING: &str = "/";
+    #[cfg(target_os = "windows")]
+    const REQUIRED_BEGINNING: &str = "\\";
+
+    if !virtual_path.starts_with(REQUIRED_BEGINNING) {
+        return Err(AccountError::IoError(format!(
+            "Path {virtual_path:?} is not a valid remote encrypted virtual directory"
+        )));
+    }
+
+    let buf = format!("{}", virtual_path.display());
+
+    // we cannot use path.is_dir() since that checks for file existence, which we don't want
+    if buf.ends_with(REQUIRED_BEGINNING) {
+        return Err(AccountError::IoError(format!(
+            "Path {virtual_path:?} is a directory, not a file"
+        )));
+    }
+
+    Ok(())
+}
+
+// The goal of this function is to ensure that the provided virtual path is appropriate for
+// the local operating system
+pub fn prepare_virtual_path<P: AsRef<Path>>(path: P) -> PathBuf {
+    let path = format!("{}", path.as_ref().display());
+    format_path(path).into()
+}
+
+#[cfg(not(target_os = "windows"))]
+/// #
+pub fn format_path(input: String) -> String {
+    input.replace('\\', "/")
+}
+
+#[cfg(target_os = "windows")]
+/// #
+pub fn format_path(input: String) -> String {
+    input.replace("/", "\\")
+}
+
+pub const VIRTUAL_FILE_METADATA_EXT: &str = ".vxe";
+
+#[cfg(test)]
+mod tests {
+    use crate::misc::{prepare_virtual_path, validate_virtual_path};
+    use rstest::rstest;
+    use std::path::PathBuf;
+
+    #[rstest]
+    #[case("/hello/world/tmp.txt")]
+    #[case("/hello/world/tmp")]
+    #[case("/tmp.txt")]
+    #[case("\\hello\\world\\tmp.txt")]
+    #[case("\\hello\\world\\tmp")]
+    #[case("\\tmp.txt")]
+    fn test_virtual_dir_formatting_okay(#[case] good_path: &str) {
+        let virtual_dir = PathBuf::from(good_path);
+        let formatted = prepare_virtual_path(virtual_dir);
+        validate_virtual_path(formatted).unwrap();
+    }
+
+    #[rstest]
+    #[case("/hello/")]
+    #[case("/")]
+    #[case("tmp.txt")]
+    #[case("\\hello\\")]
+    #[case("\\")]
+    fn test_virtual_dir_formatting_bad(#[case] bad_path: &str) {
+        let virtual_dir = PathBuf::from(bad_path);
+        let formatted = prepare_virtual_path(virtual_dir);
+        assert!(validate_virtual_path(formatted).is_err());
+    }
+}


Test patch:
diff --git a/citadel_crypt/tests/primary.rs b/citadel_crypt/tests/primary.rs
index e04fe7f94..4b5c12762 100644
--- a/citadel_crypt/tests/primary.rs
+++ b/citadel_crypt/tests/primary.rs
@@ -5,13 +5,13 @@ mod tests {
         ArgonSettings, ArgonStatus, AsyncArgon, ServerArgonContainer,
     };
     use citadel_crypt::endpoint_crypto_container::EndpointRatchetConstructor;
-    use citadel_crypt::entropy_bank::SecurityLevel;
-    use citadel_crypt::misc::CryptError;
+    use citadel_crypt::entropy_bank::{EntropyBank, SecurityLevel};
+    use citadel_crypt::misc::TransferType;
+    use citadel_crypt::packet_vector::PacketVector;
     use citadel_crypt::scramble::crypt_splitter::{par_scramble_encrypt_group, GroupReceiver};
     use citadel_crypt::secure_buffer::sec_bytes::SecBuffer;
     use citadel_crypt::secure_buffer::sec_string::SecString;
     use citadel_crypt::stacked_ratchet::{Ratchet, StackedRatchet};
-    use citadel_crypt::streaming_crypt_scrambler::{FixedSizedStream, ObjectSource};
     use citadel_crypt::toolset::{Toolset, UpdateStatus, MAX_HYPER_RATCHETS_IN_MEMORY};
     use citadel_pqcrypto::algorithm_dictionary::{
         AlgorithmsExt, CryptoParameters, EncryptionAlgorithm, KemAlgorithm, SigAlgorithm,
@@ -555,25 +555,88 @@ mod tests {
         KemAlgorithm::Kyber,
         SigAlgorithm::None
     )]
+    #[case(
+        EncryptionAlgorithm::Xchacha20Poly_1305,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
+    #[case(
+        EncryptionAlgorithm::Kyber,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::Falcon1024
+    )]
+    fn scrambler_transmission_length_spectrum(
+        #[case] enx: EncryptionAlgorithm,
+        #[case] kem: KemAlgorithm,
+        #[case] sig: SigAlgorithm,
+    ) {
+        scrambler_transmission_spectrum::<StackedRatchet>(
+            enx,
+            kem,
+            sig,
+            TransferType::FileTransfer,
+            |decrypted, plaintext, _, _| debug_assert_eq!(decrypted, plaintext),
+        );
+        #[cfg(feature = "fcm")]
+        scrambler_transmission_spectrum::<citadel_crypt::fcm::fcm_ratchet::ThinRatchet>(
+            enx,
+            kem,
+            sig,
+            TransferType::FileTransfer,
+            |decrypted, plaintext, _, _| debug_assert_eq!(decrypted, plaintext),
+        );
+    }
+
+    #[rstest]
+    #[case(
+        EncryptionAlgorithm::AES_GCM_256_SIV,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
+    #[case(
+        EncryptionAlgorithm::Xchacha20Poly_1305,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
     #[case(
         EncryptionAlgorithm::Kyber,
         KemAlgorithm::Kyber,
         SigAlgorithm::Falcon1024
     )]
-    fn scrambler_transmission_all(
+    fn scrambler_transmission_length_spectrum_remote(
         #[case] enx: EncryptionAlgorithm,
         #[case] kem: KemAlgorithm,
         #[case] sig: SigAlgorithm,
     ) {
-        scrambler_transmission::<StackedRatchet>(enx, kem, sig);
+        let tx_type = TransferType::RemoteEncryptedVirtualFilesystem {
+            virtual_path: PathBuf::from("/"),
+            security_level: SecurityLevel::Standard,
+        };
+
+        fn verifier<R: Ratchet>(decrypted: &[u8], plaintext: &[u8], sa_alice: &R, sa_bob: &R) {
+            assert_ne!(decrypted, plaintext);
+            let decrypted_real = sa_alice
+                .local_decrypt(decrypted, SecurityLevel::Standard)
+                .unwrap();
+            assert_eq!(decrypted_real, plaintext);
+            assert!(sa_bob
+                .local_decrypt(decrypted, SecurityLevel::Standard)
+                .is_err());
+        }
+
+        scrambler_transmission_spectrum::<StackedRatchet>(enx, kem, sig, tx_type, verifier);
         #[cfg(feature = "fcm")]
-        scrambler_transmission::<citadel_crypt::fcm::fcm_ratchet::ThinRatchet>(enx, kem, sig);
+        scrambler_transmission_spectrum::<citadel_crypt::fcm::fcm_ratchet::ThinRatchet>(
+            enx, kem, sig, tx_type, verifier,
+        );
     }
 
-    fn scrambler_transmission<R: Ratchet>(
+    fn scrambler_transmission_spectrum<R: Ratchet>(
         enx: EncryptionAlgorithm,
         kem: KemAlgorithm,
         sig: SigAlgorithm,
+        transfer_type: TransferType,
+        verifier: impl for<'a> Fn(&'a [u8], &'a [u8], &R, &R),
     ) {
         citadel_logging::setup_log();
 
@@ -582,6 +645,8 @@ mod tests {
 
         let mut data = BytesMut::with_capacity(1500);
         let (ratchet_alice, ratchet_bob) = gen::<R>(10, 0, SECURITY_LEVEL, enx + kem + sig);
+        let (pseudo_static_aux_ratchet_alice, pseudo_static_aux_ratchet_bob) =
+            gen::<R>(10, 0, SECURITY_LEVEL, enx + kem + sig);
 
         for x in 0..1500_usize {
             data.put_u8((x % 256) as u8);
@@ -592,10 +657,12 @@ mod tests {
                     input_data,
                     SECURITY_LEVEL,
                     &ratchet_alice,
+                    &pseudo_static_aux_ratchet_alice,
                     HEADER_SIZE_BYTES,
                     0,
                     0,
                     0,
+                    transfer_type.clone(),
                     |_vec, _drill, _target_cid, _, buffer| {
                         for x in 0..HEADER_SIZE_BYTES {
                             buffer.put_u8(x as u8)
@@ -621,10 +688,20 @@ mod tests {
                 //println!("Wave {} result: {:?}", packet.vector.wave_id, result);
             }
 
-            //println!("Possibly done with the descrambling/decryption process ... ");
-
             let decrypted_descrambled_plaintext = receiver.finalize();
-            debug_assert_eq!(decrypted_descrambled_plaintext.as_slice(), input_data);
+            verifier(
+                &decrypted_descrambled_plaintext,
+                input_data,
+                &pseudo_static_aux_ratchet_alice,
+                &pseudo_static_aux_ratchet_bob,
+            )
+        }
+    }
+
+    const HEADER_LEN: usize = 52;
+    fn header_inscribe(_: &PacketVector, _: &EntropyBank, _: u32, _: u64, packet: &mut BytesMut) {
+        for x in 0..HEADER_LEN {
+            packet.put_u8((x % 255) as u8)
         }
     }
 
@@ -635,55 +712,99 @@ mod tests {
         KemAlgorithm::Kyber,
         SigAlgorithm::None
     )]
+    #[case(
+        EncryptionAlgorithm::Xchacha20Poly_1305,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
+    #[case(
+        EncryptionAlgorithm::Kyber,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::Falcon1024
+    )]
+    #[tokio::test]
+    async fn encrypt_decrypt_file_transfer(
+        #[case] enx: EncryptionAlgorithm,
+        #[case] kem: KemAlgorithm,
+        #[case] sig: SigAlgorithm,
+    ) {
+        citadel_logging::setup_log();
+
+        let (bytes, bytes_ret, _sa_alice, _sa_bob) = test_file_transfer_inner(
+            TransferType::FileTransfer,
+            enx,
+            kem,
+            sig,
+            Default::default(),
+        )
+        .await;
+        assert_eq!(bytes, bytes_ret);
+    }
+
+    #[cfg(feature = "filesystem")]
+    #[rstest]
+    #[case(
+        EncryptionAlgorithm::AES_GCM_256_SIV,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
+    #[case(
+        EncryptionAlgorithm::Xchacha20Poly_1305,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
     #[case(
         EncryptionAlgorithm::Kyber,
         KemAlgorithm::Kyber,
         SigAlgorithm::Falcon1024
     )]
     #[tokio::test]
-    async fn encrypt_decrypt_test(
+    async fn encrypt_decrypt_file_transfer_remote(
         #[case] enx: EncryptionAlgorithm,
         #[case] kem: KemAlgorithm,
         #[case] sig: SigAlgorithm,
+        #[values(SecurityLevel::Standard, SecurityLevel::Reinforced)] security_level: SecurityLevel,
     ) {
-        use bytes::{BufMut, BytesMut};
-        use citadel_crypt::entropy_bank::SecurityLevel;
-        use citadel_crypt::scramble::crypt_splitter::{GroupReceiver, GroupReceiverStatus};
+        citadel_logging::setup_log();
+        let (plaintext, bytes_ret, sa_alice, sa_bob) = test_file_transfer_inner(
+            TransferType::RemoteEncryptedVirtualFilesystem {
+                virtual_path: PathBuf::from("/"),
+                security_level,
+            },
+            enx,
+            kem,
+            sig,
+            security_level,
+        )
+        .await;
+        assert_ne!(plaintext, bytes_ret);
+        let decrypted = sa_alice.local_decrypt(&bytes_ret, security_level).unwrap();
+        assert_eq!(decrypted, plaintext);
+        assert!(sa_bob.local_decrypt(&bytes_ret, security_level).is_err());
+    }
+
+    async fn test_file_transfer_inner(
+        transfer_type: TransferType,
+        enx: EncryptionAlgorithm,
+        kem: KemAlgorithm,
+        sig: SigAlgorithm,
+        security_level: SecurityLevel,
+    ) -> (&'static [u8], Vec<u8>, StackedRatchet, StackedRatchet) {
+        use citadel_crypt::scramble::crypt_splitter::GroupReceiverStatus;
         use std::time::Instant;
         use tokio::sync::mpsc::channel;
 
-        use citadel_crypt::prelude::{EntropyBank, PacketVector};
-        use citadel_crypt::stacked_ratchet::StackedRatchet;
         use citadel_crypt::streaming_crypt_scrambler::scramble_encrypt_source;
-        citadel_logging::setup_log();
-        fn header_inscribe(
-            _: &PacketVector,
-            _: &EntropyBank,
-            _: u32,
-            _: u64,
-            packet: &mut BytesMut,
-        ) {
-            for x in 0..HEADER_LEN {
-                packet.put_u8((x % 255) as u8)
-            }
-        }
-
-        let security_level = SecurityLevel::Standard;
 
         let (alice, bob) = gen::<StackedRatchet>(0, 0, security_level, enx + kem + sig);
-        const HEADER_LEN: usize = 52;
+        let (pseudo_static_aux_ratchet_alice, pseudo_static_aux_ratchet_bob) =
+            gen::<StackedRatchet>(0, 0, security_level, enx + kem + sig);
 
         let cmp = include_bytes!("../../resources/TheBridge.pdf");
         let source = PathBuf::from("../resources/TheBridge.pdf");
-        // TODO: why does packets needed *JUMP* from 14 to 31 when going from 60000 to 70000 bytes input?
-        /*let cmp = (0..70000)
-            .into_iter()
-            .map(|r| (r % 256) as u8)
-            .collect::<Vec<u8>>();
-        let source = VecWrapper { inner: cmp.clone() };*/
         let (group_sender_tx, mut group_sender_rx) = channel(1);
         let (_stop_tx, stop_rx) = tokio::sync::oneshot::channel();
-        let (bytes, _num_groups) = scramble_encrypt_source::<_, _, HEADER_LEN>(
+        let (bytes, _num_groups, _mxbpg) = scramble_encrypt_source::<_, _, HEADER_LEN>(
             source,
             None,
             99,
@@ -691,9 +812,11 @@ mod tests {
             stop_rx,
             security_level,
             alice.clone(),
+            pseudo_static_aux_ratchet_alice.clone(),
             HEADER_LEN,
             bob.get_cid(),
             0,
+            transfer_type,
             header_inscribe,
         )
         .unwrap();
@@ -740,27 +863,25 @@ mod tests {
             bytes,
             mbs
         );
-
-        assert_eq!(bytes, bytes_ret.len());
-        if bytes_ret.as_slice() != &cmp[..] {
-            log::error!("{:?} != {:?}", &bytes_ret.as_slice()[..10], &cmp[..10]);
-            log::error!(
-                "{:?} != {:?}",
-                &bytes_ret.as_slice()[bytes_ret.len() - 10..],
-                &cmp[cmp.len() - 10..]
-            );
-            panic!("Vectors not equal")
-        }
+        (
+            cmp,
+            bytes_ret,
+            pseudo_static_aux_ratchet_alice,
+            pseudo_static_aux_ratchet_bob,
+        )
     }
 
-    const DATA: &[u8] = b"Hello, world!";
-
     #[rstest]
     #[case(
         EncryptionAlgorithm::AES_GCM_256_SIV,
         KemAlgorithm::Kyber,
         SigAlgorithm::None
     )]
+    #[case(
+        EncryptionAlgorithm::Xchacha20Poly_1305,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
     #[case(
         EncryptionAlgorithm::Kyber,
         KemAlgorithm::Kyber,
@@ -772,10 +893,10 @@ mod tests {
         #[case] sig: SigAlgorithm,
     ) {
         citadel_logging::setup_log();
-        test_harness(enx + kem + sig, |alice, bob| {
-            let encrypted = alice.encrypt(DATA).unwrap();
+        test_harness(enx + kem + sig, |alice, bob, _, data| {
+            let encrypted = alice.encrypt(data).unwrap();
             let decrypted = bob.decrypt(encrypted).unwrap();
-            assert_eq!(decrypted, DATA);
+            assert_eq!(decrypted, data);
         });
     }
 
@@ -785,6 +906,11 @@ mod tests {
         KemAlgorithm::Kyber,
         SigAlgorithm::None
     )]
+    #[case(
+        EncryptionAlgorithm::Xchacha20Poly_1305,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
     #[case(
         EncryptionAlgorithm::Kyber,
         KemAlgorithm::Kyber,
@@ -796,48 +922,55 @@ mod tests {
         #[case] sig: SigAlgorithm,
     ) {
         citadel_logging::setup_log();
-        test_harness(enx + kem + sig, |alice, bob| {
-            let encrypted = alice.encrypt_scrambler(DATA).unwrap();
+        test_harness(enx + kem + sig, |alice, bob, _, data| {
+            let encrypted = alice.encrypt_scrambler(data).unwrap();
             let decrypted = bob.decrypt_scrambler(encrypted).unwrap();
-            assert_eq!(decrypted, DATA);
+            assert_eq!(decrypted, data);
         });
     }
 
-    fn test_harness(params: CryptoParameters, fx: impl FnOnce(&StackedRatchet, &StackedRatchet)) {
-        let (hr_alice, hr_bob) = gen::<StackedRatchet>(0, 0, SecurityLevel::High, params);
-        (fx)(&hr_alice, &hr_bob);
-    }
-
-    struct VecWrapper {
-        pub inner: Vec<u8>,
+    #[rstest]
+    #[case(
+        EncryptionAlgorithm::AES_GCM_256_SIV,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
+    #[case(
+        EncryptionAlgorithm::Xchacha20Poly_1305,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::None
+    )]
+    #[case(
+        EncryptionAlgorithm::Kyber,
+        KemAlgorithm::Kyber,
+        SigAlgorithm::Falcon1024
+    )]
+    fn test_drill_local_encrypt_decrypt(
+        #[case] enx: EncryptionAlgorithm,
+        #[case] kem: KemAlgorithm,
+        #[case] sig: SigAlgorithm,
+    ) {
+        citadel_logging::setup_log();
+        test_harness(enx + kem + sig, |alice, bob, sec, data| {
+            let encrypted = alice.local_encrypt(data, sec).unwrap();
+            assert!(bob.local_decrypt(&*encrypted, sec).is_err());
+            let decrypted = alice.local_decrypt(encrypted, sec).unwrap();
+            assert_eq!(decrypted, data);
+        });
     }
 
-    impl ObjectSource for VecWrapper {
-        fn try_get_stream(&mut self) -> Result<Box<dyn FixedSizedStream>, CryptError> {
-            struct VecReader {
-                len: usize,
-                cursor: std::io::Cursor<Vec<u8>>,
-            }
-
-            impl std::io::Read for VecReader {
-                fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
-                    self.cursor.read(buf)
-                }
-            }
+    fn test_harness(
+        params: CryptoParameters,
+        fx: impl Fn(&StackedRatchet, &StackedRatchet, SecurityLevel, &[u8]),
+    ) {
+        let data = Vec::from("Hello, world!");
 
-            impl FixedSizedStream for VecReader {
-                fn length(&self) -> std::io::Result<u64> {
-                    Ok(self.len as u64)
-                }
+        for sec in 0..5 {
+            let security_level = SecurityLevel::from(sec);
+            let (hr_alice, hr_bob) = gen::<StackedRatchet>(0, 0, security_level, params);
+            for idx in 0..data.len() {
+                (fx)(&hr_alice, &hr_bob, security_level, &data[..idx]);
             }
-
-            let len = self.inner.len();
-            let cursor = std::io::Cursor::new(self.inner.clone());
-            Ok(Box::new(VecReader { len, cursor }))
-        }
-
-        fn get_source_name(&self) -> Result<String, CryptError> {
-            Ok(String::from("Debug object"))
         }
     }
 }
diff --git a/citadel_pqcrypto/tests/primary.rs b/citadel_pqcrypto/tests/primary.rs
index ac1448a64..a4d57fcd0 100644
--- a/citadel_pqcrypto/tests/primary.rs
+++ b/citadel_pqcrypto/tests/primary.rs
@@ -105,14 +105,13 @@ mod tests {
                 plaintext.push((x % 256) as u8)
             }
 
-            let nonce = &Vec::from_iter(0..(encryption_algorithm.nonce_len()) as u8);
+            let nonce = &Vec::from_iter(0..citadel_pqcrypto::LARGEST_NONCE_LEN as u8);
 
             let mut ciphertext = alice_container
                 .encrypt(plaintext.as_slice(), nonce)
                 .unwrap();
             let mut ptr = &mut ciphertext[..];
 
-            //let decrypted = bob_container.decrypt(ciphertext, nonce).unwrap();
             let decrypted = bob_container.decrypt(&mut ptr, nonce).unwrap();
             assert_eq!(plaintext.as_slice(), decrypted);
 
@@ -123,11 +122,35 @@ mod tests {
             let decrypted = alice_container.decrypt(&mut ptr, nonce).unwrap();
 
             assert_eq!(plaintext.as_slice(), decrypted);
+
+            // test local encryption
+            local_encryption(&alice_container, &bob_container, &plaintext, nonce);
         }
 
         Ok(())
     }
 
+    fn local_encryption(
+        alice_container: &PostQuantumContainer,
+        bob_container: &PostQuantumContainer,
+        plaintext: impl AsRef<[u8]>,
+        nonce: impl AsRef<[u8]>,
+    ) {
+        let plaintext = plaintext.as_ref();
+        let nonce = nonce.as_ref();
+        // alice wants to save her contents on bob's machine
+        let ciphertext = alice_container.local_encrypt(plaintext, nonce).unwrap();
+        assert_ne!(&ciphertext, plaintext);
+        // Alice sends her ciphertext remotely to bob. Bob receives ciphertext
+        // prove that bob cannot get the valid ciphertext
+        assert!(bob_container.local_decrypt(&ciphertext, nonce).is_err());
+        assert!(bob_container.decrypt(&ciphertext, nonce).is_err());
+        // Alice now wants the ciphertext back
+        // prove that alice can still decrypt her data
+        let decrypted = alice_container.local_decrypt(ciphertext, nonce).unwrap();
+        assert_eq!(plaintext, decrypted);
+    }
+
     #[test]
     fn in_place_sequential() {
         const HEADER_LEN: usize = 50;