@@ -13,8 +13,9 @@ use fula_crypto::{
1313 hpke:: { Encryptor , Decryptor , EncryptedData } ,
1414 symmetric:: { Aead , Nonce } ,
1515 private_metadata:: { PrivateMetadata , EncryptedPrivateMetadata , KeyObfuscation , obfuscate_key} ,
16+ private_forest:: { PrivateForest , EncryptedForest , ForestFileEntry , derive_index_key} ,
1617} ;
17- use std:: sync:: Arc ;
18+ use std:: sync:: { Arc , RwLock } ;
1819use std:: collections:: HashMap ;
1920
2021/// Configuration for client-side encryption
@@ -46,6 +47,23 @@ impl EncryptionConfig {
4647 }
4748 }
4849
50+ /// Create with FlatNamespace mode - RECOMMENDED for maximum privacy
51+ ///
52+ /// This mode provides complete structure hiding:
53+ /// - Storage keys look like random CID-style hashes (e.g., `QmX7a8f3...`)
54+ /// - No prefixes or structure hints visible to server
55+ /// - Server cannot determine folder structure or parent/child relationships
56+ /// - File tree is stored in an encrypted PrivateForest index
57+ ///
58+ /// Inspired by WNFS (WebNative File System) and Peergos.
59+ pub fn new_flat_namespace ( ) -> Self {
60+ Self {
61+ key_manager : Arc :: new ( KeyManager :: new ( ) ) ,
62+ metadata_privacy : true ,
63+ obfuscation_mode : KeyObfuscation :: FlatNamespace ,
64+ }
65+ }
66+
4967 /// Create from an existing secret key
5068 pub fn from_secret_key ( secret : fula_crypto:: keys:: SecretKey ) -> Self {
5169 Self {
@@ -98,13 +116,19 @@ impl Default for EncryptionConfig {
98116pub struct EncryptedClient {
99117 inner : FulaClient ,
100118 encryption : EncryptionConfig ,
119+ /// Private forest index for FlatNamespace mode (cached)
120+ forest_cache : RwLock < HashMap < String , PrivateForest > > ,
101121}
102122
103123impl EncryptedClient {
104124 /// Create a new encrypted client
105125 pub fn new ( config : Config , encryption : EncryptionConfig ) -> Result < Self > {
106126 let inner = FulaClient :: new ( config) ?;
107- Ok ( Self { inner, encryption } )
127+ Ok ( Self {
128+ inner,
129+ encryption,
130+ forest_cache : RwLock :: new ( HashMap :: new ( ) ) ,
131+ } )
108132 }
109133
110134 /// Get the underlying client
@@ -578,6 +602,11 @@ impl EncryptedClient {
578602 bucket : & str ,
579603 prefix : Option < & str > ,
580604 ) -> Result < DirectoryListing > {
605+ // For FlatNamespace, use the forest directly
606+ if self . encryption . obfuscation_mode == KeyObfuscation :: FlatNamespace {
607+ return self . list_directory_from_forest ( bucket, prefix) . await ;
608+ }
609+
581610 let options = prefix. map ( |p| ListObjectsOptions {
582611 prefix : Some ( p. to_string ( ) ) ,
583612 ..Default :: default ( )
@@ -603,6 +632,296 @@ impl EncryptedClient {
603632 directories,
604633 } )
605634 }
635+
636+ // ═══════════════════════════════════════════════════════════════════════════
637+ // FLATNAMESPACE / PRIVATE FOREST SUPPORT
638+ // Complete structure hiding - server sees only random CID-like hashes
639+ // ═══════════════════════════════════════════════════════════════════════════
640+
641+ /// Check if FlatNamespace mode is enabled
642+ pub fn is_flat_namespace ( & self ) -> bool {
643+ self . encryption . obfuscation_mode == KeyObfuscation :: FlatNamespace
644+ }
645+
646+ /// Load the private forest index for a bucket
647+ ///
648+ /// The forest contains the encrypted directory structure and path→storage_key mapping.
649+ /// This is only used in FlatNamespace mode.
650+ pub async fn load_forest ( & self , bucket : & str ) -> Result < PrivateForest > {
651+ // Check cache first
652+ {
653+ let cache = self . forest_cache . read ( ) . unwrap ( ) ;
654+ if let Some ( forest) = cache. get ( bucket) {
655+ return Ok ( forest. clone ( ) ) ;
656+ }
657+ }
658+
659+ // Derive the index key deterministically
660+ let path_dek = self . encryption . key_manager . generate_dek ( ) ;
661+ let index_key = derive_index_key ( & path_dek, bucket) ;
662+
663+ // Try to load from storage
664+ match self . inner . get_object_with_metadata ( bucket, & index_key) . await {
665+ Ok ( result) => {
666+ // Decrypt the forest
667+ let encrypted = EncryptedForest :: from_bytes ( & result. data )
668+ . map_err ( ClientError :: Encryption ) ?;
669+ let forest = encrypted. decrypt ( & path_dek)
670+ . map_err ( ClientError :: Encryption ) ?;
671+
672+ // Cache it
673+ {
674+ let mut cache = self . forest_cache . write ( ) . unwrap ( ) ;
675+ cache. insert ( bucket. to_string ( ) , forest. clone ( ) ) ;
676+ }
677+
678+ Ok ( forest)
679+ }
680+ Err ( _) => {
681+ // No forest exists yet - create empty one
682+ let forest = PrivateForest :: new ( ) ;
683+
684+ // Cache it
685+ {
686+ let mut cache = self . forest_cache . write ( ) . unwrap ( ) ;
687+ cache. insert ( bucket. to_string ( ) , forest. clone ( ) ) ;
688+ }
689+
690+ Ok ( forest)
691+ }
692+ }
693+ }
694+
695+ /// Save the private forest index for a bucket
696+ pub async fn save_forest ( & self , bucket : & str , forest : & PrivateForest ) -> Result < ( ) > {
697+ let path_dek = self . encryption . key_manager . generate_dek ( ) ;
698+ let index_key = derive_index_key ( & path_dek, bucket) ;
699+
700+ // Encrypt the forest
701+ let encrypted = EncryptedForest :: encrypt ( forest, & path_dek)
702+ . map_err ( ClientError :: Encryption ) ?;
703+ let data = encrypted. to_bytes ( )
704+ . map_err ( ClientError :: Encryption ) ?;
705+
706+ // Upload (the index looks like any other encrypted blob)
707+ let metadata = ObjectMetadata :: new ( )
708+ . with_content_type ( "application/octet-stream" )
709+ . with_metadata ( "x-fula-forest" , "true" ) ;
710+
711+ self . inner . put_object_with_metadata (
712+ bucket,
713+ & index_key,
714+ Bytes :: from ( data) ,
715+ Some ( metadata) ,
716+ ) . await ?;
717+
718+ // Update cache
719+ {
720+ let mut cache = self . forest_cache . write ( ) . unwrap ( ) ;
721+ cache. insert ( bucket. to_string ( ) , forest. clone ( ) ) ;
722+ }
723+
724+ Ok ( ( ) )
725+ }
726+
727+ /// Put an encrypted object using FlatNamespace mode
728+ ///
729+ /// This automatically updates the forest index.
730+ pub async fn put_object_flat (
731+ & self ,
732+ bucket : & str ,
733+ key : & str ,
734+ data : impl Into < Bytes > ,
735+ content_type : Option < & str > ,
736+ ) -> Result < PutObjectResult > {
737+ let data = data. into ( ) ;
738+ let original_size = data. len ( ) as u64 ;
739+
740+ // Load or create forest
741+ let mut forest = self . load_forest ( bucket) . await ?;
742+
743+ // Generate a DEK for this object
744+ let dek = self . encryption . key_manager . generate_dek ( ) ;
745+
746+ // Encrypt the data
747+ let nonce = Nonce :: generate ( ) ;
748+ let aead = Aead :: new_default ( & dek) ;
749+ let ciphertext = aead. encrypt ( & nonce, & data)
750+ . map_err ( ClientError :: Encryption ) ?;
751+
752+ // Generate flat storage key (no structure hints!)
753+ let storage_key = forest. generate_key ( key, & dek) ;
754+
755+ // Encrypt the DEK with HPKE
756+ let encryptor = Encryptor :: new ( self . encryption . public_key ( ) ) ;
757+ let wrapped_dek = encryptor. encrypt_dek ( & dek)
758+ . map_err ( ClientError :: Encryption ) ?;
759+
760+ // Create private metadata
761+ let private_meta = PrivateMetadata :: new ( key, original_size)
762+ . with_content_type ( content_type. unwrap_or ( "application/octet-stream" ) ) ;
763+
764+ let encrypted_meta = EncryptedPrivateMetadata :: encrypt ( & private_meta, & dek)
765+ . map_err ( ClientError :: Encryption ) ?;
766+
767+ // Add to forest index
768+ let entry = ForestFileEntry :: from_metadata ( & private_meta, storage_key. clone ( ) ) ;
769+ forest. upsert_file ( entry) ;
770+
771+ // Serialize encryption metadata
772+ let enc_metadata = serde_json:: json!( {
773+ "version" : 2 ,
774+ "algorithm" : "AES-256-GCM" ,
775+ "nonce" : base64:: Engine :: encode( & base64:: engine:: general_purpose:: STANDARD , nonce. as_bytes( ) ) ,
776+ "wrapped_key" : serde_json:: to_value( & wrapped_dek) . unwrap( ) ,
777+ "metadata_privacy" : true ,
778+ "obfuscation_mode" : "flat" ,
779+ "private_metadata" : encrypted_meta. to_json( ) . map_err( ClientError :: Encryption ) ?,
780+ } ) ;
781+
782+ // Upload the file (server sees only QmX7a8f3...)
783+ let metadata = ObjectMetadata :: new ( )
784+ . with_content_type ( "application/octet-stream" )
785+ . with_metadata ( "x-fula-encrypted" , "true" )
786+ . with_metadata ( "x-fula-encryption" , & enc_metadata. to_string ( ) ) ;
787+
788+ let result = self . inner . put_object_with_metadata (
789+ bucket,
790+ & storage_key,
791+ Bytes :: from ( ciphertext) ,
792+ Some ( metadata) ,
793+ ) . await ?;
794+
795+ // Save updated forest
796+ self . save_forest ( bucket, & forest) . await ?;
797+
798+ Ok ( result)
799+ }
800+
801+ /// Get an object using FlatNamespace mode
802+ ///
803+ /// Uses the forest index to resolve original path → storage key.
804+ pub async fn get_object_flat (
805+ & self ,
806+ bucket : & str ,
807+ key : & str ,
808+ ) -> Result < Bytes > {
809+ // Load forest to get the storage key
810+ let forest = self . load_forest ( bucket) . await ?;
811+
812+ let storage_key = forest. get_storage_key ( key)
813+ . ok_or_else ( || ClientError :: NotFound { bucket : bucket. to_string ( ) , key : key. to_string ( ) } ) ?;
814+
815+ self . get_object_decrypted_by_storage_key ( bucket, storage_key) . await
816+ }
817+
818+ /// List directory from forest index (FlatNamespace mode)
819+ ///
820+ /// This is much faster than HEAD requests because the forest already
821+ /// contains all metadata.
822+ async fn list_directory_from_forest (
823+ & self ,
824+ bucket : & str ,
825+ prefix : Option < & str > ,
826+ ) -> Result < DirectoryListing > {
827+ let forest = self . load_forest ( bucket) . await ?;
828+
829+ let prefix_str = prefix. unwrap_or ( "/" ) ;
830+ let files = forest. list_recursive ( prefix_str) ;
831+
832+ let mut directories: HashMap < String , Vec < FileMetadata > > = HashMap :: new ( ) ;
833+
834+ for entry in files {
835+ let dir = if let Some ( last_slash) = entry. path . rfind ( '/' ) {
836+ entry. path [ ..last_slash] . to_string ( )
837+ } else {
838+ "/" . to_string ( )
839+ } ;
840+
841+ let metadata = FileMetadata {
842+ storage_key : entry. storage_key . clone ( ) ,
843+ original_key : entry. path . clone ( ) ,
844+ original_size : entry. size ,
845+ content_type : entry. content_type . clone ( ) ,
846+ created_at : Some ( entry. created_at ) ,
847+ modified_at : Some ( entry. modified_at ) ,
848+ user_metadata : entry. user_metadata . clone ( ) ,
849+ is_encrypted : true ,
850+ } ;
851+
852+ directories. entry ( dir) . or_default ( ) . push ( metadata) ;
853+ }
854+
855+ Ok ( DirectoryListing {
856+ bucket : bucket. to_string ( ) ,
857+ prefix : prefix. map ( |s| s. to_string ( ) ) ,
858+ directories,
859+ } )
860+ }
861+
862+ /// List all files from forest (FlatNamespace mode)
863+ ///
864+ /// No network requests needed - uses cached/loaded forest index.
865+ pub async fn list_files_from_forest (
866+ & self ,
867+ bucket : & str ,
868+ ) -> Result < Vec < FileMetadata > > {
869+ let forest = self . load_forest ( bucket) . await ?;
870+
871+ let files: Vec < FileMetadata > = forest. list_all_files ( )
872+ . map ( |entry| FileMetadata {
873+ storage_key : entry. storage_key . clone ( ) ,
874+ original_key : entry. path . clone ( ) ,
875+ original_size : entry. size ,
876+ content_type : entry. content_type . clone ( ) ,
877+ created_at : Some ( entry. created_at ) ,
878+ modified_at : Some ( entry. modified_at ) ,
879+ user_metadata : entry. user_metadata . clone ( ) ,
880+ is_encrypted : true ,
881+ } )
882+ . collect ( ) ;
883+
884+ Ok ( files)
885+ }
886+
887+ /// Delete a file in FlatNamespace mode
888+ ///
889+ /// Removes from storage and updates forest index.
890+ pub async fn delete_object_flat (
891+ & self ,
892+ bucket : & str ,
893+ key : & str ,
894+ ) -> Result < ( ) > {
895+ let mut forest = self . load_forest ( bucket) . await ?;
896+
897+ // Get storage key before removing from forest
898+ let storage_key = forest. get_storage_key ( key)
899+ . ok_or_else ( || ClientError :: NotFound { bucket : bucket. to_string ( ) , key : key. to_string ( ) } ) ?
900+ . to_string ( ) ;
901+
902+ // Remove from storage
903+ self . inner . delete_object ( bucket, & storage_key) . await ?;
904+
905+ // Remove from forest
906+ forest. remove_file ( key) ;
907+
908+ // Save updated forest
909+ self . save_forest ( bucket, & forest) . await ?;
910+
911+ Ok ( ( ) )
912+ }
913+
914+ /// Get the private forest for sharing (extract subtree)
915+ ///
916+ /// This allows sharing a portion of your file tree with someone else.
917+ pub async fn get_forest_subtree (
918+ & self ,
919+ bucket : & str ,
920+ prefix : & str ,
921+ ) -> Result < PrivateForest > {
922+ let forest = self . load_forest ( bucket) . await ?;
923+ Ok ( forest. extract_subtree ( prefix) )
924+ }
606925}
607926
608927/// File metadata (without file content) - optimized for file managers
0 commit comments