π Security & Encryption
In-Depth Technical Documentation of Fula's Cryptographic Implementation
Security Overview
Fula implements a Trust-No-One (TNO) security model where all encryption happens client-side. Storage nodes and the gateway never see your plaintext data or encryption keys.
Client-Side Encryption
All encryption/decryption happens on your device before data leaves
User-Held Keys
Only you possess the private keys needed to decrypt your data
Verified Integrity
Cryptographic proofs detect any tampering, even from malicious nodes
Forward Secrecy
Ephemeral keys ensure past communications remain secure
Cryptographic Primitives Stack
| Layer | Algorithm | Purpose | Security Level |
|---|---|---|---|
| π‘οΈ Quantum-Safe KEM | X25519 + ML-KEM-768 | Hybrid key encapsulation (classical + post-quantum) | NIST Level 3 + 128-bit classical |
| Key Exchange | X25519 | Classical Elliptic Curve Diffie-Hellman | 128-bit classical |
| Post-Quantum KEM | ML-KEM-768 (Kyber) | NIST FIPS 203 lattice-based KEM | 192-bit post-quantum (NIST Level 3) |
| Key Derivation | HKDF-SHA256 | Combine X25519 + ML-KEM shared secrets | 256-bit output |
| Symmetric Encryption | AES-256-GCM | Authenticated encryption of data | 256-bit (quantum-resistant) |
| Alternative Cipher | ChaCha20-Poly1305 | Authenticated encryption (software-optimized) | 256-bit (quantum-resistant) |
| Hashing | BLAKE3 | Content addressing, integrity verification | 256-bit (quantum-resistant) |
| Streaming Verification | Bao | Incremental integrity verification | Merkle tree based |
Trust Model
Security Assumptions
- Your device is secure - If compromised, keys can be extracted
- Cryptographic primitives are secure - X25519, AES-256-GCM, BLAKE3 are industry-standard
- Random number generator is secure - Using OS-provided CSPRNG (OsRng)
API Authentication
Fula supports both Bearer tokens and AWS Signature V4, enabling full S3 SDK compatibility.
Authentication Methods
π Bearer Token (Simple)
For HTTP clients, REST APIs, and custom integrations.
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
π¦ AWS Signature V4 (S3 Compatible)
For boto3, AWS CLI, aws-sdk-js, and all S3 tools.
Authorization: AWS4-HMAC-SHA256 Credential=JWT:eyJhbGci.../20231207/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=abc123...
AWS Sig V4 with JWT
Embed your JWT token in the AWS access key with a JWT: prefix. This enables full compatibility with standard S3 clients while maintaining JWT-based authentication.
# Python (boto3)
import boto3
s3 = boto3.client('s3',
endpoint_url='https://gateway.example.com',
aws_access_key_id=f'JWT:{jwt_token}', # JWT embedded here
aws_secret_access_key='not-used', # Not validated
region_name='us-east-1'
)
s3.put_object(Bucket='my-bucket', Key='file.txt', Body=b'Hello!')
# AWS CLI (~/.aws/credentials) [fula] aws_access_key_id = JWT:eyJhbGciOiJIUzI1NiIs... aws_secret_access_key = not-used # Usage: aws s3 cp file.txt s3://my-bucket/ --endpoint-url https://gateway.example.com --profile fula
// JavaScript (AWS SDK v3)
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({
endpoint: "https://gateway.example.com",
region: "us-east-1",
forcePathStyle: true,
credentials: {
accessKeyId: `JWT:${jwtToken}`,
secretAccessKey: "not-used"
}
});
await s3.send(new PutObjectCommand({ Bucket: "my-bucket", Key: "file.txt", Body: "Hello!" }));
Security Properties
- JWT Validation: Full signature, expiry, and claims validation
- Replay Protection: x-amz-date must be within 15 minutes
- Scope Enforcement: JWT scopes control read/write/delete permissions
- S3 Compatibility: Works with boto3, AWS CLI, aws-sdk-js, and all S3 tools
Encryption Flow
Complete data flow from plaintext to encrypted storage and back.
π€ Upload (Encryption) Flow
A fresh 256-bit Data Encryption Key (DEK) is generated using CSPRNG for each object.
DEK = random_bytes(32) // 256 bits from OsRng
Plaintext is encrypted using AES-256-GCM with a random 96-bit nonce.
nonce = random_bytes(12) // 96 bits ciphertext = AES-256-GCM.encrypt(DEK, nonce, plaintext) // ciphertext includes 128-bit authentication tag
The DEK is encrypted using HPKE for the owner's public key.
ephemeral_secret = X25519.generate()
ephemeral_public = X25519.public_key(ephemeral_secret)
shared_secret = X25519.dh(ephemeral_secret, recipient_public)
wrap_key = BLAKE3.derive_key("fula-hpke-v1", shared_secret)
wrapped_DEK = AES-256-GCM.encrypt(wrap_key, nonce2, DEK)
Ciphertext is uploaded with encryption metadata (nonce, wrapped DEK, ephemeral public key).
metadata = {
"x-fula-encrypted": "true",
"x-fula-encryption": {
"version": 1,
"algorithm": "AES-256-GCM",
"nonce": base64(nonce),
"wrapped_key": {
"ephemeral_public": base64(ephemeral_public),
"ciphertext": base64(wrapped_DEK)
}
}
}
π₯ Download (Decryption) Flow
Retrieve ciphertext and encryption metadata from the gateway.
Use your secret key to derive shared secret and decrypt the DEK.
shared_secret = X25519.dh(your_secret_key, ephemeral_public)
wrap_key = BLAKE3.derive_key("fula-hpke-v1", shared_secret)
DEK = AES-256-GCM.decrypt(wrap_key, nonce2, wrapped_DEK)
Use the recovered DEK to decrypt the ciphertext.
plaintext = AES-256-GCM.decrypt(DEK, nonce, ciphertext) // Authentication tag verified automatically
Key Architecture (KEK/DEK)
Fula uses a two-tier key hierarchy for efficient and secure encryption.
Files, photos, documents, any content you store
Benefits of KEK/DEK Architecture
Rotate KEK without re-encrypting all data - just re-wrap existing DEKs
Share access by wrapping DEK with recipient's public key
Symmetric DEKs are fast; asymmetric KEK only used for small DEKs
Compromise of one DEK doesn't affect other files
HPKE Implementation
Hybrid Public Key Encryption following RFC 9180 for secure key encapsulation.
HPKE Configuration
| KEM (Key Encapsulation) | DHKEM(X25519, HKDF-SHA256) |
| KDF (Key Derivation) | BLAKE3 with context "fula-hpke-v1" |
| AEAD (Encryption) | AES-256-GCM (default) or ChaCha20-Poly1305 |
HPKE Encryption Process
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SENDER (Encryptor) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββ ββββββββββββββββββββββββββββββββββββββββ β
β β Ephemeral β β Recipient's Public Key β β
β β Secret Key β β (known to sender) β β
β β (random) β ββββββββββββββββββββββββββββββββββββββββ β
β ββββββββ¬βββββββ β β
β β β β
β βΌ βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β X25519 Diffie-Hellman Key Exchange β β
β β shared_secret = DH(ephemeral_secret, recipient_public) β β
β ββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β BLAKE3 Key Derivation Function β β
β β encryption_key = derive_key("fula-hpke-v1", β β
β β shared_secret) β β
β ββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β AES-256-GCM Encryption β β
β β ciphertext = encrypt(encryption_key, nonce, DEK) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β OUTPUT: { ephemeral_public_key, nonce, ciphertext } β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β RECIPIENT (Decryptor) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββ ββββββββββββββββββββββββββββββββββββββββ β
β β Recipient's β β Ephemeral Public Key β β
β β Secret Key β β (from ciphertext) β β
β β (private) β ββββββββββββββββββββββββββββββββββββββββ β
β ββββββββ¬βββββββ β β
β β β β
β βΌ βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β X25519 Diffie-Hellman Key Exchange β β
β β shared_secret = DH(recipient_secret, ephemeral_public) β β
β ββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Same shared_secret β Same encryption_key β β
β ββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β AES-256-GCM Decryption β β
β β DEK = decrypt(encryption_key, nonce, β β
β β ciphertext) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Security Properties
Each encryption uses a fresh ephemeral key pair. Compromise of long-term keys doesn't decrypt past messages.
Only the holder of the recipient's secret key can derive the shared secret and decrypt.
AES-GCM provides authenticated encryption - any tampering is detected.
Symmetric Encryption (AEAD)
Authenticated Encryption with Associated Data for file content.
Ciphertext Structure
output_length = plaintext_length + 16 bytes (tag)
Nonce Generation
β οΈ Critical: Nonces must NEVER be reused with the same key.
Fula's Strategy: Random Nonces
- 96-bit nonces generated from CSPRNG (OsRng)
- Collision probability: ~2-48 after 248 encryptions
- Each file gets a unique DEK, so nonce space is per-DEK
- Safe for virtually unlimited encryptions per file
Data Integrity Verification
Cryptographic guarantees that your data hasn't been tampered with.
Multi-Layer Integrity
AEAD Authentication Tag
Every encrypted block includes a 128-bit authentication tag that verifies both ciphertext integrity and the encryption key used.
Content Addressing (CID)
Every block stored in IPFS has a CID derived from its BLAKE3 hash. Retrieving by CID guarantees content matches.
Bao Streaming Verification
For large files, Bao provides Merkle tree-based verification allowing you to verify chunks before downloading the entire file.
BLAKE3 Hashing
Faster than MD5, SHA-1, SHA-256, SHA-3 on modern CPUs
256-bit security level, resistant to length extension
Scales with CPU cores for large data
Built-in key derivation with context separation
Bao Verified Streaming
Root Hash (32 bytes)
β
βββββββββββββββββ΄ββββββββββββββββ
β β
Hash(L||R) Hash(L||R)
β β
ββββββββ΄βββββββ ββββββββ΄βββββββ
β β β β
Hash(L||R) Hash(L||R) Hash(L||R) Hash(L||R)
β β β β
ββββ΄βββ ββββ΄βββ ββββ΄βββ ββββ΄βββ
β β β β β β β β
Chunk Chunk Chunk Chunk Chunk Chunk Chunk Chunk
1 2 3 4 5 6 7 8
β’ Verify any chunk with O(log n) proof from root
β’ Detect corruption before downloading entire file
β’ Stream large files with incremental verification
Key Management
How Fula handles key generation, storage, rotation, and recovery.
Key Generation
| Random Source | OsRng (Operating System CSPRNG) |
| KEK Generation | 32 random bytes β X25519 secret key |
| DEK Generation | 32 random bytes β AES-256 key |
| Nonce Generation | 12 random bytes per encryption |
Key Rotation
Create a fresh X25519 key pair
Decrypt each DEK with old KEK, re-encrypt with new KEK
Store new wrapped DEKs, increment version counter
Zeroize old secret key from memory
Key Backup & Recovery
Fula uses true end-to-end encryption. There are no backdoors, no "forgot password" recovery, no master keys.
Memory Security
All key types implement Zeroize and ZeroizeOnDrop:
- Keys are automatically zeroed when dropped from memory
- Prevents keys from lingering in memory after use
- Protects against memory dump attacks
// From fula-crypto/src/keys.rs
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct SecretKey {
bytes: [u8; 32],
}
// When SecretKey is dropped, bytes are overwritten with zeros
Async/Offline Inbox Sharing (WNFS-Inspired)
Store-and-forward sharing where recipients can pick up shares later without the sharer being online.
How It Works
Inspired by WNFS's exchange directories, Fula supports asynchronous sharing where:
- Sharer creates an encrypted
ShareEnvelopeand stores it in the recipient's inbox - Recipient can later list, decrypt, and accept shares when convenient
- No coordination required - both parties can be offline at different times
Sharer Storage Recipient β β β β 1. Create ShareEnvelope β β β + HPKE encrypt for recipientβ β β β β β 2. Store in inbox βββββββββββββββΊ[Encrypted entry stored] β β β β β β [Later, recipient comes online] β β β β ββββ3. List inbox entries ββββββββ β β β β ββββ4. Decrypt & accept ββββββββββ β β β β β 5. Use ShareToken to access β β β shared content β
use fula_crypto::{ShareEnvelopeBuilder, ShareInbox, KekKeyPair, DekKey};
// === SHARER FLOW ===
let sharer = KekKeyPair::generate();
let recipient = KekKeyPair::generate();
let dek = DekKey::generate();
// Create share envelope with metadata
let (envelope, entry) = ShareEnvelopeBuilder::new(
&sharer,
recipient.public_key(),
&dek
)
.path_scope("/photos/vacation/")
.expires_in(7 * 24 * 3600) // 1 week
.read_only()
.label("Vacation Photos 2024")
.message("Check out these pics from Hawaii!")
.sharer_name("Alice")
.build()?;
// Store entry in recipient's inbox (via PrivateForest)
let inbox_path = ShareInbox::entry_storage_path(recipient.public_key(), &entry.id);
// put_object_flat(bucket, &inbox_path, serialize(&entry), ...);
// === RECIPIENT FLOW ===
let mut inbox = ShareInbox::new();
// Load entries from storage
inbox.add_entry(entry);
// List pending shares
let pending = inbox.list_pending(&recipient);
println!("You have {} new shares", pending.len());
// Accept a share
let accepted = inbox.accept_entry(&entry_id, &recipient)?;
println!("From: {:?}", accepted.sharer_name); // "Alice"
println!("Message: {:?}", accepted.message); // "Check out these pics..."
// Now use the ShareToken to access content
let dek = recipient.accept_share(&accepted.token)?;
Key Features
Shares are stored encrypted until recipient comes online
Only recipient can decrypt inbox entries using HPKE
Include labels, messages, and sharer info with each share
Configurable TTL for inbox entries (default: 30 days)
Subtree Keys (Peergos Cryptree-Inspired)
Allocate separate DEKs for major folders for better revocation and least privilege.
What are Subtree Keys?
Inspired by Peergos Cryptree, Fula supports a shallow key hierarchy where top-level folders can have their own DEKs:
Master DEK (bucket-level)
β
βββ /photos/ βββ Subtree DEK A βββ [beach.jpg, sunset.jpg, ...]
β
βββ /documents/ βββ Subtree DEK B βββ [report.pdf, notes.txt, ...]
β
βββ /apps/myapp/ βββ Subtree DEK C βββ [config.json, data.bin, ...]
Sharing /photos/ only exposes Subtree DEK A.
Revoking that share only requires re-keying /photos/, not the whole bucket.
Key Benefits
Re-key just one subtree instead of rotating the entire bucket
A subtree share cannot be escalated to access unrelated data
Still uses a single PrivateForest - no structural changes needed
use fula_crypto::{SubtreeKeyManager, SubtreeShareBuilder, DekKey, KekKeyPair};
// Create manager with master DEK
let master_dek = DekKey::generate();
let mut manager = SubtreeKeyManager::with_master_dek(master_dek);
// Create subtrees with their own DEKs
let (photos_dek, encrypted_photos) = manager.create_subtree("/photos/")?;
let (docs_dek, encrypted_docs) = manager.create_subtree("/documents/")?;
// Resolve DEK for a file path
let dek = manager.resolve_dek("/photos/vacation/beach.jpg"); // Returns photos_dek
// Share a subtree with someone
let owner = KekKeyPair::generate();
let recipient = KekKeyPair::generate();
let share = SubtreeShareBuilder::new(
&owner,
recipient.public_key(),
&photos_dek,
"/photos/",
1, // version
)
.expires_in(86400) // 24 hours
.read_only()
.build()?;
// Rotate subtree key after revocation
let rotation = manager.rotate_subtree("/photos/")?;
// rotation.new_dek is now used for /photos/*
// Old shares become invalid
When to Use Subtree Keys
| Scenario | Recommendation |
|---|---|
| Sharing app-specific data folders | Use subtree DEK per app namespace |
| Sharing project folders with different teams | Use subtree DEK per project |
| Need to revoke access to specific shared folder | Rotate just that subtree's DEK |
| Single user, no sharing | Master DEK is sufficient |
Full Filesystem Key Rotation
Rotate your KEK without re-encrypting file content.
Why Rotate Keys?
- Suspected Compromise: If you think your key might be exposed
- Regular Security Hygiene: Periodic rotation limits exposure window
- Personnel Changes: When someone with key access leaves
- Compliance Requirements: Some regulations mandate key rotation
Rotation Process
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β KEY ROTATION FLOW β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β β β BEFORE ROTATION: β β βββββββββββββββ wraps ββββββββββββββ β β β KEK v1 β ββββββββββββββββ DEK A ββββ File A β β β (current) β ββββββββββββββ β β β β wraps ββββββββββββββ β β β β ββββββββββββββββ DEK B ββββ File B β β βββββββββββββββ ββββββββββββββ β β β β ROTATION STEP 1: Generate new KEK β β βββββββββββββββ βββββββββββββββ β β β KEK v1 β β KEK v2 β β β β (previous) β β (current) β β β βββββββββββββββ βββββββββββββββ β β β β ROTATION STEP 2: Re-wrap each DEK β β β β For each file: β β 1. Decrypt DEK with KEK v1 β β 2. Re-encrypt DEK with KEK v2 β β 3. Update wrapped DEK in metadata β β β β AFTER ROTATION: β β βββββββββββββββ wraps ββββββββββββββ β β β KEK v2 β ββββββββββββββββ DEK A ββββ File A β β β (current) β ββββββββββββββ (unchanged) β β β β wraps ββββββββββββββ β β β β ββββββββββββββββ DEK B ββββ File B β β βββββββββββββββ ββββββββββββββ (unchanged) β β β β Note: File content is NEVER re-encrypted! β β Only the DEK wrappers are updated. β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Code Example
use fula_crypto::{KekKeyPair, DekKey, FileSystemRotation};
// Initialize with current keypair
let keypair = KekKeyPair::generate();
let mut fs = FileSystemRotation::new(keypair)
.with_batch_size(100); // Process 100 files per batch
// Register existing files
for file in files {
fs.wrap_new_file(&file.path, &file.dek)?;
}
// Initiate rotation (generates new KEK)
let new_public_key = fs.rotate();
// Rotate in batches (for large filesystems)
while !fs.is_rotation_complete() {
let result = fs.rotate_batch();
println!("Rotated {} files", result.rotated_count);
}
// All files now use KEK v2
// Old KEK is automatically cleared
Key Benefits
Only re-wraps DEKs (32 bytes each), never re-encrypts file content
Process files in batches to avoid overwhelming large systems
Old wrapped keys still work during transition period
Track rotation progress and verify completion
Metadata Privacy
Protect not just your data, but also your file names, sizes, and timestamps.
Why Metadata Privacy Matters
Even with encrypted content, metadata can reveal sensitive information:
- File names like "tax_returns_2024.pdf" or "medical_records.docx" reveal intent
- File sizes can identify document types or media content
- Timestamps show when you accessed or modified files
- Content types indicate what kind of data you're storing
How It Works
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β METADATA PRIVACY FLOW β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β β β CLIENT SIDE (Original Metadata): β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β Key: /finances/investment_portfolio_2024.xlsx β β β β Size: 156,789 bytes β β β β Type: application/vnd.openxmlformats... β β β β Modified: 2024-12-07T15:30:00Z β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β β β βΌ β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β METADATA ENCRYPTION β β β β 1. Create PrivateMetadata struct with original values β β β β 2. Encrypt with file's DEK using AES-256-GCM β β β β 3. Generate obfuscated storage key via BLAKE3 β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β β β βΌ β β SERVER SIDE (What storage nodes see): β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β Key: e/a7c3f9b2e8d14a6f (obfuscated hash) β β β β Size: 156,821 bytes (ciphertext size) β β β β Type: application/octet-stream (generic) β β β β Metadata: [encrypted blob] β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key Obfuscation Modes
π FlatNamespace (DEFAULT)
Complete structure hiding. Server sees only random CID-like hashes. Inspired by WNFS and Peergos.
/photos/beach.jpg β QmX7a8f3e2d1c9b4a5
No prefixes, no structure hints. Directory tree stored in encrypted PrivateForest index.
DeterministicHash
Same file path β Same storage key. Allows retrieval without local index.
/photos/beach.jpg β e/a7c3f9b2e8d14a6f
RandomUuid
Each upload gets a random key. Maximum privacy but requires local mapping.
/photos/beach.jpg β e/random-uuid-here
PreserveStructure
Keep directory paths, hash only filenames. Allows folder browsing.
/photos/beach.jpg β /photos/e_a7c3f9b2
Code Example
use fula_client::{EncryptedClient, EncryptionConfig, KeyObfuscation};
// DEFAULT: FlatNamespace for complete structure hiding
let encryption = EncryptionConfig::new(); // FlatNamespace by default!
let client = EncryptedClient::new(config, encryption)?;
// Upload - server sees: QmX7a8f3e2d1c9b4a5e6f7d8...
client.put_object_flat(bucket, "/secret/file.txt", data, None).await?;
// List files from encrypted PrivateForest index
let files = client.list_files_from_forest(bucket).await?;
// Server sees NOTHING about your folder structure!
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Alternative modes (for specific use cases):
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// DeterministicHash - same path = same key, no local index needed
let encryption = EncryptionConfig::new()
.with_obfuscation_mode(KeyObfuscation::DeterministicHash);
// PreserveStructure - keeps folder paths visible
let encryption = EncryptionConfig::new()
.with_obfuscation_mode(KeyObfuscation::PreserveStructure);
// Disable metadata privacy entirely (not recommended)
let encryption = EncryptionConfig::new_without_privacy();
// Get full metadata including original filename
let info = client.get_object_with_private_metadata(bucket, storage_key).await?;
println!("Original name: {}", info.original_key);
println!("Original size: {}", info.original_size);
Security Guarantees
What the current implementation provides and its limitations.
β Provided
- Confidentiality: Data encrypted with AES-256-GCM; only key holder can decrypt
- Integrity: AEAD tags detect any tampering with ciphertext
- Authenticity: Only holder of secret key can produce valid decryption
- Forward Secrecy: Ephemeral keys in HPKE protect past data
- Key Isolation: Each file has unique DEK; compromise affects only that file
- Semantic Security: Same plaintext encrypts to different ciphertext each time
- Ciphertext Indistinguishability: Encrypted data appears random
- Metadata Privacy: File names, sizes, timestamps encrypted (optional, enabled by default)
β Not Provided
- Sender Authentication: Recipients can't verify who encrypted the data
- Deniability: Encryption proves you had the key
- Traffic Analysis Resistance: Access patterns may be observable
- Post-Quantum Security: X25519 vulnerable to quantum computers
Threat Model
What attacks Fula protects against and what it doesn't.
π‘οΈ Protected Against
Nodes only see encrypted data. They cannot read your files or forge valid data.
All data in transit is encrypted. Even if intercepted, it's unreadable.
AEAD tags and content addressing detect any modifications.
Gateway never sees plaintext or keys. Compromise only affects availability, not confidentiality.
Unique nonces prevent replaying old ciphertexts.
β οΈ Not Protected Against
If your device is compromised, attacker can extract keys.
Coercion/legal compulsion to reveal keys.
Timing, power analysis on the local device.
X25519 can be broken by sufficiently powerful quantum computers.
Multi-Device Key Management
Securely manage encryption keys across multiple devices.
Key Management Patterns
Fula supports multiple patterns for managing keys across devices:
Pattern A: Shared Identity
All devices share the same master key. Simple but all devices are equally trusted.
// All devices use same backed-up secret let master_secret = SecretKey::from_bytes(backed_up_secret)?; let keypair = KekKeyPair::from_secret(master_secret); let key_manager = KeyManager::new(&keypair); // Files encrypted on one device readable on all
Pattern B: Per-Device Keys
Each device has its own KEK. Access granted via ShareToken.
// Device A (primary)
let device_a = KekKeyPair::generate();
// Device B (secondary)
let device_b = KekKeyPair::generate();
// Grant Device B access to specific folders
let share = ShareBuilder::new(
&device_a,
device_b.public_key(),
&folder_dek
)
.path_scope("/shared/")
.build()?;
Handling Device Loss
When a device is lost or stolen:
- Immediate: Rotate KEK on remaining devices
- Re-wrap: Use
FileSystemRotationto re-wrap all DEKs - Revoke: Remove device from any shared access
- Audit: Review what data the device had access to
// On remaining device - rotate immediately
let mut rotation = FileSystemRotation::new(current_keypair);
let new_public_key = rotation.rotate();
// Re-wrap all DEKs with new KEK
while !rotation.is_rotation_complete() {
rotation.rotate_batch();
}
// Lost device's wrapped keys are now useless
Key Backup Strategy
Recovery Scenarios
| Scenario | Recovery Method | Data Loss |
|---|---|---|
| Lost device (backup exists) | Restore from backup | None |
| Lost device (no backup) | Cannot recover | All data |
| Compromised device (detected early) | Rotate + restore | None |
| Compromised device (delayed detection) | Rotate + audit | Potentially exposed |
π Full Documentation: See THREAT_MODEL.md for comprehensive multi-device patterns and threat analysis.
Security Best Practices
Recommendations for securely using Fula encryption.
Key Management
- Generate keys on a secure, trusted device
- Back up your secret key in multiple secure locations
- Never share or transmit your secret key
- Rotate keys periodically or after potential compromise
Client Security
- Keep your device and OS updated
- Use full-disk encryption
- Use a hardware security module if available
- Clear keys from memory after use
Data Handling
- Always enable encryption for sensitive data
- Verify file integrity after download
- Don't store sensitive metadata in unencrypted fields
- Use unique DEKs per file (default behavior)
Operational Security
- Monitor for unauthorized access attempts
- Have an incident response plan for key compromise
- Test key recovery procedures periodically
- Audit encryption settings regularly