Skip to content

Commit 9728905

Browse files
committed
updated docs
1 parent bd61682 commit 9728905

11 files changed

Lines changed: 339 additions & 240 deletions

README.md

Lines changed: 93 additions & 78 deletions
Large diffs are not rendered by default.

docs/flutter-integration.md

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -248,42 +248,61 @@ await deleteFlat(encryptedClient, 'my-bucket', '/photos/vacation/beach.jpg');
248248

249249
### Secure Sharing
250250

251-
Create and accept share tokens for secure file sharing:
251+
Create and accept share tokens for secure file sharing. Note that since `fula-api` v0.3.0 (commit `9069817`) the `getWithShare` / `getWithToken` bindings take an additional `originalKey` argument so the recipient's client can enforce the token's `path_scope` — the share token granted by `create_share_token` sets `path_scope = storage_key` for FxFiles-style single-file shares, so `originalKey` and `storageKey` are typically the same value.
252252

253253
```dart
254-
// Create a read-only share token
255-
final shareToken = createShareToken(
254+
// Create a read-only share token (FxFiles defaults to path_scope = storage_key)
255+
final shareToken = await createShareToken(
256256
encryptedClient,
257+
bucket,
257258
storageKey,
258-
ShareMode.read,
259-
null, // No expiration
259+
recipientPublicKey, // 32-byte X25519 public key
260+
null, // expiresAt: null = no expiration
260261
);
261262
262263
// Create a time-limited share
263-
final expiresAt = DateTime.now().add(Duration(hours: 24)).millisecondsSinceEpoch ~/ 1000;
264-
final temporalToken = createShareToken(
264+
final expiresAt = DateTime.now()
265+
.add(Duration(hours: 24))
266+
.millisecondsSinceEpoch ~/ 1000;
267+
final temporalToken = await createShareTokenWithMode(
265268
encryptedClient,
269+
bucket,
266270
storageKey,
267-
ShareMode.temporal,
271+
recipientPublicKey,
272+
ShareMode.temporal, // always resolves to the current version of the path
268273
expiresAt,
269274
);
270275
271-
// Accept a share from someone else
272-
final acceptedShare = acceptShare(shareTokenJson);
276+
// Accept a share from someone else (parses the token locally; no network)
277+
final acceptedShare = await acceptShare(encryptedClient, shareTokenJson);
273278
274-
// Get file using the share
279+
// Get the file using the accepted share — 5-arg form since v0.3.0.
280+
// originalKey must match (or be a prefix-match of) the token's path_scope.
275281
final sharedData = await getWithShare(
276282
encryptedClient,
277283
'shared-bucket',
278-
storageKey,
284+
storageKey, // obfuscated CID-like storage key
285+
storageKey, // originalKey: path_scope check is starts_with
279286
acceptedShare,
280287
);
281288
282-
// Check share permissions
289+
// Or the single-step helper (same 5-arg shape):
290+
final sharedDataOneStep = await getWithToken(
291+
encryptedClient,
292+
'shared-bucket',
293+
storageKey,
294+
storageKey,
295+
shareTokenJson,
296+
);
297+
298+
// Inspect the accepted share
283299
final permissions = getSharePermissions(acceptedShare);
284300
print('Can read: ${permissions.canRead}, Can write: ${permissions.canWrite}');
301+
print('Valid: ${isShareValid(acceptedShare)}');
285302
```
286303

304+
Since commit `9069817`, fula-flutter's `create_share_token` also stamps the content `encryption_version` (v4) into the token so the recipient knows to decrypt with AAD — if you see `decryption failed: aead::Error` on an old share, rebuild and re-issue the link with the current fula-flutter.
305+
287306
### Key Rotation
288307

289308
Rotate encryption keys for enhanced security:

docs/website/api.html

Lines changed: 77 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
<div class="sidebar-header">
4444
<div class="logo">
4545
<h1>Fula API</h1>
46-
<span class="version">v0.1.0</span>
46+
<span class="version">v0.3.6</span>
4747
</div>
4848
<button class="theme-toggle" aria-label="Toggle theme">
4949
<span class="icon-sun">☀️</span>
@@ -982,45 +982,69 @@ <h2>🔐 Client-Side Encryption Headers</h2>
982982
<h3>Encryption Metadata Header</h3>
983983
<p>The <code>x-amz-meta-encryption</code> header contains JSON with encryption info:</p>
984984

985-
<h3>Fields</h3>
985+
<h3>Fields (content format v4, HPKE envelope v2)</h3>
986986
<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 &gt; 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>
993995
</ul>
994996
</div>
995997
<div class="example">
996998
<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>
9981000
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
9991001
</div>
10001002
<pre><code class="language-json">{
1001-
"version": 2,
1003+
"version": 4,
10021004
"algorithm": "AES-256-GCM",
1003-
"nonce": "base64_encoded_nonce",
1005+
"nonce": "&lt;base64, 12 bytes&gt;",
10041006
"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": "&lt;base64, 32 bytes&gt;" },
1009+
"cipher": "chacha20poly1305",
1010+
"ciphertext": "&lt;base64, 48 bytes (32-byte DEK + 16-byte tag)&gt;"
10081011
},
1012+
"kek_version": 1,
10091013
"metadata_privacy": true,
1010-
"private_metadata": "{\"version\":1,\"ciphertext\":\"...\",\"nonce\":\"...\"}"
1014+
"private_metadata": "&lt;base64 encrypted PrivateMetadata&gt;"
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": "&lt;hex, 32-byte Bao root&gt;",
1032+
"chunk_nonces": ["&lt;base64&gt;", "&lt;base64&gt;", "..."]
1033+
}
10111034
}</code></pre>
10121035

10131036
<div class="example-header">
10141037
<span class="lang-label">Head Request to Get Metadata</span>
10151038
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
10161039
</div>
10171040
<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" \
10191042
-H "Authorization: Bearer $TOKEN"
10201043

10211044
# 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)
10241048
# Content-Type: application/octet-stream
10251049
# Content-Length: 156821 (ciphertext size)</code></pre>
10261050
</div>
@@ -1109,44 +1133,52 @@ <h2>🤝 Secure Sharing API</h2>
11091133
<p>Share encrypted files without exposing your master key. Uses HPKE to re-encrypt the DEK for each recipient.</p>
11101134

11111135
<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>
11131137
<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>
11201150
</ul>
1121-
1122-
<h3>Permissions</h3>
1151+
1152+
<h3>Permissions builder helpers</h3>
11231153
<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>
11271157
</ul>
11281158
</div>
11291159
<div class="example">
11301160
<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>
11321162
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
11331163
</div>
11341164
<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": "&lt;base64, 32 bytes&gt;" },
1171+
"cipher": "chacha20poly1305",
1172+
"ciphertext": "&lt;base64&gt;"
11481173
},
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": "&lt;base64, 12 bytes&gt;",
1178+
"chunked_metadata": null,
1179+
"encryption_version": 4,
1180+
"created_at": 1777500000,
1181+
"expires_at": 1778104800
11501182
}</code></pre>
11511183

11521184
<div class="example-header">

docs/website/benchmark.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
<div class="sidebar-header">
4343
<div class="logo">
4444
<h1>Fula API</h1>
45-
<span class="version">v0.1.0</span>
45+
<span class="version">v0.3.6</span>
4646
</div>
4747
<button class="theme-toggle" aria-label="Toggle theme">
4848
<span class="icon-sun">☀️</span>

docs/website/index.html

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
<div class="sidebar-header">
4545
<div class="logo">
4646
<h1>Fula API</h1>
47-
<span class="version">v0.1.0</span>
47+
<span class="version">v0.3.6</span>
4848
</div>
4949
<button class="theme-toggle" aria-label="Toggle theme">
5050
<span class="icon-sun">☀️</span>
@@ -67,9 +67,10 @@ <h3 data-i18n="nav.documentation">Documentation</h3>
6767
<h3 data-i18n="nav.onThisPage">On This Page</h3>
6868
<ul>
6969
<li><a href="#overview" data-i18n="overview.title">Overview</a></li>
70+
<li><a href="#how-it-works-plain">How It Works (plain English)</a></li>
7071
<li><a href="#why-fula" data-i18n="whyFula.title">Why Fula?</a></li>
7172
<li><a href="#architecture">Architecture</a></li>
72-
<li><a href="#how-it-works">How It Works</a></li>
73+
<li><a href="#how-it-works">Architecture Details</a></li>
7374
<li><a href="#encryption">Encryption</a></li>
7475
<li><a href="#data-structures">Data Structures</a></li>
7576
<li><a href="#getting-started" data-i18n="gettingStarted.title">Getting Started</a></li>
@@ -129,6 +130,23 @@ <h3 data-i18n="feature.sync.title">Conflict-Free Sync</h3>
129130
</div>
130131
</section>
131132

133+
<!-- How It Works (plain English) -->
134+
<section id="how-it-works-plain" class="intro-section">
135+
<h2>How it works (plain English)</h2>
136+
<div class="overview-grid">
137+
<div class="overview-text">
138+
<h3>Encryption &amp; storage</h3>
139+
<p>Every file gets its own random 32-byte key. Files up to <strong>768&nbsp;KB</strong> are sealed as a single blob; anything larger is sliced into <strong>256&nbsp;KB pieces</strong>, and each piece is sealed separately with a tag that names the file and the piece — so pieces can't be shuffled, swapped, or replayed across files. The real filename and folder are replaced with a random-looking ID before anything leaves your machine, and the per-file key is itself wrapped with your own keypair so only you can open it. A small encrypted index (the <em>"private forest"</em>) remembers which scrambled ID belongs to which real filename; for large libraries it is stored as a sharded hash-array-mapped-trie (HAMT v7, borrowed from <a href="https://github.com/wnfs-wg/rs-wnfs">rs-wnfs</a>) so the client only loads the pieces it needs.</p>
140+
141+
<h3>Decryption</h3>
142+
<p>Your personal key unwraps the per-file key, the client decrypts the blob (or reassembles and checks each piece against its tag and the file's BLAKE3 root hash), and you get your bytes back. The forest entry also pins a hash of the original plaintext, so tampering after upload is detected even if the server produced a blob with a valid inner tag.</p>
143+
144+
<h3>Sharing</h3>
145+
<p>To share, your client re-wraps the file's key for the recipient's public key and attaches a short note — what they can do, when the share expires, and which path it covers. The result is a small token placed in the URL after <code>#</code>, so the server never sees the key. The recipient pastes the link, their own key unwraps the token, and they fetch the encrypted bytes through a lightweight proxy endpoint (no S3 account required for the recipient). Your personal key stays private; you only hand over that one file's lock. Shares come in two flavors: <strong>temporal</strong> (always resolves to the current version of the shared path) or <strong>snapshot</strong> (locked to the exact content hash at share time).</p>
146+
</div>
147+
</div>
148+
</section>
149+
132150
<!-- Why Fula Section -->
133151
<section id="why-fula" class="intro-section alt-bg">
134152
<h2 data-i18n="whyFula.title">Why Fula?</h2>

docs/website/platforms.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
<div class="sidebar-header">
4343
<div class="logo">
4444
<h1>Fula API</h1>
45-
<span class="version">v0.1.0</span>
45+
<span class="version">v0.3.6</span>
4646
</div>
4747
<button class="theme-toggle" aria-label="Toggle theme">
4848
<span class="icon-sun">☀️</span>

docs/website/sdk.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
<div class="sidebar-header">
4343
<div class="logo">
4444
<h1>Fula API</h1>
45-
<span class="version">v0.1.0</span>
45+
<span class="version">v0.3.6</span>
4646
</div>
4747
<button class="theme-toggle" aria-label="Toggle theme">
4848
<span class="icon-sun">☀️</span>

0 commit comments

Comments
 (0)