|
43 | 43 | <div class="sidebar-header"> |
44 | 44 | <div class="logo"> |
45 | 45 | <h1>Fula API</h1> |
46 | | - <span class="version">v0.1.0</span> |
| 46 | + <span class="version">v0.3.6</span> |
47 | 47 | </div> |
48 | 48 | <button class="theme-toggle" aria-label="Toggle theme"> |
49 | 49 | <span class="icon-sun">☀️</span> |
@@ -982,45 +982,69 @@ <h2>🔐 Client-Side Encryption Headers</h2> |
982 | 982 | <h3>Encryption Metadata Header</h3> |
983 | 983 | <p>The <code>x-amz-meta-encryption</code> header contains JSON with encryption info:</p> |
984 | 984 |
|
985 | | - <h3>Fields</h3> |
| 985 | + <h3>Fields (content format v4, HPKE envelope v2)</h3> |
986 | 986 | <ul> |
987 | | - <li><strong>version</strong> - Encryption format version (currently 2)</li> |
988 | | - <li><strong>algorithm</strong> - Cipher used (AES-256-GCM)</li> |
989 | | - <li><strong>nonce</strong> - Base64-encoded nonce</li> |
990 | | - <li><strong>wrapped_key</strong> - HPKE-encrypted DEK</li> |
991 | | - <li><strong>metadata_privacy</strong> - Whether private metadata is included</li> |
992 | | - <li><strong>private_metadata</strong> - Encrypted original filename, size, etc.</li> |
| 987 | + <li><strong>version</strong> — Content AEAD format. <code>4</code> means the ciphertext is bound with AAD <code>fula:v4:content:{storage_key}</code> (single-object) or <code>fula:v4:chunk:{storage_key}:{index}</code> (chunked). Legacy value <code>2</code> means no content AAD.</li> |
| 988 | + <li><strong>algorithm</strong> — Content cipher (<code>AES-256-GCM</code> default, <code>ChaCha20-Poly1305</code> also supported).</li> |
| 989 | + <li><strong>nonce</strong> — Base64-encoded 12-byte nonce for the single-object path (absent for chunked; per-chunk nonces live inside <code>chunked.chunk_nonces</code>).</li> |
| 990 | + <li><strong>wrapped_key</strong> — HPKE envelope (RFC 9180). Contains its own <code>version</code> (<code>2</code> = RFC 9180 HPKE), <code>encapsulated_key.ephemeral_public</code> (32-byte X25519), <code>cipher</code> (<code>chacha20poly1305</code>), and <code>ciphertext</code> (48 bytes = 32-byte DEK + 16-byte tag). DEK-wrap AAD is <code>fula:v2:dek-wrap</code>.</li> |
| 991 | + <li><strong>kek_version</strong> — Which KEK rotation generation unwraps this DEK.</li> |
| 992 | + <li><strong>chunked</strong> — Present for files > 768 KB. <code>format: "streaming-v2"</code> for AAD-bound chunks, <code>chunk_size</code>, <code>num_chunks</code>, <code>total_size</code>, <code>root_hash</code> (BLAKE3/Bao hex), and <code>chunk_nonces</code> array.</li> |
| 993 | + <li><strong>metadata_privacy</strong> — Whether <code>private_metadata</code> is included.</li> |
| 994 | + <li><strong>private_metadata</strong> — <code>EncryptedPrivateMetadata</code> (original filename, size, content-type, user metadata, content_hash) encrypted under the DEK.</li> |
993 | 995 | </ul> |
994 | 996 | </div> |
995 | 997 | <div class="example"> |
996 | 998 | <div class="example-header"> |
997 | | - <span class="lang-label">Encryption Metadata Structure</span> |
| 999 | + <span class="lang-label">Encryption Metadata Structure (single-object, v4)</span> |
998 | 1000 | <button class="copy-btn" onclick="copyCode(this)">Copy</button> |
999 | 1001 | </div> |
1000 | 1002 | <pre><code class="language-json">{ |
1001 | | - "version": 2, |
| 1003 | + "version": 4, |
1002 | 1004 | "algorithm": "AES-256-GCM", |
1003 | | - "nonce": "base64_encoded_nonce", |
| 1005 | + "nonce": "<base64, 12 bytes>", |
1004 | 1006 | "wrapped_key": { |
1005 | | - "encapsulated_key": "base64_hpke_encapsulated_key", |
1006 | | - "ciphertext": "base64_encrypted_dek", |
1007 | | - "nonce": "base64_inner_nonce" |
| 1007 | + "version": 2, |
| 1008 | + "encapsulated_key": { "ephemeral_public": "<base64, 32 bytes>" }, |
| 1009 | + "cipher": "chacha20poly1305", |
| 1010 | + "ciphertext": "<base64, 48 bytes (32-byte DEK + 16-byte tag)>" |
1008 | 1011 | }, |
| 1012 | + "kek_version": 1, |
1009 | 1013 | "metadata_privacy": true, |
1010 | | - "private_metadata": "{\"version\":1,\"ciphertext\":\"...\",\"nonce\":\"...\"}" |
| 1014 | + "private_metadata": "<base64 encrypted PrivateMetadata>" |
| 1015 | +}</code></pre> |
| 1016 | + |
| 1017 | + <div class="example-header"> |
| 1018 | + <span class="lang-label">Encryption Metadata Structure (chunked, streaming-v2)</span> |
| 1019 | + <button class="copy-btn" onclick="copyCode(this)">Copy</button> |
| 1020 | + </div> |
| 1021 | + <pre><code class="language-json">{ |
| 1022 | + "version": 4, |
| 1023 | + "algorithm": "AES-256-GCM", |
| 1024 | + "wrapped_key": { "version": 2, "encapsulated_key": {"ephemeral_public": "..."}, "cipher": "chacha20poly1305", "ciphertext": "..." }, |
| 1025 | + "kek_version": 1, |
| 1026 | + "chunked": { |
| 1027 | + "format": "streaming-v2", |
| 1028 | + "chunk_size": 262144, |
| 1029 | + "num_chunks": 10, |
| 1030 | + "total_size": 2621440, |
| 1031 | + "root_hash": "<hex, 32-byte Bao root>", |
| 1032 | + "chunk_nonces": ["<base64>", "<base64>", "..."] |
| 1033 | + } |
1011 | 1034 | }</code></pre> |
1012 | 1035 |
|
1013 | 1036 | <div class="example-header"> |
1014 | 1037 | <span class="lang-label">Head Request to Get Metadata</span> |
1015 | 1038 | <button class="copy-btn" onclick="copyCode(this)">Copy</button> |
1016 | 1039 | </div> |
1017 | 1040 | <pre><code class="language-bash"># Get metadata without downloading content |
1018 | | -curl -I "http://localhost:9000/my-bucket/e/a7c3f9b2e8d14a6f" \ |
| 1041 | +curl -I "http://localhost:9000/my-bucket/Qm0bb7a3f40dcd6057bd5abd04d2aa80f68680ea56a978" \ |
1019 | 1042 | -H "Authorization: Bearer $TOKEN" |
1020 | 1043 |
|
1021 | 1044 | # Response headers include: |
1022 | | -# x-amz-meta-encrypted: true |
1023 | | -# x-amz-meta-encryption: {"version":2,"algorithm":"AES-256-GCM",...} |
| 1045 | +# x-amz-meta-x-fula-encrypted: true |
| 1046 | +# x-amz-meta-x-fula-encryption: {"version":4,"algorithm":"AES-256-GCM",...} |
| 1047 | +# x-amz-meta-x-fula-chunked: true (if chunked) |
1024 | 1048 | # Content-Type: application/octet-stream |
1025 | 1049 | # Content-Length: 156821 (ciphertext size)</code></pre> |
1026 | 1050 | </div> |
@@ -1109,44 +1133,52 @@ <h2>🤝 Secure Sharing API</h2> |
1109 | 1133 | <p>Share encrypted files without exposing your master key. Uses HPKE to re-encrypt the DEK for each recipient.</p> |
1110 | 1134 |
|
1111 | 1135 | <h3>Share Token Structure</h3> |
1112 | | - <p>A share token is a JSON object containing:</p> |
| 1136 | + <p>A share token is a JSON object containing (see <code>crates/fula-crypto/src/sharing.rs::ShareToken</code>):</p> |
1113 | 1137 | <ul> |
1114 | | - <li><strong>share_id</strong> - Unique identifier (random 16-byte hex)</li> |
1115 | | - <li><strong>path_scope</strong> - Folder path this share grants access to</li> |
1116 | | - <li><strong>expires_at</strong> - Unix timestamp when share expires (optional)</li> |
1117 | | - <li><strong>permissions</strong> - Read, write, delete flags</li> |
1118 | | - <li><strong>encrypted_dek</strong> - HPKE-encrypted DEK for recipient</li> |
1119 | | - <li><strong>owner_public_key</strong> - Owner's public key (for verification)</li> |
| 1138 | + <li><strong>id</strong> — 16-byte hex identifier.</li> |
| 1139 | + <li><strong>version</strong> — Share token envelope version (<code>SHARE_TOKEN_AAD_V5</code> as of v0.3.x; bumped when the AAD binding shape changes).</li> |
| 1140 | + <li><strong>path_scope</strong> — Scope the share grants access to. <code>is_valid_for_path(p)</code> accepts any <code>p</code> that <em>starts with</em> <code>path_scope</code>. For FxFiles-style single-file shares this is the file's <code>storage_key</code> (CID).</li> |
| 1141 | + <li><strong>wrapped_key</strong> — HPKE-wrapped DEK for the recipient (same envelope shape as the storage metadata's <code>wrapped_key</code>; DEK-wrap AAD here is domain-separated by the token body so any mutation of other fields invalidates the unwrap).</li> |
| 1142 | + <li><strong>permissions</strong> — <code>can_read</code>, <code>can_write</code>, <code>can_delete</code> flags.</li> |
| 1143 | + <li><strong>mode</strong> — <code>Temporal</code> (resolves to the latest version under the path, the default) or <code>Snapshot</code> (locked to <code>snapshot_binding</code>).</li> |
| 1144 | + <li><strong>snapshot_binding</strong> (optional, only in Snapshot mode) — BLAKE3 content hash, size, modified_at, and storage_key captured at share time.</li> |
| 1145 | + <li><strong>nonce</strong> (optional) — 12-byte content nonce for single-object shares. Present since `1b82b95` so the recipient can decrypt without fetching S3 metadata headers.</li> |
| 1146 | + <li><strong>chunked_metadata</strong> (optional) — serialized <code>ChunkedFileMetadata</code> JSON for chunked files. Present since `1b82b95`.</li> |
| 1147 | + <li><strong>encryption_version</strong> (optional, <code>u8</code>) — the content AEAD version (e.g. <code>4</code>) the recipient should decrypt with. Stamped by fula-flutter since commit `9069817`; if absent, the recipient tries v4-AAD first and falls back to v2 (no AAD) for legacy tokens.</li> |
| 1148 | + <li><strong>created_at</strong> — Unix seconds.</li> |
| 1149 | + <li><strong>expires_at</strong> — Unix seconds (optional; <code>None</code> = never expires).</li> |
1120 | 1150 | </ul> |
1121 | | - |
1122 | | - <h3>Permissions</h3> |
| 1151 | + |
| 1152 | + <h3>Permissions builder helpers</h3> |
1123 | 1153 | <ul> |
1124 | | - <li><code>read_only()</code> - Can decrypt and read files</li> |
1125 | | - <li><code>read_write()</code> - Can read and upload new files</li> |
1126 | | - <li><code>full_access()</code> - Can read, write, and delete</li> |
| 1154 | + <li><code>ShareBuilder::read_only()</code> — Can decrypt and read files within <code>path_scope</code>.</li> |
| 1155 | + <li><code>ShareBuilder::read_write()</code> — Can also upload under <code>path_scope</code>.</li> |
| 1156 | + <li><code>ShareBuilder::full_access()</code> — Read, write, and delete.</li> |
1127 | 1157 | </ul> |
1128 | 1158 | </div> |
1129 | 1159 | <div class="example"> |
1130 | 1160 | <div class="example-header"> |
1131 | | - <span class="lang-label">Share Token JSON Structure</span> |
| 1161 | + <span class="lang-label">Share Token JSON Structure (post-v0.3.0)</span> |
1132 | 1162 | <button class="copy-btn" onclick="copyCode(this)">Copy</button> |
1133 | 1163 | </div> |
1134 | 1164 | <pre><code class="language-json">{ |
1135 | | - "share_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", |
1136 | | - "path_scope": "/photos/vacation/", |
1137 | | - "created_at": 1701388800, |
1138 | | - "expires_at": 1702252800, |
1139 | | - "permissions": { |
1140 | | - "read": true, |
1141 | | - "write": false, |
1142 | | - "delete": false |
1143 | | - }, |
1144 | | - "encrypted_dek": { |
1145 | | - "encapsulated_key": "base64...", |
1146 | | - "ciphertext": "base64...", |
1147 | | - "nonce": "base64..." |
| 1165 | + "id": "a137662366191e7456ff20213f2ff012", |
| 1166 | + "version": 5, |
| 1167 | + "path_scope": "Qmf6de2bfec2be33a7effd5e791ceed78ff5375e586bd3", |
| 1168 | + "wrapped_key": { |
| 1169 | + "version": 2, |
| 1170 | + "encapsulated_key": { "ephemeral_public": "<base64, 32 bytes>" }, |
| 1171 | + "cipher": "chacha20poly1305", |
| 1172 | + "ciphertext": "<base64>" |
1148 | 1173 | }, |
1149 | | - "owner_public_key": "base64_x25519_public_key" |
| 1174 | + "permissions": { "can_read": true, "can_write": false, "can_delete": false }, |
| 1175 | + "mode": "Temporal", |
| 1176 | + "snapshot_binding": null, |
| 1177 | + "nonce": "<base64, 12 bytes>", |
| 1178 | + "chunked_metadata": null, |
| 1179 | + "encryption_version": 4, |
| 1180 | + "created_at": 1777500000, |
| 1181 | + "expires_at": 1778104800 |
1150 | 1182 | }</code></pre> |
1151 | 1183 |
|
1152 | 1184 | <div class="example-header"> |
|
0 commit comments