Skip to content

Commit 0324671

Browse files
committed
Fix chunked file sharing - files >768KB now decrypt correctly
1 parent 6bd066c commit 0324671

File tree

6 files changed

+97
-29
lines changed

6 files changed

+97
-29
lines changed

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ name = "encrypted_upload_test"
7474
path = "examples/encrypted_upload_test.rs"
7575

7676
[workspace.package]
77-
version = "0.2.15"
77+
version = "0.2.16"
7878
edition = "2021"
7979
license = "MIT OR Apache-2.0"
8080
repository = "https://github.com/functionland/fula-api"

crates/fula-client/src/encryption.rs

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,18 +1225,21 @@ impl EncryptedClient {
12251225
// ═══════════════════════════════════════════════════════════════════════════
12261226

12271227
/// Get and decrypt an object using a ShareToken
1228-
///
1228+
///
12291229
/// This allows recipients of a share to read encrypted objects without
12301230
/// having the owner's keys. The ShareToken contains a wrapped DEK that
12311231
/// was encrypted for the recipient's public key.
1232-
///
1232+
///
12331233
/// # Arguments
12341234
/// * `bucket` - The bucket containing the object
12351235
/// * `storage_key` - The storage key of the object
12361236
/// * `accepted_share` - An accepted share containing the DEK
1237-
///
1237+
///
12381238
/// # Returns
12391239
/// The decrypted object data
1240+
///
1241+
/// # Note
1242+
/// Handles both single-block and chunked files automatically.
12401243
pub async fn get_object_with_share(
12411244
&self,
12421245
bucket: &str,
@@ -1268,7 +1271,7 @@ impl EncryptedClient {
12681271

12691272
// Fetch the object
12701273
let result = self.inner.get_object_with_metadata(bucket, storage_key).await?;
1271-
1274+
12721275
// Check if encrypted
12731276
let is_encrypted = result.metadata
12741277
.get("x-fula-encrypted")
@@ -1279,6 +1282,12 @@ impl EncryptedClient {
12791282
return Ok(result.data);
12801283
}
12811284

1285+
// Check if this is a chunked object
1286+
let is_chunked = result.metadata
1287+
.get("x-fula-chunked")
1288+
.map(|v| v == "true")
1289+
.unwrap_or(false);
1290+
12821291
// Parse encryption metadata
12831292
let enc_metadata_str = result.metadata
12841293
.get("x-fula-encryption")
@@ -1291,24 +1300,65 @@ impl EncryptedClient {
12911300
fula_crypto::CryptoError::Decryption(e.to_string())
12921301
))?;
12931302

1294-
// Extract nonce
1295-
let nonce_b64 = enc_metadata["nonce"].as_str()
1296-
.ok_or_else(|| ClientError::Encryption(
1297-
fula_crypto::CryptoError::Decryption("Missing nonce".to_string())
1303+
if is_chunked {
1304+
// CHUNKED DOWNLOAD: Download and decrypt each chunk using the share's DEK
1305+
self.get_object_chunked_with_share(bucket, storage_key, &enc_metadata, &accepted_share.dek).await
1306+
} else {
1307+
// SINGLE OBJECT: Decrypt directly
1308+
let nonce_b64 = enc_metadata["nonce"].as_str()
1309+
.ok_or_else(|| ClientError::Encryption(
1310+
fula_crypto::CryptoError::Decryption("Missing nonce".to_string())
1311+
))?;
1312+
let nonce_bytes = base64::Engine::decode(
1313+
&base64::engine::general_purpose::STANDARD,
1314+
nonce_b64,
1315+
).map_err(|e| ClientError::Encryption(
1316+
fula_crypto::CryptoError::Decryption(e.to_string())
12981317
))?;
1299-
let nonce_bytes = base64::Engine::decode(
1300-
&base64::engine::general_purpose::STANDARD,
1301-
nonce_b64,
1318+
let nonce = Nonce::from_bytes(&nonce_bytes)
1319+
.map_err(ClientError::Encryption)?;
1320+
1321+
// Use the DEK from the accepted share (already decrypted for recipient)
1322+
let aead = Aead::new_default(&accepted_share.dek);
1323+
let plaintext = aead.decrypt(&nonce, &result.data)
1324+
.map_err(ClientError::Encryption)?;
1325+
1326+
Ok(Bytes::from(plaintext))
1327+
}
1328+
}
1329+
1330+
/// Internal: Download and decrypt a chunked file using a share's DEK
1331+
async fn get_object_chunked_with_share(
1332+
&self,
1333+
bucket: &str,
1334+
storage_key: &str,
1335+
enc_metadata: &serde_json::Value,
1336+
dek: &fula_crypto::keys::DekKey,
1337+
) -> Result<Bytes> {
1338+
// Parse chunked metadata
1339+
let chunked_meta: ChunkedFileMetadata = serde_json::from_value(
1340+
enc_metadata["chunked"].clone()
13021341
).map_err(|e| ClientError::Encryption(
1303-
fula_crypto::CryptoError::Decryption(e.to_string())
1342+
fula_crypto::CryptoError::Decryption(format!("Invalid chunked metadata: {}", e))
13041343
))?;
1305-
let nonce = Nonce::from_bytes(&nonce_bytes)
1306-
.map_err(ClientError::Encryption)?;
13071344

1308-
// Use the DEK from the accepted share (already decrypted for recipient)
1309-
let aead = Aead::new_default(&accepted_share.dek);
1310-
let plaintext = aead.decrypt(&nonce, &result.data)
1311-
.map_err(ClientError::Encryption)?;
1345+
// Create decoder
1346+
let mut decoder = fula_crypto::ChunkedDecoder::new(dek.clone(), chunked_meta.clone());
1347+
1348+
// Pre-allocate result buffer
1349+
let mut plaintext = Vec::with_capacity(chunked_meta.total_size as usize);
1350+
1351+
// Download and decrypt each chunk in order
1352+
for chunk_index in 0..chunked_meta.num_chunks {
1353+
let chunk_key = ChunkedFileMetadata::chunk_key(storage_key, chunk_index);
1354+
1355+
let chunk_result = self.inner.get_object(bucket, &chunk_key).await?;
1356+
1357+
let chunk_plaintext = decoder.decrypt_chunk(chunk_index, &chunk_result)
1358+
.map_err(ClientError::Encryption)?;
1359+
1360+
plaintext.extend_from_slice(&chunk_plaintext);
1361+
}
13121362

13131363
Ok(Bytes::from(plaintext))
13141364
}

packages/fula_client/CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.16] - 2026-01-13
9+
10+
### Fixed
11+
12+
- **CRITICAL: Share decryption fails for chunked files (files > 768KB)**
13+
- `get_object_with_share` was using single-block decryption for all files
14+
- Chunked files store each chunk with its own nonce in `{storage_key}.chunks/{index}`
15+
- Share flow was ignoring chunked file metadata and trying to decrypt assembled bytes as single block
16+
- **Result**: Large shared files (images, videos) returned garbage data instead of correct content
17+
- **Fix**: `get_object_with_share` now checks `x-fula-chunked` metadata and uses `ChunkedDecoder` with per-chunk nonces when needed
18+
19+
### Technical Details
20+
21+
- Added `get_object_chunked_with_share()` internal method for chunked file handling in share flow
22+
- Downloads each chunk from `{storage_key}.chunks/{index}`, decrypts with chunk-specific nonce
23+
- Concatenates decrypted chunks and returns complete plaintext
24+
- Works identically to normal `get_object_decrypted_by_storage_key()` but uses share's DEK
25+
826
## [0.2.15] - 2026-01-13
927

1028
### Fixed

packages/fula_client/ios/fula_client.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
Pod::Spec.new do |s|
88
s.name = 'fula_client'
9-
s.version = '0.2.12'
9+
s.version = '0.2.16'
1010
s.summary = 'Flutter SDK for Fula decentralized storage'
1111
s.description = <<-DESC
1212
A Flutter plugin providing client-side encryption, metadata privacy,

packages/fula_client/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: fula_client
22
description: Flutter SDK for Fula decentralized storage with client-side encryption, metadata privacy, and secure sharing.
3-
version: 0.2.15
3+
version: 0.2.16
44
homepage: https://fx.land
55
repository: https://github.com/functionland/fula-api
66
issue_tracker: https://github.com/functionland/fula-api/issues

0 commit comments

Comments
 (0)