citadel_sdk/
fs.rs

1//! Remote Encrypted Virtual Filesystem (RE-VFS)
2//!
3//! This module provides high-level operations for interacting with the Remote Encrypted
4//! Virtual Filesystem in the Citadel Protocol. RE-VFS enables secure storage and
5//! retrieval of files with end-to-end encryption.
6//!
7//! # Features
8//! - Secure file storage and retrieval
9//! - Configurable security levels
10//! - Automatic encryption handling
11//! - Virtual path management
12//! - File deletion support
13//! - Take operations for atomic reads
14//!
15//! # Example
16//! ```rust
17//! use citadel_sdk::prelude::*;
18//! use citadel_sdk::fs;
19//!
20//! async fn store_file<R: Ratchet>(remote: &impl TargetLockedRemote<R>) -> Result<(), NetworkError> {
21//!     // Write a file to RE-VFS
22//!     fs::write(remote, "/local/file.txt", "/virtual/file.txt").await?;
23//!
24//!     // Read the file back
25//!     let local_path = fs::read(remote, "/virtual/file.txt").await?;
26//!
27//!     // Delete the file
28//!     fs::delete(remote, "/virtual/file.txt").await?;
29//!
30//!     Ok(())
31//! }
32//! ```
33//!
34//! # Important Notes
35//! - All operations are end-to-end encrypted
36//! - Security levels are configurable per operation
37//! - Take operations atomically read and delete
38//! - Virtual paths are independent of local paths
39//!
40//! # Related Components
41//! - [`TargetLockedRemote`]: Remote connection interface
42//! - [`SecurityLevel`]: Encryption strength configuration
43//! - [`ObjectSource`]: File source abstraction
44//! - [`NetworkError`]: Error handling
45//!
46//! [`TargetLockedRemote`]: crate::prelude::TargetLockedRemote
47//! [`SecurityLevel`]: crate::prelude::SecurityLevel
48//! [`ObjectSource`]: crate::prelude::ObjectSource
49//! [`NetworkError`]: crate::prelude::NetworkError
50//!
51
52use crate::prelude::{ObjectSource, ProtocolRemoteTargetExt, TargetLockedRemote};
53
54use citadel_proto::prelude::NetworkError;
55use citadel_proto::prelude::*;
56use citadel_types::crypto::SecurityLevel;
57use std::path::PathBuf;
58
59/// Writes a file or BytesSource to the Remote Encrypted Virtual Filesystem
60pub async fn write<T: ObjectSource, P: Into<PathBuf> + Send, R: Ratchet>(
61    remote: &impl TargetLockedRemote<R>,
62    source: T,
63    virtual_path: P,
64) -> Result<(), NetworkError> {
65    write_with_security_level(remote, source, Default::default(), virtual_path).await
66}
67
68/// Writes a file or BytesSource to the Remote Encrypted Virtual Filesystem with a custom security level.
69pub async fn write_with_security_level<T: ObjectSource, P: Into<PathBuf> + Send, R: Ratchet>(
70    remote: &impl TargetLockedRemote<R>,
71    source: T,
72    security_level: SecurityLevel,
73    virtual_path: P,
74) -> Result<(), NetworkError> {
75    remote
76        .remote_encrypted_virtual_filesystem_push(source, virtual_path, security_level)
77        .await
78}
79
80/// Reads a file from the Remote Encrypted Virtual Filesystem
81pub async fn read<P: Into<PathBuf> + Send, R: Ratchet>(
82    remote: &impl TargetLockedRemote<R>,
83    virtual_path: P,
84) -> Result<PathBuf, NetworkError> {
85    read_with_security_level(remote, Default::default(), virtual_path).await
86}
87
88/// Reads a file from the Remote Encrypted Virtual Filesystem with a custom transport security level
89pub async fn read_with_security_level<P: Into<PathBuf> + Send, R: Ratchet>(
90    remote: &impl TargetLockedRemote<R>,
91    transfer_security_level: SecurityLevel,
92    virtual_path: P,
93) -> Result<PathBuf, NetworkError> {
94    remote
95        .remote_encrypted_virtual_filesystem_pull(virtual_path, transfer_security_level, false)
96        .await
97}
98
99/// Takes a file from the Remote Encrypted Virtual Filesystem
100pub async fn take<P: Into<PathBuf> + Send, R: Ratchet>(
101    remote: &impl TargetLockedRemote<R>,
102    virtual_path: P,
103) -> Result<PathBuf, NetworkError> {
104    remote
105        .remote_encrypted_virtual_filesystem_pull(virtual_path, Default::default(), true)
106        .await
107}
108
109/// Takes a file from the Remote Encrypted Virtual Filesystem with a custom security level.
110pub async fn take_with_security_level<P: Into<PathBuf> + Send, R: Ratchet>(
111    remote: &impl TargetLockedRemote<R>,
112    transfer_security_level: SecurityLevel,
113    virtual_path: P,
114) -> Result<PathBuf, NetworkError> {
115    remote
116        .remote_encrypted_virtual_filesystem_pull(virtual_path, transfer_security_level, true)
117        .await
118}
119
120/// Deletes a file from the Remote Encrypted Virtual Filesystem
121pub async fn delete<P: Into<PathBuf> + Send, R: Ratchet>(
122    remote: &impl TargetLockedRemote<R>,
123    virtual_path: P,
124) -> Result<(), NetworkError> {
125    remote
126        .remote_encrypted_virtual_filesystem_delete(virtual_path)
127        .await
128}
129
130#[cfg(test)]
131mod tests {
132    use crate::prefabs::client::single_connection::SingleClientServerConnectionKernel;
133    use crate::prefabs::server::accept_file_transfer_kernel::AcceptFileTransferKernel;
134
135    use crate::prefabs::client::peer_connection::{FileTransferHandleRx, PeerConnectionKernel};
136    use crate::prefabs::client::DefaultServerConnectionSettingsBuilder;
137    use crate::prelude::*;
138    use crate::test_common::wait_for_peers;
139    use citadel_io::tokio;
140    use futures::StreamExt;
141    use rstest::rstest;
142    use std::net::SocketAddr;
143    use std::path::PathBuf;
144    use std::sync::atomic::{AtomicBool, Ordering};
145    use std::time::Duration;
146    use uuid::Uuid;
147
148    pub fn server_info<'a, R: Ratchet>() -> (NodeFuture<'a, AcceptFileTransferKernel<R>>, SocketAddr)
149    {
150        crate::test_common::server_test_node(AcceptFileTransferKernel::<R>::default(), |_| {})
151    }
152
153    #[rstest]
154    #[case(
155        EncryptionAlgorithm::AES_GCM_256,
156        KemAlgorithm::Kyber,
157        SigAlgorithm::None
158    )]
159    #[case(
160        EncryptionAlgorithm::KyberHybrid,
161        KemAlgorithm::Kyber,
162        SigAlgorithm::Dilithium65
163    )]
164    #[timeout(Duration::from_secs(90))]
165    #[citadel_io::tokio::test]
166    async fn test_c2s_file_transfer_revfs(
167        #[case] enx: EncryptionAlgorithm,
168        #[case] kem: KemAlgorithm,
169        #[case] sig: SigAlgorithm,
170        #[values(SecurityLevel::Standard, SecurityLevel::Reinforced)] security_level: SecurityLevel,
171    ) {
172        citadel_logging::setup_log();
173        let client_success = &AtomicBool::new(false);
174        let (server, server_addr) = server_info::<StackedRatchet>();
175        let uuid = Uuid::new_v4();
176
177        let source_dir = PathBuf::from("../resources/TheBridge.pdf");
178
179        let session_security_settings = SessionSecuritySettingsBuilder::default()
180            .with_crypto_params(enx + kem + sig)
181            .with_security_level(security_level)
182            .build()
183            .unwrap();
184
185        let server_connection_settings =
186            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
187                .disable_udp()
188                .with_session_security_settings(session_security_settings)
189                .build()
190                .unwrap();
191
192        let client_kernel = SingleClientServerConnectionKernel::new(
193            server_connection_settings,
194            |connection| async move {
195                log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
196                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
197                // write to file to the RE-VFS
198                crate::fs::write_with_security_level(
199                    &connection.remote,
200                    source_dir.clone(),
201                    security_level,
202                    &virtual_path,
203                )
204                .await?;
205                log::info!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
206                // now, pull it
207                let save_dir = crate::fs::read(&connection.remote, virtual_path).await?;
208                // now, compare bytes
209                log::info!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
210                let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
211                let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
212                assert_eq!(original_bytes, revfs_pulled_bytes);
213                log::info!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
214                client_success.store(true, Ordering::Relaxed);
215                connection.shutdown_kernel().await
216            },
217        );
218
219        let client = DefaultNodeBuilder::default().build(client_kernel).unwrap();
220
221        let result = citadel_io::tokio::select! {
222            res0 = client => res0.map(|_| ()),
223            res1 = server => res1.map(|_| ())
224        };
225
226        result.unwrap();
227
228        assert!(client_success.load(Ordering::Relaxed));
229    }
230
231    #[rstest]
232    #[case(
233        EncryptionAlgorithm::AES_GCM_256,
234        KemAlgorithm::Kyber,
235        SigAlgorithm::None
236    )]
237    #[timeout(std::time::Duration::from_secs(90))]
238    #[citadel_io::tokio::test]
239    async fn test_c2s_file_transfer_revfs_take(
240        #[case] enx: EncryptionAlgorithm,
241        #[case] kem: KemAlgorithm,
242        #[case] sig: SigAlgorithm,
243        #[values(SecurityLevel::Standard)] security_level: SecurityLevel,
244    ) {
245        citadel_logging::setup_log();
246        let client_success = &AtomicBool::new(false);
247        let (server, server_addr) = server_info::<StackedRatchet>();
248        let uuid = Uuid::new_v4();
249
250        let source_dir = PathBuf::from("../resources/TheBridge.pdf");
251
252        let session_security_settings = SessionSecuritySettingsBuilder::default()
253            .with_crypto_params(enx + kem + sig)
254            .with_security_level(security_level)
255            .build()
256            .unwrap();
257
258        let server_connection_settings =
259            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
260                .disable_udp()
261                .with_session_security_settings(session_security_settings)
262                .build()
263                .unwrap();
264
265        let client_kernel = SingleClientServerConnectionKernel::new(
266            server_connection_settings,
267            |connection| async move {
268                log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
269                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
270                // write to file to the RE-VFS
271                crate::fs::write_with_security_level(
272                    &connection.remote,
273                    source_dir.clone(),
274                    security_level,
275                    &virtual_path,
276                )
277                .await?;
278                log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
279                // now, pull it
280                let save_dir = crate::fs::take(&connection.remote, &virtual_path).await?;
281                // now, compare bytes
282                log::trace!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
283                let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
284                let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
285                assert_eq!(original_bytes, revfs_pulled_bytes);
286                log::trace!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
287                // prove we can no longer read from this virtual file
288                assert!(crate::fs::read(&connection.remote, &virtual_path)
289                    .await
290                    .is_err());
291                client_success.store(true, Ordering::Relaxed);
292                connection.shutdown_kernel().await
293            },
294        );
295
296        let client = DefaultNodeBuilder::default().build(client_kernel).unwrap();
297
298        let result = citadel_io::tokio::select! {
299            res0 = client => res0.map(|_| ()),
300            res1 = server => res1.map(|_| ())
301        };
302
303        result.unwrap();
304
305        assert!(client_success.load(Ordering::Relaxed));
306    }
307
308    #[rstest]
309    #[case(
310        EncryptionAlgorithm::AES_GCM_256,
311        KemAlgorithm::Kyber,
312        SigAlgorithm::None
313    )]
314    #[timeout(std::time::Duration::from_secs(90))]
315    #[citadel_io::tokio::test]
316    async fn test_c2s_file_transfer_revfs_delete(
317        #[case] enx: EncryptionAlgorithm,
318        #[case] kem: KemAlgorithm,
319        #[case] sig: SigAlgorithm,
320        #[values(SecurityLevel::Standard)] security_level: SecurityLevel,
321    ) {
322        citadel_logging::setup_log();
323        let client_success = &AtomicBool::new(false);
324        let (server, server_addr) = server_info::<StackedRatchet>();
325        let uuid = Uuid::new_v4();
326
327        let source_dir = PathBuf::from("../resources/TheBridge.pdf");
328
329        let session_security_settings = SessionSecuritySettingsBuilder::default()
330            .with_crypto_params(enx + kem + sig)
331            .with_security_level(security_level)
332            .build()
333            .unwrap();
334
335        let server_connection_settings =
336            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
337                .disable_udp()
338                .with_session_security_settings(session_security_settings)
339                .build()
340                .unwrap();
341
342        let client_kernel = SingleClientServerConnectionKernel::new(
343            server_connection_settings,
344            |connection| async move {
345                log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
346                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
347                // write to file to the RE-VFS
348                crate::fs::write_with_security_level(
349                    &connection.remote,
350                    source_dir.clone(),
351                    security_level,
352                    &virtual_path,
353                )
354                .await?;
355                log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
356                // now, pull it
357                let save_dir = crate::fs::read(&connection.remote, &virtual_path).await?;
358                // now, compare bytes
359                log::trace!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
360                let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
361                let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
362                assert_eq!(original_bytes, revfs_pulled_bytes);
363                log::trace!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
364                crate::fs::delete(&connection.remote, &virtual_path).await?;
365                // prove we can no longer read from this virtual file since it was just deleted
366                assert!(crate::fs::read(&connection.remote, &virtual_path)
367                    .await
368                    .is_err());
369                client_success.store(true, Ordering::Relaxed);
370                connection.shutdown_kernel().await
371            },
372        );
373
374        let client = DefaultNodeBuilder::default().build(client_kernel).unwrap();
375
376        let result = citadel_io::tokio::select! {
377            res0 = client => res0.map(|_| ()),
378            res1 = server => res1.map(|_| ())
379        };
380
381        result.unwrap();
382
383        assert!(client_success.load(Ordering::Relaxed));
384    }
385
386    #[rstest]
387    #[case(SecrecyMode::BestEffort)]
388    #[timeout(Duration::from_secs(60))]
389    #[citadel_io::tokio::test(flavor = "multi_thread")]
390    async fn test_p2p_file_transfer_revfs(
391        #[case] secrecy_mode: SecrecyMode,
392        #[values(KemAlgorithm::Kyber)] kem: KemAlgorithm,
393        #[values(EncryptionAlgorithm::AES_GCM_256)] enx: EncryptionAlgorithm,
394    ) {
395        citadel_logging::setup_log();
396        crate::test_common::TestBarrier::setup(2);
397        let client0_success = &AtomicBool::new(false);
398        let client1_success = &AtomicBool::new(false);
399
400        let (server, server_addr) = crate::test_common::server_info::<StackedRatchet>();
401
402        let uuid0 = Uuid::new_v4();
403        let uuid1 = Uuid::new_v4();
404        let session_security = SessionSecuritySettingsBuilder::default()
405            .with_secrecy_mode(secrecy_mode)
406            .with_crypto_params(kem + enx)
407            .build()
408            .unwrap();
409
410        let security_level = SecurityLevel::Standard;
411
412        let source_dir = &PathBuf::from("../resources/TheBridge.pdf");
413
414        let server_connection_settings =
415            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid0)
416                .disable_udp()
417                .with_session_security_settings(session_security)
418                .build()
419                .unwrap();
420
421        let peer_conn_0 = PeerConnectionSetupAggregator::default()
422            .with_peer_custom(uuid1)
423            .ensure_registered()
424            .with_session_security_settings(session_security)
425            .enable_udp()
426            .add();
427
428        // TODO: SinglePeerConnectionKernel
429        let client_kernel0 = PeerConnectionKernel::new(
430            server_connection_settings,
431            peer_conn_0,
432            move |mut connection, remote_outer| async move {
433                wait_for_peers().await;
434                let mut connection = connection.recv().await.unwrap()?;
435                let cid = connection.channel.get_session_cid();
436                wait_for_peers().await;
437                // The other peer will send the file first
438                log::info!(target: "citadel", "***CLIENT A {cid} LOGIN SUCCESS :: File transfer next ***");
439                let remote = connection.remote.clone();
440                let handle_orig = connection.incoming_object_transfer_handles.take().unwrap();
441                let _file_transfer_task = accept_all(handle_orig);
442
443                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
444                // write the file to the RE-VFS
445                crate::fs::write_with_security_level(
446                    &remote,
447                    source_dir.clone(),
448                    security_level,
449                    &virtual_path,
450                )
451                .await?;
452                log::info!(target: "citadel", "***CLIENT A {cid} FILE TRANSFER SUCCESS***");
453                tokio::time::sleep(Duration::from_secs(1)).await;
454                wait_for_peers().await;
455                // now, pull it
456                let save_dir = crate::fs::read(&remote, virtual_path).await?;
457                // now, compare bytes
458                log::info!(target: "citadel", "***CLIENT A {cid} REVFS PULL SUCCESS");
459                let original_bytes = tokio::fs::read(&source_dir).await.unwrap();
460                let revfs_pulled_bytes = tokio::fs::read(&save_dir).await.unwrap();
461                assert_eq!(original_bytes, revfs_pulled_bytes);
462                log::info!(target: "citadel", "***CLIENT A {cid} REVFS PULL COMPARE SUCCESS");
463                wait_for_peers().await;
464                client0_success.store(true, Ordering::Relaxed);
465                remote_outer.shutdown_kernel().await
466            },
467        );
468
469        let server_connection_settings =
470            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid1)
471                .disable_udp()
472                .with_session_security_settings(session_security)
473                .build()
474                .unwrap();
475
476        let peer_conn_1 = PeerConnectionSetupAggregator::default()
477            .with_peer_custom(uuid0)
478            .ensure_registered()
479            .with_session_security_settings(session_security)
480            .enable_udp()
481            .add();
482
483        let client_kernel1 = PeerConnectionKernel::new(
484            server_connection_settings,
485            peer_conn_1,
486            move |mut connection, remote_outer| async move {
487                wait_for_peers().await;
488                let mut connection = connection.recv().await.unwrap()?;
489                let cid = connection.channel.get_session_cid();
490                wait_for_peers().await;
491                let remote = connection.remote.clone();
492                let handle_orig = connection.incoming_object_transfer_handles.take().unwrap();
493                let _file_transfer_task = accept_all(handle_orig);
494                log::info!(target: "citadel", "***CLIENT B {cid} LOGIN SUCCESS :: File transfer next ***");
495                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
496                // write the file to the RE-VFS
497                crate::fs::write_with_security_level(
498                    &remote,
499                    source_dir.clone(),
500                    security_level,
501                    &virtual_path,
502                )
503                .await?;
504                log::info!(target: "citadel", "***CLIENT B {cid} FILE TRANSFER SUCCESS***");
505                // Wait some time for the file to synchronize
506                tokio::time::sleep(Duration::from_secs(1)).await;
507                wait_for_peers().await;
508                // now, pull it
509                let save_dir = crate::fs::read(&remote, virtual_path).await?;
510                // now, compare bytes
511                log::info!(target: "citadel", "***CLIENT B {cid} REVFS PULL SUCCESS");
512                let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
513                let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
514                assert_eq!(original_bytes, revfs_pulled_bytes);
515                log::info!(target: "citadel", "***CLIENT B {cid} REVFS PULL COMPARE SUCCESS");
516                wait_for_peers().await;
517                client1_success.store(true, Ordering::Relaxed);
518                remote_outer.shutdown_kernel().await
519            },
520        );
521
522        let client0 = DefaultNodeBuilder::default().build(client_kernel0).unwrap();
523        let client1 = DefaultNodeBuilder::default().build(client_kernel1).unwrap();
524        let clients = futures::future::try_join(client0, client1);
525
526        let task = async move {
527            citadel_io::tokio::select! {
528                server_res = server => Err(NetworkError::msg(format!("Server ended prematurely: {:?}", server_res.map(|_| ())))),
529                client_res = clients => client_res.map(|_| ())
530            }
531        };
532
533        let _ = citadel_io::tokio::time::timeout(Duration::from_secs(120), task)
534            .await
535            .unwrap();
536
537        assert!(client0_success.load(Ordering::Relaxed));
538        assert!(client1_success.load(Ordering::Relaxed));
539    }
540
541    fn accept_all(mut rx: FileTransferHandleRx) -> citadel_io::tokio::task::JoinHandle<()> {
542        citadel_io::tokio::task::spawn(async move {
543            while let Some(mut handle) = rx.recv().await {
544                if let Err(err) = handle.accept() {
545                    log::error!(target: "citadel", "Failed to accept file transfer: {err:?}");
546                    continue;
547                }
548
549                // Wait for this transfer to complete before accepting the next one
550                exhaust_file_transfer_async(handle).await;
551            }
552        })
553    }
554
555    async fn exhaust_file_transfer_async(mut handle: ObjectTransferHandler) {
556        while let Some(evt) = handle.next().await {
557            log::info!(target: "citadel", "File Transfer Event: {evt:?}");
558            if let ObjectTransferStatus::Fail(err) = &evt {
559                log::error!(target: "citadel", "File Transfer Failed: {err:?}");
560                break;
561            } else if let ObjectTransferStatus::TransferComplete = &evt {
562                break;
563            } else if let ObjectTransferStatus::ReceptionComplete = &evt {
564                break;
565            }
566        }
567    }
568}