Skip to main content

encryption/encryptor/
salsa20.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3//! Salsa20/20 流密码(D. J. Bernstein)+ HMAC-SHA256(encrypt-then-MAC);
4//! 载荷:`v1 | nonce(8) | MAC(32) | 密文(与密钥流异或)`。密钥 32 字节。
5//! 与国密 ZUC(`src/guomi/zuc.rs`)同一套帧与 etm 约定。
6
7use cbc::cipher::{KeyIvInit, StreamCipher};
8use rand_core::{OsRng, RngCore};
9use salsa20::Salsa20;
10
11use crate::contract::{Identified, SymmetricCipher};
12use crate::error::Result;
13use crate::internal::etm;
14use crate::key::Key;
15
16const LABEL: &str = "Salsa20";
17/// Salsa20 的 nonce 是 64 位(8 字节);即名称里的 20 之外的变体参数都在密钥里。
18const NONCE_LEN: usize = 8;
19
20/// Salsa20/20 加密器。
21#[derive(Debug)]
22pub struct Salsa20Encryptor {
23    key: Key<32>,
24}
25
26impl Salsa20Encryptor {
27    pub const IDENTIFIER: &'static str = "salsa20";
28
29    pub fn new(key: [u8; 32]) -> Self {
30        Self { key: Key::new(key) }
31    }
32
33    pub fn from_slice(key: &[u8]) -> Result<Self> {
34        Ok(Self {
35            key: Key::from_slice(key, LABEL)?,
36        })
37    }
38}
39
40impl Identified for Salsa20Encryptor {
41    fn identifier(&self) -> &str {
42        Self::IDENTIFIER
43    }
44}
45
46impl SymmetricCipher for Salsa20Encryptor {
47    fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
48        let mut nonce = [0u8; NONCE_LEN];
49        OsRng.fill_bytes(&mut nonce);
50
51        let mut buffer = plaintext.to_vec();
52        Salsa20::new_from_slices(self.key.as_bytes(), &nonce)
53            .expect("key and nonce lengths are fixed")
54            .apply_keystream(&mut buffer);
55
56        Ok(etm::pack(&nonce, &buffer, self.key.as_bytes()))
57    }
58
59    fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
60        let (nonce, body) =
61            etm::unpack_and_verify(ciphertext, NONCE_LEN, self.key.as_bytes(), LABEL)?;
62
63        let mut buffer = body.to_vec();
64        Salsa20::new_from_slices(self.key.as_bytes(), nonce)
65            .expect("key and nonce lengths were verified")
66            .apply_keystream(&mut buffer);
67        Ok(buffer)
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::error::Error;
75
76    /// 十六进制字符串 → 字节:KAT 向量照抄 eSTREAM 验证向量文本,避免手工转写。
77    fn unhex(s: &str) -> Vec<u8> {
78        (0..s.len() / 2)
79            .map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap())
80            .collect()
81    }
82
83    /// Salsa20/20(256 位密钥、8 字节 nonce)官方向量:直接调用裸密钥流 ——
84    /// 封装层的往返测试抓不出「密钥流整体错位」。第一条 = eSTREAM/ECRYPT 验证
85    /// 向量(256-bit Set 1 vector 0,只覆盖计数块 0);第二条 = Bernstein
86    /// 《The Salsa20 family of stream ciphers》§4.1 规范向量(256 字节跨 4 个
87    /// 计数块,覆盖计数器的字节序与跨块递增)。两条均与 salsa20 crate 自带
88    /// 测试一致,第二条数值另经独立 Python 复算核对。
89    #[test]
90    fn bare_keystream_matches_estream_vectors() {
91        let key = unhex("8000000000000000000000000000000000000000000000000000000000000000");
92        let nonce = unhex("0000000000000000");
93        let mut keystream = [0u8; 64];
94        Salsa20::new_from_slices(&key, &nonce)
95            .unwrap()
96            .apply_keystream(&mut keystream);
97        assert_eq!(
98            keystream.as_slice(),
99            unhex(concat!(
100                "e3be8fdd8beca2e3ea8ef9475b29a6e7",
101                "003951e1097a5c38d23b7a5fad9f6844",
102                "b22c97559e2723c7cbbd3fe4fc8d9a07",
103                "44652a83e72a9c461876af4d7ef1a117"
104            ))
105            .as_slice()
106        );
107
108        let key = unhex("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20");
109        let nonce = unhex("0301040105090206");
110        let mut keystream = [0u8; 256];
111        Salsa20::new_from_slices(&key, &nonce)
112            .unwrap()
113            .apply_keystream(&mut keystream);
114        assert_eq!(
115            keystream.as_slice(),
116            unhex(concat!(
117                "6ebcbdbf76fccc64ab05542bee8a67cb",
118                "c28fa2e141fbefbb3a2f9b221909c8d7",
119                "d4295258cb539770dd24d7ac3443769f",
120                "fa27a50e60644264dc8b6b612683372e",
121                "085d0a12bf240b189ce2b78289862b56",
122                "fdc9fcffc33bef9325a2e81b98fb3fb9",
123                "aa04cf434615ceffeb985c1cb08d8440",
124                "e90b1d56ddeaea16d9e15affff1f698c",
125                "483c7a466af1fe062574adfd2b06a62b",
126                "4d98440719ea776385c470349a7ed696",
127                "9583463ed5d26b8fefccb205da0f5bfa",
128                "98c77812fe756b09eacc282aa42f4baf",
129                "a79633189046e2b20f35b3e0e54aa3b9",
130                "29e23c0f47dc7bcd4f928b2a9764be7d",
131                "4b8a50f980a50b35ad8087375e0c556e",
132                "cbe6a7161e8653ce9391e1e6710ed4f1"
133            ))
134            .as_slice()
135        );
136    }
137
138    #[test]
139    fn roundtrip_including_multi_block_and_empty() {
140        let encryptor = Salsa20Encryptor::new([3u8; 32]);
141        // 200 字节跨 4 个计数块(每块 64 字节)。
142        for plaintext in [b"".as_slice(), b"a", b"abc", b"abcd", &[0xffu8; 200]] {
143            let blob = encryptor.encrypt(plaintext).unwrap();
144            assert_eq!(encryptor.decrypt(&blob).unwrap(), plaintext);
145        }
146    }
147
148    #[test]
149    fn ciphertext_differs_from_plaintext_and_tamper_fails() {
150        let encryptor = Salsa20Encryptor::new([3u8; 32]);
151        let blob = encryptor.encrypt(b"0123456789").unwrap();
152        // 帧头 2 + nonce 8 + MAC 32 = 42。
153        assert_ne!(&blob[42..], b"0123456789");
154
155        let mut tampered = blob.clone();
156        let last = tampered.len() - 1;
157        tampered[last] ^= 0x01;
158        assert_eq!(
159            encryptor.decrypt(&tampered).unwrap_err(),
160            Error::MacVerificationFailed { label: LABEL }
161        );
162    }
163
164    #[test]
165    fn wrong_key_fails_mac() {
166        let blob = Salsa20Encryptor::new([3u8; 32]).encrypt(b"secret").unwrap();
167        let other = Salsa20Encryptor::new([4u8; 32]);
168        assert_eq!(
169            other.decrypt(&blob).unwrap_err(),
170            Error::MacVerificationFailed { label: LABEL }
171        );
172    }
173
174    #[test]
175    fn nonce_is_fresh_per_encryption() {
176        let encryptor = Salsa20Encryptor::new([3u8; 32]);
177        let first = encryptor.encrypt(b"same plaintext").unwrap();
178        let second = encryptor.encrypt(b"same plaintext").unwrap();
179        assert_ne!(first, second);
180    }
181
182    #[test]
183    fn invalid_prefix_and_too_short_are_rejected() {
184        let encryptor = Salsa20Encryptor::new([3u8; 32]);
185        assert_eq!(
186            encryptor.decrypt(b"v9 not a blob").unwrap_err(),
187            Error::InvalidPrefix { label: LABEL }
188        );
189        assert_eq!(
190            encryptor.decrypt(b"v1 short").unwrap_err(),
191            Error::TooShort { label: LABEL }
192        );
193    }
194
195    #[test]
196    fn key_length_is_checked() {
197        assert_eq!(
198            Salsa20Encryptor::from_slice(&[0u8; 16]).unwrap_err(),
199            Error::InvalidKeyLength {
200                label: LABEL,
201                expected: 32,
202                got: 16
203            }
204        );
205    }
206}