max / makenotwork
8 files changed,
+120 insertions,
-39 deletions
| @@ -766,13 +766,13 @@ client.<span class="fn">setup_encryption_new</span>(<span class="str">"pass"</sp | |||
| 766 | 766 | <div class="code-block"><pre><span class="cmt">// Upload: hash locally, request URL, upload encrypted, confirm</span> | |
| 767 | 767 | <span class="kw">let</span> resp = client.<span class="fn">blob_upload_url</span>(&hash, size).<span class="kw">await</span>?; | |
| 768 | 768 | <span class="kw">if</span> !resp.already_exists { | |
| 769 | - | client.<span class="fn">blob_upload</span>(&resp.upload_url, data).<span class="kw">await</span>?; <span class="cmt">// encrypted on wire</span> | |
| 769 | + | client.<span class="fn">blob_upload</span>(&hash, &resp.upload_url, data).<span class="kw">await</span>?; <span class="cmt">// encrypted + hash-bound on wire</span> | |
| 770 | 770 | } | |
| 771 | 771 | client.<span class="fn">blob_confirm</span>(&hash, size).<span class="kw">await</span>?; | |
| 772 | 772 | ||
| 773 | - | <span class="cmt">// Download: request URL, download + decrypt</span> | |
| 773 | + | <span class="cmt">// Download: request URL, download + decrypt + verify content hash</span> | |
| 774 | 774 | <span class="kw">let</span> url = client.<span class="fn">blob_download_url</span>(&hash).<span class="kw">await</span>?; | |
| 775 | - | <span class="kw">let</span> plaintext = client.<span class="fn">blob_download</span>(&url).<span class="kw">await</span>?;</pre></div> | |
| 775 | + | <span class="kw">let</span> plaintext = client.<span class="fn">blob_download</span>(&hash, &url).<span class="kw">await</span>?;</pre></div> | |
| 776 | 776 | ||
| 777 | 777 | <div class="highlight-box"> | |
| 778 | 778 | <p>Blobs are content-addressed by SHA-256 hash. If <code>already_exists</code> returns true, the file is already on the server — skip the upload. Binary encryption uses raw bytes (no base64 wrapping), so overhead is exactly 40 bytes per file regardless of size.</p> |
| @@ -55,6 +55,7 @@ tracing = "0.1" | |||
| 55 | 55 | ||
| 56 | 56 | [dev-dependencies] | |
| 57 | 57 | wiremock = "0.6" | |
| 58 | + | sha2 = "0.10" | |
| 58 | 59 | # Self dev-dependency: turns on the `testing` feature for this crate's own | |
| 59 | 60 | # unit/integration test builds without leaking it into a consumer's default set. | |
| 60 | 61 | synckit-client = { path = ".", features = ["testing"] } |
| @@ -204,15 +204,20 @@ upload: | |||
| 204 | 204 | ||
| 205 | 205 | 1. `blob_upload_url(hash, size)` requests a presigned S3 PUT URL from the server. | |
| 206 | 206 | If the blob already exists (content-addressed by hash), no upload is needed. | |
| 207 | - | 2. `blob_upload(presigned_url, data)` encrypts the plaintext bytes with the | |
| 208 | - | master key and PUTs the ciphertext to S3. | |
| 207 | + | 2. `blob_upload(hash, presigned_url, data)` encrypts the plaintext bytes with the | |
| 208 | + | master key, **binding the content hash as AEAD associated data**, and PUTs the | |
| 209 | + | ciphertext to S3. | |
| 209 | 210 | 3. `blob_confirm(hash, size)` tells the server the upload completed. | |
| 210 | 211 | 4. `blob_download_url(hash)` requests a presigned GET URL. | |
| 211 | - | 5. `blob_download(presigned_url)` fetches the ciphertext from S3 and decrypts it. | |
| 212 | - | ||
| 213 | - | Blob encryption uses `encrypt_bytes`/`decrypt_bytes` (raw bytes, no base64) to | |
| 214 | - | avoid the ~33% base64 overhead on large files. Encryption overhead is fixed at | |
| 215 | - | 40 bytes per blob: 24-byte XChaCha20 nonce + 16-byte Poly1305 authentication | |
| 212 | + | 5. `blob_download(expected_hash, presigned_url)` fetches the ciphertext, decrypts | |
| 213 | + | with the hash as AAD, then **re-verifies** that the plaintext hashes to | |
| 214 | + | `expected_hash` (returning `IntegrityFailed` otherwise). The AEAD tag proves | |
| 215 | + | the bytes were not forged; the re-hash proves the server served the bytes for | |
| 216 | + | *this* address, closing the substitution/rollback gap. | |
| 217 | + | ||
| 218 | + | Blob encryption uses `encrypt_bytes_aad`/`decrypt_bytes_aad` (raw bytes, no | |
| 219 | + | base64) to avoid the ~33% base64 overhead on large files. v2 overhead is 44 bytes | |
| 220 | + | per blob: a 4-byte `sk2:` wire tag + 24-byte XChaCha20 nonce + 16-byte Poly1305 | |
| 216 | 221 | tag. | |
| 217 | 222 | ||
| 218 | 223 | ## Keychain integration | |
| @@ -284,8 +289,10 @@ storing long-lived credentials in memory. | |||
| 284 | 289 | - **Minimum ciphertext size**: Decryption rejects inputs shorter than 40 bytes | |
| 285 | 290 | (24-byte nonce + 16-byte tag), preventing trivial malformed-input attacks. | |
| 286 | 291 | - **Ciphertext position binding (AAD)**: v2 entry ciphertext is sealed with | |
| 287 | - | `(table, row_id)` as AEAD associated data, so the untrusted server cannot | |
| 288 | - | relocate a valid ciphertext to another row/table without the open failing | |
| 292 | + | `(table, row_id)` as AEAD associated data, and blob ciphertext with the content | |
| 293 | + | hash; downloaded blobs are additionally re-hashed and rejected if they do not | |
| 294 | + | match their requested address. So the untrusted server cannot relocate or | |
| 295 | + | substitute a valid ciphertext without the open (or the hash check) failing | |
| 289 | 296 | closed. See "Wire version tag and AAD binding". | |
| 290 | 297 | - **Envelope versioning**: The key envelope includes a version field (`v: 1`) | |
| 291 | 298 | and travels with its Argon2 cost parameters, so the work factor can be raised |
| @@ -1,15 +1,21 @@ | |||
| 1 | 1 | use bytes::Bytes; | |
| 2 | + | use sha2::{Digest, Sha256}; | |
| 2 | 3 | use tracing::instrument; | |
| 3 | 4 | ||
| 4 | 5 | use crate::{ | |
| 5 | 6 | crypto, | |
| 6 | - | error::Result, | |
| 7 | + | error::{Result, SyncKitError}, | |
| 7 | 8 | types::*, | |
| 8 | 9 | }; | |
| 9 | 10 | ||
| 10 | 11 | use super::SyncKitClient; | |
| 11 | 12 | use super::helpers::check_response; | |
| 12 | 13 | ||
| 14 | + | /// Upper bound on a single decrypted blob held in memory, guarding against a | |
| 15 | + | /// hostile server returning an absurdly large body that would OOM the client. | |
| 16 | + | /// Generous (4 GiB) so legitimate large media still flow. | |
| 17 | + | const MAX_BLOB_BYTES: usize = 4 * 1024 * 1024 * 1024; | |
| 18 | + | ||
| 13 | 19 | impl SyncKitClient { | |
| 14 | 20 | /// Request a presigned upload URL for a blob. | |
| 15 | 21 | /// Returns (upload_url, already_exists). If `already_exists` is true, | |
| @@ -46,12 +52,15 @@ impl SyncKitClient { | |||
| 46 | 52 | ||
| 47 | 53 | /// Upload blob data directly to S3 via a presigned PUT URL. | |
| 48 | 54 | /// | |
| 49 | - | /// Encrypts the data with the master key before uploading. The plaintext | |
| 50 | - | /// is never sent to the server, preserving the E2E encryption guarantee. | |
| 55 | + | /// Encrypts the data with the master key before uploading, binding the | |
| 56 | + | /// content `hash` as AEAD associated data so the ciphertext is tied to its | |
| 57 | + | /// content address. The plaintext is never sent to the server, preserving | |
| 58 | + | /// the E2E encryption guarantee. | |
| 51 | 59 | #[instrument(skip(self, presigned_url, data))] | |
| 52 | - | pub async fn blob_upload(&self, presigned_url: &str, data: Vec<u8>) -> Result<()> { | |
| 60 | + | pub async fn blob_upload(&self, hash: &str, presigned_url: &str, data: Vec<u8>) -> Result<()> { | |
| 53 | 61 | let master_key = self.require_master_key()?; | |
| 54 | - | let encrypted = Bytes::from(crypto::encrypt_bytes(&data, &master_key)?); | |
| 62 | + | let encrypted = | |
| 63 | + | Bytes::from(crypto::encrypt_bytes_aad(&data, &master_key, hash.as_bytes())?); | |
| 55 | 64 | ||
| 56 | 65 | self.retry_request(|| { | |
| 57 | 66 | let req = self | |
| @@ -118,12 +127,19 @@ impl SyncKitClient { | |||
| 118 | 127 | Ok(download.download_url) | |
| 119 | 128 | } | |
| 120 | 129 | ||
| 121 | - | /// Download blob data from S3 via a presigned GET URL. | |
| 130 | + | /// Download blob data from S3 via a presigned GET URL and verify it. | |
| 131 | + | /// | |
| 132 | + | /// Decrypts with the master key (binding the content `expected_hash` as AEAD | |
| 133 | + | /// associated data), then **re-verifies** that the plaintext hashes to | |
| 134 | + | /// `expected_hash`. The AEAD tag alone proves the bytes were not forged, but | |
| 135 | + | /// not that the untrusted server served the bytes for *this* address; the | |
| 136 | + | /// re-hash closes the substitution/rollback gap. Returns | |
| 137 | + | /// [`SyncKitError::IntegrityFailed`] on mismatch. | |
| 122 | 138 | /// | |
| 123 | - | /// Decrypts the data with the master key after downloading. The server | |
| 124 | - | /// only ever stores encrypted blobs, preserving the E2E encryption guarantee. | |
| 139 | + | /// The blob is held in memory (the caller writes it to disk); the decrypted | |
| 140 | + | /// size is capped at [`MAX_BLOB_BYTES`]. | |
| 125 | 141 | #[instrument(skip(self, presigned_url))] | |
| 126 | - | pub async fn blob_download(&self, presigned_url: &str) -> Result<Vec<u8>> { | |
| 142 | + | pub async fn blob_download(&self, expected_hash: &str, presigned_url: &str) -> Result<Vec<u8>> { | |
| 127 | 143 | let resp = self | |
| 128 | 144 | .retry_request(|| { | |
| 129 | 145 | let req = self.http.get(presigned_url); | |
| @@ -131,9 +147,24 @@ impl SyncKitClient { | |||
| 131 | 147 | }) | |
| 132 | 148 | .await?; | |
| 133 | 149 | ||
| 134 | - | let encrypted = resp.bytes().await?.to_vec(); | |
| 150 | + | // Owned `Bytes`; decrypt borrows it directly (no redundant copy). | |
| 151 | + | let encrypted = resp.bytes().await?; | |
| 152 | + | if encrypted.len() > MAX_BLOB_BYTES { | |
| 153 | + | return Err(SyncKitError::Internal(format!( | |
| 154 | + | "blob exceeds {MAX_BLOB_BYTES}-byte limit", | |
| 155 | + | ))); | |
| 156 | + | } | |
| 135 | 157 | let master_key = self.require_master_key()?; | |
| 136 | - | crypto::decrypt_bytes(&encrypted, &master_key) | |
| 158 | + | let plaintext = crypto::decrypt_bytes_aad(&encrypted, &master_key, expected_hash.as_bytes())?; | |
| 159 | + | ||
| 160 | + | let actual = format!("{:x}", Sha256::digest(&plaintext)); | |
| 161 | + | if actual != expected_hash { | |
| 162 | + | return Err(SyncKitError::IntegrityFailed { | |
| 163 | + | expected: expected_hash.to_string(), | |
| 164 | + | actual, | |
| 165 | + | }); | |
| 166 | + | } | |
| 167 | + | Ok(plaintext) | |
| 137 | 168 | } | |
| 138 | 169 | } | |
| 139 | 170 | ||
| @@ -167,6 +198,17 @@ mod tests { | |||
| 167 | 198 | } | |
| 168 | 199 | ||
| 169 | 200 | #[test] | |
| 201 | + | fn blob_content_hash_format_matches_consumer() { | |
| 202 | + | use sha2::{Digest, Sha256}; | |
| 203 | + | // The integrity check in blob_download compares against this exact form: | |
| 204 | + | // lowercase hex of SHA-256, the same string consumers store as the blob | |
| 205 | + | // hash. If this drifts, every verified download would falsely reject. | |
| 206 | + | let h = format!("{:x}", Sha256::digest(b"hello blob")); | |
| 207 | + | assert_eq!(h.len(), 64); | |
| 208 | + | assert!(h.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())); | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | #[test] | |
| 170 | 212 | fn blob_confirm_request_serialization() { | |
| 171 | 213 | let req = BlobConfirmRequest { | |
| 172 | 214 | hash: "sha256-def456".to_string(), |
| @@ -69,7 +69,7 @@ impl SyncKitClient { | |||
| 69 | 69 | Err(e) => { | |
| 70 | 70 | let err = SyncKitError::Http(e); | |
| 71 | 71 | if attempt < MAX_RETRIES { | |
| 72 | - | let delay = BASE_DELAY * 2u32.pow(attempt); | |
| 72 | + | let delay = retry_delay(&err, attempt); | |
| 73 | 73 | tracing::debug!( | |
| 74 | 74 | attempt = attempt + 1, | |
| 75 | 75 | max_retries = MAX_RETRIES, | |
| @@ -339,13 +339,28 @@ fn parse_retry_after(resp: &reqwest::Response) -> Option<u64> { | |||
| 339 | 339 | /// Compute the backoff delay for a retry attempt. | |
| 340 | 340 | /// | |
| 341 | 341 | /// Uses the server's `Retry-After` header (if present on a 429) capped at | |
| 342 | - | /// 60 seconds. Falls back to exponential backoff (1s, 2s, 4s). | |
| 342 | + | /// 60 seconds. Otherwise exponential backoff (1s, 2s, 4s) with ±20% jitter so | |
| 343 | + | /// many clients that hit a transient blip simultaneously don't retry in lockstep | |
| 344 | + | /// and re-amplify the load. | |
| 343 | 345 | fn retry_delay(err: &SyncKitError, attempt: u32) -> std::time::Duration { | |
| 344 | 346 | if let SyncKitError::Server { retry_after_secs: Some(secs), .. } = err { | |
| 345 | 347 | let capped = (*secs).min(60); | |
| 346 | 348 | return std::time::Duration::from_secs(capped); | |
| 347 | 349 | } | |
| 348 | - | BASE_DELAY * 2u32.pow(attempt) | |
| 350 | + | jittered(BASE_DELAY * 2u32.pow(attempt)) | |
| 351 | + | } | |
| 352 | + | ||
| 353 | + | /// Spread a backoff delay by ±20% to avoid synchronized retries (thundering herd). | |
| 354 | + | fn jittered(base: std::time::Duration) -> std::time::Duration { | |
| 355 | + | use rand::Rng; | |
| 356 | + | let millis = base.as_millis() as u64; | |
| 357 | + | let span = millis / 5; // 20% | |
| 358 | + | if span == 0 { | |
| 359 | + | return base; | |
| 360 | + | } | |
| 361 | + | // Uniform in [millis - span, millis + span]. | |
| 362 | + | let delta = rand::rng().random_range(0..=2 * span); | |
| 363 | + | std::time::Duration::from_millis(millis - span + delta) | |
| 349 | 364 | } | |
| 350 | 365 | ||
| 351 | 366 | /// Returns true if the error is transient and worth retrying. |
| @@ -397,6 +397,10 @@ pub fn decrypt_bytes_aad( | |||
| 397 | 397 | /// Encryption overhead in bytes (24-byte nonce + 16-byte Poly1305 tag). | |
| 398 | 398 | pub const ENCRYPTION_OVERHEAD: usize = NONCE_SIZE + 16; | |
| 399 | 399 | ||
| 400 | + | /// Overhead of a v2 (AAD-bound, `sk2:`-tagged) blob: the wire tag plus the | |
| 401 | + | /// nonce + Poly1305 tag. | |
| 402 | + | pub const ENCRYPTION_OVERHEAD_V2: usize = WIRE_V2_TAG_BYTES.len() + ENCRYPTION_OVERHEAD; | |
| 403 | + | ||
| 400 | 404 | /// Encrypt a JSON value (legacy, untagged, empty AAD). Prefer [`encrypt_json_aad`]. | |
| 401 | 405 | pub fn encrypt_json( | |
| 402 | 406 | value: &serde_json::Value, |
| @@ -35,6 +35,12 @@ pub enum SyncKitError { | |||
| 35 | 35 | #[error("Wrong password or corrupted key envelope")] | |
| 36 | 36 | DecryptionFailed, | |
| 37 | 37 | ||
| 38 | + | /// A downloaded blob decrypted cleanly but did not hash to its requested | |
| 39 | + | /// content address — the server served a different-but-authentic or | |
| 40 | + | /// rolled-back ciphertext under this hash. The bytes are discarded. | |
| 41 | + | #[error("Blob integrity check failed: expected {expected}, got {actual}")] | |
| 42 | + | IntegrityFailed { expected: String, actual: String }, | |
| 43 | + | ||
| 38 | 44 | /// Key envelope JSON has an unrecognized version or missing fields. | |
| 39 | 45 | #[error("Invalid key envelope: {0}")] | |
| 40 | 46 | InvalidEnvelope(String), |
| @@ -8,6 +8,7 @@ use std::sync::Arc; | |||
| 8 | 8 | use base64::Engine; | |
| 9 | 9 | use chrono::Utc; | |
| 10 | 10 | use serde_json::json; | |
| 11 | + | use sha2::Digest; | |
| 11 | 12 | use uuid::Uuid; | |
| 12 | 13 | use wiremock::matchers::{method, path}; | |
| 13 | 14 | use wiremock::{Mock, MockServer, ResponseTemplate}; | |
| @@ -443,7 +444,7 @@ async fn blob_upload_encrypts_data() { | |||
| 443 | 444 | let plaintext = b"hello blob data"; | |
| 444 | 445 | let presigned = format!("{}{}", server.uri(), upload_path); | |
| 445 | 446 | client | |
| 446 | - | .blob_upload(&presigned, plaintext.to_vec()) | |
| 447 | + | .blob_upload("sha256-test", &presigned, plaintext.to_vec()) | |
| 447 | 448 | .await | |
| 448 | 449 | .unwrap(); | |
| 449 | 450 | ||
| @@ -472,8 +473,10 @@ async fn blob_download_decrypts_data() { | |||
| 472 | 473 | let key = synckit_client::crypto::generate_master_key(); | |
| 473 | 474 | client.set_master_key_raw(key); | |
| 474 | 475 | ||
| 475 | - | // Encrypt data to simulate what S3 would return | |
| 476 | + | // Encrypt data to simulate what S3 would return. A legacy (untagged) blob | |
| 477 | + | // still decrypts through the AAD-aware reader and must pass the hash check. | |
| 476 | 478 | let plaintext = b"decrypted blob content"; | |
| 479 | + | let hash = format!("{:x}", sha2::Sha256::digest(plaintext)); | |
| 477 | 480 | let encrypted = synckit_client::crypto::encrypt_bytes(plaintext, &key).unwrap(); | |
| 478 | 481 | ||
| 479 | 482 | let download_path = "/s3/download"; | |
| @@ -484,7 +487,7 @@ async fn blob_download_decrypts_data() { | |||
| 484 | 487 | .await; | |
| 485 | 488 | ||
| 486 | 489 | let presigned = format!("{}{}", server.uri(), download_path); | |
| 487 | - | let result = client.blob_download(&presigned).await.unwrap(); | |
| 490 | + | let result = client.blob_download(&hash, &presigned).await.unwrap(); | |
| 488 | 491 | assert_eq!(result, plaintext); | |
| 489 | 492 | } | |
| 490 | 493 | ||
| @@ -511,7 +514,7 @@ async fn blob_upload_retries_on_503() { | |||
| 511 | 514 | client.set_master_key_raw(key); | |
| 512 | 515 | ||
| 513 | 516 | let presigned = format!("{}{}", server.uri(), upload_path); | |
| 514 | - | let result = client.blob_upload(&presigned, b"data".to_vec()).await; | |
| 517 | + | let result = client.blob_upload("sha256-x", &presigned, b"data".to_vec()).await; | |
| 515 | 518 | assert!(result.is_ok(), "Should succeed after retry: {result:?}"); | |
| 516 | 519 | } | |
| 517 | 520 | ||
| @@ -1301,10 +1304,10 @@ async fn blob_upload_zero_byte_data() { | |||
| 1301 | 1304 | client.set_master_key_raw(key); | |
| 1302 | 1305 | ||
| 1303 | 1306 | let presigned = format!("{}{}", server.uri(), upload_path); | |
| 1304 | - | let result = client.blob_upload(&presigned, vec![]).await; | |
| 1307 | + | let result = client.blob_upload("sha256-empty", &presigned, vec![]).await; | |
| 1305 | 1308 | assert!(result.is_ok(), "Zero-byte blob upload should succeed"); | |
| 1306 | 1309 | ||
| 1307 | - | // Verify the uploaded data has encryption overhead only | |
| 1310 | + | // Verify the uploaded data has encryption overhead only (v2: + sk2: tag) | |
| 1308 | 1311 | let requests = server.received_requests().await.unwrap(); | |
| 1309 | 1312 | let req = requests | |
| 1310 | 1313 | .iter() | |
| @@ -1312,7 +1315,7 @@ async fn blob_upload_zero_byte_data() { | |||
| 1312 | 1315 | .unwrap(); | |
| 1313 | 1316 | assert_eq!( | |
| 1314 | 1317 | req.body.len(), | |
| 1315 | - | synckit_client::crypto::ENCRYPTION_OVERHEAD, | |
| 1318 | + | synckit_client::crypto::ENCRYPTION_OVERHEAD_V2, | |
| 1316 | 1319 | "Empty plaintext should produce exactly overhead bytes" | |
| 1317 | 1320 | ); | |
| 1318 | 1321 | } | |
| @@ -1326,6 +1329,7 @@ async fn blob_upload_download_roundtrip() { | |||
| 1326 | 1329 | client.set_master_key_raw(key); | |
| 1327 | 1330 | ||
| 1328 | 1331 | let plaintext = b"roundtrip blob data with special bytes \x00\xFF\x01"; | |
| 1332 | + | let hash = format!("{:x}", sha2::Sha256::digest(plaintext)); | |
| 1329 | 1333 | ||
| 1330 | 1334 | // Upload | |
| 1331 | 1335 | let upload_path = "/s3/roundtrip-upload"; | |
| @@ -1337,6 +1341,7 @@ async fn blob_upload_download_roundtrip() { | |||
| 1337 | 1341 | ||
| 1338 | 1342 | client | |
| 1339 | 1343 | .blob_upload( | |
| 1344 | + | &hash, | |
| 1340 | 1345 | &format!("{}{}", server.uri(), upload_path), | |
| 1341 | 1346 | plaintext.to_vec(), | |
| 1342 | 1347 | ) | |
| @@ -1360,7 +1365,7 @@ async fn blob_upload_download_roundtrip() { | |||
| 1360 | 1365 | .await; | |
| 1361 | 1366 | ||
| 1362 | 1367 | let downloaded = client | |
| 1363 | - | .blob_download(&format!("{}{}", server.uri(), download_path)) | |
| 1368 | + | .blob_download(&hash, &format!("{}{}", server.uri(), download_path)) | |
| 1364 | 1369 | .await | |
| 1365 | 1370 | .unwrap(); | |
| 1366 | 1371 | ||
| @@ -1907,7 +1912,7 @@ async fn blob_download_with_wrong_key_fails() { | |||
| 1907 | 1912 | client.set_master_key_raw(key2); | |
| 1908 | 1913 | ||
| 1909 | 1914 | let result = client | |
| 1910 | - | .blob_download(&format!("{}{}", server.uri(), download_path)) | |
| 1915 | + | .blob_download("sha256-x", &format!("{}{}", server.uri(), download_path)) | |
| 1911 | 1916 | .await; | |
| 1912 | 1917 | ||
| 1913 | 1918 | assert!( | |
| @@ -2461,6 +2466,7 @@ async fn blob_download_retries_on_503() { | |||
| 2461 | 2466 | client.set_master_key_raw(key); | |
| 2462 | 2467 | ||
| 2463 | 2468 | let plaintext = b"retry download test"; | |
| 2469 | + | let hash = format!("{:x}", sha2::Sha256::digest(plaintext)); | |
| 2464 | 2470 | let encrypted = synckit_client::crypto::encrypt_bytes(plaintext, &key).unwrap(); | |
| 2465 | 2471 | ||
| 2466 | 2472 | let download_path = "/s3/retry-download"; | |
| @@ -2478,7 +2484,7 @@ async fn blob_download_retries_on_503() { | |||
| 2478 | 2484 | .await; | |
| 2479 | 2485 | ||
| 2480 | 2486 | let presigned = format!("{}{}", server.uri(), download_path); | |
| 2481 | - | let result = client.blob_download(&presigned).await.unwrap(); | |
| 2487 | + | let result = client.blob_download(&hash, &presigned).await.unwrap(); | |
| 2482 | 2488 | assert_eq!(result, plaintext); | |
| 2483 | 2489 | } | |
| 2484 | 2490 | ||
| @@ -2499,14 +2505,14 @@ async fn blob_upload_1mb_with_correct_overhead() { | |||
| 2499 | 2505 | ||
| 2500 | 2506 | let plaintext: Vec<u8> = (0..1_048_576u32).map(|i| (i % 256) as u8).collect(); | |
| 2501 | 2507 | let presigned = format!("{}{}", server.uri(), upload_path); | |
| 2502 | - | client.blob_upload(&presigned, plaintext.clone()).await.unwrap(); | |
| 2508 | + | client.blob_upload("sha256-1mb", &presigned, plaintext.clone()).await.unwrap(); | |
| 2503 | 2509 | ||
| 2504 | 2510 | let requests = server.received_requests().await.unwrap(); | |
| 2505 | 2511 | let upload_req = requests.iter().find(|r| r.url.path() == upload_path).unwrap(); | |
| 2506 | 2512 | assert_eq!( | |
| 2507 | 2513 | upload_req.body.len(), | |
| 2508 | - | plaintext.len() + synckit_client::crypto::ENCRYPTION_OVERHEAD, | |
| 2509 | - | "1MB upload should have exactly ENCRYPTION_OVERHEAD bytes added" | |
| 2514 | + | plaintext.len() + synckit_client::crypto::ENCRYPTION_OVERHEAD_V2, | |
| 2515 | + | "1MB upload should have exactly ENCRYPTION_OVERHEAD_V2 bytes added" | |
| 2510 | 2516 | ); | |
| 2511 | 2517 | } | |
| 2512 | 2518 |