Skip to content

Commit 35c1d43

Browse files
committed
audit 2
1 parent 34cc63c commit 35c1d43

15 files changed

Lines changed: 1686 additions & 23 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ reqwest = { workspace = true, features = ["json", "multipart"] }
4242
axum = { workspace = true }
4343
base64 = { workspace = true }
4444
md-5 = { workspace = true }
45+
blake3 = { workspace = true }
46+
hex = { workspace = true }
4547

4648
[[example]]
4749
name = "basic_usage"

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,44 @@ cargo run --example flat_namespace_demo
262262
- Export/backup secret keys securely
263263
- Lost keys = lost data (no recovery possible)
264264

265+
### Privacy Notice
266+
267+
⚠️ **Important**: For private data, always use the **Encrypted Client SDK** (`EncryptedClient`).
268+
269+
Raw S3 tools (AWS CLI, boto3) do NOT encrypt data - they upload plaintext that gateway operators can see.
270+
271+
**What's encrypted** (with EncryptedClient):
272+
- ✅ File content
273+
- ✅ File names (FlatNamespace mode)
274+
- ✅ Directory structure
275+
- ✅ User IDs (hashed)
276+
277+
**What remains visible**:
278+
- ⚠️ Bucket names
279+
- ⚠️ Approximate file sizes
280+
- ⚠️ Request timestamps
281+
282+
See [docs/PRIVACY.md](docs/PRIVACY.md) for full privacy policy.
283+
284+
## Production Deployment
285+
286+
For production Ubuntu deployments with security hardening:
287+
288+
```bash
289+
# Download and run the installer
290+
curl -fsSL https://raw.githubusercontent.com/functionland/fula-api/main/install.sh | sudo bash
291+
```
292+
293+
The installer will:
294+
- Install Docker and dependencies
295+
- Configure nginx with TLS (Let's Encrypt)
296+
- Set up rate limiting and fail2ban
297+
- Configure firewall (UFW)
298+
- Create systemd service
299+
- Optionally set up local IPFS node
300+
301+
See [install.sh](install.sh) for details.
302+
265303
## License
266304

267305
Licensed under either of:

crates/fula-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ tracing-subscriber = { workspace = true }
5757
uuid = { workspace = true }
5858
base64 = { workspace = true }
5959
hex = { workspace = true }
60+
blake3 = { workspace = true }
6061
urlencoding = "2.1"
6162
url = "2.5"
6263
mime = "0.3"

crates/fula-cli/src/auth.rs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,11 @@ pub fn claims_to_session(claims: Claims) -> UserSession {
9999
.map(|s| s.to_string())
100100
.collect();
101101

102-
UserSession {
103-
user_id: claims.sub,
104-
display_name: claims.name,
105-
scopes,
106-
expires_at: DateTime::from_timestamp(claims.exp, 0)
107-
.unwrap_or_else(|| Utc::now() + Duration::hours(1)),
108-
}
102+
let expires_at = DateTime::from_timestamp(claims.exp, 0)
103+
.unwrap_or_else(|| Utc::now() + Duration::hours(1));
104+
105+
// Security audit fix A3: Use UserSession::new() to auto-hash user ID
106+
UserSession::new(claims.sub, claims.name, scopes, expires_at)
109107
}
110108

111109
/// Extract bearer token from Authorization header
@@ -129,12 +127,13 @@ pub fn anonymous_user_id() -> String {
129127

130128
/// Create a development/test session
131129
pub fn dev_session() -> UserSession {
132-
UserSession {
133-
user_id: "dev-user".to_string(),
134-
display_name: Some("Development User".to_string()),
135-
scopes: vec!["storage:*".to_string()],
136-
expires_at: Utc::now() + Duration::days(365),
137-
}
130+
// Security audit fix A3: Use UserSession::new() to auto-hash user ID
131+
UserSession::new(
132+
"dev-user".to_string(),
133+
Some("Development User".to_string()),
134+
vec!["storage:*".to_string()],
135+
Utc::now() + Duration::days(365),
136+
)
138137
}
139138

140139
#[cfg(test)]

crates/fula-cli/src/handlers/bucket.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ pub async fn create_bucket(
2222
return Err(ApiError::s3(S3ErrorCode::AccessDenied, "Write access required"));
2323
}
2424

25-
let owner = Owner::new(&session.user_id)
26-
.with_display_name(session.display_name.unwrap_or_default());
25+
// Security audit fix A3: Use hashed user ID for privacy
26+
let owner = Owner::new(&session.hashed_user_id)
27+
.with_display_name(session.display_name.clone().unwrap_or_default());
2728

2829
state.bucket_manager.create_bucket(bucket.clone(), owner).await?;
2930

@@ -44,11 +45,11 @@ pub async fn delete_bucket(
4445
return Err(ApiError::s3(S3ErrorCode::AccessDenied, "Write access required"));
4546
}
4647

47-
// Check ownership
48+
// Check ownership (Security audit fix A3: compare hashed IDs)
4849
let metadata = state.bucket_manager.get_bucket_metadata(&bucket)
4950
.ok_or_else(|| ApiError::s3(S3ErrorCode::NoSuchBucket, "Bucket not found"))?;
5051

51-
if metadata.owner_id != session.user_id && !session.has_scope("admin") {
52+
if !session.can_access_bucket(&metadata.owner_id) {
5253
return Err(ApiError::s3(S3ErrorCode::AccessDenied, "Not bucket owner"));
5354
}
5455

crates/fula-cli/src/handlers/multipart.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,11 @@ pub async fn create_multipart_upload(
5757
}
5858
}
5959

60+
// Security audit fix A3: Use hashed user ID
6061
let upload = state.multipart_manager.create_upload_with_metadata(
6162
bucket.clone(),
6263
key.clone(),
63-
session.user_id.clone(),
64+
session.hashed_user_id.clone(),
6465
content_type,
6566
metadata,
6667
);

crates/fula-cli/src/handlers/object.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,9 +378,10 @@ pub async fn copy_object(
378378
))?;
379379

380380
// Copy to destination
381+
// Security audit fix A3: Use hashed user ID
381382
let mut dest_metadata = source_metadata.clone();
382383
dest_metadata.last_modified = chrono::Utc::now();
383-
dest_metadata.owner_id = Some(session.user_id.clone());
384+
dest_metadata.owner_id = Some(session.hashed_user_id.clone());
384385

385386
let mut dest_bucket_handle = state.bucket_manager.open_bucket(&dest_bucket).await?;
386387

crates/fula-cli/src/handlers/service.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@ pub async fn list_buckets(
1818
let buckets = state.bucket_manager.list_buckets();
1919

2020
// Filter to buckets owned by this user (or show all for admin)
21+
// Security audit fix A3: Compare using hashed user IDs
2122
let user_buckets: Vec<_> = buckets
2223
.into_iter()
23-
.filter(|b| b.owner_id == session.user_id || session.has_scope("admin"))
24+
.filter(|b| session.can_access_bucket(&b.owner_id))
2425
.map(|b| (b.name, b.created_at))
2526
.collect();
2627

28+
// Return hashed user ID in XML for privacy (Security audit fix A3)
2729
let xml_response = xml::list_all_my_buckets_result(
28-
&session.user_id,
29-
session.display_name.as_deref().unwrap_or(&session.user_id),
30+
&session.hashed_user_id,
31+
session.display_name.as_deref().unwrap_or("User"),
3032
&user_buckets,
3133
);
3234

crates/fula-cli/src/state.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use crate::config::GatewayConfig;
44
use crate::multipart::MultipartManager;
5+
use blake3::Hasher;
56
use dashmap::DashMap;
67
use fula_blockstore::{
78
FlexibleBlockStore, IpfsPinningBlockStore, IpfsPinningConfig, MemoryBlockStore,
@@ -10,6 +11,17 @@ use fula_core::BucketManager;
1011
use std::sync::Arc;
1112
use tracing::{info, warn};
1213

14+
/// Hash a user ID for privacy (Security audit fix A3)
15+
/// This prevents exposing raw user IDs (e.g., email addresses) in stored metadata
16+
pub fn hash_user_id(user_id: &str) -> String {
17+
let mut hasher = Hasher::new();
18+
hasher.update(b"fula:user_id:"); // Domain separation
19+
hasher.update(user_id.as_bytes());
20+
let hash = hasher.finalize();
21+
// Use first 16 bytes (128 bits) encoded as hex for reasonable uniqueness
22+
hex::encode(&hash.as_bytes()[..16])
23+
}
24+
1325
/// Application state shared across handlers
1426
pub struct AppState {
1527
/// Gateway configuration
@@ -95,6 +107,8 @@ impl AppState {
95107
pub struct UserSession {
96108
/// User ID (from JWT sub claim)
97109
pub user_id: String,
110+
/// Hashed user ID for storage (Security audit fix A3)
111+
pub hashed_user_id: String,
98112
/// Display name
99113
pub display_name: Option<String>,
100114
/// Scopes
@@ -104,6 +118,18 @@ pub struct UserSession {
104118
}
105119

106120
impl UserSession {
121+
/// Create a new user session with automatic ID hashing
122+
pub fn new(user_id: String, display_name: Option<String>, scopes: Vec<String>, expires_at: chrono::DateTime<chrono::Utc>) -> Self {
123+
let hashed_user_id = hash_user_id(&user_id);
124+
Self {
125+
user_id,
126+
hashed_user_id,
127+
display_name,
128+
scopes,
129+
expires_at,
130+
}
131+
}
132+
107133
/// Check if the session has expired
108134
pub fn is_expired(&self) -> bool {
109135
chrono::Utc::now() > self.expires_at
@@ -130,7 +156,8 @@ impl UserSession {
130156
}
131157

132158
/// Check if user can access a bucket (owner or admin)
159+
/// Security audit fix A3: Uses hashed user ID for comparison
133160
pub fn can_access_bucket(&self, bucket_owner_id: &str) -> bool {
134-
self.user_id == bucket_owner_id || self.is_admin()
161+
self.hashed_user_id == bucket_owner_id || self.is_admin()
135162
}
136163
}

0 commit comments

Comments
 (0)