From 441242283d4f34a00ad9cde9326baef950f61064 Mon Sep 17 00:00:00 2001 From: chohee Date: Sat, 5 Jul 2025 04:55:07 +0800 Subject: [PATCH 1/8] docs: Add peer-cache-encryption-storage.md Signed-off-by: chohee --- .../peer-cache-encryption-storage.md | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 systems-analysis/peer-cache-encryption-storage.md diff --git a/systems-analysis/peer-cache-encryption-storage.md b/systems-analysis/peer-cache-encryption-storage.md new file mode 100644 index 0000000..c4d916e --- /dev/null +++ b/systems-analysis/peer-cache-encryption-storage.md @@ -0,0 +1,170 @@ +# Design Document: P2P Peer Cache Encryption Storage + +## Overview + +This design document proposes adding encryption storage for peer cache in Dragonfly's P2P file transfer mechanism. The goal is to enhance data security for cached files within the P2P network, ensuring that sensitive information remains protected during storage and transfer. + +## Motivation + + * **Data Security**: Encrypting cached data protects sensitive information from unauthorized access, especially in environments where peers might store data on insecure disks. + * **Compliance**: Addresses potential compliance requirements for data at rest and in transit within the P2P network. + * **Integrity**: Helps ensure the integrity of cached data by detecting tampering attempts. + +## Goals + +1. Introduce encryption and decryption capabilities for peer-cached data. +2. Maintain backward compatibility with existing Dragonfly functionalities. +3. Support configurable encryption options, allowing users to enable/disable and select encryption algorithms. +4. Integrate seamlessly with the existing Persistent Cache workflow. +5. Ensure minimal performance overhead for encryption/decryption operations. + +## Architecture + +The proposed architecture involves introducing an encryption module within the Dragonfly client's storage layer. This module will intercept data during writing to and reading from the persistent cache, performing encryption and decryption operations respectively. + +``` +P2P Network -> Downloader -> Piece Writer (Encryption) -> Persistent Cache Storage +Persistent Cache Storage -> Piece Reader (Decryption) -> Downloader -> P2P Network +``` + +### Modules + +``` +dragonfly-client-storage/src/ +└── entrypt/ + └── entryptor.rs # Entryptor implementation +``` +* `piece.rs`: Add data encryption/decryption +* `storage/src/lib.rs`: Add key generation for Task +* `metadata.rs`: Add key or key_id in Task metadata + + +## Implementation + +### Encryptor + +```rust +/// Encryption algorithm trait +pub trait CryptoAlgo { + fn encrypt(&self, plaintext: Vec) -> Result>; + fn decrypt(&self, ciphertext: Vec) -> Result>; + ...... +} + +pub struct Encryptor { + algo: A, +} + +impl Encryptor { + pub fn new(algo: A) -> Self { + Self { algo } + } + + pub fn encrypt_piece(&self, mut reader: R, mut writer: W) -> Result<()> { + let ciphertext = self.algo.encrypt(...)?; + ...... + Ok(()) + } + + pub fn decrypt_file(&self, mut reader: R, mut writer: W) -> Result<()> { + let plain = self.algo.decrypt(...)?; + ...... + Ok(()) + } +} + +/// AES-CTR implement +pub mod aes_ctr_impl { + use super::*; + use aes::Aes256; + use ctr::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}; + use ctr::Ctr128BE; + + pub struct AesCtrAlgo { + key: [u8; 32], + base_iv: [u8; 16], + } + + impl AesCtrAlgo { + pub fn new(key: [u8; 32], base_iv: [u8; 16]) -> Self { + Self { key, base_iv } + } + } + + impl CryptoAlgo for AesCtrAlgo { + fn encrypt(&self, plaintext: Vec) -> Result> { + let mut cipher = Ctr128BE::::new(&self.key.into(), &self.base_iv.into()) + cipher.apply_keystream(&mut plaintext); + Ok(buf) + } + + fn decrypt(&self, ciphertext: Vec) -> Result> { + self.encrypt(ciphertext) + } + } +} + +``` + +### PersistentCacheTask + +```rust +/// New member for key +pub struct PersistentCacheTask { + ... + // TODO +    pub entrypt_id_or_key: Option, +} + +/// Generate key when task started +pub async fn download_persistent_cache_task_started(...) -> Result { + // ADD TODO + let key = generate_key(); + + let metadata = self.metadata.download_persistent_cache_task_started( + id, + ttl, + persistent, + piece_length, + content_length, + created_at, + //ADD TODO + key, + )?; + ...... +} +``` + + + +### Piece Write/Read Operations + + * **Writing**: Before a piece of data is written to the persistent cache, the `entryptor` will encrypt it. The encrypted piece will then be stored. + * **Reading**: When a piece of data is requested from the persistent cache, the encrypted piece will be retrieved, and the `entryptor` will decrypt it before it's made available. + + + +## Testing + +1. **Unit Tests**: Test individual components of the encryption module with high coverage (over 85%). +2. **Integration Tests**: End-to-end functional verification, ensuring that data is correctly encrypted and decrypted during the P2P transfer process. +3. **Performance Tests**: Benchmark the performance impact of encryption/decryption on file transfer speeds compared to unencrypted transfers. +4. **Stress Tests**: High-concurrency and long-duration testing to ensure stability and reliability with encryption enabled. + +## Compatibility + +1. **Backward Compatibility**: The existing `Task` and `PersistentCacheTask` functionalities will remain unchanged for non-encrypted operations. The encryption feature will be optional and configurable. +2. **Configuration**: New encryption-related configurations will be added with sensible defaults (e.g., encryption disabled by default). +3. **Migration**: Users can enable or disable the encryption feature via configuration without requiring code changes. + +## Future + +1. **Key Management Enhancements**: Explore integration with Key Management Systems (KMS) for more robust key handling. +2. **Algorithm Flexibility**: Support for a wider range of encryption algorithms and modes. +3. **Dynamic Key Rotation**: Implement mechanisms for automatic key rotation to enhance security. + +----- + +This design provides a foundation for adding P2P Peer Cache Encryption Storage to Dragonfly while maintaining system stability and backward compatibility. + + \ No newline at end of file From 5fe60e1b9ee35a5547e45e126a078b9430b79f22 Mon Sep 17 00:00:00 2001 From: chohee Date: Wed, 9 Jul 2025 15:14:36 +0800 Subject: [PATCH 2/8] docs: add introduction of algorithm and implementation Signed-off-by: chohee --- .../peer-cache-encryption-storage.md | 156 +++++++++++++++++- 1 file changed, 148 insertions(+), 8 deletions(-) diff --git a/systems-analysis/peer-cache-encryption-storage.md b/systems-analysis/peer-cache-encryption-storage.md index c4d916e..0f822c3 100644 --- a/systems-analysis/peer-cache-encryption-storage.md +++ b/systems-analysis/peer-cache-encryption-storage.md @@ -31,8 +31,8 @@ Persistent Cache Storage -> Piece Reader (Decryption) -> Downloader -> P2P Netwo ``` dragonfly-client-storage/src/ -└── entrypt/ - └── entryptor.rs # Entryptor implementation +└── encrypt/ + └── cryptor.rs # Crypt implementation ``` * `piece.rs`: Add data encryption/decryption * `storage/src/lib.rs`: Add key generation for Task @@ -41,7 +41,51 @@ dragonfly-client-storage/src/ ## Implementation -### Encryptor +### Encryption Algorithm + +Cache may involve large files, in which case symmetric encryption is usually more efficient and simpler to manage. + +Currently, commonly used algorithms are AES and ChaCha20, each with some enhanced versions: + + +| Property/Algorithm | AES-CTR | AES-GCM | ChaCha20 | XChaCha20 | ChaCha20-Poly1305 | +| :----------------- | :------ | :------ | :------- | :-------- | :---------------- | +| Algorithm Type | Stream Cipher (non-authenticated) | AEAD (Authenticated) | Stream Cipher (non-authenticated) | Stream Cipher (non-authenticated) | AEAD (Authenticated) | +| Authentication (Integrity Check) | None | Yes | None | None | Yes | +| Content to be saved for decryption | key + iv | key + iv + tag (per block) | key + nonce | key + nonce | key + nonce + tag (per block) | +| Nonce/IV Length | 16 bytes (IV) | 12 bytes | 12 bytes | 24 bytes | 12 bytes | +| Speed (with hardware support) | Very Fast | Fast (has authentication cost) | Fast | Fast | Moderate | +| Speed (without hardware support) | Slow | Slow | Still fast | Still fast | Still Moderate | +| Hardware Acceleration Support | Widely supports AES-NI | Widely supports AES-NI | Relies mainly on SSE/AVX | Relies mainly on SSE/AVX | Relies mainly on SSE/AVX | +| Security | No authentication (easily tampered) | High | No authentication | No authentication | High | +| Common Uses | Disk cache, fast encryption | TLS, QUIC, HTTPS | WireGuard, embedded | WireGuard, large file streaming | age, Cloudflare file encryption | +| Ciphertext length equal to plaintext | Yes | No (+tag) | Yes | Yes | No (+tag) | + + + +The advantage of AES is its more widespread hardware acceleration support, making it very fast when hardware support is available. + +However, on platforms without hardware support, its performance might not be as good as ChaCha20. + +Besides encryption, authentication can check if data has been maliciously tampered with, but at the cost of needing to store additional authentication information. AES-GCM and ChaCha20-Poly1305 have this capability. + + +Dragonfly's current design writes Pieces to their corresponding offsets in the final file. This implies that each Piece must be encrypted prior to being written to disk to ensure no plaintext information is stored during the entire process. + + +In this case, authenticated algorithms would introduce additional complexity because authentication information would need to be saved for each encrypted Piece block. + +Given that Dragonfly is typically deployed on servers, it is highly likely to have AES hardware acceleration, so AES speed should be optimal. + +At the same time, Dragonfly uses CRC32 and other verification methods, which to some extent defend against tampering (but not completely). Authentication might not be the core task, so AES-CTR could be be a reasonable choice. + +Multiple encryption methods can be provided for selection in the actual code. + + +### Cryptor + +The `Cryptor` can receive multiple `CryptoAlgo` implementations and use them for encryption and decryption. +This allows for supporting different encryption algorithms by writing various implementations. ```rust /// Encryption algorithm trait @@ -51,11 +95,11 @@ pub trait CryptoAlgo { ...... } -pub struct Encryptor { +pub struct Cryptor { algo: A, } -impl Encryptor { +impl Cryptor { pub fn new(algo: A) -> Self { Self { algo } } @@ -106,14 +150,41 @@ pub mod aes_ctr_impl { ``` +Use of different encryption algorithms: + +```rust +// AES-CTR +let (key, iv) = gen_aes_iv(); +let algo = AesCtrAlgo::new(key, iv); +let aes_cryptor = Cryptor::new(algo); + +aes_cryptor.encrypt_piece(...) +aes_cryptor.decrypt_piece(...) + + +// ChaCha20 +let (key, nonce) = gen_key_nonce(); +let algo = ChaChaAlgo::new(&key, nonce); +let chacha_cryptor = Cryptor::new(algo); + +chacha_cryptor.encrypt_piece(...) +chacha_cryptor.decrypt_piece(...) +``` + + + + ### PersistentCacheTask +For each `Task` that requires encryption, a corresponding key is generated and saved in the Task's `metadata`. +Key generation should be completed before new `metadata` is created. + ```rust /// New member for key pub struct PersistentCacheTask { ... // TODO -    pub entrypt_id_or_key: Option, + pub entrypt_id_or_key: Option, } /// Generate key when task started @@ -139,8 +210,77 @@ pub async fn download_persistent_cache_task_started(...) -> Result { + ...... + match self + .content + .read_piece(task_id, piece.offset, piece.length, range) + .await + { + Ok(reader) => { + // ADD TODO + reader = cryptor.decrypt_piece(...); + // Finish uploading the task. + self.metadata.upload_task_finished(task_id)?; + Ok(Either::Right(reader)) + } + ...... + } + } + ...... + } + } + ``` From e28fe3701f170443d8c050425d9218edf4082508 Mon Sep 17 00:00:00 2001 From: chohee Date: Tue, 15 Jul 2025 16:01:53 +0800 Subject: [PATCH 3/8] docs: update design Signed-off-by: chohee --- .../peer-cache-encryption-storage.md | 641 ++++++++++++++---- 1 file changed, 491 insertions(+), 150 deletions(-) diff --git a/systems-analysis/peer-cache-encryption-storage.md b/systems-analysis/peer-cache-encryption-storage.md index 0f822c3..840b273 100644 --- a/systems-analysis/peer-cache-encryption-storage.md +++ b/systems-analysis/peer-cache-encryption-storage.md @@ -29,14 +29,25 @@ Persistent Cache Storage -> Piece Reader (Decryption) -> Downloader -> P2P Netwo ### Modules +A new module `dragonfly-client-crypto` will be added to handle encryption-related functionality. + ``` -dragonfly-client-storage/src/ -└── encrypt/ - └── cryptor.rs # Crypt implementation +dragonfly-client-crypto/ +└── src/ + ├── cryptor/ + │ ├── crypto_type.rs + │ ├── piece_cryptor.rs + │ └── mod.rs + ├── algorithm/ + │ ├── chacha20poly1305.rs + │ ├── ...other_algorithms.rs + │ └── mod.rs + └── lib.rs ``` -* `piece.rs`: Add data encryption/decryption -* `storage/src/lib.rs`: Add key generation for Task -* `metadata.rs`: Add key or key_id in Task metadata + +* `cryptor/crypto_type.rs`: Defines the types of encryption. +* `cryptor/piece_cryptor.rs`: Add implementation of encryptor/decryptor. +* `algorithm/*.rs`: Add implementation of specific encryption algorithms. ## Implementation @@ -65,7 +76,7 @@ Currently, commonly used algorithms are AES and ChaCha20, each with some enhance The advantage of AES is its more widespread hardware acceleration support, making it very fast when hardware support is available. -However, on platforms without hardware support, its performance might not be as good as ChaCha20. +However, on platforms without hardware support, AES's performance might not be as good as ChaCha20. Besides encryption, authentication can check if data has been maliciously tampered with, but at the cost of needing to store additional authentication information. AES-GCM and ChaCha20-Poly1305 have this capability. @@ -77,211 +88,541 @@ In this case, authenticated algorithms would introduce additional complexity bec Given that Dragonfly is typically deployed on servers, it is highly likely to have AES hardware acceleration, so AES speed should be optimal. -At the same time, Dragonfly uses CRC32 and other verification methods, which to some extent defend against tampering (but not completely). Authentication might not be the core task, so AES-CTR could be be a reasonable choice. +At the same time, Dragonfly uses CRC32 and other verification methods, which to some extent defend against tampering (but not completely). Authentication might not be the core task. + +Different scenarios call for different suitable algorithms: + +| Scenario | With AES Acceleration | Without AES Acceleration | +|-------------------------|----------------------|-------------------------| +| Tamper detection needed | AES-GCM | ChaCha20-Poly1305 | +| Tamper detection not needed | AES-CTR | ChaCha20/XChaCha20 | Multiple encryption methods can be provided for selection in the actual code. -### Cryptor +### Configuration + +Configuration options need to be added under `Config/Storage` to indicate whether encrypted storage is enabled and which encryption algorithm is selected. +The `CryptoType` enum is used to represent the various algorithms. + +```rust +// dragonfly-client-crypto/src/cryptor/crypto_type.rs +pub enum CryptoType { + #[serde(rename = "chacha20-poly1305")] + ChaCha20Poly1305, + #[serde(rename = "aes-gcm")] + AesGcm, + #[serde(rename = "aes-ctr")] + AesCtr, + +} + +// dragonfly-client-config/src/dfdaemon.rs +pub struct Storage { + ...... + /// enable_encryption indicates whether to enable encryption for persistent cache storage. + #[serde(default = "default_storage_enable_encryption")] + pub enable_encryption: bool, + + /// encryption_algorithm indicates which algorithm will be used when encryption + #[serde(default = "default_storage_encryption_algo")] + pub encryption_algorithm: CryptoType, +} +``` -The `Cryptor` can receive multiple `CryptoAlgo` implementations and use them for encryption and decryption. -This allows for supporting different encryption algorithms by writing various implementations. +Config yaml example: + +```yaml +storage: + dir: /var/lib/dragonfly/ + keep: true + writeBufferSize: 4194304 + readBufferSize: 4194304 + # ADDED + enableEncryption: true + encryptionAlgorithm: chacha20-poly1305 +``` + + +### Metadata + +The information required for encryption includes the plaintext, key, and nonce; the output is the ciphertext. +For algorithms that support authentication, an authentication tag will also be generated. + +For decryption, the required information includes the ciphertext, key, and nonce. +For authenticated algorithms (AEAD), the tag is also necessary; the output is the corresponding plaintext. + +The key, nonce, and tag used for decryption must match those used during encryption. + +When encrypting different content with the same key, it is important to ensure that the nonce is different each time to enhance security. +If the same key and the same nonce are used to encrypt different content, an attacker may be able to deduce information about the plaintext from multiple ciphertexts. + +A constructed nonce can be used to ensure uniqueness under the same key. +For example, by combining part of the `task_id` with part of the `piece_number`, a unique nonce value can be generated. +This approach eliminates the need to store a separate nonce for each piece. + +Currently, the plan is to generate a key for each `PersistentCacheTask`, and all related `Pieces` will use this key for encryption and decryption. +Since the algorithm used may change, the type of algorithm also needs to be recorded. + +This information is stored in the metadata: ```rust -/// Encryption algorithm trait -pub trait CryptoAlgo { - fn encrypt(&self, plaintext: Vec) -> Result>; - fn decrypt(&self, ciphertext: Vec) -> Result>; +// dragonfly-client-storage/src/metadata.rs +pub struct PersistentCacheTask{ ...... + /// crypto_info is the info saved for encryption/decryption + pub crypto_info: Option, } -pub struct Cryptor { - algo: A, +// dragonfly-client-crypto/src/cryptor/crypto_type.rs +pub struct CryptoInfo { + // algorithm type + pub crypto_type: CryptoType, + pub key: Vec, } +``` -impl Cryptor { - pub fn new(algo: A) -> Self { - Self { algo } - } +For authenticated algorithms (AEAD), each encrypted `Piece` needs to store its corresponding authentication tag: - pub fn encrypt_piece(&self, mut reader: R, mut writer: W) -> Result<()> { - let ciphertext = self.algo.encrypt(...)?; - ...... - Ok(()) +```rust +// dragonfly-client-storage/src/metadata.rs +pub struct Piece { + ...... + /// auth_tag is the tag generated by encryption algorithm when use AEAD + pub auth_tag: Option>, +} +``` + + +### Cryptor + +The `PieceCryptor` trait defines the interface for the Cryptor: + +```rust +// dragonfly-client-crypto/src/cryptor/piece_cryptor.rs +pub struct EncryptResult { + pub ciphertext: Vec, + pub tag: Option>, +} + +pub trait PieceCryptor { + fn encrypt_piece(&self, plaintext: &[u8], task_id: &str, piece_num: u32) -> Result; + fn decrypt_piece(&self, ciphertext: &[u8], task_id: &str, piece_num: u32, tag: Option<&[u8]>) -> Result>; + fn key_size() -> usize; + fn nonce_size() -> usize; + fn tag_size() -> Option; +} +``` + +`task_id` and `piece_num` are used to construct the nonce. + +The `encrypt_piece` function returns an `EncryptResult`, which contains the encrypted `ciphertext` and an optional `tag` (an authentication tag is produced when using AEAD). + +The `decrypt_piece` function returns the plaintext; its parameters include an optional `tag` (which is required for decryption when using AEAD). + +The formats of the key, nonce, and other parameters differ between algorithms. For example, RustCrypto’s AES-CTR uses a 16-byte nonce, while AES-GCM uses a 12-byte nonce. + +Therefore, interfaces like `key_size` and `nonce_size` are important for generating or constructing the corresponding information. + +Below is an example implementation of `PieceCryptor` using the chacha20poly1305 algorithm: + +```rust +pub struct ChaCha20Poly1305Cryptor { + cipher: ChaCha20Poly1305, +} + +impl ChaCha20Poly1305Cryptor { + pub fn new(key: &[u8]) -> Self { + let key = Key::from_slice(key); + Self { + // use RustCrypto crate + cipher: ChaCha20Poly1305::new(key), + } } - pub fn decrypt_file(&self, mut reader: R, mut writer: W) -> Result<()> { - let plain = self.algo.decrypt(...)?; - ...... - Ok(()) + // construct nonce from task_id and piece_number, it does not need to be loaded/saved from/to disk + fn build_nonce(task_id: &str, piece_num: u32) -> Vec { + let nonce_size = Self::nonce_size(); + let mut nonce = vec![0u8; nonce_size]; + let task_bytes = task_id.as_bytes(); + assert!(task_bytes.len() > 8); + nonce[..8].copy_from_slice(&task_bytes[..8]); // nonce's first 8 bytes for task_id + nonce[8..].copy_from_slice(&piece_num.to_be_bytes()); // remaining bytes for piece number + assert!(nonce.len() == Self::nonce_size()); + nonce } + } -/// AES-CTR implement -pub mod aes_ctr_impl { - use super::*; - use aes::Aes256; - use ctr::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}; - use ctr::Ctr128BE; +impl PieceCryptor for ChaCha20Poly1305Cryptor { + fn encrypt_piece(&self, plaintext: &[u8], task_id: &str, piece_num: u32) -> Result{ + let nonce = Self::build_nonce(task_id, piece_num); + let nonce = Nonce::from_slice(&nonce); + + // AEAD output will append auth tag after the ciphertext, so the output will be longer then plaintext + // res is ciphertext + tag + let res = self + .cipher + .encrypt(nonce, plaintext) + .or_err(ErrorType::StorageError)?; + + // split res to ciphertext and tag, + let tag_size = Self::tag_size().expect("should have tag size"); + let (ciphertext, tag) = res.split_at(res.len() - tag_size); + Ok(EncryptResult { ciphertext: ciphertext.to_vec(), tag: Some(tag.to_vec()) }) + } + + fn decrypt_piece(&self, ciphertext: &[u8], task_id: &str, piece_num: u32, tag: Option<&[u8]>) -> Result>{ + let nonce = Self::build_nonce(task_id, piece_num); + let nonce = Nonce::from_slice(&nonce); - pub struct AesCtrAlgo { - key: [u8; 32], - base_iv: [u8; 16], + // concatenate ciphertext and tag for decryption + let tag = tag.expect("should have tag"); + let mut combined = Vec::with_capacity(ciphertext.len() + tag.len()); + combined.extend_from_slice(ciphertext); + combined.extend_from_slice(tag); + let combined_slice: &[u8] = &combined; + + // decrypt + let plaintext = self + .cipher + .decrypt(nonce, combined_slice) + .or_err(ErrorType::StorageError)?; + + Ok(plaintext) } - impl AesCtrAlgo { - pub fn new(key: [u8; 32], base_iv: [u8; 16]) -> Self { - Self { key, base_iv } - } + fn key_size() -> usize { + 32 // key size of chacha20-poly1305 is 32 bytes } - impl CryptoAlgo for AesCtrAlgo { - fn encrypt(&self, plaintext: Vec) -> Result> { - let mut cipher = Ctr128BE::::new(&self.key.into(), &self.base_iv.into()) - cipher.apply_keystream(&mut plaintext); - Ok(buf) + fn nonce_size() -> usize { + 12 // nonce size of chacha20-poly1305 is 12 bytes + } + + fn tag_size() -> Option { + Some(16) // tag size of chacha20-poly1305 is 16 bytes + // algorithm without authentication such as aes-ctr, this should return None + } +} +``` + +`CryptorImpl` is the outermost wrapper for the encryptor/decryptor. +By using an enum-based dispatch mechanism instead of `dyn`, it supports multiple encryption algorithm implementations: + +```rust +// dragonfly-client-crypto/src/cryptor/piece_cryptor.rs +pub enum CryptorImpl { + ChaCha20Poly1305(ChaCha20Poly1305Cryptor), + // AesGcm(AesGcmCryptor), + // AesCtr(AesCtrCryptor), +} + +impl CryptorImpl { + pub fn encrypt_piece(&self, plaintext: &[u8], task_id: &str, piece_num: u32) -> Result { + match self { + CryptorImpl::ChaCha20Poly1305(inner) => Ok(inner.encrypt_piece(plaintext, task_id, piece_num)?), + // CryptorImpl::AesGcm(inner) => ... } + } - fn decrypt(&self, ciphertext: Vec) -> Result> { - self.encrypt(ciphertext) + pub fn decrypt_piece(&self, ciphertext: &[u8], task_id: &str, piece_num: u32, tag: Option<&[u8]>) -> Result> { + match self { + CryptorImpl::ChaCha20Poly1305(inner) => Ok(inner.decrypt_piece(ciphertext, task_id, piece_num, tag)?), + // CryptorImpl::AesGcm(inner) => ... } } } - ``` -Use of different encryption algorithms: +`CryptorImpl` is obtained via `get_cryptor`, where the key parameter is generated according to the `CryptoType`. -```rust -// AES-CTR -let (key, iv) = gen_aes_iv(); -let algo = AesCtrAlgo::new(key, iv); -let aes_cryptor = Cryptor::new(algo); +Once the algorithm type is determined, the key format is also determined, so the key can be generated accordingly: -aes_cryptor.encrypt_piece(...) -aes_cryptor.decrypt_piece(...) +```rust +pub fn get_cryptor(crypto_type: &CryptoType, key: &[u8]) -> CryptorImpl { + match crypto_type { + // Select the specific encryption algorithm + CryptoType::ChaCha20Poly1305 => { + CryptorImpl::ChaCha20Poly1305(ChaCha20Poly1305Cryptor::new(key)) + } + ...... + _ => unimplemented!("algorithm not supported"), + } +} +impl CryptoType { + // The algorithm type determines the format of the key/nonce/tag + pub fn generate_key(&self) -> Vec { + match self { + // Generate the key according to the key format required by each algorithm + CryptoType::ChaCha20Poly1305 => { + // TODO random key + vec![0u8; ChaCha20Poly1305Cryptor::key_size()] + }, + ...... + _ => todo!(), + } + } +} +``` -// ChaCha20 -let (key, nonce) = gen_key_nonce(); -let algo = ChaChaAlgo::new(&key, nonce); -let chacha_cryptor = Cryptor::new(algo); +Encryption/Decryption example: -chacha_cryptor.encrypt_piece(...) -chacha_cryptor.decrypt_piece(...) +```rust +// encryption +let crypto_type = get_type_from_config_or_metadata(); // get algorithm used +let key = crypto_type.generate_key(); // key may need to be stored in task metadata +let cryptor = get_cryptor(&info.crypto_type, &info.key); +// get ciphertext, tag after encryption +let EncryptResult { ciphertext, tag } = cryptor.encrypt_piece_by_id(&plaintext, piece_id)?; + +// decryption +let crypto_info = get_info_from_metadata(); +let cryptor = get_cryptor(&info.crypto_type, &info.key); +// use key and tag(when use AEAD) to decrypt +let plaintext = cryptor.decrypt_piece_by_id(&ciphertext, piece_id, Some(tag)/None)?; ``` +### Piece Encryption/Decryption +The main changes are concentrated in `dragonfly-client-storage/src/lib.rs` and `dragonfly-client-storage/src/content.rs`. -### PersistentCacheTask +Taking the download process as an example, after entering `dfdaemon_download.rs/download_persistent_cache_task`, +the function `storage.download_persistent_cache_task_started` will be called first. -For each `Task` that requires encryption, a corresponding key is generated and saved in the Task's `metadata`. -Key generation should be completed before new `metadata` is created. +In this function, encryption is enabled or disabled based on the configuration, and the encryption information is stored in the task metadata. ```rust -/// New member for key -pub struct PersistentCacheTask { - ... - // TODO - pub entrypt_id_or_key: Option, +// dragonfly-client-storage/src/lib.rs +pub async fn download_persistent_cache_task_started( + &self, args... + )-> Result { + + // ADDED do encryption when config enable is set + let crypto_info = if self.config.storage.enable_encryption { + let crypto_type = self.config.storage.encryption_algorithm.clone(); + let key = crypto_type.generate_key(); + Some(CryptoInfo{crypto_type: crypto_type, key: key}) + } else { + None + }; + + let metadata = self.metadata.download_persistent_cache_task_started( + id, + ttl, + persistent, + piece_length, + content_length, + created_at, + // ADDED store key/algorithm type in the metadata of task + crypto_info, + )?; + + self.content + .create_persistent_cache_task(id, content_length) + .await?; + Ok(metadata) } +``` -/// Generate key when task started -pub async fn download_persistent_cache_task_started(...) -> Result { - // ADD TODO - let key = generate_key(); - - let metadata = self.metadata.download_persistent_cache_task_started( - id, - ttl, - persistent, - piece_length, - content_length, - created_at, - //ADD TODO - key, - )?; - ...... -} +The subsequent execution flow is as follows: + +``` +task_manager_clone.download() + PersistentCacheTask.download_partial_from_local() + if (need_piece_content) piece.download_persistent_cache_from_local_into_async_read() + storage.upload_persistent_cache_piece() + content.read_persistent_cache_piece() ``` +The program will check for any existing local cache. When `need_piece_content` is set, it will read content from the local cache. + +Therefore, decryption needs to be handled here. If the task metadata read by the upper layer contains encryption information, +the necessary decryption parameters should be passed in as arguments. +```rust +// dragonfly-client-storage/src/content.rs +pub async fn read_persistent_cache_piece( + &self, + task_id: &str, + offset: u64, + length: u64, + range: Option, + // ADDED + piece_id: &str, + crypto_info: Option<&CryptoInfo>, + auth_tag: Option<&[u8]>, +) -> Result { + // original code ↓ + let task_path = self.get_persistent_cache_task_path(task_id); + + let (target_offset, target_length) = calculate_piece_range(offset, length, range); + + let f = File::open(task_path.as_path()).await.inspect_err(|err| { + error!("open {:?} failed: {}", task_path, err); + })?; + let mut f_reader = BufReader::with_capacity(self.config.storage.read_buffer_size, f); + + f_reader + .seek(SeekFrom::Start(target_offset)) + .await + .inspect_err(|err| { + error!("seek {:?} failed: {}", task_path, err); + })?; + // original code ↑ -### Piece Write/Read Operations + // ADDED + let res = if let Some(info) = crypto_info { + // Read ciphertext from file + let mut ciphertext = vec![0u8; target_length as usize]; + f_reader.read_exact(&mut ciphertext).await?; -* **Writing**: Before a piece of data is written to the persistent cache, the `cryptor` will encrypt it. The encrypted piece will then be stored. + // decryption + let cryptor = get_cryptor(&info.crypto_type, &info.key); + let plaintext = cryptor.decrypt_piece_by_id(&ciphertext, piece_id, auth_tag)?; - For example, when downloading a piece: + Either::Left(Cursor::new(plaintext)) + } else { + Either::Right(f_reader.take(target_length)) + }; - ```rust - // dragonfly-client/src/resource/piece.rs + Ok(res) +} +``` - let (content, offset, digest) = self - .downloader - .download_persistent_cache_piece( - ... - ) +The following is the subsequent call process: + +``` +task_manager_clone.download() + PersistentCacheTask.download_partial_with_scheduler() + PersistentCacheTask.download_partial_with_scheduler_from_parent() + download_from_parent() + piece_manager.download_persistent_cache_from_parent() + storage.download_persistent_cache_piece_started() + downloader.download_persistent_cache_piece() + storage.download_persistent_cache_piece_from_parent_finished() + content.write_persistent_cache_piece() +``` + +The downloaded content is then passed to `content.write_persistent_cache_piece()`, which is responsible for writing it to the file. +Therefore, encryption logic needs to be added here. + +During the encryption process, the CRC should be calculated based on the original plaintext, +and the return value should include the tag to pass the AEAD authentication tag. + +```rust +// dragonfly-client-storage/src/content.rs +pub async fn write_persistent_cache_piece( + &self, + task_id: &str, + offset: u64, + expected_length: u64, + reader: &mut R, + // ADDED + piece_id: &str, + crypto_info: Option<&CryptoInfo>, +) -> Result { + // original code ↓ + // Open the file and seek to the offset. + let task_path = self.get_persistent_cache_task_path(task_id); + let mut f = OpenOptions::new() + .truncate(false) + .write(true) + .open(task_path.as_path()) .await .inspect_err(|err| { - ... + error!("open {:?} failed: {}", task_path, err); })?; - // ADD TODO - let encrypted_piece = cryptor.entrypt_piece(content).unwarp(); - let mut reader = Cursor::new(encrypted_piece); - - // Record the finish of downloading piece. - match self - .storage - .download_persistent_cache_piece_from_parent_finished( - piece_id, - task_id, - offset, - length, - digest.as_str(), - parent.id.as_str(), - &mut reader, - ) - .await - { - ... - } - ``` - + f.seek(SeekFrom::Start(offset)).await.inspect_err(|err| { + error!("seek {:?} failed: {}", task_path, err); + })?; + // original code ↑ + + // ADDED need encrypt + if let Some(info) = crypto_info { + // 1. read plaintext + let mut plaintext = Vec::new(); + reader.read_to_end(&mut plaintext).await?; + + // 2. use plaintext to calculate crc + let mut hasher = crc32fast::Hasher::new(); + hasher.update(&plaintext); + let hash = hasher.finalize().to_string(); + + // 3. encrypt + let cryptor = get_cryptor(&info.crypto_type, &info.key); + let EncryptResult { ciphertext, tag } = cryptor.encrypt_piece_by_id(&plaintext, piece_id)?; + + // 4. write + let mut writer = BufWriter::with_capacity(self.config.storage.write_buffer_size, f); + writer.write_all(&ciphertext).await?; + writer.flush().await?; + + // check length + if ciphertext.len() as u64 != expected_length { + return Err(Error::Unknown(format!( + "expected length {} but got {}", + expected_length, ciphertext.len() + ))); + } + Ok(WritePieceResponse { + length: ciphertext.len() as u64, + hash: hash, + // ADDED + auth_tag: tag, + }) -* **Reading**: When a piece of data is requested from the persistent cache, the encrypted piece will be retrieved, and the `cryptor` will decrypt it before it's made available. - - For example, when uploading a piece: - - ```rust - // dragonfly-client-storage/src/lib.rs - pub async fn upload_piece(...) { + } else { + // if don not need encryption, do original process ...... - match self.metadata.get_piece(piece_id) { - Ok(Some(piece)) => { - ...... - match self - .content - .read_piece(task_id, piece.offset, piece.length, range) - .await - { - Ok(reader) => { - // ADD TODO - reader = cryptor.decrypt_piece(...); - // Finish uploading the task. - self.metadata.upload_task_finished(task_id)?; - Ok(Either::Right(reader)) - } - ...... - } - } - ...... - } + Ok(WritePieceResponse { + length, + hash: hasher.finalize().to_string(), + // ADDED + auth_tag: None + }) } - ``` +} +``` + +--- + +The upload process reuses many of the same `Storage` calls as the download process, such as `content.write_persistent_cache_piece`, and similar modifications can be made to the differing parts: +```rust +// dragonfly-client-storage/src/lib.rs +// This is unique to the upload process. +pub async fn create_persistent_cache_task_started( + &self, ... +) -> Result { + // ADDED whether need to encrypt + let info = if self.config.storage.enable_encryption { + let crypto_type = self.config.storage.encryption_algorithm.clone(); + let key = crypto_type.generate_key(); + Some(CryptoInfo{crypto_type: crypto_type, key: key}) + } else { + None + }; + + let metadata = self.metadata.create_persistent_cache_task_started( + id, + ttl, + piece_length, + content_length, + // ADDED store crypto_info in metadata + info, + )?; + + self.content + .create_persistent_cache_task(id, content_length) + .await?; + Ok(metadata) +} +``` ## Testing From 47d8c9729820d8d1c5b2feaefb25504bee10f017 Mon Sep 17 00:00:00 2001 From: chohee Date: Thu, 17 Jul 2025 14:58:13 +0800 Subject: [PATCH 4/8] docs: modify module design Signed-off-by: chohee --- .../peer-cache-encryption-storage.md | 80 +++++++++++-------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/systems-analysis/peer-cache-encryption-storage.md b/systems-analysis/peer-cache-encryption-storage.md index 840b273..fea7fad 100644 --- a/systems-analysis/peer-cache-encryption-storage.md +++ b/systems-analysis/peer-cache-encryption-storage.md @@ -29,26 +29,26 @@ Persistent Cache Storage -> Piece Reader (Decryption) -> Downloader -> P2P Netwo ### Modules -A new module `dragonfly-client-crypto` will be added to handle encryption-related functionality. +A new module `encrypt` will be added in `dragonfly-client-storage` to handle encryption-related functionality. ``` -dragonfly-client-crypto/ +dragonfly-client-storage/ └── src/ - ├── cryptor/ - │ ├── crypto_type.rs - │ ├── piece_cryptor.rs - │ └── mod.rs - ├── algorithm/ - │ ├── chacha20poly1305.rs - │ ├── ...other_algorithms.rs - │ └── mod.rs - └── lib.rs + ├── encrypt/ + │ ├── mod.rs + │ ├── cryptor/ + │ │ ├── mod.rs + │ │ ├── piece_cryptor.rs + │ │ └── crypto_type.rs + │ └── algorithm/ + │ ├── mod.rs + │ └── chacha20poly1305.rs + └── ...... ``` -* `cryptor/crypto_type.rs`: Defines the types of encryption. -* `cryptor/piece_cryptor.rs`: Add implementation of encryptor/decryptor. -* `algorithm/*.rs`: Add implementation of specific encryption algorithms. - +* `encrypt/cryptor/crypto_type.rs`: Defines encryption-related types. +* `encrypt/cryptor/piece_cryptor.rs`: Add implementation of encryptor/decryptor. +* `encrypt/algorithm/*.rs`: Add implementation of specific encryption algorithms. ## Implementation @@ -78,17 +78,17 @@ The advantage of AES is its more widespread hardware acceleration support, makin However, on platforms without hardware support, AES's performance might not be as good as ChaCha20. -Besides encryption, authentication can check if data has been maliciously tampered with, but at the cost of needing to store additional authentication information. AES-GCM and ChaCha20-Poly1305 have this capability. +In addition to encryption, authentication can detect whether data has been tampered with, but it requires storing additional authentication information. AES-GCM and ChaCha20-Poly1305 have this capability. -Dragonfly's current design writes Pieces to their corresponding offsets in the final file. This implies that each Piece must be encrypted prior to being written to disk to ensure no plaintext information is stored during the entire process. +Dragonfly's current design writes Pieces to its corresponding offsets in the final file. This implies that each Piece must be encrypted prior to being written to disk to ensure no plaintext information is stored during the entire process. In this case, authenticated algorithms would introduce additional complexity because authentication information would need to be saved for each encrypted Piece block. Given that Dragonfly is typically deployed on servers, it is highly likely to have AES hardware acceleration, so AES speed should be optimal. -At the same time, Dragonfly uses CRC32 and other verification methods, which to some extent defend against tampering (but not completely). Authentication might not be the core task. +At the same time, Dragonfly uses CRC32 and other verification methods, which to some extent defend against tampering (but not completely). Authentication might not be the primary concern. Different scenarios call for different suitable algorithms: @@ -97,7 +97,7 @@ Different scenarios call for different suitable algorithms: | Tamper detection needed | AES-GCM | ChaCha20-Poly1305 | | Tamper detection not needed | AES-CTR | ChaCha20/XChaCha20 | -Multiple encryption methods can be provided for selection in the actual code. +Multiple encryption methods can be provided for users to choose from in the actual implementation. ### Configuration @@ -106,7 +106,7 @@ Configuration options need to be added under `Config/Storage` to indicate whethe The `CryptoType` enum is used to represent the various algorithms. ```rust -// dragonfly-client-crypto/src/cryptor/crypto_type.rs +// dragonfly-client-config/src/dfdaemon.rs pub enum CryptoType { #[serde(rename = "chacha20-poly1305")] ChaCha20Poly1305, @@ -114,10 +114,9 @@ pub enum CryptoType { AesGcm, #[serde(rename = "aes-ctr")] AesCtr, - + ...... } -// dragonfly-client-config/src/dfdaemon.rs pub struct Storage { ...... /// enable_encryption indicates whether to enable encryption for persistent cache storage. @@ -159,7 +158,7 @@ If the same key and the same nonce are used to encrypt different content, an att A constructed nonce can be used to ensure uniqueness under the same key. For example, by combining part of the `task_id` with part of the `piece_number`, a unique nonce value can be generated. -This approach eliminates the need to store a separate nonce for each piece. +This approach removes the need to store a separate nonce for each piece. Currently, the plan is to generate a key for each `PersistentCacheTask`, and all related `Pieces` will use this key for encryption and decryption. Since the algorithm used may change, the type of algorithm also needs to be recorded. @@ -174,7 +173,7 @@ pub struct PersistentCacheTask{ pub crypto_info: Option, } -// dragonfly-client-crypto/src/cryptor/crypto_type.rs +// dragonfly-client-storage/src/encrypt/cryptor/crypto_type.rs pub struct CryptoInfo { // algorithm type pub crypto_type: CryptoType, @@ -199,7 +198,7 @@ pub struct Piece { The `PieceCryptor` trait defines the interface for the Cryptor: ```rust -// dragonfly-client-crypto/src/cryptor/piece_cryptor.rs +// dragonfly-client-storage/src/encrypt/cryptor/piece_cryptor.rs pub struct EncryptResult { pub ciphertext: Vec, pub tag: Option>, @@ -311,7 +310,7 @@ impl PieceCryptor for ChaCha20Poly1305Cryptor { By using an enum-based dispatch mechanism instead of `dyn`, it supports multiple encryption algorithm implementations: ```rust -// dragonfly-client-crypto/src/cryptor/piece_cryptor.rs +// dragonfly-client-storage/src/encrypt/cryptor/piece_cryptor.rs pub enum CryptorImpl { ChaCha20Poly1305(ChaCha20Poly1305Cryptor), // AesGcm(AesGcmCryptor), @@ -337,9 +336,13 @@ impl CryptorImpl { `CryptorImpl` is obtained via `get_cryptor`, where the key parameter is generated according to the `CryptoType`. -Once the algorithm type is determined, the key format is also determined, so the key can be generated accordingly: +Once the algorithm type is determined, the key format is also determined, so the key can be generated accordingly. + +Additionally, because `CryptoType` is defined in `dragonfly-client-config`, we cannot implement methods for structs from other modules directly. +As a result, we introduce `CryptoTypeExt` trait to achieve this functionality. ```rust +// dragonfly-client-storage/src/encrypt/cryptor/piece_cryptor.rs pub fn get_cryptor(crypto_type: &CryptoType, key: &[u8]) -> CryptorImpl { match crypto_type { // Select the specific encryption algorithm @@ -351,20 +354,33 @@ pub fn get_cryptor(crypto_type: &CryptoType, key: &[u8]) -> CryptorImpl { } } +// can not impl because CryptoType in defined in dragonfly-client-config +// but we are in dragonfly-client-storage impl CryptoType { - // The algorithm type determines the format of the key/nonce/tag + // wrong pub fn generate_key(&self) -> Vec { + ...... + } +} + +// this is right +pub trait CryptoTypeExt { + fn generate_key(&self) -> Vec; +} + +impl CryptoTypeExt for CryptoType { + fn generate_key(&self) -> Vec { match self { - // Generate the key according to the key format required by each algorithm CryptoType::ChaCha20Poly1305 => { - // TODO random key + // todo!("random key"); vec![0u8; ChaCha20Poly1305Cryptor::key_size()] - }, - ...... + } _ => todo!(), } } } + + ``` Encryption/Decryption example: From 3768e3ff2bfaf9c8cd2493dfa100fd7cfdca938b Mon Sep 17 00:00:00 2001 From: chohee Date: Tue, 29 Jul 2025 22:14:45 +0800 Subject: [PATCH 5/8] docs: request key from manager Signed-off-by: chohee --- .../peer-cache-encryption-storage.md | 522 ++++-------------- 1 file changed, 117 insertions(+), 405 deletions(-) diff --git a/systems-analysis/peer-cache-encryption-storage.md b/systems-analysis/peer-cache-encryption-storage.md index fea7fad..fb5e6b8 100644 --- a/systems-analysis/peer-cache-encryption-storage.md +++ b/systems-analysis/peer-cache-encryption-storage.md @@ -1,4 +1,4 @@ -# Design Document: P2P Peer Cache Encryption Storage +# Peer Cache Encryption Storage ## Overview @@ -8,7 +8,6 @@ This design document proposes adding encryption storage for peer cache in Dragon * **Data Security**: Encrypting cached data protects sensitive information from unauthorized access, especially in environments where peers might store data on insecure disks. * **Compliance**: Addresses potential compliance requirements for data at rest and in transit within the P2P network. - * **Integrity**: Helps ensure the integrity of cached data by detecting tampering attempts. ## Goals @@ -39,14 +38,12 @@ dragonfly-client-storage/ │ ├── cryptor/ │ │ ├── mod.rs │ │ ├── piece_cryptor.rs - │ │ └── crypto_type.rs │ └── algorithm/ │ ├── mod.rs - │ └── chacha20poly1305.rs + │ └── aes_ctr.rs └── ...... ``` -* `encrypt/cryptor/crypto_type.rs`: Defines encryption-related types. * `encrypt/cryptor/piece_cryptor.rs`: Add implementation of encryptor/decryptor. * `encrypt/algorithm/*.rs`: Add implementation of specific encryption algorithms. @@ -73,7 +70,6 @@ Currently, commonly used algorithms are AES and ChaCha20, each with some enhance | Ciphertext length equal to plaintext | Yes | No (+tag) | Yes | Yes | No (+tag) | - The advantage of AES is its more widespread hardware acceleration support, making it very fast when hardware support is available. However, on platforms without hardware support, AES's performance might not be as good as ChaCha20. @@ -88,44 +84,32 @@ In this case, authenticated algorithms would introduce additional complexity bec Given that Dragonfly is typically deployed on servers, it is highly likely to have AES hardware acceleration, so AES speed should be optimal. -At the same time, Dragonfly uses CRC32 and other verification methods, which to some extent defend against tampering (but not completely). Authentication might not be the primary concern. - -Different scenarios call for different suitable algorithms: - -| Scenario | With AES Acceleration | Without AES Acceleration | -|-------------------------|----------------------|-------------------------| -| Tamper detection needed | AES-GCM | ChaCha20-Poly1305 | -| Tamper detection not needed | AES-CTR | ChaCha20/XChaCha20 | +At the same time, Dragonfly uses CRC32 and other verification methods, which to some extent defend against tampering (but not completely). -Multiple encryption methods can be provided for users to choose from in the actual implementation. +In this case, authentication may not be the primary concern, so AES-CTR is a good choice. ### Configuration -Configuration options need to be added under `Config/Storage` to indicate whether encrypted storage is enabled and which encryption algorithm is selected. -The `CryptoType` enum is used to represent the various algorithms. +Configuration options need to be added under `Config/Storage` to indicate whether encrypted storage is enabled and +store the key from `Manager`. ```rust // dragonfly-client-config/src/dfdaemon.rs -pub enum CryptoType { - #[serde(rename = "chacha20-poly1305")] - ChaCha20Poly1305, - #[serde(rename = "aes-gcm")] - AesGcm, - #[serde(rename = "aes-ctr")] - AesCtr, - ...... +/// Encryption is the storage encryption configuration for dfdaemon. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Encryption { + pub enable: bool, + /// encryption_key is the global encryption key obtained from manager at runtime. + /// This field is not configurable via YAML and is populated dynamically. + #[serde(skip)] + pub key: Option>, } pub struct Storage { ...... - /// enable_encryption indicates whether to enable encryption for persistent cache storage. - #[serde(default = "default_storage_enable_encryption")] - pub enable_encryption: bool, - - /// encryption_algorithm indicates which algorithm will be used when encryption - #[serde(default = "default_storage_encryption_algo")] - pub encryption_algorithm: CryptoType, + #[serde(default = "default_storage_encryption")] + pub encryption: Encryption, } ``` @@ -138,57 +122,51 @@ storage: writeBufferSize: 4194304 readBufferSize: 4194304 # ADDED - enableEncryption: true - encryptionAlgorithm: chacha20-poly1305 + encryption: + enable: true ``` -### Metadata - -The information required for encryption includes the plaintext, key, and nonce; the output is the ciphertext. -For algorithms that support authentication, an authentication tag will also be generated. - -For decryption, the required information includes the ciphertext, key, and nonce. -For authenticated algorithms (AEAD), the tag is also necessary; the output is the corresponding plaintext. - -The key, nonce, and tag used for decryption must match those used during encryption. - -When encrypting different content with the same key, it is important to ensure that the nonce is different each time to enhance security. -If the same key and the same nonce are used to encrypt different content, an attacker may be able to deduce information about the plaintext from multiple ciphertexts. +### Key Management -A constructed nonce can be used to ensure uniqueness under the same key. -For example, by combining part of the `task_id` with part of the `piece_number`, a unique nonce value can be generated. -This approach removes the need to store a separate nonce for each piece. - -Currently, the plan is to generate a key for each `PersistentCacheTask`, and all related `Pieces` will use this key for encryption and decryption. -Since the algorithm used may change, the type of algorithm also needs to be recorded. - -This information is stored in the metadata: +When the client starts, it sends an RPC request to the `Manager` to obtain a key, which will be used for cache encryption. +The client does not persist the key. ```rust -// dragonfly-client-storage/src/metadata.rs -pub struct PersistentCacheTask{ +// dragonfly-client/src/bin/dfdaemon/main.rs +async fn main() -> Result<(), anyhow::Error> { + ...... + // Initialize manager client. + let manager_client = ManagerClient::new(...); + let manager_client = Arc::new(manager_client); + + // Request a key from Manager + let key = manager_client.request_encryption_key().await?; + // Save key in config + config.storage.encryption.set_key(key); ...... - /// crypto_info is the info saved for encryption/decryption - pub crypto_info: Option, -} - -// dragonfly-client-storage/src/encrypt/cryptor/crypto_type.rs -pub struct CryptoInfo { - // algorithm type - pub crypto_type: CryptoType, - pub key: Vec, } ``` -For authenticated algorithms (AEAD), each encrypted `Piece` needs to store its corresponding authentication tag: +The new RPC will be defined in `dragonfly-api`. -```rust -// dragonfly-client-storage/src/metadata.rs -pub struct Piece { - ...... - /// auth_tag is the tag generated by encryption algorithm when use AEAD - pub auth_tag: Option>, +``` +// dragonfly-api/pkg/apis/manager/v2/manager.proto + +// RequestEncryptionKeyRequest represents request of RequestEncryptionKey. +message RequestEncryptionKeyRequest { + // Request source type. + SourceType source_type = 1 [(validate.rules).enum.defined_only = true]; + // Source service hostname. + string hostname = 2 [(validate.rules).string.hostname = true]; + // Source service ip. + string ip = 3 [(validate.rules).string.ip = true]; +} + +// RequestEncryptionKeyResponse represents response of RequestEncryptionKey. +message RequestEncryptionKeyResponse { + // Encryption key provided by manager. + bytes encryption_key = 1; } ``` @@ -199,366 +177,102 @@ The `PieceCryptor` trait defines the interface for the Cryptor: ```rust // dragonfly-client-storage/src/encrypt/cryptor/piece_cryptor.rs -pub struct EncryptResult { - pub ciphertext: Vec, - pub tag: Option>, -} pub trait PieceCryptor { - fn encrypt_piece(&self, plaintext: &[u8], task_id: &str, piece_num: u32) -> Result; - fn decrypt_piece(&self, ciphertext: &[u8], task_id: &str, piece_num: u32, tag: Option<&[u8]>) -> Result>; - fn key_size() -> usize; - fn nonce_size() -> usize; - fn tag_size() -> Option; + fn encrypt(&self, plaintext: &[u8], task_id: &str, piece_num: u32) -> Result>; + fn decrypt(&self, ciphertext: &[u8], task_id: &str, piece_num: u32) -> Result>; } ``` `task_id` and `piece_num` are used to construct the nonce. -The `encrypt_piece` function returns an `EncryptResult`, which contains the encrypted `ciphertext` and an optional `tag` (an authentication tag is produced when using AEAD). +The `encrypt` function returns an `Vec` as the encrypted `ciphertext`. -The `decrypt_piece` function returns the plaintext; its parameters include an optional `tag` (which is required for decryption when using AEAD). +The `decrypt` function returns an `Vec` as the `plaintext`. -The formats of the key, nonce, and other parameters differ between algorithms. For example, RustCrypto’s AES-CTR uses a 16-byte nonce, while AES-GCM uses a 12-byte nonce. - -Therefore, interfaces like `key_size` and `nonce_size` are important for generating or constructing the corresponding information. - -Below is an example implementation of `PieceCryptor` using the chacha20poly1305 algorithm: +Below is an example implementation of `PieceCryptor` using the AES-CTR algorithm: ```rust -pub struct ChaCha20Poly1305Cryptor { - cipher: ChaCha20Poly1305, +use aes::Aes256; +use ctr::Ctr128BE; + +pub struct AesCtrCryptor { + cipher: Ctr128BE, } -impl ChaCha20Poly1305Cryptor { +impl AesCtrCryptor { pub fn new(key: &[u8]) -> Self { let key = Key::from_slice(key); Self { // use RustCrypto crate - cipher: ChaCha20Poly1305::new(key), + cipher: Ctr128BE::new(key), } } // construct nonce from task_id and piece_number, it does not need to be loaded/saved from/to disk fn build_nonce(task_id: &str, piece_num: u32) -> Vec { - let nonce_size = Self::nonce_size(); - let mut nonce = vec![0u8; nonce_size]; - let task_bytes = task_id.as_bytes(); - assert!(task_bytes.len() > 8); - nonce[..8].copy_from_slice(&task_bytes[..8]); // nonce's first 8 bytes for task_id - nonce[8..].copy_from_slice(&piece_num.to_be_bytes()); // remaining bytes for piece number - assert!(nonce.len() == Self::nonce_size()); - nonce + // Take certain bytes from task_id and piece_num to form the nonce. + // For example, use the first 12 bytes of task_id and the first 4 bytes of piece_num to construct a 16-byte nonce. + ...... } } -impl PieceCryptor for ChaCha20Poly1305Cryptor { - fn encrypt_piece(&self, plaintext: &[u8], task_id: &str, piece_num: u32) -> Result{ +impl PieceCryptor for AesCtrCryptor { + fn encrypt(&self, plaintext: &[u8], task_id: &str, piece_num: u32) -> Result{ let nonce = Self::build_nonce(task_id, piece_num); - let nonce = Nonce::from_slice(&nonce); - // AEAD output will append auth tag after the ciphertext, so the output will be longer then plaintext - // res is ciphertext + tag + // Construct the nonce and use the key to perform encryption. let res = self .cipher - .encrypt(nonce, plaintext) - .or_err(ErrorType::StorageError)?; + .encrypt(nonce, plaintext)?; - // split res to ciphertext and tag, - let tag_size = Self::tag_size().expect("should have tag size"); - let (ciphertext, tag) = res.split_at(res.len() - tag_size); - Ok(EncryptResult { ciphertext: ciphertext.to_vec(), tag: Some(tag.to_vec()) }) + Ok(res) } - fn decrypt_piece(&self, ciphertext: &[u8], task_id: &str, piece_num: u32, tag: Option<&[u8]>) -> Result>{ - let nonce = Self::build_nonce(task_id, piece_num); - let nonce = Nonce::from_slice(&nonce); - - // concatenate ciphertext and tag for decryption - let tag = tag.expect("should have tag"); - let mut combined = Vec::with_capacity(ciphertext.len() + tag.len()); - combined.extend_from_slice(ciphertext); - combined.extend_from_slice(tag); - let combined_slice: &[u8] = &combined; + fn decrypt(&self, ciphertext: &[u8], task_id: &str, piece_num: u32) -> Result>{ + // The process of constructing the nonce is the same as in encryption. // decrypt let plaintext = self .cipher - .decrypt(nonce, combined_slice) - .or_err(ErrorType::StorageError)?; + .decrypt(nonce, combined_slice)?; Ok(plaintext) } - - fn key_size() -> usize { - 32 // key size of chacha20-poly1305 is 32 bytes - } - - fn nonce_size() -> usize { - 12 // nonce size of chacha20-poly1305 is 12 bytes - } - - fn tag_size() -> Option { - Some(16) // tag size of chacha20-poly1305 is 16 bytes - // algorithm without authentication such as aes-ctr, this should return None - } -} -``` - -`CryptorImpl` is the outermost wrapper for the encryptor/decryptor. -By using an enum-based dispatch mechanism instead of `dyn`, it supports multiple encryption algorithm implementations: - -```rust -// dragonfly-client-storage/src/encrypt/cryptor/piece_cryptor.rs -pub enum CryptorImpl { - ChaCha20Poly1305(ChaCha20Poly1305Cryptor), - // AesGcm(AesGcmCryptor), - // AesCtr(AesCtrCryptor), -} - -impl CryptorImpl { - pub fn encrypt_piece(&self, plaintext: &[u8], task_id: &str, piece_num: u32) -> Result { - match self { - CryptorImpl::ChaCha20Poly1305(inner) => Ok(inner.encrypt_piece(plaintext, task_id, piece_num)?), - // CryptorImpl::AesGcm(inner) => ... - } - } - - pub fn decrypt_piece(&self, ciphertext: &[u8], task_id: &str, piece_num: u32, tag: Option<&[u8]>) -> Result> { - match self { - CryptorImpl::ChaCha20Poly1305(inner) => Ok(inner.decrypt_piece(ciphertext, task_id, piece_num, tag)?), - // CryptorImpl::AesGcm(inner) => ... - } - } -} -``` - -`CryptorImpl` is obtained via `get_cryptor`, where the key parameter is generated according to the `CryptoType`. - -Once the algorithm type is determined, the key format is also determined, so the key can be generated accordingly. - -Additionally, because `CryptoType` is defined in `dragonfly-client-config`, we cannot implement methods for structs from other modules directly. -As a result, we introduce `CryptoTypeExt` trait to achieve this functionality. - -```rust -// dragonfly-client-storage/src/encrypt/cryptor/piece_cryptor.rs -pub fn get_cryptor(crypto_type: &CryptoType, key: &[u8]) -> CryptorImpl { - match crypto_type { - // Select the specific encryption algorithm - CryptoType::ChaCha20Poly1305 => { - CryptorImpl::ChaCha20Poly1305(ChaCha20Poly1305Cryptor::new(key)) - } - ...... - _ => unimplemented!("algorithm not supported"), - } } - -// can not impl because CryptoType in defined in dragonfly-client-config -// but we are in dragonfly-client-storage -impl CryptoType { - // wrong - pub fn generate_key(&self) -> Vec { - ...... - } -} - -// this is right -pub trait CryptoTypeExt { - fn generate_key(&self) -> Vec; -} - -impl CryptoTypeExt for CryptoType { - fn generate_key(&self) -> Vec { - match self { - CryptoType::ChaCha20Poly1305 => { - // todo!("random key"); - vec![0u8; ChaCha20Poly1305Cryptor::key_size()] - } - _ => todo!(), - } - } -} - - -``` - -Encryption/Decryption example: - -```rust -// encryption -let crypto_type = get_type_from_config_or_metadata(); // get algorithm used -let key = crypto_type.generate_key(); // key may need to be stored in task metadata -let cryptor = get_cryptor(&info.crypto_type, &info.key); -// get ciphertext, tag after encryption -let EncryptResult { ciphertext, tag } = cryptor.encrypt_piece_by_id(&plaintext, piece_id)?; - -// decryption -let crypto_info = get_info_from_metadata(); -let cryptor = get_cryptor(&info.crypto_type, &info.key); -// use key and tag(when use AEAD) to decrypt -let plaintext = cryptor.decrypt_piece_by_id(&ciphertext, piece_id, Some(tag)/None)?; ``` ### Piece Encryption/Decryption -The main changes are concentrated in `dragonfly-client-storage/src/lib.rs` and `dragonfly-client-storage/src/content.rs`. +The main changes are focused in `dragonfly-client-storage/src/lib.rs` and `dragonfly-client-storage/src/content.rs`. -Taking the download process as an example, after entering `dfdaemon_download.rs/download_persistent_cache_task`, -the function `storage.download_persistent_cache_task_started` will be called first. +When encryption is enabled, writing a `Piece` involves the following steps: +1. Read the plaintext of the `Piece` into memory +2. Calculate the CRC from the plaintext +3. Use the key obtained from the Manager and the constructed nonce to encrypt the data and obtain the ciphertext +4. Write the ciphertext to the same position in the file -In this function, encryption is enabled or disabled based on the configuration, and the encryption information is stored in the task metadata. - -```rust -// dragonfly-client-storage/src/lib.rs -pub async fn download_persistent_cache_task_started( - &self, args... - )-> Result { - - // ADDED do encryption when config enable is set - let crypto_info = if self.config.storage.enable_encryption { - let crypto_type = self.config.storage.encryption_algorithm.clone(); - let key = crypto_type.generate_key(); - Some(CryptoInfo{crypto_type: crypto_type, key: key}) - } else { - None - }; - - let metadata = self.metadata.download_persistent_cache_task_started( - id, - ttl, - persistent, - piece_length, - content_length, - created_at, - // ADDED store key/algorithm type in the metadata of task - crypto_info, - )?; - - self.content - .create_persistent_cache_task(id, content_length) - .await?; - Ok(metadata) -} -``` - -The subsequent execution flow is as follows: - -``` -task_manager_clone.download() - PersistentCacheTask.download_partial_from_local() - if (need_piece_content) piece.download_persistent_cache_from_local_into_async_read() - storage.upload_persistent_cache_piece() - content.read_persistent_cache_piece() -``` - -The program will check for any existing local cache. When `need_piece_content` is set, it will read content from the local cache. - -Therefore, decryption needs to be handled here. If the task metadata read by the upper layer contains encryption information, -the necessary decryption parameters should be passed in as arguments. - -```rust -// dragonfly-client-storage/src/content.rs -pub async fn read_persistent_cache_piece( - &self, - task_id: &str, - offset: u64, - length: u64, - range: Option, - // ADDED - piece_id: &str, - crypto_info: Option<&CryptoInfo>, - auth_tag: Option<&[u8]>, -) -> Result { - // original code ↓ - let task_path = self.get_persistent_cache_task_path(task_id); - - let (target_offset, target_length) = calculate_piece_range(offset, length, range); - - let f = File::open(task_path.as_path()).await.inspect_err(|err| { - error!("open {:?} failed: {}", task_path, err); - })?; - let mut f_reader = BufReader::with_capacity(self.config.storage.read_buffer_size, f); - - f_reader - .seek(SeekFrom::Start(target_offset)) - .await - .inspect_err(|err| { - error!("seek {:?} failed: {}", task_path, err); - })?; - // original code ↑ - - // ADDED - let res = if let Some(info) = crypto_info { - // Read ciphertext from file - let mut ciphertext = vec![0u8; target_length as usize]; - f_reader.read_exact(&mut ciphertext).await?; - - // decryption - let cryptor = get_cryptor(&info.crypto_type, &info.key); - let plaintext = cryptor.decrypt_piece_by_id(&ciphertext, piece_id, auth_tag)?; - - Either::Left(Cursor::new(plaintext)) - } else { - Either::Right(f_reader.take(target_length)) - }; - - Ok(res) -} -``` - -The following is the subsequent call process: - -``` -task_manager_clone.download() - PersistentCacheTask.download_partial_with_scheduler() - PersistentCacheTask.download_partial_with_scheduler_from_parent() - download_from_parent() - piece_manager.download_persistent_cache_from_parent() - storage.download_persistent_cache_piece_started() - downloader.download_persistent_cache_piece() - storage.download_persistent_cache_piece_from_parent_finished() - content.write_persistent_cache_piece() -``` - -The downloaded content is then passed to `content.write_persistent_cache_piece()`, which is responsible for writing it to the file. -Therefore, encryption logic needs to be added here. - -During the encryption process, the CRC should be calculated based on the original plaintext, -and the return value should include the tag to pass the AEAD authentication tag. ```rust // dragonfly-client-storage/src/content.rs pub async fn write_persistent_cache_piece( &self, - task_id: &str, - offset: u64, - expected_length: u64, - reader: &mut R, + ...... // ADDED piece_id: &str, - crypto_info: Option<&CryptoInfo>, ) -> Result { // original code ↓ // Open the file and seek to the offset. let task_path = self.get_persistent_cache_task_path(task_id); - let mut f = OpenOptions::new() - .truncate(false) - .write(true) - .open(task_path.as_path()) - .await - .inspect_err(|err| { - error!("open {:?} failed: {}", task_path, err); - })?; - - f.seek(SeekFrom::Start(offset)).await.inspect_err(|err| { - error!("seek {:?} failed: {}", task_path, err); - })?; + let mut f = OpenFile...?; + + f.seek(......)?; // original code ↑ // ADDED need encrypt - if let Some(info) = crypto_info { + if self.config.storage.encryption.enable { // 1. read plaintext let mut plaintext = Vec::new(); reader.read_to_end(&mut plaintext).await?; @@ -569,8 +283,8 @@ pub async fn write_persistent_cache_piece( let hash = hasher.finalize().to_string(); // 3. encrypt - let cryptor = get_cryptor(&info.crypto_type, &info.key); - let EncryptResult { ciphertext, tag } = cryptor.encrypt_piece_by_id(&plaintext, piece_id)?; + let cryptor = get_cryptor(self.config.storage.encryption.key); + let ciphertext = cryptor.encrypt_piece_by_id(&plaintext, piece_id)?; // 4. write let mut writer = BufWriter::with_capacity(self.config.storage.write_buffer_size, f); @@ -579,17 +293,12 @@ pub async fn write_persistent_cache_piece( // check length if ciphertext.len() as u64 != expected_length { - return Err(Error::Unknown(format!( - "expected length {} but got {}", - expected_length, ciphertext.len() - ))); + ...... } Ok(WritePieceResponse { length: ciphertext.len() as u64, hash: hash, - // ADDED - auth_tag: tag, }) } else { @@ -598,8 +307,6 @@ pub async fn write_persistent_cache_piece( Ok(WritePieceResponse { length, hash: hasher.finalize().to_string(), - // ADDED - auth_tag: None }) } } @@ -607,36 +314,41 @@ pub async fn write_persistent_cache_piece( --- -The upload process reuses many of the same `Storage` calls as the download process, such as `content.write_persistent_cache_piece`, and similar modifications can be made to the differing parts: +When encryption is enabled, reading a `Piece` involves the following steps: +1. Read the ciphertext of the `Piece` from the file into memory +2. Use the key obtained from the Manager and the constructed nonce to decrypt the data and obtain the plaintext ```rust -// dragonfly-client-storage/src/lib.rs -// This is unique to the upload process. -pub async fn create_persistent_cache_task_started( - &self, ... -) -> Result { - // ADDED whether need to encrypt - let info = if self.config.storage.enable_encryption { - let crypto_type = self.config.storage.encryption_algorithm.clone(); - let key = crypto_type.generate_key(); - Some(CryptoInfo{crypto_type: crypto_type, key: key}) +// dragonfly-client-storage/src/content.rs +pub async fn read_persistent_cache_piece( + &self, + ...... + // ADDED + piece_id: &str, +) -> Result { + // original code ↓ + let f = File::open(......)?; + let mut f_reader = BufReader::with_capacity(self.config.storage.read_buffer_size, f); + + f_reader.seek(......) + // original code ↑ + + // ADDED + let res = if self.config.storage.encryption.enable { + // Read ciphertext from file + let mut ciphertext = vec![0u8; target_length as usize]; + f_reader.read_exact(&mut ciphertext).await?; + + // decryption + let cryptor = get_cryptor(self.config.storage.encryption.key); + let plaintext = cryptor.decrypt_piece_by_id(&ciphertext, piece_id)?; + + Either::Left(Cursor::new(plaintext)) } else { - None + Either::Right(f_reader.take(target_length)) }; - let metadata = self.metadata.create_persistent_cache_task_started( - id, - ttl, - piece_length, - content_length, - // ADDED store crypto_info in metadata - info, - )?; - - self.content - .create_persistent_cache_task(id, content_length) - .await?; - Ok(metadata) + Ok(res) } ``` From 0d2c883f6e4a00403b45fd792098e5d2c9ac285f Mon Sep 17 00:00:00 2001 From: chohee Date: Sun, 10 Aug 2025 21:02:33 +0800 Subject: [PATCH 6/8] docs: add Manager design Signed-off-by: chohee --- .../peer-cache-encryption-storage.md | 273 ++++++++---------- 1 file changed, 116 insertions(+), 157 deletions(-) diff --git a/systems-analysis/peer-cache-encryption-storage.md b/systems-analysis/peer-cache-encryption-storage.md index fb5e6b8..e3b29e4 100644 --- a/systems-analysis/peer-cache-encryption-storage.md +++ b/systems-analysis/peer-cache-encryption-storage.md @@ -37,14 +37,14 @@ dragonfly-client-storage/ │ ├── mod.rs │ ├── cryptor/ │ │ ├── mod.rs - │ │ ├── piece_cryptor.rs + │ │ ├── reader.rs │ └── algorithm/ │ ├── mod.rs │ └── aes_ctr.rs └── ...... ``` -* `encrypt/cryptor/piece_cryptor.rs`: Add implementation of encryptor/decryptor. +* `encrypt/cryptor/reader.rs`: Add implementation of encryptor/decryptor. * `encrypt/algorithm/*.rs`: Add implementation of specific encryption algorithms. ## Implementation @@ -89,10 +89,42 @@ At the same time, Dragonfly uses CRC32 and other verification methods, which to In this case, authentication may not be the primary concern, so AES-CTR is a good choice. -### Configuration +### Manager: Key Management -Configuration options need to be added under `Config/Storage` to indicate whether encrypted storage is enabled and -store the key from `Manager`. + +The manager is responsible for storing keys, and clients obtain keys from the manager through RPC requests. + +Users can specify a key in the manager's config using base64 encoding. The manager will save this key to the database and overwrite any existing key in the database. (The key in the config file has higher priority than the key in the database.) + +If the user does not specify a key, the manager will use the key from the database. If there is no key in the manager's database, it will randomly generate one, use it, and save it. + +The manager will perform this work after initializing the database. + +The new RPC will be defined in `dragonfly-api`. + +``` +// dragonfly-api/pkg/apis/manager/v2/manager.proto + +// RequestEncryptionKeyRequest represents request of RequestEncryptionKey. +message RequestEncryptionKeyRequest { + // Request source type. + SourceType source_type = 1 [(validate.rules).enum.defined_only = true]; + // Source service hostname. + string hostname = 2 [(validate.rules).string.hostname = true]; + // Source service ip. + string ip = 3 [(validate.rules).string.ip = true]; +} + +// RequestEncryptionKeyResponse represents response of RequestEncryptionKey. +message RequestEncryptionKeyResponse { + // Encryption key provided by manager. + bytes encryption_key = 1; +} +``` + +### Client: Configuration + +Configuration options need to be added under `Config/Storage` to indicate whether encrypted storage is enabled. ```rust // dragonfly-client-config/src/dfdaemon.rs @@ -100,10 +132,6 @@ store the key from `Manager`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Encryption { pub enable: bool, - /// encryption_key is the global encryption key obtained from manager at runtime. - /// This field is not configurable via YAML and is populated dynamically. - #[serde(skip)] - pub key: Option>, } pub struct Storage { @@ -127,10 +155,11 @@ storage: ``` -### Key Management +### Client: Request Key When the client starts, it sends an RPC request to the `Manager` to obtain a key, which will be used for cache encryption. -The client does not persist the key. + +The client does not persist the obtained key, so it will request a key from the manager every time it restarts. ```rust // dragonfly-client/src/bin/dfdaemon/main.rs @@ -142,118 +171,63 @@ async fn main() -> Result<(), anyhow::Error> { // Request a key from Manager let key = manager_client.request_encryption_key().await?; - // Save key in config - config.storage.encryption.set_key(key); + // Pass key as a parameter of Storage + let storage = Storage::new(..., key).await?; ...... } ``` -The new RPC will be defined in `dragonfly-api`. - -``` -// dragonfly-api/pkg/apis/manager/v2/manager.proto +### Client: Cryptor -// RequestEncryptionKeyRequest represents request of RequestEncryptionKey. -message RequestEncryptionKeyRequest { - // Request source type. - SourceType source_type = 1 [(validate.rules).enum.defined_only = true]; - // Source service hostname. - string hostname = 2 [(validate.rules).string.hostname = true]; - // Source service ip. - string ip = 3 [(validate.rules).string.ip = true]; -} - -// RequestEncryptionKeyResponse represents response of RequestEncryptionKey. -message RequestEncryptionKeyResponse { - // Encryption key provided by manager. - bytes encryption_key = 1; -} -``` +The `EncryptAlgo` trait defines the interface for the Encryption alogorithm. +```rust +// dragonfly-client-storage/src/encrypt/algorithm/mod.rs -### Cryptor +pub trait EncryptAlgo { + const NONCE_SIZE: usize; + const KEY_SIZE: usize; -The `PieceCryptor` trait defines the interface for the Cryptor: + fn new(key: &[u8], nonce: &[u8]) -> Self; -```rust -// dragonfly-client-storage/src/encrypt/cryptor/piece_cryptor.rs + fn apply_keystream(&mut self, data: &mut [u8]); -pub trait PieceCryptor { - fn encrypt(&self, plaintext: &[u8], task_id: &str, piece_num: u32) -> Result>; - fn decrypt(&self, ciphertext: &[u8], task_id: &str, piece_num: u32) -> Result>; + fn build_nonce(task_id: &str, piece_num: u32); } ``` `task_id` and `piece_num` are used to construct the nonce. -The `encrypt` function returns an `Vec` as the encrypted `ciphertext`. +The `apply_keystream` function performs in-place encryption/decryption on `data`. -The `decrypt` function returns an `Vec` as the `plaintext`. +--- -Below is an example implementation of `PieceCryptor` using the AES-CTR algorithm: +`EncryptReader`/`DecryptReader` provides asynchronous encryption/decryption capabilities. +It can adapt well to existing impl AsyncRead parameter types. ```rust -use aes::Aes256; -use ctr::Ctr128BE; - -pub struct AesCtrCryptor { - cipher: Ctr128BE, -} - -impl AesCtrCryptor { - pub fn new(key: &[u8]) -> Self { - let key = Key::from_slice(key); - Self { - // use RustCrypto crate - cipher: Ctr128BE::new(key), - } - } - - // construct nonce from task_id and piece_number, it does not need to be loaded/saved from/to disk - fn build_nonce(task_id: &str, piece_num: u32) -> Vec { - // Take certain bytes from task_id and piece_num to form the nonce. - // For example, use the first 12 bytes of task_id and the first 4 bytes of piece_num to construct a 16-byte nonce. - ...... - } - +// dragonfly-client-storage/src/encrypt/cryptor/reader.rs +pub struct EncryptReader { + inner: R, + cipher: A, } -impl PieceCryptor for AesCtrCryptor { - fn encrypt(&self, plaintext: &[u8], task_id: &str, piece_num: u32) -> Result{ - let nonce = Self::build_nonce(task_id, piece_num); - - // Construct the nonce and use the key to perform encryption. - let res = self - .cipher - .encrypt(nonce, plaintext)?; - - Ok(res) - } - - fn decrypt(&self, ciphertext: &[u8], task_id: &str, piece_num: u32) -> Result>{ - // The process of constructing the nonce is the same as in encryption. - - // decrypt - let plaintext = self - .cipher - .decrypt(nonce, combined_slice)?; - - Ok(plaintext) - } +impl AsyncRead for EncryptReader { + // implement AsyncRead + ...... } ``` -### Piece Encryption/Decryption + +### Client: Piece Encryption/Decryption The main changes are focused in `dragonfly-client-storage/src/lib.rs` and `dragonfly-client-storage/src/content.rs`. When encryption is enabled, writing a `Piece` involves the following steps: -1. Read the plaintext of the `Piece` into memory -2. Calculate the CRC from the plaintext -3. Use the key obtained from the Manager and the constructed nonce to encrypt the data and obtain the ciphertext -4. Write the ciphertext to the same position in the file - +1. Get the key for encryption +2. Create an `EncryptReader` to wrap the original `impl AsyncRead` parameter +3. Use the `EncryptReader` to copy the ciphertext to the corresponding position in the file ```rust // dragonfly-client-storage/src/content.rs @@ -264,59 +238,37 @@ pub async fn write_persistent_cache_piece( piece_id: &str, ) -> Result { // original code ↓ - // Open the file and seek to the offset. - let task_path = self.get_persistent_cache_task_path(task_id); - let mut f = OpenFile...?; - - f.seek(......)?; + // Open the target file and set CRC32 hasher + let writer = ... + let tee = hasher_inspect_reader(); // original code ↑ - // ADDED need encrypt - if self.config.storage.encryption.enable { - // 1. read plaintext - let mut plaintext = Vec::new(); - reader.read_to_end(&mut plaintext).await?; - - // 2. use plaintext to calculate crc - let mut hasher = crc32fast::Hasher::new(); - hasher.update(&plaintext); - let hash = hasher.finalize().to_string(); - - // 3. encrypt - let cryptor = get_cryptor(self.config.storage.encryption.key); - let ciphertext = cryptor.encrypt_piece_by_id(&plaintext, piece_id)?; - - // 4. write - let mut writer = BufWriter::with_capacity(self.config.storage.write_buffer_size, f); - writer.write_all(&ciphertext).await?; - writer.flush().await?; - - // check length - if ciphertext.len() as u64 != expected_length { - ...... - } - - Ok(WritePieceResponse { - length: ciphertext.len() as u64, - hash: hash, - }) - + // Modified + let mut tee_warpper = if self.config.storage.encryption.enable { + // 1. get key + let key = get_key(); + // 2. use EncryptReader + let encrypt_reader = EncryptReader::new(tee, key, ...); + Either::Left(encrypt_reader) } else { - // if don not need encryption, do original process - ...... - Ok(WritePieceResponse { - length, - hash: hasher.finalize().to_string(), - }) - } + Either::Right(tee) + }; + + // 3. copy to file + let length = io::copy(&mut tee_warpper, &mut writer).await.inspect_err(|err| { + error!("copy {:?} failed: {}", task_path, err); + })?; + + ...... } ``` --- When encryption is enabled, reading a `Piece` involves the following steps: -1. Read the ciphertext of the `Piece` from the file into memory -2. Use the key obtained from the Manager and the constructed nonce to decrypt the data and obtain the plaintext +1. Get the key for encryption +2. Create an `DecryptReader` to wrap the original `impl AsyncRead` parameter +3. Return the `DecryptReader` ```rust // dragonfly-client-storage/src/content.rs @@ -327,28 +279,28 @@ pub async fn read_persistent_cache_piece( piece_id: &str, ) -> Result { // original code ↓ - let f = File::open(......)?; - let mut f_reader = BufReader::with_capacity(self.config.storage.read_buffer_size, f); - - f_reader.seek(......) + // Open file + let f_reader = ...; // original code ↑ // ADDED - let res = if self.config.storage.encryption.enable { - // Read ciphertext from file - let mut ciphertext = vec![0u8; target_length as usize]; - f_reader.read_exact(&mut ciphertext).await?; - - // decryption - let cryptor = get_cryptor(self.config.storage.encryption.key); - let plaintext = cryptor.decrypt_piece_by_id(&ciphertext, piece_id)?; - - Either::Left(Cursor::new(plaintext)) - } else { - Either::Right(f_reader.take(target_length)) - }; + if self.config.storage.encryption.enable { + // 1. get key + let key = get_key(); + + let limited_reader = f_reader.take(target_length); + // 2. use DecryptReader + let decrypt_reader = DecryptReader::new( + limited_reader, + key, + piece_id + ); + + // 3. return DecryptReader + return Ok(Either::Left(decrypt_reader)); + } - Ok(res) + Ok(Either::Right(f_reader.take(target_length))) } ``` @@ -376,4 +328,11 @@ pub async fn read_persistent_cache_piece( This design provides a foundation for adding P2P Peer Cache Encryption Storage to Dragonfly while maintaining system stability and backward compatibility. - \ No newline at end of file + + + +# TODO + +- [ ] Graph in Architecture +- [ ] Normal `Task` Implementation + From 8912c595b1ccf33a46ae70219bd021fc3fa003f6 Mon Sep 17 00:00:00 2001 From: chohee Date: Sat, 23 Aug 2025 21:18:16 +0800 Subject: [PATCH 7/8] docs: update graph and rust api Signed-off-by: chohee --- .../peer-cache-encryption-storage.md | 134 ++++++++++++++++-- 1 file changed, 124 insertions(+), 10 deletions(-) diff --git a/systems-analysis/peer-cache-encryption-storage.md b/systems-analysis/peer-cache-encryption-storage.md index e3b29e4..ac35fd1 100644 --- a/systems-analysis/peer-cache-encryption-storage.md +++ b/systems-analysis/peer-cache-encryption-storage.md @@ -2,7 +2,8 @@ ## Overview -This design document proposes adding encryption storage for peer cache in Dragonfly's P2P file transfer mechanism. The goal is to enhance data security for cached files within the P2P network, ensuring that sensitive information remains protected during storage and transfer. +This design document proposes adding encryption storage for peer cache in Dragonfly's P2P file transfer mechanism. +The goal is to enhance data security for cached files within the P2P network, ensuring that sensitive information remains protected during storage and transfer. ## Motivation @@ -19,12 +20,28 @@ This design document proposes adding encryption storage for peer cache in Dragon ## Architecture -The proposed architecture involves introducing an encryption module within the Dragonfly client's storage layer. This module will intercept data during writing to and reading from the persistent cache, performing encryption and decryption operations respectively. +The proposed architecture involves introducing an encryption module within the Dragonfly client's storage layer. +This module will intercept data during writing to and reading from the persistent cache, performing encryption and decryption operations respectively. +```mermaid +sequenceDiagram + participant storage as Storage + participant content as Content + + activate storage + storage->>content: read_persistent_cache_piece() + + activate content + alt disable encryption(default) + content-->>storage: BufReader + else enable encryption + content-->>storage: DecryptReader + end + + deactivate content + deactivate storage ``` -P2P Network -> Downloader -> Piece Writer (Encryption) -> Persistent Cache Storage -Persistent Cache Storage -> Piece Reader (Decryption) -> Downloader -> P2P Network -``` + ### Modules @@ -74,10 +91,11 @@ The advantage of AES is its more widespread hardware acceleration support, makin However, on platforms without hardware support, AES's performance might not be as good as ChaCha20. -In addition to encryption, authentication can detect whether data has been tampered with, but it requires storing additional authentication information. AES-GCM and ChaCha20-Poly1305 have this capability. - +In addition to encryption, authentication can detect whether data has been tampered with, but it requires storing additional authentication information. +AES-GCM and ChaCha20-Poly1305 have this capability. -Dragonfly's current design writes Pieces to its corresponding offsets in the final file. This implies that each Piece must be encrypted prior to being written to disk to ensure no plaintext information is stored during the entire process. +Dragonfly's current design writes Pieces to its corresponding offsets in the final file. +This implies that each Piece must be encrypted prior to being written to disk to ensure no plaintext information is stored during the entire process. In this case, authenticated algorithms would introduce additional complexity because authentication information would need to be saved for each encrypted Piece block. @@ -94,12 +112,81 @@ In this case, authentication may not be the primary concern, so AES-CTR is a goo The manager is responsible for storing keys, and clients obtain keys from the manager through RPC requests. -Users can specify a key in the manager's config using base64 encoding. The manager will save this key to the database and overwrite any existing key in the database. (The key in the config file has higher priority than the key in the database.) +Users can specify a key in the manager's config using base64 encoding. The manager will save this key to the database and overwrite any existing key in the database. +(The key in the config file has higher priority than the key in the database.) + +```yaml +# Manager configuration. +encryption: + enable: true + # base64 + key: 'jqe8buWT8rsfBMYt8mpwSbnjy44WNy/5v1gN1JfFsNk=' +``` If the user does not specify a key, the manager will use the key from the database. If there is no key in the manager's database, it will randomly generate one, use it, and save it. The manager will perform this work after initializing the database. +```mermaid +sequenceDiagram + actor User + participant Config as Config File + participant Manager + participant Database + participant Client + + %% User config and Start + User->>Config: 1. Edit Config (may contain key) + User->>Manager: 2. Start Manager + activate Manager + + %% Manager Initialization and Key Choice + Manager->>Config: 3. Read Config + + alt [Key found in Config] + Config-->>Manager: 3.1 Return Config (with key) + Manager->>Database: 4. Write the Key to Database + Note right of Manager: Use the Key in Config + + else [No Key in Config] + Config-->>Manager: 3.2 Return Config (without key) + Manager->>Database: 4. Query if Key Exsists in Database + activate Database + + alt [Key Exsists in Database] + Database-->>Manager: 4.1 Return the Key in Database + Note right of Manager: Use the Key in Database + + else [Key not Exsists in Database] + Database-->>Manager: 4.2 Key not Found + Manager->>Manager: Generate a New Key Randomly + Manager->>Database: Write the Key to Database + Note right of Manager: Use the Key Generated + end + deactivate Database + end + + Note over Manager: Key Initialization Complete + deactivate Manager + + + %% 3. Client Start and Get Key + User->>Client: 5. Start Client + activate Client + + Client->>Manager: 6. Request key + activate Manager + + Manager->>Database: Query + activate Database + Database-->>Manager: Return the Key + deactivate Database + + Manager-->>Client: Respond with the Current Key + deactivate Manager + deactivate Client +``` + The new RPC will be defined in `dragonfly-api`. ``` @@ -122,6 +209,34 @@ message RequestEncryptionKeyResponse { } ``` +```rust +// dragonfly-api/src/manager.v2.rs +/// RequestEncryptionKeyRequest represents request of RequestEncryptionKey. +#[derive(serde::Serialize, serde::Deserialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RequestEncryptionKeyRequest { + /// Request source type. + #[prost(enumeration = "SourceType", tag = "1")] + pub source_type: i32, + /// Source service hostname. + #[prost(string, tag = "2")] + pub hostname: ::prost::alloc::string::String, + /// Source service ip. + #[prost(string, tag = "3")] + pub ip: ::prost::alloc::string::String, +} +/// RequestEncryptionKeyResponse represents response of RequestEncryptionKey. +#[derive(serde::Serialize, serde::Deserialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RequestEncryptionKeyResponse { + /// Encryption key provided by manager. + #[prost(bytes = "vec", tag = "1")] + pub encryption_key: ::prost::alloc::vec::Vec, +} +``` + ### Client: Configuration Configuration options need to be added under `Config/Storage` to indicate whether encrypted storage is enabled. @@ -333,6 +448,5 @@ This design provides a foundation for adding P2P Peer Cache Encryption Storage t # TODO -- [ ] Graph in Architecture - [ ] Normal `Task` Implementation From c0fcd6cbd6b99d1da0e47ca8044013481ac60702 Mon Sep 17 00:00:00 2001 From: chohee Date: Sat, 23 Aug 2025 21:41:52 +0800 Subject: [PATCH 8/8] docs: update graph with write piece Signed-off-by: chohee --- .../peer-cache-encryption-storage.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/systems-analysis/peer-cache-encryption-storage.md b/systems-analysis/peer-cache-encryption-storage.md index ac35fd1..d190c4f 100644 --- a/systems-analysis/peer-cache-encryption-storage.md +++ b/systems-analysis/peer-cache-encryption-storage.md @@ -28,6 +28,8 @@ sequenceDiagram participant storage as Storage participant content as Content + %% Read + Note over storage: Read activate storage storage->>content: read_persistent_cache_piece() @@ -40,6 +42,23 @@ sequenceDiagram deactivate content deactivate storage + + %% Write + Note over storage: Write + activate storage + storage->>content: write_persistent_cache_piece(reader) + + activate content + alt disable encryption(default) + content->>content: Copy Reader to File + else enable encryption + %% content->>content: DecryptReader + content->>content: Copy EncryptReader to File + end + content-->>storage: WritePieceResponse + + deactivate content + deactivate storage ```