Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,6 @@ jobs:
- name: Clippy checks
run: cargo clippy --all-targets --all-features

check_pr_size:
if: "!startsWith(github.event.pull_request.title, 'Automated version bump')"
name: Check PR size doesn't break set limit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: "0"
- uses: maidsafe/pr_size_checker@v2
with:
max_lines_changed: 200

coverage:
if: "!startsWith(github.event.pull_request.title, 'Automated version bump')"
name: Code coverage check
Expand Down
163 changes: 163 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Development Commands

### Building and Testing

```bash
# Format code (MANDATORY before commits)
cargo fmt --all

# Run clippy linter with strict settings
cargo clippy --all-features -- -D warnings

# Run all Rust tests
cargo test --release

# Run comprehensive test script (includes Python tests)
./scripts/test.sh

# Build Python package with maturin
maturin develop --features python

# Run Python tests
pytest tests/ -v

# Run benchmarks
cargo bench

# Check for unused dependencies
cargo udeps --all-targets

# Publish dry run
cargo publish --dry-run
```

### Single Test Execution

```bash
# Run a specific Rust test
cargo test test_name --release

# Run a specific Python test
pytest tests/test_file.py::test_name -v

# Run tests with output
cargo test -- --nocapture
```

## Architecture Overview

### Core Encryption Process

The self_encryption crate implements convergent encryption with obfuscation through a three-stage process:

1. **Content Chunking**: Files are split into chunks (up to 1MB each)
2. **Per-Chunk Processing**:
- Compression (Brotli with configurable quality)
- Encryption (AES-256-CBC)
- XOR obfuscation
3. **Key Derivation**: Each chunk's encryption keys are derived from a circular dependency pattern:
- Chunks 0 and 1 have special handling due to circular dependencies
- For chunk N (where N ≥ 2): uses hashes from chunks N, (N+1) % total, (N+2) % total
- Creates interdependency where modifying any chunk affects multiple others

### Key Components

- **`src/lib.rs`**: Main library interface, exports public API including `encrypt`, `decrypt_full_set`
- **`src/encrypt.rs`**: Core encryption logic, handles chunk processing and key generation
- **`src/decrypt.rs`**: Decryption logic, reverses the encryption process
- **`src/data_map.rs`**: DataMap structure that stores chunk metadata (src/dst hashes, sizes, indices)
- **`src/stream.rs`**: Streaming encryption/decryption for memory-efficient large file handling
- **`src/chunk.rs`**: Chunk data structures (`EncryptedChunk`, `ChunkInfo`) and validation
- **`src/aes.rs`**: AES encryption implementation using CBC mode
- **`src/utils.rs`**: Utility functions for key derivation, hash extraction, chunk size calculation
- **`src/python.rs`**: PyO3 bindings for Python interface
- **`src/error.rs`**: Error types and handling

### Storage Backend Design

The library uses a trait-based design for flexible storage backends:
- Store functions: `Fn(XorName, Bytes) -> Result<()>`
- Retrieve functions: `Fn(XorName) -> Result<Bytes>`
- Supports memory, disk, or custom storage implementations

### DataMap Hierarchy

For large files, DataMaps can be shrunk hierarchically:
- Serialize large DataMap → Encrypt as data → Create new smaller DataMap
- Process repeats until manageable size reached
- `child` field tracks hierarchy level

## Critical Constraints

- **Minimum file size**: 3072 bytes (3 * MIN_CHUNK_SIZE) for self-encryption
- **Chunk size**: Maximum 1MB per chunk
- **Key security**: The returned secret key from encryption requires secure handling
- **Hash verification**: All chunks are self-validating through SHA3-256 hashes

## Python Bindings

The Python interface is built with PyO3 and maturin:
- CLI tool: `self-encryption` command
- Module: `self_encryption` Python package
- Supports both in-memory and streaming operations

## CI/CD Workflow

- **PR checks**: Format, clippy, tests, coverage, unused deps
- **Warnings as errors**: `RUSTFLAGS="-D warnings"` enforced in CI
- **Code coverage**: Uses cargo-llvm-cov and reports to coveralls.io
- **32-bit testing**: Includes i686 target testing
- **Python package**: Automated publishing via GitHub Actions

## Performance Considerations

- Parallel chunk processing via rayon in standard implementation
- Streaming APIs for memory efficiency with large files
- Benchmarks in `benches/lib.rs` for tracking performance
- Optimized compression settings in Brotli
- Chunk size optimization based on file size

## StreamSelfEncryptor Implementation Notes

The streaming implementation differs from the standard implementation in several important ways:

### Design Differences

1. **Memory Usage**:
- Standard: Loads entire file into memory, processes all chunks at once
- Streaming: Processes one chunk at a time, O(1) memory usage

2. **API Pattern**:
- Standard: Functional approach with `encrypt(bytes) -> (DataMap, Vec<EncryptedChunk>)`
- Streaming: Stateful object with `next_encryption()` returning chunks incrementally

3. **Chunk Processing**:
- Standard: Special handling for chunks 0 and 1 (deferred processing due to circular dependencies)
- Streaming: Processes all chunks uniformly (potential issue)

### Known Issues with StreamSelfEncryptor

1. **First Two Chunks**: Does not implement the special handling for chunks 0 and 1 that the standard implementation uses. This could lead to incorrect encryption in edge cases.

2. **Error Handling**: Less robust error handling compared to standard implementation, particularly around chunk validation.

3. **File System Dependency**: StreamSelfDecryptor uses temporary files extensively, which adds complexity and potential failure points.

### When to Use Each Implementation

- **Standard Implementation**: Use for files that fit comfortably in memory (< 1GB)
- **Streaming Implementation**: Use for large files where memory usage is a concern
- **Note**: Both implementations produce compatible output when working correctly

### Potential Improvements Needed

1. **Unify Chunk Processing**: Align StreamSelfEncryptor's chunk processing with standard implementation, especially for chunks 0 and 1
2. **Error Handling**: Improve error handling in streaming implementation to match standard implementation's robustness
3. **Reduce File System Operations**: Consider memory-mapping or buffering strategies for StreamSelfDecryptor
4. **Progress Callbacks**: Add progress reporting capabilities to streaming implementation
5. **Test Coverage**: Ensure streaming implementation has comprehensive tests for edge cases
6. **API Consistency**: Consider refactoring to provide more consistent APIs between implementations
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ features = ["rt"]
criterion = "0.5.1"
docopt = "~0.9.0"
clap = { version = "4.4", features = ["derive"] }
serde_json = "1.0"

[dev-dependencies.tokio]
version = "1.34.0"
Expand Down
24 changes: 9 additions & 15 deletions examples/basic_encryptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ struct Args {
}

fn to_hex(ch: u8) -> String {
fmt::format(format_args!("{:02x}", ch))
fmt::format(format_args!("{ch:02x}"))
}

fn file_name(name: XorName) -> String {
Expand Down Expand Up @@ -115,7 +115,7 @@ impl DiskBasedStorage {
let mut file = File::create(&path)?;
file.write_all(&data[..])
.map(|_| {
println!("Chunk written to {:?}", path);
println!("Chunk written to {path:?}");
})
.map_err(From::from)
}
Expand All @@ -128,7 +128,7 @@ async fn main() {
.and_then(|d| d.deserialize())
.unwrap_or_else(|e| e.exit());
if args.flag_help {
println!("{:?}", args)
println!("{args:?}")
}

let mut chunk_store_dir = env::temp_dir();
Expand All @@ -145,7 +145,7 @@ async fn main() {
let mut data = Vec::new();
match file.read_to_end(&mut data) {
Ok(_) => (),
Err(error) => return println!("{}", error),
Err(error) => return println!("{error}"),
}

let (data_map, encrypted_chunks) = encrypt(Bytes::from(data)).unwrap();
Expand All @@ -163,20 +163,14 @@ async fn main() {
Ok(mut file) => {
let encoded = serialize(&data_map).unwrap();
match file.write_all(&encoded[..]) {
Ok(_) => println!("Data map written to {:?}", data_map_file),
Ok(_) => println!("Data map written to {data_map_file:?}"),
Err(error) => {
println!(
"Failed to write data map to {:?} - {:?}",
data_map_file, error
);
println!("Failed to write data map to {data_map_file:?} - {error:?}");
}
}
}
Err(error) => {
println!(
"Failed to create data map at {:?} - {:?}",
data_map_file, error
);
println!("Failed to create data map at {data_map_file:?} - {error:?}");
}
}
} else {
Expand Down Expand Up @@ -214,7 +208,7 @@ async fn main() {
let content =
decrypt(&DataMap::new(keys), encrypted_chunks.as_ref()).unwrap();
match file.write_all(&content[..]) {
Err(error) => println!("File write failed - {:?}", error),
Err(error) => println!("File write failed - {error:?}"),
Ok(_) => {
println!("File decrypted to {:?}", args.arg_destination.unwrap())
}
Expand All @@ -228,7 +222,7 @@ async fn main() {
}
}
} else {
println!("Failed to open data map at {:?}", data_map_file);
println!("Failed to open data map at {data_map_file:?}");
}
}
}
6 changes: 3 additions & 3 deletions examples/parallel_streaming_decryptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ fn main() -> Result<()> {
let mut chunk_data = Vec::new();
File::open(&chunk_path)
.and_then(|mut file| file.read_to_end(&mut chunk_data))
.map_err(|e| Error::Generic(format!("Failed to read chunk: {}", e)))?;
.map_err(|e| Error::Generic(format!("Failed to read chunk: {e}")))?;
Ok(Bytes::from(chunk_data))
})
.collect()
Expand All @@ -106,9 +106,9 @@ fn main() -> Result<()> {
// Helper function to load data map from a file
fn load_data_map(path: &str) -> Result<DataMap> {
let mut file =
File::open(path).map_err(|e| Error::Generic(format!("Failed to open data map: {}", e)))?;
File::open(path).map_err(|e| Error::Generic(format!("Failed to open data map: {e}")))?;
let mut data = Vec::new();
file.read_to_end(&mut data)
.map_err(|e| Error::Generic(format!("Failed to read data map: {}", e)))?;
.map_err(|e| Error::Generic(format!("Failed to read data map: {e}")))?;
deserialize(&data)
}
Loading
Loading