Skip to main content

max / synckit-client

33.4 KB · 1034 lines History Blame Raw
1 //! Encryption engine: key derivation, wrapping, and per-entry encrypt/decrypt.
2 //!
3 //! Key hierarchy:
4 //! password + (app_id, user_id) → Argon2id → wrapping_key
5 //! wrapping_key encrypts/decrypts the random master_key
6 //! master_key encrypts/decrypts individual data entries
7 //!
8 //! All encryption uses XChaCha20-Poly1305 (192-bit nonces, safe for random generation).
9
10 use argon2::{Argon2, Algorithm, Version, Params};
11 use base64::{engine::general_purpose::STANDARD as B64, Engine};
12 use chacha20poly1305::{
13 aead::{Aead, KeyInit},
14 XChaCha20Poly1305, XNonce,
15 };
16 use rand::RngCore;
17 use serde::{Deserialize, Serialize};
18 use unicode_normalization::UnicodeNormalization;
19
20 use crate::error::{Result, SyncKitError};
21
22 /// Size of XChaCha20-Poly1305 nonce in bytes.
23 const NONCE_SIZE: usize = 24;
24 /// Size of the encryption key in bytes.
25 const KEY_SIZE: usize = 32;
26
27 /// Current envelope version.
28 const ENVELOPE_VERSION: u8 = 1;
29
30 /// Argon2id parameters: 64 MB memory, 3 iterations (OWASP interactive minimum).
31 const ARGON2_MEM_COST_KB: u32 = 65_536; // 64 MB
32 const ARGON2_TIME_COST: u32 = 3;
33 const ARGON2_PARALLELISM: u32 = 1;
34
35 /// Encrypted master key envelope stored on the server.
36 #[derive(Debug, Serialize, Deserialize)]
37 pub(crate) struct KeyEnvelope {
38 /// Envelope version (currently 1).
39 pub v: u8,
40 /// Argon2 salt (base64).
41 pub salt: String,
42 /// XChaCha20-Poly1305 nonce for the wrapping (base64).
43 pub nonce: String,
44 /// Encrypted master key (base64).
45 pub ciphertext: String,
46 }
47
48 /// Generate a random 256-bit master key.
49 pub fn generate_master_key() -> [u8; KEY_SIZE] {
50 let mut key = [0u8; KEY_SIZE];
51 rand::thread_rng().fill_bytes(&mut key);
52 key
53 }
54
55 /// Maximum password length in bytes. Passwords longer than this are rejected
56 /// to prevent denial-of-service via extreme Argon2 input sizes.
57 const MAX_PASSWORD_BYTES: usize = 1024;
58
59 /// Normalize a password to NFC form and validate constraints.
60 ///
61 /// Returns the NFC-normalized password string. Rejects empty passwords
62 /// and passwords exceeding [`MAX_PASSWORD_BYTES`].
63 fn normalize_password(password: &str) -> Result<String> {
64 if password.is_empty() {
65 return Err(SyncKitError::Crypto("password must not be empty".into()));
66 }
67
68 let normalized: String = password.nfc().collect();
69
70 if normalized.len() > MAX_PASSWORD_BYTES {
71 return Err(SyncKitError::Crypto(format!(
72 "password exceeds maximum length of {MAX_PASSWORD_BYTES} bytes"
73 )));
74 }
75
76 Ok(normalized)
77 }
78
79 /// Derive a wrapping key from a password and salt using Argon2id.
80 ///
81 /// The password is NFC-normalized before derivation to ensure consistent
82 /// keys across platforms with different default Unicode normalization forms.
83 fn derive_wrapping_key(
84 password: &str,
85 salt: &[u8; 32],
86 ) -> Result<ZeroizeOnDrop> {
87 let normalized = normalize_password(password)?;
88
89 let params = Params::new(
90 ARGON2_MEM_COST_KB,
91 ARGON2_TIME_COST,
92 ARGON2_PARALLELISM,
93 Some(KEY_SIZE),
94 )
95 .map_err(|e| SyncKitError::Crypto(format!("Argon2 params: {e}")))?;
96
97 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
98
99 let mut wrapping_key = ZeroizeOnDrop([0u8; KEY_SIZE]);
100 argon2
101 .hash_password_into(normalized.as_bytes(), salt, &mut wrapping_key.0)
102 .map_err(|e| SyncKitError::Crypto(format!("Argon2 hash: {e}")))?;
103
104 Ok(wrapping_key)
105 }
106
107 /// Verify that a password correctly derives to the given master key by
108 /// attempting to unwrap the envelope with it.
109 ///
110 /// This is used by `change_password` to validate the old password before
111 /// allowing a re-wrap with the new password. The cached key may still be
112 /// used for the actual re-encryption, but the old password must be proven
113 /// correct first.
114 pub fn verify_password_against_envelope(
115 envelope_json: &str,
116 password: &str,
117 ) -> Result<[u8; KEY_SIZE]> {
118 unwrap_master_key(envelope_json, password)
119 }
120
121 /// Encrypt the master key with a password, producing a JSON envelope.
122 ///
123 /// Generates a random 32-byte salt for Argon2id key derivation and stores it
124 /// in the envelope. Each wrap operation uses a unique salt, preventing
125 /// precomputation attacks.
126 pub fn wrap_master_key(
127 master_key: &[u8; KEY_SIZE],
128 password: &str,
129 ) -> Result<String> {
130 let mut salt = [0u8; 32];
131 rand::thread_rng().fill_bytes(&mut salt);
132
133 let wrapping_key = derive_wrapping_key(password, &salt)?;
134
135 let mut nonce_bytes = [0u8; NONCE_SIZE];
136 rand::thread_rng().fill_bytes(&mut nonce_bytes);
137
138 let cipher = XChaCha20Poly1305::new((&*wrapping_key).into());
139 let nonce = XNonce::from_slice(&nonce_bytes);
140
141 let ciphertext = cipher
142 .encrypt(nonce, master_key.as_ref())
143 .map_err(|e| SyncKitError::Crypto(format!("wrap encrypt: {e}")))?;
144
145 let envelope = KeyEnvelope {
146 v: ENVELOPE_VERSION,
147 salt: B64.encode(salt),
148 nonce: B64.encode(nonce_bytes),
149 ciphertext: B64.encode(ciphertext),
150 };
151
152 serde_json::to_string(&envelope).map_err(Into::into)
153 }
154
155 /// Decrypt the master key from a JSON envelope using a password.
156 ///
157 /// Reads the random salt from the envelope, derives the wrapping key via
158 /// Argon2id, then decrypts the master key.
159 pub fn unwrap_master_key(
160 envelope_json: &str,
161 password: &str,
162 ) -> Result<[u8; KEY_SIZE]> {
163 let envelope: KeyEnvelope =
164 serde_json::from_str(envelope_json).map_err(|e| {
165 SyncKitError::InvalidEnvelope(format!("JSON parse: {e}"))
166 })?;
167
168 if envelope.v != ENVELOPE_VERSION {
169 return Err(SyncKitError::InvalidEnvelope(format!(
170 "unsupported version {}",
171 envelope.v
172 )));
173 }
174
175 let salt_bytes = B64.decode(&envelope.salt)?;
176 let nonce_bytes = B64.decode(&envelope.nonce)?;
177 let ciphertext = B64.decode(&envelope.ciphertext)?;
178
179 if salt_bytes.len() != 32 {
180 return Err(SyncKitError::InvalidEnvelope(
181 "invalid salt length".into(),
182 ));
183 }
184
185 if nonce_bytes.len() != NONCE_SIZE {
186 return Err(SyncKitError::InvalidEnvelope(
187 "invalid nonce length".into(),
188 ));
189 }
190
191 let mut salt = [0u8; 32];
192 salt.copy_from_slice(&salt_bytes);
193 let wrapping_key = derive_wrapping_key(password, &salt)?;
194
195 let cipher = XChaCha20Poly1305::new((&*wrapping_key).into());
196 let nonce = XNonce::from_slice(&nonce_bytes);
197
198 let plaintext = cipher
199 .decrypt(nonce, ciphertext.as_ref())
200 .map_err(|_| SyncKitError::DecryptionFailed)?;
201
202 if plaintext.len() != KEY_SIZE {
203 return Err(SyncKitError::InvalidEnvelope(
204 "decrypted key has wrong length".into(),
205 ));
206 }
207
208 let mut key = [0u8; KEY_SIZE];
209 key.copy_from_slice(&plaintext);
210 Ok(key)
211 }
212
213 /// Encrypt a data entry with the master key.
214 /// Returns base64(nonce[24] || ciphertext || poly1305_tag[16]).
215 pub fn encrypt_data(
216 plaintext: &[u8],
217 master_key: &[u8; KEY_SIZE],
218 ) -> Result<String> {
219 let mut nonce_bytes = [0u8; NONCE_SIZE];
220 rand::thread_rng().fill_bytes(&mut nonce_bytes);
221
222 let cipher = XChaCha20Poly1305::new(master_key.into());
223 let nonce = XNonce::from_slice(&nonce_bytes);
224
225 let ciphertext = cipher
226 .encrypt(nonce, plaintext)
227 .map_err(|e| SyncKitError::Crypto(format!("encrypt: {e}")))?;
228
229 // Wire format: nonce || ciphertext (which includes poly1305 tag)
230 let mut blob = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
231 blob.extend_from_slice(&nonce_bytes);
232 blob.extend_from_slice(&ciphertext);
233
234 Ok(B64.encode(blob))
235 }
236
237 /// Decrypt a data entry with the master key.
238 /// Input is base64(nonce[24] || ciphertext || poly1305_tag[16]).
239 pub fn decrypt_data(
240 encoded: &str,
241 master_key: &[u8; KEY_SIZE],
242 ) -> Result<Vec<u8>> {
243 let blob = B64.decode(encoded)?;
244
245 if blob.len() < NONCE_SIZE + 16 {
246 // Minimum: nonce + poly1305 tag (empty plaintext)
247 return Err(SyncKitError::Crypto(
248 "ciphertext too short".into(),
249 ));
250 }
251
252 let (nonce_bytes, ciphertext) = blob.split_at(NONCE_SIZE);
253 let cipher = XChaCha20Poly1305::new(master_key.into());
254 let nonce = XNonce::from_slice(nonce_bytes);
255
256 cipher
257 .decrypt(nonce, ciphertext)
258 .map_err(|_| SyncKitError::DecryptionFailed)
259 }
260
261 /// Encrypt raw bytes with the master key.
262 /// Returns `nonce[24] || ciphertext || poly1305_tag[16]` as raw bytes (no base64).
263 /// Use this for blob data where base64 overhead is undesirable.
264 pub fn encrypt_bytes(
265 plaintext: &[u8],
266 master_key: &[u8; KEY_SIZE],
267 ) -> Result<Vec<u8>> {
268 let mut nonce_bytes = [0u8; NONCE_SIZE];
269 rand::thread_rng().fill_bytes(&mut nonce_bytes);
270
271 let cipher = XChaCha20Poly1305::new(master_key.into());
272 let nonce = XNonce::from_slice(&nonce_bytes);
273
274 let ciphertext = cipher
275 .encrypt(nonce, plaintext)
276 .map_err(|e| SyncKitError::Crypto(format!("encrypt: {e}")))?;
277
278 let mut blob = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
279 blob.extend_from_slice(&nonce_bytes);
280 blob.extend_from_slice(&ciphertext);
281
282 Ok(blob)
283 }
284
285 /// Decrypt raw bytes with the master key.
286 /// Input is `nonce[24] || ciphertext || poly1305_tag[16]`.
287 pub fn decrypt_bytes(
288 encrypted: &[u8],
289 master_key: &[u8; KEY_SIZE],
290 ) -> Result<Vec<u8>> {
291 if encrypted.len() < NONCE_SIZE + 16 {
292 return Err(SyncKitError::Crypto(
293 "ciphertext too short".into(),
294 ));
295 }
296
297 let (nonce_bytes, ciphertext) = encrypted.split_at(NONCE_SIZE);
298 let cipher = XChaCha20Poly1305::new(master_key.into());
299 let nonce = XNonce::from_slice(nonce_bytes);
300
301 cipher
302 .decrypt(nonce, ciphertext)
303 .map_err(|_| SyncKitError::DecryptionFailed)
304 }
305
306 /// Encryption overhead in bytes (24-byte nonce + 16-byte Poly1305 tag).
307 pub const ENCRYPTION_OVERHEAD: usize = NONCE_SIZE + 16;
308
309 /// Encrypt a JSON value, returning a JSON string suitable for the `data` field.
310 pub fn encrypt_json(
311 value: &serde_json::Value,
312 master_key: &[u8; KEY_SIZE],
313 ) -> Result<serde_json::Value> {
314 use zeroize::Zeroize;
315 let mut plaintext = serde_json::to_vec(value)?;
316 let encrypted = encrypt_data(&plaintext, master_key);
317 plaintext.zeroize();
318 Ok(serde_json::Value::String(encrypted?))
319 }
320
321 /// Decrypt a JSON string from the `data` field back into the original value.
322 pub fn decrypt_json(
323 encrypted_value: &serde_json::Value,
324 master_key: &[u8; KEY_SIZE],
325 ) -> Result<serde_json::Value> {
326 use zeroize::Zeroize;
327 let encoded = encrypted_value
328 .as_str()
329 .ok_or_else(|| SyncKitError::Crypto("data field is not a string".into()))?;
330
331 let mut plaintext = decrypt_data(encoded, master_key)?;
332 let result = serde_json::from_slice(&plaintext);
333 plaintext.zeroize();
334 result.map_err(Into::into)
335 }
336
337 /// Zero out a key on drop using the `zeroize` crate.
338 pub(crate) struct ZeroizeOnDrop(pub(crate) [u8; KEY_SIZE]);
339
340 impl std::fmt::Debug for ZeroizeOnDrop {
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 f.write_str("[REDACTED 32-byte key]")
343 }
344 }
345
346 impl Drop for ZeroizeOnDrop {
347 fn drop(&mut self) {
348 use zeroize::Zeroize;
349 self.0.zeroize();
350 }
351 }
352
353 impl std::ops::Deref for ZeroizeOnDrop {
354 type Target = [u8; KEY_SIZE];
355 fn deref(&self) -> &Self::Target {
356 &self.0
357 }
358 }
359
360 #[cfg(test)]
361 mod tests {
362 use super::*;
363
364 #[test]
365 fn master_key_generation_is_random() {
366 let k1 = generate_master_key();
367 let k2 = generate_master_key();
368 assert_ne!(k1, k2, "Two generated keys must differ");
369 assert_eq!(k1.len(), 32);
370 }
371
372 #[test]
373 fn wrapping_key_derivation_is_deterministic() {
374 let salt = [42u8; 32];
375 let k1 = derive_wrapping_key("password123", &salt).unwrap();
376 let k2 = derive_wrapping_key("password123", &salt).unwrap();
377 assert_eq!(*k1, *k2, "Same inputs must produce same wrapping key");
378 }
379
380 #[test]
381 fn different_passwords_produce_different_keys() {
382 let salt = [42u8; 32];
383 let k1 = derive_wrapping_key("password1", &salt).unwrap();
384 let k2 = derive_wrapping_key("password2", &salt).unwrap();
385 assert_ne!(*k1, *k2);
386 }
387
388 #[test]
389 fn different_salts_produce_different_keys() {
390 let salt1 = [1u8; 32];
391 let salt2 = [2u8; 32];
392 let k1 = derive_wrapping_key("password", &salt1).unwrap();
393 let k2 = derive_wrapping_key("password", &salt2).unwrap();
394 assert_ne!(*k1, *k2);
395 }
396
397 // ── Password normalization (NFC/NFD) ──
398
399 #[test]
400 fn nfc_and_nfd_passwords_derive_same_key() {
401 // "e" + combining acute accent (NFD form of e-acute)
402 let nfd_password = "caf\u{0065}\u{0301}"; // "cafe" with decomposed accent
403 // Pre-composed e-acute (NFC form)
404 let nfc_password = "caf\u{00e9}"; // "cafe" with composed accent
405
406 // Verify they are actually different byte sequences
407 assert_ne!(
408 nfd_password.as_bytes(),
409 nfc_password.as_bytes(),
410 "NFD and NFC should have different raw bytes"
411 );
412
413 let salt = [99u8; 32];
414 let k1 = derive_wrapping_key(nfd_password, &salt).unwrap();
415 let k2 = derive_wrapping_key(nfc_password, &salt).unwrap();
416 assert_eq!(
417 *k1, *k2,
418 "Same password in NFC and NFD forms must derive the same key"
419 );
420 }
421
422 #[test]
423 fn nfc_nfd_wrap_unwrap_roundtrip() {
424 let master_key = generate_master_key();
425 // Wrap with NFC form
426 let nfc_password = "caf\u{00e9}";
427 let envelope = wrap_master_key(&master_key, nfc_password).unwrap();
428
429 // Unwrap with NFD form
430 let nfd_password = "caf\u{0065}\u{0301}";
431 let recovered = unwrap_master_key(&envelope, nfd_password).unwrap();
432 assert_eq!(master_key, recovered);
433 }
434
435 #[test]
436 fn nfd_wrap_nfc_unwrap_roundtrip() {
437 let master_key = generate_master_key();
438 // Wrap with NFD form
439 let nfd_password = "caf\u{0065}\u{0301}";
440 let envelope = wrap_master_key(&master_key, nfd_password).unwrap();
441
442 // Unwrap with NFC form
443 let nfc_password = "caf\u{00e9}";
444 let recovered = unwrap_master_key(&envelope, nfc_password).unwrap();
445 assert_eq!(master_key, recovered);
446 }
447
448 #[test]
449 fn normalize_password_converts_to_nfc() {
450 let nfd = "caf\u{0065}\u{0301}";
451 let nfc = "caf\u{00e9}";
452 let normalized = normalize_password(nfd).unwrap();
453 assert_eq!(normalized, nfc);
454 }
455
456 // ── Empty password rejection ──
457
458 #[test]
459 fn empty_password_rejected_by_normalize() {
460 let result = normalize_password("");
461 assert!(result.is_err());
462 let msg = result.unwrap_err().to_string();
463 assert!(msg.contains("empty"), "Error should mention empty: {msg}");
464 }
465
466 #[test]
467 fn empty_password_rejected_by_derive() {
468 let salt = [0u8; 32];
469 let result = derive_wrapping_key("", &salt);
470 assert!(result.is_err());
471 }
472
473 #[test]
474 fn empty_password_rejected_by_wrap() {
475 let master_key = generate_master_key();
476 let result = wrap_master_key(&master_key, "");
477 assert!(result.is_err());
478 }
479
480 #[test]
481 fn empty_password_rejected_by_unwrap() {
482 let master_key = generate_master_key();
483 let envelope = wrap_master_key(&master_key, "valid").unwrap();
484 let result = unwrap_master_key(&envelope, "");
485 assert!(result.is_err());
486 }
487
488 // ── Password length limit ──
489
490 #[test]
491 fn very_long_password_rejected() {
492 let long_password = "a".repeat(MAX_PASSWORD_BYTES + 1);
493 let result = normalize_password(&long_password);
494 assert!(result.is_err());
495 let msg = result.unwrap_err().to_string();
496 assert!(
497 msg.contains("maximum length"),
498 "Error should mention max length: {msg}"
499 );
500 }
501
502 #[test]
503 fn password_at_max_length_accepted() {
504 let max_password = "a".repeat(MAX_PASSWORD_BYTES);
505 let result = normalize_password(&max_password);
506 assert!(result.is_ok());
507 }
508
509 #[test]
510 fn password_just_under_max_length_accepted() {
511 let password = "a".repeat(MAX_PASSWORD_BYTES - 1);
512 let result = normalize_password(&password);
513 assert!(result.is_ok());
514 }
515
516 // ── Salt reuse detection ──
517
518 #[test]
519 fn two_wraps_use_different_salts() {
520 let master_key = generate_master_key();
521 let e1_json = wrap_master_key(&master_key, "pass").unwrap();
522 let e2_json = wrap_master_key(&master_key, "pass").unwrap();
523
524 let e1: KeyEnvelope = serde_json::from_str(&e1_json).unwrap();
525 let e2: KeyEnvelope = serde_json::from_str(&e2_json).unwrap();
526
527 assert_ne!(e1.salt, e2.salt, "Each wrap must use a unique random salt");
528 assert_ne!(e1.nonce, e2.nonce, "Each wrap must use a unique random nonce");
529 }
530
531 // ── Key derivation determinism ──
532
533 #[test]
534 fn key_derivation_deterministic_multiple_calls() {
535 let salt = [77u8; 32];
536 let password = "deterministic-test-password";
537
538 let k1 = derive_wrapping_key(password, &salt).unwrap();
539 let k2 = derive_wrapping_key(password, &salt).unwrap();
540 let k3 = derive_wrapping_key(password, &salt).unwrap();
541
542 assert_eq!(*k1, *k2);
543 assert_eq!(*k2, *k3);
544 }
545
546 // ── Key rotation: re-wrap with new password, old data still readable ──
547
548 #[test]
549 fn key_rotation_preserves_data_access() {
550 let master_key = generate_master_key();
551 let plaintext = b"encrypted before password change";
552
553 // Encrypt data with the master key
554 let encrypted = encrypt_data(plaintext, &master_key).unwrap();
555
556 // Wrap master key with old password
557 let old_envelope = wrap_master_key(&master_key, "old-pass").unwrap();
558
559 // Simulate password change: unwrap with old, re-wrap with new
560 let recovered_key = unwrap_master_key(&old_envelope, "old-pass").unwrap();
561 assert_eq!(recovered_key, master_key);
562
563 let new_envelope = wrap_master_key(&recovered_key, "new-pass").unwrap();
564
565 // Verify: unwrap with new password gives same key
566 let key_from_new = unwrap_master_key(&new_envelope, "new-pass").unwrap();
567 assert_eq!(key_from_new, master_key);
568
569 // Verify: old encrypted data can still be decrypted
570 let decrypted = decrypt_data(&encrypted, &key_from_new).unwrap();
571 assert_eq!(decrypted, plaintext);
572
573 // Verify: old password no longer works on new envelope
574 let result = unwrap_master_key(&new_envelope, "old-pass");
575 assert!(result.is_err());
576 }
577
578 // ── Encryption roundtrip with various data sizes ──
579
580 #[test]
581 fn encrypt_decrypt_empty_data() {
582 let master_key = generate_master_key();
583 let encrypted = encrypt_data(b"", &master_key).unwrap();
584 let decrypted = decrypt_data(&encrypted, &master_key).unwrap();
585 assert!(decrypted.is_empty());
586 }
587
588 #[test]
589 fn encrypt_decrypt_single_byte() {
590 let master_key = generate_master_key();
591 let encrypted = encrypt_data(&[42], &master_key).unwrap();
592 let decrypted = decrypt_data(&encrypted, &master_key).unwrap();
593 assert_eq!(decrypted, vec![42]);
594 }
595
596 #[test]
597 fn encrypt_decrypt_large_payload() {
598 let master_key = generate_master_key();
599 // 1MB of data
600 let plaintext: Vec<u8> = (0..1_000_000).map(|i| (i % 256) as u8).collect();
601 let encrypted = encrypt_data(&plaintext, &master_key).unwrap();
602 let decrypted = decrypt_data(&encrypted, &master_key).unwrap();
603 assert_eq!(decrypted, plaintext);
604 }
605
606 // ── Wrong key gives error, not garbage ──
607
608 #[test]
609 fn wrong_key_gives_decryption_error_not_garbage() {
610 let key1 = generate_master_key();
611 let key2 = generate_master_key();
612 let plaintext = b"this should fail cleanly with wrong key";
613
614 let encrypted = encrypt_data(plaintext, &key1).unwrap();
615 let result = decrypt_data(&encrypted, &key2);
616
617 // Must be an error, not a successful decryption to garbage
618 assert!(result.is_err());
619 assert!(
620 matches!(result.unwrap_err(), SyncKitError::DecryptionFailed),
621 "Wrong key must produce DecryptionFailed, not garbage output"
622 );
623 }
624
625 #[test]
626 fn wrong_key_bytes_gives_decryption_error_not_garbage() {
627 let key1 = generate_master_key();
628 let key2 = generate_master_key();
629 let plaintext = b"binary data check";
630
631 let encrypted = encrypt_bytes(plaintext, &key1).unwrap();
632 let result = decrypt_bytes(&encrypted, &key2);
633
634 assert!(result.is_err());
635 assert!(matches!(result.unwrap_err(), SyncKitError::DecryptionFailed));
636 }
637
638 // ── JSON encryption edge cases ──
639
640 #[test]
641 fn json_encrypt_decrypt_null() {
642 let master_key = generate_master_key();
643 let original = serde_json::Value::Null;
644 let encrypted = encrypt_json(&original, &master_key).unwrap();
645 let decrypted = decrypt_json(&encrypted, &master_key).unwrap();
646 assert_eq!(decrypted, original);
647 }
648
649 #[test]
650 fn json_encrypt_decrypt_nested_object() {
651 let master_key = generate_master_key();
652 let original = serde_json::json!({
653 "level1": {
654 "level2": {
655 "level3": [1, 2, 3],
656 "flag": true
657 }
658 },
659 "empty_array": [],
660 "empty_object": {}
661 });
662
663 let encrypted = encrypt_json(&original, &master_key).unwrap();
664 let decrypted = decrypt_json(&encrypted, &master_key).unwrap();
665 assert_eq!(decrypted, original);
666 }
667
668 #[test]
669 fn json_decrypt_with_wrong_key_fails() {
670 let key1 = generate_master_key();
671 let key2 = generate_master_key();
672 let original = serde_json::json!({"secret": "data"});
673
674 let encrypted = encrypt_json(&original, &key1).unwrap();
675 let result = decrypt_json(&encrypted, &key2);
676 assert!(result.is_err());
677 }
678
679 #[test]
680 fn json_decrypt_non_string_value_fails() {
681 let master_key = generate_master_key();
682 let not_a_string = serde_json::json!(42);
683 let result = decrypt_json(&not_a_string, &master_key);
684 assert!(result.is_err());
685 }
686
687 // ── Blob (bytes) edge cases ──
688
689 #[test]
690 fn bytes_zero_byte_blob_roundtrip() {
691 let master_key = generate_master_key();
692 let empty: &[u8] = &[];
693 let encrypted = encrypt_bytes(empty, &master_key).unwrap();
694 assert_eq!(encrypted.len(), ENCRYPTION_OVERHEAD);
695 let decrypted = decrypt_bytes(&encrypted, &master_key).unwrap();
696 assert!(decrypted.is_empty());
697 }
698
699 #[test]
700 fn bytes_boundary_size_blob() {
701 let master_key = generate_master_key();
702 // Test at exactly the nonce size boundary
703 let data = vec![0xAB; NONCE_SIZE];
704 let encrypted = encrypt_bytes(&data, &master_key).unwrap();
705 let decrypted = decrypt_bytes(&encrypted, &master_key).unwrap();
706 assert_eq!(decrypted, data);
707 }
708
709 #[test]
710 fn bytes_1mb_blob_roundtrip() {
711 let master_key = generate_master_key();
712 let data: Vec<u8> = (0..1_048_576).map(|i| (i % 256) as u8).collect();
713 let encrypted = encrypt_bytes(&data, &master_key).unwrap();
714 assert_eq!(encrypted.len(), data.len() + ENCRYPTION_OVERHEAD);
715 let decrypted = decrypt_bytes(&encrypted, &master_key).unwrap();
716 assert_eq!(decrypted, data);
717 }
718
719 // ── Tampered ciphertext detection ──
720
721 #[test]
722 fn tampered_ciphertext_detected() {
723 let master_key = generate_master_key();
724 let plaintext = b"integrity check";
725 let encrypted = encrypt_data(plaintext, &master_key).unwrap();
726
727 let mut blob = B64.decode(&encrypted).unwrap();
728 // Flip a byte in the ciphertext portion (after the nonce)
729 let idx = NONCE_SIZE + 1;
730 blob[idx] ^= 0xFF;
731 let tampered = B64.encode(&blob);
732
733 let result = decrypt_data(&tampered, &master_key);
734 assert!(result.is_err());
735 assert!(matches!(result.unwrap_err(), SyncKitError::DecryptionFailed));
736 }
737
738 #[test]
739 fn tampered_nonce_detected() {
740 let master_key = generate_master_key();
741 let plaintext = b"nonce tamper check";
742 let encrypted = encrypt_data(plaintext, &master_key).unwrap();
743
744 let mut blob = B64.decode(&encrypted).unwrap();
745 // Flip a byte in the nonce
746 blob[0] ^= 0xFF;
747 let tampered = B64.encode(&blob);
748
749 let result = decrypt_data(&tampered, &master_key);
750 assert!(result.is_err());
751 }
752
753 // ── Envelope validation edge cases ──
754
755 #[test]
756 fn invalid_envelope_json_rejected() {
757 let result = unwrap_master_key("not valid json at all", "pass");
758 assert!(result.is_err());
759 assert!(matches!(
760 result.unwrap_err(),
761 SyncKitError::InvalidEnvelope(_)
762 ));
763 }
764
765 #[test]
766 fn envelope_with_wrong_salt_length_rejected() {
767 let envelope = KeyEnvelope {
768 v: ENVELOPE_VERSION,
769 salt: B64.encode([0u8; 16]), // 16 bytes, should be 32
770 nonce: B64.encode([0u8; NONCE_SIZE]),
771 ciphertext: B64.encode([0u8; 48]),
772 };
773 let json = serde_json::to_string(&envelope).unwrap();
774
775 let result = unwrap_master_key(&json, "pass");
776 assert!(result.is_err());
777 assert!(matches!(
778 result.unwrap_err(),
779 SyncKitError::InvalidEnvelope(_)
780 ));
781 }
782
783 #[test]
784 fn envelope_with_wrong_nonce_length_rejected() {
785 let envelope = KeyEnvelope {
786 v: ENVELOPE_VERSION,
787 salt: B64.encode([0u8; 32]),
788 nonce: B64.encode([0u8; 12]), // 12 bytes, should be 24
789 ciphertext: B64.encode([0u8; 48]),
790 };
791 let json = serde_json::to_string(&envelope).unwrap();
792
793 let result = unwrap_master_key(&json, "pass");
794 assert!(result.is_err());
795 assert!(matches!(
796 result.unwrap_err(),
797 SyncKitError::InvalidEnvelope(_)
798 ));
799 }
800
801 // ── verify_password_against_envelope ──
802
803 #[test]
804 fn verify_password_correct() {
805 let master_key = generate_master_key();
806 let envelope = wrap_master_key(&master_key, "correct").unwrap();
807 let result = verify_password_against_envelope(&envelope, "correct");
808 assert!(result.is_ok());
809 assert_eq!(result.unwrap(), master_key);
810 }
811
812 #[test]
813 fn verify_password_wrong() {
814 let master_key = generate_master_key();
815 let envelope = wrap_master_key(&master_key, "correct").unwrap();
816 let result = verify_password_against_envelope(&envelope, "wrong");
817 assert!(result.is_err());
818 assert!(matches!(result.unwrap_err(), SyncKitError::DecryptionFailed));
819 }
820
821 #[test]
822 fn wrap_unwrap_roundtrip() {
823 let master_key = generate_master_key();
824
825 let envelope = wrap_master_key(&master_key, "mypassword").unwrap();
826 let recovered = unwrap_master_key(&envelope, "mypassword").unwrap();
827
828 assert_eq!(master_key, recovered);
829 }
830
831 #[test]
832 fn wrap_uses_random_salt() {
833 let master_key = generate_master_key();
834 let e1 = wrap_master_key(&master_key, "pass").unwrap();
835 let e2 = wrap_master_key(&master_key, "pass").unwrap();
836
837 // Different envelopes (random salt + random nonce)
838 assert_ne!(e1, e2);
839
840 // Both decrypt correctly
841 assert_eq!(unwrap_master_key(&e1, "pass").unwrap(), master_key);
842 assert_eq!(unwrap_master_key(&e2, "pass").unwrap(), master_key);
843 }
844
845 #[test]
846 fn wrong_password_fails_unwrap() {
847 let master_key = generate_master_key();
848
849 let envelope = wrap_master_key(&master_key, "correct").unwrap();
850 let result = unwrap_master_key(&envelope, "wrong");
851
852 assert!(result.is_err());
853 assert!(matches!(result.unwrap_err(), SyncKitError::DecryptionFailed));
854 }
855
856 #[test]
857 fn data_encrypt_decrypt_roundtrip() {
858 let master_key = generate_master_key();
859 let plaintext = b"Hello, world! This is sensitive data.";
860
861 let encrypted = encrypt_data(plaintext, &master_key).unwrap();
862 let decrypted = decrypt_data(&encrypted, &master_key).unwrap();
863
864 assert_eq!(decrypted, plaintext);
865 }
866
867 #[test]
868 fn same_plaintext_different_ciphertext() {
869 let master_key = generate_master_key();
870 let plaintext = b"same data";
871
872 let e1 = encrypt_data(plaintext, &master_key).unwrap();
873 let e2 = encrypt_data(plaintext, &master_key).unwrap();
874
875 assert_ne!(e1, e2, "Random nonces must produce different ciphertext");
876
877 // But both decrypt to the same plaintext
878 assert_eq!(decrypt_data(&e1, &master_key).unwrap(), plaintext);
879 assert_eq!(decrypt_data(&e2, &master_key).unwrap(), plaintext);
880 }
881
882 #[test]
883 fn wrong_key_fails_decrypt() {
884 let key1 = generate_master_key();
885 let key2 = generate_master_key();
886 let plaintext = b"secret";
887
888 let encrypted = encrypt_data(plaintext, &key1).unwrap();
889 let result = decrypt_data(&encrypted, &key2);
890
891 assert!(result.is_err());
892 assert!(matches!(result.unwrap_err(), SyncKitError::DecryptionFailed));
893 }
894
895 #[test]
896 fn envelope_version_check() {
897 let master_key = generate_master_key();
898
899 let envelope_json = wrap_master_key(&master_key, "pass").unwrap();
900
901 // Tamper with version
902 let mut envelope: KeyEnvelope = serde_json::from_str(&envelope_json).unwrap();
903 envelope.v = 99;
904 let tampered = serde_json::to_string(&envelope).unwrap();
905
906 let result = unwrap_master_key(&tampered, "pass");
907 assert!(result.is_err());
908 assert!(matches!(
909 result.unwrap_err(),
910 SyncKitError::InvalidEnvelope(_)
911 ));
912 }
913
914 #[test]
915 fn truncated_ciphertext_rejected() {
916 let master_key = generate_master_key();
917 let encrypted = encrypt_data(b"data", &master_key).unwrap();
918
919 // Decode, truncate, re-encode
920 let mut blob = B64.decode(&encrypted).unwrap();
921 blob.truncate(10); // Way too short
922 let truncated = B64.encode(&blob);
923
924 let result = decrypt_data(&truncated, &master_key);
925 assert!(result.is_err());
926 }
927
928 #[test]
929 fn json_encrypt_decrypt_roundtrip() {
930 let master_key = generate_master_key();
931 let original = serde_json::json!({
932 "title": "Buy milk",
933 "priority": 3,
934 "tags": ["groceries", "urgent"]
935 });
936
937 let encrypted = encrypt_json(&original, &master_key).unwrap();
938 assert!(encrypted.is_string(), "Encrypted JSON should be a string");
939
940 let decrypted = decrypt_json(&encrypted, &master_key).unwrap();
941 assert_eq!(decrypted, original);
942 }
943
944 #[test]
945 fn zeroize_on_drop() {
946 let key = generate_master_key();
947 let guarded = ZeroizeOnDrop(key);
948 // Verify we can use it
949 assert_eq!(guarded.len(), 32);
950 // Drop happens automatically — we can't easily test memory zeroing
951 // but we verify the API works without panic.
952 drop(guarded);
953 }
954
955 // ── encrypt_bytes / decrypt_bytes ──
956
957 #[test]
958 fn bytes_encrypt_decrypt_roundtrip() {
959 let master_key = generate_master_key();
960 let plaintext = b"raw binary blob data \x00\x01\x02\xff";
961
962 let encrypted = encrypt_bytes(plaintext, &master_key).unwrap();
963 let decrypted = decrypt_bytes(&encrypted, &master_key).unwrap();
964
965 assert_eq!(decrypted, plaintext);
966 }
967
968 #[test]
969 fn bytes_encrypt_has_correct_overhead() {
970 let master_key = generate_master_key();
971 let plaintext = vec![0u8; 1000];
972
973 let encrypted = encrypt_bytes(&plaintext, &master_key).unwrap();
974 assert_eq!(encrypted.len(), plaintext.len() + ENCRYPTION_OVERHEAD);
975 }
976
977 #[test]
978 fn bytes_same_plaintext_different_ciphertext() {
979 let master_key = generate_master_key();
980 let plaintext = b"same data";
981
982 let e1 = encrypt_bytes(plaintext, &master_key).unwrap();
983 let e2 = encrypt_bytes(plaintext, &master_key).unwrap();
984
985 assert_ne!(e1, e2);
986 assert_eq!(decrypt_bytes(&e1, &master_key).unwrap(), plaintext);
987 assert_eq!(decrypt_bytes(&e2, &master_key).unwrap(), plaintext);
988 }
989
990 #[test]
991 fn bytes_wrong_key_fails() {
992 let key1 = generate_master_key();
993 let key2 = generate_master_key();
994
995 let encrypted = encrypt_bytes(b"secret", &key1).unwrap();
996 let result = decrypt_bytes(&encrypted, &key2);
997
998 assert!(result.is_err());
999 assert!(matches!(result.unwrap_err(), SyncKitError::DecryptionFailed));
1000 }
1001
1002 #[test]
1003 fn bytes_truncated_rejected() {
1004 let master_key = generate_master_key();
1005 let encrypted = encrypt_bytes(b"data", &master_key).unwrap();
1006
1007 let result = decrypt_bytes(&encrypted[..10], &master_key);
1008 assert!(result.is_err());
1009 }
1010
1011 #[test]
1012 fn bytes_empty_plaintext_roundtrip() {
1013 let master_key = generate_master_key();
1014 let plaintext = b"";
1015
1016 let encrypted = encrypt_bytes(plaintext, &master_key).unwrap();
1017 assert_eq!(encrypted.len(), ENCRYPTION_OVERHEAD);
1018
1019 let decrypted = decrypt_bytes(&encrypted, &master_key).unwrap();
1020 assert_eq!(decrypted, plaintext);
1021 }
1022
1023 #[test]
1024 fn bytes_large_blob_roundtrip() {
1025 let master_key = generate_master_key();
1026 let plaintext: Vec<u8> = (0..100_000).map(|i| (i % 256) as u8).collect();
1027
1028 let encrypted = encrypt_bytes(&plaintext, &master_key).unwrap();
1029 let decrypted = decrypt_bytes(&encrypted, &master_key).unwrap();
1030
1031 assert_eq!(decrypted, plaintext);
1032 }
1033 }
1034