Skip to main content

eggress_protocol_shadowsocks/
aead.rs

1use aes_gcm::{
2    aead::{consts::U12, Aead},
3    aes::Aes192,
4    Aes128Gcm, Aes256Gcm, AesGcm, KeyInit, Nonce,
5};
6use chacha20poly1305::ChaCha20Poly1305;
7
8use crate::error::ShadowsocksError;
9use crate::method::CipherMethod;
10
11/// Maximum plaintext payload per AEAD chunk in the standard Shadowsocks framing.
12///
13/// pproxy limits each AEAD packet to 16 KiB - 1 bytes even though the length
14/// field itself is a u16.
15pub const MAX_CHUNK_PAYLOAD: usize = 16 * 1024 - 1;
16
17/// Authentication tag size shared by every supported AEAD method.
18const AEAD_TAG_SIZE: usize = 16;
19
20type Aes192Gcm = AesGcm<Aes192, U12>;
21
22pub(crate) enum AeadCipher {
23    Aes128(Aes128Gcm),
24    Aes192(Aes192Gcm),
25    Aes256(Aes256Gcm),
26    ChaCha20(ChaCha20Poly1305),
27}
28
29impl AeadCipher {
30    pub(crate) fn new(method: CipherMethod, key: &[u8]) -> Result<Self, ShadowsocksError> {
31        match method {
32            CipherMethod::Aes128Gcm => Aes128Gcm::new_from_slice(key)
33                .map(Self::Aes128)
34                .map_err(|e| ShadowsocksError::Other(e.to_string())),
35            CipherMethod::Aes192Gcm => Aes192Gcm::new_from_slice(key)
36                .map(Self::Aes192)
37                .map_err(|e| ShadowsocksError::Other(e.to_string())),
38            CipherMethod::Aes256Gcm => Aes256Gcm::new_from_slice(key)
39                .map(Self::Aes256)
40                .map_err(|e| ShadowsocksError::Other(e.to_string())),
41            CipherMethod::ChaCha20IetfPoly1305 => ChaCha20Poly1305::new_from_slice(key)
42                .map(Self::ChaCha20)
43                .map_err(|e| ShadowsocksError::Other(e.to_string())),
44        }
45    }
46
47    pub(crate) fn encrypt(
48        &self,
49        nonce: &[u8],
50        plaintext: &[u8],
51    ) -> Result<Vec<u8>, ShadowsocksError> {
52        let nonce = Nonce::from_slice(nonce);
53        match self {
54            Self::Aes128(cipher) => cipher
55                .encrypt(nonce, plaintext)
56                .map_err(|e| ShadowsocksError::EncryptionFailed(e.to_string())),
57            Self::Aes192(cipher) => cipher
58                .encrypt(nonce, plaintext)
59                .map_err(|e| ShadowsocksError::EncryptionFailed(e.to_string())),
60            Self::Aes256(cipher) => cipher
61                .encrypt(nonce, plaintext)
62                .map_err(|e| ShadowsocksError::EncryptionFailed(e.to_string())),
63            Self::ChaCha20(cipher) => cipher
64                .encrypt(nonce, plaintext)
65                .map_err(|e| ShadowsocksError::EncryptionFailed(e.to_string())),
66        }
67    }
68
69    pub(crate) fn decrypt(
70        &self,
71        nonce: &[u8],
72        ciphertext: &[u8],
73    ) -> Result<Vec<u8>, ShadowsocksError> {
74        let nonce = Nonce::from_slice(nonce);
75        match self {
76            Self::Aes128(cipher) => cipher
77                .decrypt(nonce, ciphertext)
78                .map_err(|e| ShadowsocksError::DecryptionFailed(e.to_string())),
79            Self::Aes192(cipher) => cipher
80                .decrypt(nonce, ciphertext)
81                .map_err(|e| ShadowsocksError::DecryptionFailed(e.to_string())),
82            Self::Aes256(cipher) => cipher
83                .decrypt(nonce, ciphertext)
84                .map_err(|e| ShadowsocksError::DecryptionFailed(e.to_string())),
85            Self::ChaCha20(cipher) => cipher
86                .decrypt(nonce, ciphertext)
87                .map_err(|e| ShadowsocksError::DecryptionFailed(e.to_string())),
88        }
89    }
90}
91
92/// Encrypt plaintext using AEAD with a random salt.
93///
94/// Returns: salt + encrypted(plaintext)
95pub fn encrypt_frame(
96    method: CipherMethod,
97    key: &[u8],
98    plaintext: &[u8],
99) -> Result<Vec<u8>, ShadowsocksError> {
100    use rand::RngCore;
101
102    let salt_size = method.salt_size();
103    let nonce_size = method.nonce_size();
104
105    // Generate random salt
106    let mut salt_buf = [0u8; 32];
107    rand::thread_rng().fill_bytes(&mut salt_buf[..salt_size]);
108    let salt = &salt_buf[..salt_size];
109
110    // Derive subkey
111    let subkey = method.derive_key(key, salt)?;
112
113    // Generate nonce (12 bytes, starts at 0)
114    let nonce_bytes = [0u8; 12];
115
116    // Encrypt
117    let ciphertext = aead_encrypt(method, &subkey, &nonce_bytes[..nonce_size], plaintext)?;
118
119    // Build output: salt + ciphertext
120    let mut output = Vec::with_capacity(salt_size + ciphertext.len());
121    output.extend_from_slice(salt);
122    output.extend_from_slice(&ciphertext);
123
124    Ok(output)
125}
126
127/// Decrypt ciphertext using AEAD. The salt must be prepended to the ciphertext.
128///
129/// Input: salt + encrypted(plaintext)
130/// Returns: plaintext
131pub fn decrypt_frame(
132    method: CipherMethod,
133    key: &[u8],
134    data: &[u8],
135) -> Result<Vec<u8>, ShadowsocksError> {
136    let salt_size = method.salt_size();
137    let nonce_size = method.nonce_size();
138
139    if data.len() < salt_size {
140        return Err(ShadowsocksError::DecryptionFailed(
141            "data too short for salt".into(),
142        ));
143    }
144
145    // Extract salt
146    let salt = &data[..salt_size];
147    let ciphertext = &data[salt_size..];
148
149    // Derive subkey
150    let subkey = method.derive_key(key, salt)?;
151
152    // Generate nonce (12 bytes, starts at 0)
153    let nonce_bytes = [0u8; 12];
154
155    // Decrypt
156    aead_decrypt(method, &subkey, &nonce_bytes[..nonce_size], ciphertext)
157}
158
159/// Encrypt a chunk with AEAD (for streaming).
160///
161/// Input: plaintext
162/// Returns: encrypted(2-byte length + plaintext)
163pub fn encrypt_chunk(
164    method: CipherMethod,
165    key: &[u8],
166    nonce: &[u8],
167    plaintext: &[u8],
168) -> Result<Vec<u8>, ShadowsocksError> {
169    if plaintext.len() > MAX_CHUNK_PAYLOAD {
170        return Err(ShadowsocksError::Other(format!(
171            "plaintext too large for AEAD chunk: {} bytes (max {})",
172            plaintext.len(),
173            MAX_CHUNK_PAYLOAD
174        )));
175    }
176    // Prepend length (2 bytes big-endian)
177    let len = plaintext.len() as u16;
178    let mut payload = Vec::with_capacity(2 + plaintext.len());
179    payload.extend_from_slice(&len.to_be_bytes());
180    payload.extend_from_slice(plaintext);
181
182    aead_encrypt(method, key, nonce, &payload)
183}
184
185/// Decrypt a chunk with AEAD (for streaming).
186///
187/// Input: encrypted(2-byte length + plaintext)
188/// Returns: plaintext
189pub fn decrypt_chunk(
190    method: CipherMethod,
191    key: &[u8],
192    nonce: &[u8],
193    data: &[u8],
194) -> Result<Vec<u8>, ShadowsocksError> {
195    let plaintext = aead_decrypt(method, key, nonce, data)?;
196
197    if plaintext.len() < 2 {
198        return Err(ShadowsocksError::DecryptionFailed("chunk too short".into()));
199    }
200
201    let len = u16::from_be_bytes([plaintext[0], plaintext[1]]) as usize;
202    if plaintext.len() < 2 + len {
203        return Err(ShadowsocksError::DecryptionFailed(
204            "chunk length mismatch".into(),
205        ));
206    }
207
208    Ok(plaintext[2..2 + len].to_vec())
209}
210
211/// Encrypt a standard Shadowsocks AEAD TCP chunk.
212///
213/// Wire format: AEAD(len_u16_be, nonce) + AEAD(payload, nonce+1)
214/// Returns the combined wire bytes (18 + payload.len() + 16 bytes).
215pub fn encrypt_chunk_standard(
216    method: CipherMethod,
217    key: &[u8],
218    nonce: &[u8],
219    payload: &[u8],
220) -> Result<Vec<u8>, ShadowsocksError> {
221    if payload.len() > MAX_CHUNK_PAYLOAD {
222        return Err(ShadowsocksError::Other(format!(
223            "payload too large for standard AEAD chunk: {} bytes (max {})",
224            payload.len(),
225            MAX_CHUNK_PAYLOAD,
226        )));
227    }
228    encrypt_chunk_with(&AeadCipher::new(method, key)?, nonce, payload)
229}
230
231/// Shared cipher implementation behind [`encrypt_chunk_standard`] so callers
232/// processing many chunks can derive the key schedule once per session.
233fn encrypt_chunk_with(
234    cipher: &AeadCipher,
235    nonce: &[u8],
236    payload: &[u8],
237) -> Result<Vec<u8>, ShadowsocksError> {
238    let len_bytes = (payload.len() as u16).to_be_bytes();
239
240    // Encrypt length block with nonce
241    let len_ct = cipher.encrypt(nonce, &len_bytes)?;
242
243    // Compute payload nonce = nonce + 1 (increment last byte with carry)
244    let mut payload_nonce = [0u8; 12];
245    nonce_increment(nonce, &mut payload_nonce)?;
246
247    // Encrypt payload with payload nonce
248    let payload_ct = cipher.encrypt(&payload_nonce, payload)?;
249
250    let mut output = Vec::with_capacity(len_ct.len() + payload_ct.len());
251    output.extend_from_slice(&len_ct);
252    output.extend_from_slice(&payload_ct);
253    Ok(output)
254}
255
256/// Decrypt a standard Shadowsocks AEAD TCP chunk.
257///
258/// Input: the full wire bytes of one chunk (length block + payload block).
259/// Returns: the decrypted plaintext payload.
260pub fn decrypt_chunk_standard(
261    method: CipherMethod,
262    key: &[u8],
263    nonce: &[u8],
264    data: &[u8],
265) -> Result<Vec<u8>, ShadowsocksError> {
266    decrypt_chunk_with(&AeadCipher::new(method, key)?, nonce, data)
267}
268
269/// Shared cipher implementation behind [`decrypt_chunk_standard`] so callers
270/// processing many chunks can derive the key schedule once per session.
271fn decrypt_chunk_with(
272    cipher: &AeadCipher,
273    nonce: &[u8],
274    data: &[u8],
275) -> Result<Vec<u8>, ShadowsocksError> {
276    let len_block_size = 2 + AEAD_TAG_SIZE;
277
278    if data.len() < len_block_size {
279        return Err(ShadowsocksError::DecryptionFailed(
280            "data too short for length block".into(),
281        ));
282    }
283
284    // Decrypt length block (first 18 bytes)
285    let len_plaintext = cipher.decrypt(nonce, &data[..len_block_size])?;
286    if len_plaintext.len() != 2 {
287        return Err(ShadowsocksError::DecryptionFailed(
288            "length block plaintext invalid".into(),
289        ));
290    }
291
292    let payload_len = u16::from_be_bytes([len_plaintext[0], len_plaintext[1]]) as usize;
293    if payload_len > MAX_CHUNK_PAYLOAD {
294        return Err(ShadowsocksError::DecryptionFailed(format!(
295            "payload length {} exceeds maximum {}",
296            payload_len, MAX_CHUNK_PAYLOAD
297        )));
298    }
299    let expected_total = len_block_size + payload_len + AEAD_TAG_SIZE;
300
301    if data.len() < expected_total {
302        return Err(ShadowsocksError::DecryptionFailed(format!(
303            "insufficient data: expected {} bytes, got {}",
304            expected_total,
305            data.len(),
306        )));
307    }
308
309    // Compute payload nonce = nonce + 1
310    let mut payload_nonce = [0u8; 12];
311    nonce_increment(nonce, &mut payload_nonce)?;
312
313    // Decrypt payload block
314    let payload_start = len_block_size;
315    let payload_end = payload_start + payload_len + AEAD_TAG_SIZE;
316    cipher.decrypt(&payload_nonce, &data[payload_start..payload_end])
317}
318
319/// Encrypt a sequence of pproxy AEAD chunks with consecutive nonces.
320pub(crate) fn encrypt_standard_chunks(
321    method: CipherMethod,
322    key: &[u8],
323    nonce: &[u8],
324    plaintext: &[u8],
325) -> Result<Vec<u8>, ShadowsocksError> {
326    let cipher = AeadCipher::new(method, key)?;
327    let chunk_count = plaintext.len().div_ceil(MAX_CHUNK_PAYLOAD);
328    let mut output = Vec::with_capacity(plaintext.len() + chunk_count * (2 + 2 * AEAD_TAG_SIZE));
329    let mut current_nonce = [0u8; 12];
330    current_nonce.copy_from_slice(nonce);
331    for chunk in plaintext.chunks(MAX_CHUNK_PAYLOAD) {
332        output.extend_from_slice(&encrypt_chunk_with(&cipher, &current_nonce, chunk)?);
333        let mut length_nonce = [0u8; 12];
334        nonce_increment(&current_nonce, &mut length_nonce)?;
335        nonce_increment(&length_nonce, &mut current_nonce)?;
336    }
337    Ok(output)
338}
339
340/// Decrypt a sequence of pproxy AEAD chunks with consecutive nonces.
341pub(crate) fn decrypt_standard_chunks(
342    method: CipherMethod,
343    key: &[u8],
344    nonce: &[u8],
345    data: &[u8],
346) -> Result<Vec<u8>, ShadowsocksError> {
347    let cipher = AeadCipher::new(method, key)?;
348    let mut current_nonce = [0u8; 12];
349    current_nonce.copy_from_slice(nonce);
350    let mut offset = 0;
351    let mut plaintext = Vec::with_capacity(data.len());
352
353    while offset < data.len() {
354        if data.len() - offset < 2 + AEAD_TAG_SIZE {
355            return Err(ShadowsocksError::DecryptionFailed(
356                "data too short for pproxy length block".into(),
357            ));
358        }
359        let length_plaintext =
360            cipher.decrypt(&current_nonce, &data[offset..offset + 2 + AEAD_TAG_SIZE])?;
361        let payload_len = u16::from_be_bytes([length_plaintext[0], length_plaintext[1]]) as usize;
362        if payload_len > MAX_CHUNK_PAYLOAD {
363            return Err(ShadowsocksError::DecryptionFailed(format!(
364                "payload length {} exceeds maximum {}",
365                payload_len, MAX_CHUNK_PAYLOAD
366            )));
367        }
368        offset += 2 + AEAD_TAG_SIZE;
369        let payload_wire_len = payload_len + AEAD_TAG_SIZE;
370        if data.len() - offset < payload_wire_len {
371            return Err(ShadowsocksError::DecryptionFailed(
372                "data too short for pproxy payload block".into(),
373            ));
374        }
375        let mut length_nonce = [0u8; 12];
376        nonce_increment(&current_nonce, &mut length_nonce)?;
377        let payload = cipher.decrypt(&length_nonce, &data[offset..offset + payload_wire_len])?;
378        plaintext.extend_from_slice(&payload);
379        offset += payload_wire_len;
380        nonce_increment(&length_nonce, &mut current_nonce)?;
381    }
382
383    Ok(plaintext)
384}
385
386/// Increment a nonce by 1 (little-endian, full 12-byte counter per SIP003).
387fn nonce_increment(nonce: &[u8], result: &mut [u8]) -> Result<(), ShadowsocksError> {
388    if nonce.len() != result.len() {
389        return Err(ShadowsocksError::Other("nonce size mismatch".into()));
390    }
391    result.copy_from_slice(nonce);
392    for byte in result.iter_mut() {
393        let (val, carry) = byte.overflowing_add(1);
394        *byte = val;
395        if !carry {
396            return Ok(());
397        }
398    }
399    Err(ShadowsocksError::Other("nonce increment overflow".into()))
400}
401
402/// Raw AEAD encryption without salt derivation (for address header).
403pub fn aead_encrypt_raw(
404    method: CipherMethod,
405    key: &[u8],
406    nonce: &[u8],
407    plaintext: &[u8],
408) -> Result<Vec<u8>, ShadowsocksError> {
409    AeadCipher::new(method, key)?.encrypt(nonce, plaintext)
410}
411
412/// Raw AEAD decryption without salt derivation (for address header).
413pub fn aead_decrypt_raw(
414    method: CipherMethod,
415    key: &[u8],
416    nonce: &[u8],
417    ciphertext: &[u8],
418) -> Result<Vec<u8>, ShadowsocksError> {
419    AeadCipher::new(method, key)?.decrypt(nonce, ciphertext)
420}
421
422/// Internal AEAD encryption.
423fn aead_encrypt(
424    method: CipherMethod,
425    key: &[u8],
426    nonce: &[u8],
427    plaintext: &[u8],
428) -> Result<Vec<u8>, ShadowsocksError> {
429    aead_encrypt_raw(method, key, nonce, plaintext)
430}
431
432/// Internal AEAD decryption.
433fn aead_decrypt(
434    method: CipherMethod,
435    key: &[u8],
436    nonce: &[u8],
437    ciphertext: &[u8],
438) -> Result<Vec<u8>, ShadowsocksError> {
439    aead_decrypt_raw(method, key, nonce, ciphertext)
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    #[test]
447    fn test_encrypt_decrypt_roundtrip_aes128() {
448        let key = b"0123456789abcdef";
449        let plaintext = b"hello shadowsocks";
450        let encrypted = encrypt_frame(CipherMethod::Aes128Gcm, key, plaintext).unwrap();
451        let decrypted = decrypt_frame(CipherMethod::Aes128Gcm, key, &encrypted).unwrap();
452        assert_eq!(decrypted, plaintext);
453    }
454
455    #[test]
456    fn test_encrypt_decrypt_roundtrip_aes256() {
457        let key = b"0123456789abcdef0123456789abcdef";
458        let plaintext = b"hello shadowsocks";
459        let encrypted = encrypt_frame(CipherMethod::Aes256Gcm, key, plaintext).unwrap();
460        let decrypted = decrypt_frame(CipherMethod::Aes256Gcm, key, &encrypted).unwrap();
461        assert_eq!(decrypted, plaintext);
462    }
463
464    #[test]
465    fn test_encrypt_decrypt_roundtrip_aes192() {
466        let key = b"0123456789abcdef01234567";
467        let plaintext = b"hello shadowsocks";
468        let encrypted = encrypt_frame(CipherMethod::Aes192Gcm, key, plaintext).unwrap();
469        let decrypted = decrypt_frame(CipherMethod::Aes192Gcm, key, &encrypted).unwrap();
470        assert_eq!(decrypted, plaintext);
471    }
472
473    #[test]
474    fn test_encrypt_decrypt_roundtrip_chacha20() {
475        let key = b"0123456789abcdef0123456789abcdef";
476        let plaintext = b"hello shadowsocks";
477        let encrypted = encrypt_frame(CipherMethod::ChaCha20IetfPoly1305, key, plaintext).unwrap();
478        let decrypted = decrypt_frame(CipherMethod::ChaCha20IetfPoly1305, key, &encrypted).unwrap();
479        assert_eq!(decrypted, plaintext);
480    }
481
482    #[test]
483    fn test_tampered_ciphertext_fails() {
484        let key = b"0123456789abcdef";
485        let plaintext = b"hello shadowsocks";
486        let mut encrypted = encrypt_frame(CipherMethod::Aes128Gcm, key, plaintext).unwrap();
487        // Tamper with ciphertext
488        let last = encrypted.len() - 1;
489        encrypted[last] ^= 0xFF;
490        assert!(decrypt_frame(CipherMethod::Aes128Gcm, key, &encrypted).is_err());
491    }
492
493    #[test]
494    fn test_wrong_key_fails() {
495        let key1 = b"0123456789abcdef";
496        let key2 = b"fedcba9876543210";
497        let plaintext = b"hello shadowsocks";
498        let encrypted = encrypt_frame(CipherMethod::Aes128Gcm, key1, plaintext).unwrap();
499        assert!(decrypt_frame(CipherMethod::Aes128Gcm, key2, &encrypted).is_err());
500    }
501
502    #[test]
503    fn test_encrypt_decrypt_chunk_roundtrip() {
504        let key = b"0123456789abcdef";
505        let nonce = [0u8; 12];
506        let plaintext = b"chunk data";
507        let encrypted = encrypt_chunk(CipherMethod::Aes128Gcm, key, &nonce, plaintext).unwrap();
508        let decrypted = decrypt_chunk(CipherMethod::Aes128Gcm, key, &nonce, &encrypted).unwrap();
509        assert_eq!(decrypted, plaintext);
510    }
511
512    #[test]
513    fn test_empty_plaintext() {
514        let key = b"0123456789abcdef";
515        let encrypted = encrypt_frame(CipherMethod::Aes128Gcm, key, b"").unwrap();
516        let decrypted = decrypt_frame(CipherMethod::Aes128Gcm, key, &encrypted).unwrap();
517        assert!(decrypted.is_empty());
518    }
519
520    #[test]
521    fn test_large_plaintext() {
522        let key = b"0123456789abcdef";
523        let plaintext = vec![0xABu8; 65536];
524        let encrypted = encrypt_frame(CipherMethod::Aes128Gcm, key, &plaintext).unwrap();
525        let decrypted = decrypt_frame(CipherMethod::Aes128Gcm, key, &encrypted).unwrap();
526        assert_eq!(decrypted, plaintext);
527    }
528
529    #[test]
530    fn test_different_nonces_produce_different_ciphertext() {
531        let key = b"0123456789abcdef";
532        let plaintext = b"same data";
533        let enc1 = encrypt_frame(CipherMethod::Aes128Gcm, key, plaintext).unwrap();
534        let enc2 = encrypt_frame(CipherMethod::Aes128Gcm, key, plaintext).unwrap();
535        // Different salts should produce different ciphertexts
536        assert_ne!(enc1, enc2);
537    }
538
539    #[test]
540    fn test_encrypt_decrypt_chunk_standard_roundtrip_aes128() {
541        let key = b"0123456789abcdef";
542        let nonce = vec![0u8; 12];
543        let payload = b"hello shadowsocks standard";
544        let wire = encrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, payload).unwrap();
545        // 18 (len block) + 26 (payload) + 16 (tag) = 60
546        assert_eq!(wire.len(), 18 + payload.len() + 16);
547        let decrypted =
548            decrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, &wire).unwrap();
549        assert_eq!(decrypted, payload);
550    }
551
552    #[test]
553    fn test_encrypt_decrypt_chunk_standard_roundtrip_aes256() {
554        let key = b"0123456789abcdef0123456789abcdef";
555        let nonce = vec![0u8; 12];
556        let payload = b"standard chunk test";
557        let wire = encrypt_chunk_standard(CipherMethod::Aes256Gcm, key, &nonce, payload).unwrap();
558        let decrypted =
559            decrypt_chunk_standard(CipherMethod::Aes256Gcm, key, &nonce, &wire).unwrap();
560        assert_eq!(decrypted, payload);
561    }
562
563    #[test]
564    fn test_encrypt_decrypt_chunk_standard_roundtrip_aes192() {
565        let key = b"0123456789abcdef01234567";
566        let nonce = vec![0u8; 12];
567        let payload = b"aes-192 standard chunk test";
568        let wire = encrypt_chunk_standard(CipherMethod::Aes192Gcm, key, &nonce, payload).unwrap();
569        let decrypted =
570            decrypt_chunk_standard(CipherMethod::Aes192Gcm, key, &nonce, &wire).unwrap();
571        assert_eq!(decrypted, payload);
572    }
573
574    #[test]
575    fn test_pproxy_known_answer_vectors() {
576        // Captured from pproxy==2.7.9's AEADCipher implementation with the
577        // fixed password, method-sized salt, zero nonce, and plaintext below.
578        let password = b"phase1-vector-password";
579        let plaintext = b"phase-1-known-answer";
580        let cases = [
581            (
582                CipherMethod::Aes128Gcm,
583                "000102030405060708090a0b0c0d0e0f",
584                "9c9ad70264cd061c56f1e3492fbf1528",
585                "a8e3c5be9630d97db3fdc8024c606d78aba455a8d0d5c7d7c5148cc4e076ee7fc2367b6f",
586            ),
587            (
588                CipherMethod::Aes192Gcm,
589                "000102030405060708090a0b0c0d0e0f1011121314151617",
590                "7b56c2ce83b11c5ca9d5401b08d0cea7bcc15428500352d6",
591                "e4345ba47ae93b62f6a6450a55e96f01803c46b46500f30d290566a1617c5b97f16995d5",
592            ),
593            (
594                CipherMethod::Aes256Gcm,
595                "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
596                "9e3a9a86f6293e1d6ddb2f4285a818bb9ebb9a9fe08498735ba4967425f88fc9",
597                "506e8c5971d7e254b42b6fcc6c7919c11baf60bd7406966bfaf1838bbdcd1d4ade826c22",
598            ),
599            (
600                CipherMethod::ChaCha20IetfPoly1305,
601                "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
602                "9e3a9a86f6293e1d6ddb2f4285a818bb9ebb9a9fe08498735ba4967425f88fc9",
603                "cdaa76308bb8e130e2f4d351494ca1efca87098c744a0c2b8afe27f77f1999dbd13bee5e",
604            ),
605        ];
606
607        for (method, salt_hex, subkey_hex, ciphertext_hex) in cases {
608            let salt = hex_bytes(salt_hex);
609            let expected_subkey = hex_bytes(subkey_hex);
610            let expected_ciphertext = hex_bytes(ciphertext_hex);
611            let subkey = method.derive_key(password, &salt).unwrap();
612            assert_eq!(subkey, expected_subkey, "subkey mismatch for {method}");
613            let ciphertext = aead_encrypt_raw(method, &subkey, &[0u8; 12], plaintext).unwrap();
614            assert_eq!(
615                ciphertext, expected_ciphertext,
616                "ciphertext mismatch for {method}"
617            );
618            assert_eq!(
619                aead_decrypt_raw(method, &subkey, &[0u8; 12], &ciphertext).unwrap(),
620                plaintext
621            );
622        }
623    }
624
625    fn hex_bytes(value: &str) -> Vec<u8> {
626        value
627            .as_bytes()
628            .chunks_exact(2)
629            .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())
630            .collect()
631    }
632
633    #[test]
634    fn test_encrypt_decrypt_chunk_standard_roundtrip_chacha20() {
635        let key = b"0123456789abcdef0123456789abcdef";
636        let nonce = vec![0u8; 12];
637        let payload = b"chacha standard chunk";
638        let wire = encrypt_chunk_standard(CipherMethod::ChaCha20IetfPoly1305, key, &nonce, payload)
639            .unwrap();
640        let decrypted =
641            decrypt_chunk_standard(CipherMethod::ChaCha20IetfPoly1305, key, &nonce, &wire).unwrap();
642        assert_eq!(decrypted, payload);
643    }
644
645    #[test]
646    fn test_encrypt_decrypt_chunk_standard_empty_payload() {
647        let key = b"0123456789abcdef";
648        let nonce = vec![0u8; 12];
649        let payload = b"";
650        let wire = encrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, payload).unwrap();
651        // 18 (len block) + 0 (payload) + 16 (tag) = 34
652        assert_eq!(wire.len(), 34);
653        let decrypted =
654            decrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, &wire).unwrap();
655        assert_eq!(decrypted, payload);
656    }
657
658    #[test]
659    fn test_encrypt_decrypt_chunk_standard_max_payload() {
660        let key = b"0123456789abcdef";
661        let nonce = vec![0u8; 12];
662        let payload = vec![0xABu8; MAX_CHUNK_PAYLOAD];
663        let wire = encrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, &payload).unwrap();
664        assert_eq!(wire.len(), 18 + MAX_CHUNK_PAYLOAD + 16);
665        let decrypted =
666            decrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, &wire).unwrap();
667        assert_eq!(decrypted, payload);
668    }
669
670    #[test]
671    fn test_encrypt_chunk_standard_payload_too_large() {
672        let key = b"0123456789abcdef";
673        let nonce = vec![0u8; 12];
674        let payload = vec![0xABu8; MAX_CHUNK_PAYLOAD + 1];
675        let result = encrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, &payload);
676        assert!(result.is_err());
677    }
678
679    #[test]
680    fn test_decrypt_chunk_standard_tampered_length_block() {
681        let key = b"0123456789abcdef";
682        let nonce = vec![0u8; 12];
683        let payload = b"secret data";
684        let mut wire =
685            encrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, payload).unwrap();
686        // Tamper with the length block (first byte)
687        wire[0] ^= 0xFF;
688        let result = decrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, &wire);
689        assert!(result.is_err());
690    }
691
692    #[test]
693    fn test_decrypt_chunk_standard_tampered_payload_block() {
694        let key = b"0123456789abcdef";
695        let nonce = vec![0u8; 12];
696        let payload = b"secret data";
697        let mut wire =
698            encrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, payload).unwrap();
699        // Tamper with the payload block (byte after length block)
700        wire[18] ^= 0xFF;
701        let result = decrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, &wire);
702        assert!(result.is_err());
703    }
704
705    #[test]
706    fn test_decrypt_chunk_standard_too_short() {
707        let key = b"0123456789abcdef";
708        let nonce = vec![0u8; 12];
709        let data = vec![0u8; 10]; // too short for length block (need 18)
710        let result = decrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, &data);
711        assert!(result.is_err());
712    }
713
714    #[test]
715    fn test_decrypt_chunk_standard_wrong_key() {
716        let key1 = b"0123456789abcdef";
717        let key2 = b"fedcba9876543210";
718        let nonce = vec![0u8; 12];
719        let payload = b"secret data";
720        let wire = encrypt_chunk_standard(CipherMethod::Aes128Gcm, key1, &nonce, payload).unwrap();
721        let result = decrypt_chunk_standard(CipherMethod::Aes128Gcm, key2, &nonce, &wire);
722        assert!(result.is_err());
723    }
724
725    #[test]
726    fn test_decrypt_chunk_standard_wrong_nonce() {
727        let key = b"0123456789abcdef";
728        let nonce1 = vec![0u8; 12];
729        let mut nonce2 = vec![0u8; 12];
730        nonce2[0] = 1; // different nonce (little-endian)
731        let payload = b"secret data";
732        let wire = encrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce1, payload).unwrap();
733        let result = decrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce2, &wire);
734        assert!(result.is_err());
735    }
736
737    #[test]
738    fn test_encrypt_decrypt_chunk_standard_sequential_nonces() {
739        let key = b"0123456789abcdef";
740        let mut nonce = vec![0u8; 12];
741        nonce[0] = 1; // start at nonce 1 (little-endian)
742
743        let payload1 = b"first chunk";
744        let wire1 = encrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, payload1).unwrap();
745        let dec1 = decrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, &wire1).unwrap();
746        assert_eq!(dec1, payload1);
747
748        // Advance nonce by 2 (one for length, one for payload)
749        nonce[0] = 3;
750        let payload2 = b"second chunk";
751        let wire2 = encrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, payload2).unwrap();
752        let dec2 = decrypt_chunk_standard(CipherMethod::Aes128Gcm, key, &nonce, &wire2).unwrap();
753        assert_eq!(dec2, payload2);
754    }
755
756    #[test]
757    fn test_nonce_increment_basic() {
758        let nonce = vec![0u8; 12];
759        let mut result = vec![0u8; nonce.len()];
760        nonce_increment(&nonce, &mut result).unwrap();
761        assert_eq!(result[0], 1);
762    }
763
764    #[test]
765    fn test_nonce_increment_carry() {
766        let mut nonce = vec![0u8; 12];
767        nonce[0] = 0xFF;
768        let mut result = vec![0u8; nonce.len()];
769        nonce_increment(&nonce, &mut result).unwrap();
770        assert_eq!(result[0], 0);
771        assert_eq!(result[1], 1);
772    }
773
774    #[test]
775    fn test_nonce_increment_overflow() {
776        let nonce = vec![0xFFu8; 12];
777        let mut result = vec![0u8; nonce.len()];
778        let result = nonce_increment(&nonce, &mut result);
779        assert!(result.is_err());
780    }
781}