@@ -100,6 +100,14 @@ <h3>Tagging</h3>
100100 < li > < a href ="#delete-tagging "> Delete Object Tagging</ a > </ li >
101101 </ ul >
102102 </ div >
103+ < div class ="nav-section ">
104+ < h3 > Client-Side Encryption</ h3 >
105+ < ul >
106+ < li > < a href ="#encryption-headers "> Encryption Headers</ a > </ li >
107+ < li > < a href ="#metadata-privacy "> Metadata Privacy</ a > </ li >
108+ < li > < a href ="#secure-sharing-api "> Secure Sharing</ a > </ li >
109+ </ ul >
110+ </ div >
103111 </ nav >
104112
105113 < main class ="content ">
@@ -949,6 +957,216 @@ <h3>Endpoint</h3>
949957 </ div >
950958 </ section >
951959
960+ <!-- Client-Side Encryption -->
961+ < section id ="encryption-headers " class ="endpoint ">
962+ < div class ="endpoint-content ">
963+ < div class ="description ">
964+ < h2 > 🔐 Client-Side Encryption Headers</ h2 >
965+ < p > When using client-side encryption, Fula stores encryption metadata in custom headers.</ p >
966+
967+ < h3 > Encryption Metadata Header</ h3 >
968+ < p > The < code > x-amz-meta-encryption</ code > header contains JSON with encryption info:</ p >
969+
970+ < h3 > Fields</ h3 >
971+ < ul >
972+ < li > < strong > version</ strong > - Encryption format version (currently 2)</ li >
973+ < li > < strong > algorithm</ strong > - Cipher used (AES-256-GCM)</ li >
974+ < li > < strong > nonce</ strong > - Base64-encoded nonce</ li >
975+ < li > < strong > wrapped_key</ strong > - HPKE-encrypted DEK</ li >
976+ < li > < strong > metadata_privacy</ strong > - Whether private metadata is included</ li >
977+ < li > < strong > private_metadata</ strong > - Encrypted original filename, size, etc.</ li >
978+ </ ul >
979+ </ div >
980+ < div class ="example ">
981+ < div class ="example-header ">
982+ < span class ="lang-label "> Encryption Metadata Structure</ span >
983+ < button class ="copy-btn " onclick ="copyCode(this) "> Copy</ button >
984+ </ div >
985+ < pre > < code class ="language-json "> {
986+ "version": 2,
987+ "algorithm": "AES-256-GCM",
988+ "nonce": "base64_encoded_nonce",
989+ "wrapped_key": {
990+ "encapsulated_key": "base64_hpke_encapsulated_key",
991+ "ciphertext": "base64_encrypted_dek",
992+ "nonce": "base64_inner_nonce"
993+ },
994+ "metadata_privacy": true,
995+ "private_metadata": "{\"version\":1,\"ciphertext\":\"...\",\"nonce\":\"...\"}"
996+ }</ code > </ pre >
997+
998+ < div class ="example-header ">
999+ < span class ="lang-label "> Head Request to Get Metadata</ span >
1000+ < button class ="copy-btn " onclick ="copyCode(this) "> Copy</ button >
1001+ </ div >
1002+ < pre > < code class ="language-bash "> # Get metadata without downloading content
1003+ curl -I "http://localhost:9000/my-bucket/e/a7c3f9b2e8d14a6f" \
1004+ -H "Authorization: Bearer $TOKEN"
1005+
1006+ # Response headers include:
1007+ # x-amz-meta-encrypted: true
1008+ # x-amz-meta-encryption: {"version":2,"algorithm":"AES-256-GCM",...}
1009+ # Content-Type: application/octet-stream
1010+ # Content-Length: 156821 (ciphertext size)</ code > </ pre >
1011+ </ div >
1012+ </ div >
1013+ </ section >
1014+
1015+ < section id ="metadata-privacy " class ="endpoint ">
1016+ < div class ="endpoint-content ">
1017+ < div class ="description ">
1018+ < h2 > 🔒 Metadata Privacy</ h2 >
1019+ < p > With metadata privacy enabled, the server never sees your real filenames, sizes, or content types.</ p >
1020+
1021+ < h3 > What Gets Obfuscated</ h3 >
1022+ < table style ="width:100%; margin: 1rem 0; border-collapse: collapse; ">
1023+ < tr style ="background: var(--bg-tertiary); ">
1024+ < th style ="padding: 0.5rem; border: 1px solid var(--border-color); "> Field</ th >
1025+ < th style ="padding: 0.5rem; border: 1px solid var(--border-color); "> Server Sees</ th >
1026+ < th style ="padding: 0.5rem; border: 1px solid var(--border-color); "> Client Decrypts</ th >
1027+ </ tr >
1028+ < tr >
1029+ < td style ="padding: 0.5rem; border: 1px solid var(--border-color); "> Key (filename)</ td >
1030+ < td style ="padding: 0.5rem; border: 1px solid var(--border-color); "> < code > e/a7c3f9b2e8d14a6f</ code > </ td >
1031+ < td style ="padding: 0.5rem; border: 1px solid var(--border-color); "> < code > /finances/tax_2024.pdf</ code > </ td >
1032+ </ tr >
1033+ < tr >
1034+ < td style ="padding: 0.5rem; border: 1px solid var(--border-color); "> Size</ td >
1035+ < td style ="padding: 0.5rem; border: 1px solid var(--border-color); "> 156,821 (ciphertext)</ td >
1036+ < td style ="padding: 0.5rem; border: 1px solid var(--border-color); "> 156,789 (original)</ td >
1037+ </ tr >
1038+ < tr >
1039+ < td style ="padding: 0.5rem; border: 1px solid var(--border-color); "> Content-Type</ td >
1040+ < td style ="padding: 0.5rem; border: 1px solid var(--border-color); "> < code > application/octet-stream</ code > </ td >
1041+ < td style ="padding: 0.5rem; border: 1px solid var(--border-color); "> < code > application/pdf</ code > </ td >
1042+ </ tr >
1043+ < tr >
1044+ < td style ="padding: 0.5rem; border: 1px solid var(--border-color); "> Timestamps</ td >
1045+ < td style ="padding: 0.5rem; border: 1px solid var(--border-color); "> Upload time only</ td >
1046+ < td style ="padding: 0.5rem; border: 1px solid var(--border-color); "> Original created/modified</ td >
1047+ </ tr >
1048+ </ table >
1049+
1050+ < h3 > Private Metadata Structure</ h3 >
1051+ < p > The < code > private_metadata</ code > field, when decrypted, contains:</ p >
1052+ </ div >
1053+ < div class ="example ">
1054+ < div class ="example-header ">
1055+ < span class ="lang-label "> Decrypted Private Metadata</ span >
1056+ < button class ="copy-btn " onclick ="copyCode(this) "> Copy</ button >
1057+ </ div >
1058+ < pre > < code class ="language-json "> {
1059+ "original_key": "/finances/tax_returns_2024.pdf",
1060+ "actual_size": 156789,
1061+ "content_type": "application/pdf",
1062+ "created_at": 1701388800,
1063+ "modified_at": 1701475200,
1064+ "user_metadata": {
1065+ "author": "John Doe",
1066+ "department": "Accounting"
1067+ },
1068+ "content_hash": "blake3_hash_of_plaintext"
1069+ }</ code > </ pre >
1070+
1071+ < div class ="example-header ">
1072+ < span class ="lang-label "> Key Obfuscation Modes</ span >
1073+ < button class ="copy-btn " onclick ="copyCode(this) "> Copy</ button >
1074+ </ div >
1075+ < pre > < code class ="language-plaintext "> # DeterministicHash (default)
1076+ # Same path → same storage key (allows retrieval)
1077+ /photos/beach.jpg → e/a7c3f9b2e8d14a6f
1078+
1079+ # RandomUuid
1080+ # Each upload gets random key (maximum privacy)
1081+ /photos/beach.jpg → e/550e8400-e29b-41d4-a716-446655440000
1082+
1083+ # PreserveStructure
1084+ # Keep folder paths, hash only filenames
1085+ /photos/beach.jpg → /photos/e_a7c3f9b2e8d1</ code > </ pre >
1086+ </ div >
1087+ </ div >
1088+ </ section >
1089+
1090+ < section id ="secure-sharing-api " class ="endpoint ">
1091+ < div class ="endpoint-content ">
1092+ < div class ="description ">
1093+ < h2 > 🤝 Secure Sharing API</ h2 >
1094+ < p > Share encrypted files without exposing your master key. Uses HPKE to re-encrypt the DEK for each recipient.</ p >
1095+
1096+ < h3 > Share Token Structure</ h3 >
1097+ < p > A share token is a JSON object containing:</ p >
1098+ < ul >
1099+ < li > < strong > share_id</ strong > - Unique identifier (random 16-byte hex)</ li >
1100+ < li > < strong > path_scope</ strong > - Folder path this share grants access to</ li >
1101+ < li > < strong > expires_at</ strong > - Unix timestamp when share expires (optional)</ li >
1102+ < li > < strong > permissions</ strong > - Read, write, delete flags</ li >
1103+ < li > < strong > encrypted_dek</ strong > - HPKE-encrypted DEK for recipient</ li >
1104+ < li > < strong > owner_public_key</ strong > - Owner's public key (for verification)</ li >
1105+ </ ul >
1106+
1107+ < h3 > Permissions</ h3 >
1108+ < ul >
1109+ < li > < code > read_only()</ code > - Can decrypt and read files</ li >
1110+ < li > < code > read_write()</ code > - Can read and upload new files</ li >
1111+ < li > < code > full_access()</ code > - Can read, write, and delete</ li >
1112+ </ ul >
1113+ </ div >
1114+ < div class ="example ">
1115+ < div class ="example-header ">
1116+ < span class ="lang-label "> Share Token JSON Structure</ span >
1117+ < button class ="copy-btn " onclick ="copyCode(this) "> Copy</ button >
1118+ </ div >
1119+ < pre > < code class ="language-json "> {
1120+ "share_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
1121+ "path_scope": "/photos/vacation/",
1122+ "created_at": 1701388800,
1123+ "expires_at": 1702252800,
1124+ "permissions": {
1125+ "read": true,
1126+ "write": false,
1127+ "delete": false
1128+ },
1129+ "encrypted_dek": {
1130+ "encapsulated_key": "base64...",
1131+ "ciphertext": "base64...",
1132+ "nonce": "base64..."
1133+ },
1134+ "owner_public_key": "base64_x25519_public_key"
1135+ }</ code > </ pre >
1136+
1137+ < div class ="example-header ">
1138+ < span class ="lang-label "> Sharing Workflow</ span >
1139+ < button class ="copy-btn " onclick ="copyCode(this) "> Copy</ button >
1140+ </ div >
1141+ < pre > < code class ="language-plaintext "> ┌─────────────────────────────────────────────────────────────────┐
1142+ │ SECURE SHARING FLOW │
1143+ ├─────────────────────────────────────────────────────────────────┤
1144+ │ │
1145+ │ 1. OWNER creates share: │
1146+ │ • Specify path scope: /photos/vacation/ │
1147+ │ • Set expiry: 7 days │
1148+ │ • Set permissions: read-only │
1149+ │ • Re-encrypt DEK for recipient's public key │
1150+ │ │
1151+ │ 2. OWNER sends token to RECIPIENT: │
1152+ │ • Via email, message, QR code, etc. │
1153+ │ • Owner's private key never leaves device │
1154+ │ │
1155+ │ 3. RECIPIENT accepts share: │
1156+ │ • Verify token signature │
1157+ │ • Check expiry │
1158+ │ • Decrypt DEK with own private key │
1159+ │ │
1160+ │ 4. RECIPIENT accesses files: │
1161+ │ • Fetch encrypted files from server │
1162+ │ • Decrypt with the shared DEK │
1163+ │ • Path checked against scope │
1164+ │ │
1165+ └─────────────────────────────────────────────────────────────────┘</ code > </ pre >
1166+ </ div >
1167+ </ div >
1168+ </ section >
1169+
9521170 < footer >
9531171 < p > Fula API Documentation • Built with ❤️ for decentralized storage</ p >
9541172 </ footer >
0 commit comments