Skip to main content

encryption/encryptor/
chacha20poly1305_ietf.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3//! IETF ChaCha20-Poly1305(RFC 8439,96-bit nonce);载荷:`v1 | Nonce(12) | Tag(16) | 密文`。
4//!
5//! 与 `sodium-xchacha20` 是同一族原语:密钥、tag、底层 crate 都相同,区别只在
6//! nonce 宽度(12 字节 vs 24 字节)。nonce 每次随机生成,12 字节在单密钥下
7//! 可安全处理海量消息(RFC 8439 §4 的生日界远大于本库的密钥生命周期)。
8
9use chacha20poly1305::aead::AeadInPlace;
10use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Nonce, Tag};
11use rand_core::{OsRng, RngCore};
12
13use crate::contract::{Identified, SymmetricCipher};
14use crate::error::{Error, Result};
15use crate::key::Key;
16
17const LABEL: &str = "ChaCha20-Poly1305-IETF";
18const PREFIX: &[u8] = b"v1";
19const NONCE_LEN: usize = 12;
20const TAG_LEN: usize = 16;
21
22/// IETF ChaCha20-Poly1305 加密器。
23///
24/// 密钥实例在构造时就建立,加密与解密都复用它;密钥字节随 `ChaChaPoly1305`
25/// 自身的 `Drop` 清零。
26pub struct ChaCha20Poly1305IetfEncryptor {
27    cipher: ChaCha20Poly1305,
28}
29
30impl ChaCha20Poly1305IetfEncryptor {
31    pub const IDENTIFIER: &'static str = "chacha20-poly1305-ietf";
32
33    pub fn new(key: [u8; 32]) -> Self {
34        Self {
35            cipher: ChaCha20Poly1305::new_from_slice(&key).expect("key length is fixed at 32"),
36        }
37    }
38
39    pub fn from_slice(key: &[u8]) -> Result<Self> {
40        // 借 Key<N> 做长度校验;临时副本随其 Drop 清零。
41        let key = Key::<32>::from_slice(key, LABEL)?;
42        Ok(Self {
43            cipher: ChaCha20Poly1305::new_from_slice(key.as_bytes())
44                .expect("key length is fixed at 32"),
45        })
46    }
47
48    /// 显式 nonce 的加密,返回完整载荷(`v1 | Nonce | Tag | 密文`)。
49    ///
50    /// 生产路径只经由 [`SymmetricCipher::encrypt`] 调用(`aad` 恒为 `b""`);
51    /// `aad` 参数是为了让测试能对 RFC 8439 §2.8.2 的官方向量(带 12 字节 AAD)
52    /// 做逐字节比对,而不必自造期望值。
53    pub(crate) fn encrypt_with_nonce(
54        &self,
55        nonce: &[u8; NONCE_LEN],
56        aad: &[u8],
57        plaintext: &[u8],
58    ) -> Result<Vec<u8>> {
59        let mut buffer = plaintext.to_vec();
60        let tag = self
61            .cipher
62            .encrypt_in_place_detached(Nonce::from_slice(nonce), aad, &mut buffer)
63            .map_err(|_| Error::EncryptionFailed { label: LABEL })?;
64
65        let mut blob = Vec::with_capacity(PREFIX.len() + NONCE_LEN + TAG_LEN + buffer.len());
66        blob.extend_from_slice(PREFIX);
67        blob.extend_from_slice(nonce);
68        blob.extend_from_slice(&tag);
69        blob.extend_from_slice(&buffer);
70        Ok(blob)
71    }
72
73    /// 显式 AAD 的解密(`aad` 的用途同上,生产路径恒为 `b""`)。
74    pub(crate) fn decrypt_with_aad(&self, ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
75        let body = ciphertext
76            .strip_prefix(PREFIX)
77            .ok_or(Error::InvalidPrefix { label: LABEL })?;
78        if body.len() < NONCE_LEN + TAG_LEN {
79            return Err(Error::TooShort { label: LABEL });
80        }
81
82        let (nonce, rest) = body.split_at(NONCE_LEN);
83        let (tag, ct) = rest.split_at(TAG_LEN);
84
85        let mut buffer = ct.to_vec();
86        self.cipher
87            .decrypt_in_place_detached(
88                Nonce::from_slice(nonce),
89                aad,
90                &mut buffer,
91                Tag::from_slice(tag),
92            )
93            .map_err(|_| Error::DecryptionFailed { label: LABEL })?;
94        Ok(buffer)
95    }
96}
97
98/// 只打印标识符——密钥材质不出现在 `Debug` 里
99/// (`ChaCha20Poly1305` 未实现 `Debug`,此类型无法 derive)。
100impl std::fmt::Debug for ChaCha20Poly1305IetfEncryptor {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        f.debug_struct("ChaCha20Poly1305IetfEncryptor")
103            .field("identifier", &Self::IDENTIFIER)
104            .finish_non_exhaustive()
105    }
106}
107
108impl Identified for ChaCha20Poly1305IetfEncryptor {
109    fn identifier(&self) -> &str {
110        Self::IDENTIFIER
111    }
112}
113
114impl SymmetricCipher for ChaCha20Poly1305IetfEncryptor {
115    fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
116        let mut nonce = [0u8; NONCE_LEN];
117        OsRng.fill_bytes(&mut nonce);
118        self.encrypt_with_nonce(&nonce, b"", plaintext)
119    }
120
121    fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
122        self.decrypt_with_aad(ciphertext, b"")
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::internal::hex_decode;
130
131    // 官方向量:RFC 8439 §2.8.2(Example and Test Vector for
132    // AEAD_CHACHA20_POLY1305),逐字节誊自 RFC 文本
133    // www.rfc-editor.org/rfc/rfc8439.txt(2026-10 取用)。
134    //
135    // nonce 注意:RFC 把 12 字节 nonce 拆成「IV(8 字节)4041424344454647」与
136    // 「32-bit fixed-common part(sender id=7)07000000」两段分别列出,但实际
137    // nonce 是 fixed-common part 在前 —— RFC 自己的 ChaCha 状态字给出
138    // 00000007 43424140 47464544(§2.8.2 "Setup for generating Poly1305
139    // one-time key"),RustCrypto 的 crate 测试与独立实现复算也一致。
140    // 明文 114 字节、无填充(Poly1305 的 16 字节对齐只在 MAC 输入侧,不进密文)。
141    const KEY: &str = "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f";
142    const NONCE: &str = "070000004041424344454647";
143    const AAD: &str = "50515253c0c1c2c3c4c5c6c7";
144    const PLAINTEXT: &str = "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
145    const CIPHERTEXT: &str = "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6\
146                              3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b\
147                              3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7\
148                              bc3ff4def08e4b7a9de576d26586cec64b6116";
149    const TAG: &str = "1ae10b594f09e26a7e902ecbd0600691";
150
151    #[test]
152    fn kat_rfc_8439_2_8_2_is_byte_for_byte() {
153        let nonce: [u8; NONCE_LEN] = hex_decode(NONCE).unwrap().try_into().unwrap();
154        let aad = hex_decode(AAD).unwrap();
155        let ct = hex_decode(CIPHERTEXT).unwrap();
156        let tag = hex_decode(TAG).unwrap();
157        // 先验证「无填充」:官方密文长度 == 官方明文长度。
158        assert_eq!(ct.len(), PLAINTEXT.len());
159
160        let encryptor =
161            ChaCha20Poly1305IetfEncryptor::new(hex_decode(KEY).unwrap().try_into().unwrap());
162
163        let mut expected = PREFIX.to_vec();
164        expected.extend_from_slice(&nonce);
165        expected.extend_from_slice(&tag);
166        expected.extend_from_slice(&ct);
167        assert_eq!(
168            encryptor.encrypt_with_nonce(&nonce, &aad, PLAINTEXT.as_bytes()),
169            Ok(expected)
170        );
171
172        // 反方向:官方密文帧解出官方明文。
173        let blob = encryptor
174            .encrypt_with_nonce(&nonce, &aad, PLAINTEXT.as_bytes())
175            .unwrap();
176        assert_eq!(
177            encryptor.decrypt_with_aad(&blob, &aad).unwrap(),
178            PLAINTEXT.as_bytes()
179        );
180    }
181
182    #[test]
183    fn roundtrip_and_tamper_detection() {
184        let encryptor = ChaCha20Poly1305IetfEncryptor::new([7u8; 32]);
185        let blob = encryptor.encrypt(b"data").unwrap();
186        assert_eq!(blob.len(), 2 + NONCE_LEN + TAG_LEN + 4);
187        assert_eq!(encryptor.decrypt(&blob).unwrap(), b"data");
188
189        let mut tampered = blob.clone();
190        let last = tampered.len() - 1;
191        tampered[last] ^= 0x01;
192        assert_eq!(
193            encryptor.decrypt(&tampered).unwrap_err(),
194            Error::DecryptionFailed { label: LABEL }
195        );
196    }
197
198    #[test]
199    fn every_encrypt_uses_a_fresh_nonce() {
200        let encryptor = ChaCha20Poly1305IetfEncryptor::new([7u8; 32]);
201        let first = encryptor.encrypt(b"same").unwrap();
202        let second = encryptor.encrypt(b"same").unwrap();
203        assert_ne!(first, second);
204        assert_ne!(first[2..2 + NONCE_LEN], second[2..2 + NONCE_LEN]);
205    }
206
207    #[test]
208    fn wrong_key_and_bad_prefix_fail() {
209        let blob = ChaCha20Poly1305IetfEncryptor::new([7u8; 32])
210            .encrypt(b"x")
211            .unwrap();
212        let other = ChaCha20Poly1305IetfEncryptor::new([8u8; 32]);
213        assert_eq!(
214            other.decrypt(&blob).unwrap_err(),
215            Error::DecryptionFailed { label: LABEL }
216        );
217        assert_eq!(
218            other.decrypt(b"v9....").unwrap_err(),
219            Error::InvalidPrefix { label: LABEL }
220        );
221        assert_eq!(
222            other.decrypt(b"v1short").unwrap_err(),
223            Error::TooShort { label: LABEL }
224        );
225    }
226
227    #[test]
228    fn key_length_is_checked() {
229        assert_eq!(
230            ChaCha20Poly1305IetfEncryptor::from_slice(&[0u8; 16]).unwrap_err(),
231            Error::InvalidKeyLength {
232                label: LABEL,
233                expected: 32,
234                got: 16
235            }
236        );
237    }
238}