snowv_gcm/
lib.rs

1//! The [SNOW-V-GCM] AEAD cipher.
2//!
3//! [SNOW-V-GCM]: https://tosc.iacr.org/index.php/ToSC/article/view/8356
4
5#![cfg_attr(docsrs, feature(doc_cfg))]
6#![cfg_attr(not(any(test, doctest, feature = "std")), no_std)]
7
8pub mod rust_crypto;
9
10use core::{error, fmt, hash::Hash};
11
12use inout::InOutBuf;
13use polyhash::ghash::GHash;
14use snowv::SnowV;
15use subtle::ConstantTimeEq;
16
17cfg_if::cfg_if! {
18    if #[cfg(feature = "zeroize")] {
19        use zeroize::Zeroizing;
20    } else {
21        struct Zeroizing<T>(T);
22        impl<T> Zeroizing<T> {
23            #[inline(always)]
24            #[allow(clippy::new_ret_no_self)]
25            fn new(t: T) -> T {
26                t
27            }
28        }
29    }
30}
31
32/// The error returned by [`SnowVGcm`].
33#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
34pub struct Error;
35
36impl error::Error for Error {}
37
38impl fmt::Display for Error {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        write!(f, "SNOW-V-GCM error")
41    }
42}
43
44impl From<snowv::Error> for Error {
45    #[inline]
46    fn from(_err: snowv::Error) -> Self {
47        Self
48    }
49}
50
51/// The size in bytes of a SNOW-V-GCM key.
52pub const KEY_SIZE: usize = 32;
53
54/// The size in bytes of a SNOV-V-GCM nonce.
55pub const NONCE_SIZE: usize = 16;
56
57/// The size in bytes of a SNOW-V-GCM authentication tag.
58pub const TAG_SIZE: usize = 16;
59
60/// The maximum allowed size in bytes of a plaintext.
61pub const P_MAX: u64 = (snowv::MAX_BLOCKS * snowv::BLOCK_SIZE as u64) / 8;
62
63/// The maximum allowed size in bytes of a ciphertext.
64pub const C_MAX: u64 = P_MAX + TAG_SIZE as u64;
65
66/// The maximum allowed size in bytes of additional data.
67pub const A_MAX: u64 = u64::MAX / 8;
68
69/// A SNOW-V-GCM key.
70pub type Key = [u8; KEY_SIZE];
71
72/// A SNOW-V-GCM nonce.
73pub type Nonce = [u8; NONCE_SIZE];
74
75/// A SNOW-V-GCM authentication tag.
76pub type Tag = [u8; TAG_SIZE];
77
78/// The SNOW-V-GCM AEAD.
79#[derive(Clone)]
80pub struct SnowVGcm {
81    key: [u8; KEY_SIZE],
82}
83
84impl SnowVGcm {
85    /// Creates an instance of SNOW-V-GCM.
86    #[inline]
87    pub fn new(key: &Key) -> Self {
88        Self { key: *key }
89    }
90
91    /// Encrypts and authenticates `data`, authenticates
92    /// `additional_data`, and writes the ciphertext result to
93    /// `data`.
94    #[inline]
95    pub fn seal(
96        &self,
97        nonce: &Nonce,
98        mut data: InOutBuf<'_, '_, u8>,
99        additional_data: &[u8],
100    ) -> Result<Tag, Error> {
101        if !less_or_equal(data.len(), P_MAX) || !less_or_equal(additional_data.len(), A_MAX) {
102            return Err(Error);
103        }
104
105        let mut cipher = SnowV::new_for_aead(&self.key, nonce);
106
107        let mut ghash_key = Zeroizing::new([0; 16]); // aka H
108        cipher.write_keystream_block(&mut ghash_key)?;
109
110        let mut mask = Zeroizing::new([0; 16]); // aka endPad
111        cipher.write_keystream_block(&mut mask)?;
112
113        cipher.apply_keystream(data.reborrow())?;
114
115        let tag = self.compute_tag(&ghash_key, &mask, data.get_out(), additional_data);
116
117        Ok(tag)
118    }
119
120    /// Decrypts and authenticates `data`, authenticates
121    /// `additional_data`, and writes the plaintext result to
122    /// `data`.
123    #[inline]
124    pub fn open(
125        &self,
126        nonce: &Nonce,
127        data: InOutBuf<'_, '_, u8>,
128        tag: &Tag,
129        additional_data: &[u8],
130    ) -> Result<(), Error> {
131        if !less_or_equal(data.len(), C_MAX) || !less_or_equal(additional_data.len(), A_MAX) {
132            return Err(Error);
133        }
134
135        let mut cipher = SnowV::new_for_aead(&self.key, nonce);
136
137        let mut ghash_key = Zeroizing::new([0; 16]); // aka H
138        cipher.write_keystream_block(&mut ghash_key)?;
139
140        let mut mask = Zeroizing::new([0; 16]); // aka endPad
141        cipher.write_keystream_block(&mut mask)?;
142
143        let expected_tag = self.compute_tag(&ghash_key, &mask, data.get_in(), additional_data);
144        if !bool::from(expected_tag.ct_eq(tag)) {
145            return Err(Error);
146        }
147
148        cipher.apply_keystream(data)?;
149
150        Ok(())
151    }
152
153    #[allow(
154        clippy::arithmetic_side_effects,
155        reason = "We checked the lengths of `ct` and `at`"
156    )]
157    fn compute_tag(&self, ghash_key: &[u8; 16], mask: &[u8; 16], ct: &[u8], ad: &[u8]) -> Tag {
158        let mut ghash = GHash::new_unchecked(ghash_key);
159        ghash.update_padded(ad);
160        ghash.update_padded(ct);
161
162        let ad_bits = (ad.len() as u64) * 8;
163        let ct_bits = (ct.len() as u64) * 8;
164
165        let mut lengths = [0u8; 16];
166        lengths[..8].copy_from_slice(&ad_bits.to_be_bytes());
167        lengths[8..].copy_from_slice(&ct_bits.to_be_bytes());
168        ghash.update_block(&lengths);
169
170        let mut tag: [u8; 16] = ghash.tag().into();
171        for (t, m) in tag.iter_mut().zip(mask) {
172            *t ^= *m;
173        }
174        tag
175    }
176}
177
178impl fmt::Debug for SnowVGcm {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        f.debug_struct("SnowVGcm").finish_non_exhaustive()
181    }
182}
183
184/// Reports whether `x <= y`.
185#[inline(always)]
186fn less_or_equal(x: usize, y: u64) -> bool {
187    u64::try_from(x).is_ok_and(|n| n <= y)
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    macro_rules! hex {
195        ($($s:literal)*) => {
196            &hex_literal::hex!($($s)*)
197        };
198    }
199
200    #[test]
201    fn test_round_trip() {
202        const PLAINTEXT: &[u8] = b"hello, world!";
203
204        let aead = SnowVGcm::new(&[0; KEY_SIZE]);
205        let nonce = [0; NONCE_SIZE];
206        let ad = b"additional data";
207        let mut data = PLAINTEXT.to_vec();
208        let tag = aead.seal(&nonce, data.as_mut_slice().into(), ad).unwrap();
209        aead.open(&nonce, data.as_mut_slice().into(), &tag, ad)
210            .unwrap();
211        assert_eq!(data, PLAINTEXT);
212    }
213
214    #[test]
215    fn test_vectors() {
216        type Test<'a> = (
217            &'a Key,   // key
218            &'a Nonce, // nonce
219            &'a [u8],  // ad
220            &'a [u8],  // pt
221            &'a [u8],  // ct
222            &'a Tag,   // tag
223        );
224        const TESTS: &[Test<'_>] = &[
225            (
226                hex!(
227                    "00000000000000000000000000000000"
228                    "00000000000000000000000000000000"
229                ),
230                hex!("00000000000000000000000000000000"),
231                &[],
232                &[],
233                &[],
234                hex!("029a624cdaa4d46cb9a0ef4046956c9f"),
235            ),
236            (
237                hex!(
238                    "505152535455565758595a5b5c5d5e5f"
239                    "0a1a2a3a4a5a6a7a8a9aaabacadaeafa"
240                ),
241                hex!("0123456789abcdeffedcba9876543210"),
242                &[],
243                &[],
244                &[],
245                hex!("fc7cac574c49feae6150315b9685424c"),
246            ),
247            (
248                hex!(
249                    "00000000000000000000000000000000"
250                    "00000000000000000000000000000000"
251                ),
252                hex!("00000000000000000000000000000000"),
253                hex!("30313233343536373839616263646566"),
254                &[],
255                &[],
256                hex!("5a5aa5fbd635ef1ae129614203e10384"),
257            ),
258            (
259                hex!(
260                    "505152535455565758595a5b5c5d5e5f"
261                    "0a1a2a3a4a5a6a7a8a9aaabacadaeafa"
262                ),
263                hex!("0123456789abcdeffedcba9876543210"),
264                hex!("30313233343536373839616263646566"),
265                &[],
266                &[],
267                hex!("250ec8d77a022c087adf08b65adcbb1a"),
268            ),
269            (
270                hex!(
271                    "505152535455565758595a5b5c5d5e5f"
272                    "0a1a2a3a4a5a6a7a8a9aaabacadaeafa"
273                ),
274                hex!("0123456789abcdeffedcba9876543210"),
275                &[],
276                hex!("30313233343536373839"),
277                hex!("dd7e01b2b424a2ef8250"),
278                hex!("ddfe4e31e7bfe6902331ec5ce319d90d"),
279            ),
280            (
281                hex!(
282                    "505152535455565758595a5b5c5d5e5f"
283                    "0a1a2a3a4a5a6a7a8a9aaabacadaeafa"
284                ),
285                hex!("0123456789abcdeffedcba9876543210"),
286                hex!("41414420746573742076616c756521"),
287                hex!(
288                    "30313233343536373839616263646566"
289                    "20536e6f77562d41454144206d6f6465"
290                    "21"
291                ),
292                hex!(
293                    "dd7e01b2b424a2ef82502707e87a32c1"
294                    "52b0d01818fd7f12243eb5a15659e91b"
295                    "4c"
296                ),
297                hex!("907ea6a5b73a51de747c3e9ad9ee029b"),
298            ),
299        ];
300        for (i, &(key, nonce, ad, pt, ct, tag)) in TESTS.iter().enumerate() {
301            let mut data = pt.to_vec();
302
303            let aead = SnowVGcm::new(key);
304            let got_tag = aead.seal(nonce, data.as_mut_slice().into(), ad).unwrap();
305            assert_eq!(&got_tag, tag, "#{i}: incorrect tag");
306            assert_eq!(data, ct, "#{i}: incorrect ciphertext");
307            aead.open(nonce, data.as_mut_slice().into(), tag, ad)
308                .unwrap();
309            assert_eq!(data, pt, "#{i}: incorrect plaintext");
310
311            #[cfg(feature = "rust-crypto")]
312            {
313                use aead::{AeadInPlace, KeyInit};
314
315                let aead = <SnowVGcm as KeyInit>::new(key.into());
316                let got_tag = aead
317                    .encrypt_in_place_detached(nonce.into(), ad, &mut data)
318                    .unwrap();
319                assert_eq!(got_tag.as_slice(), tag, "#{i}: incorrect tag");
320                assert_eq!(data, ct, "#{i}: incorrect ciphertext");
321                aead.decrypt_in_place_detached(nonce.into(), ad, data.as_mut_slice(), tag.into())
322                    .unwrap();
323                assert_eq!(data, pt, "#{i}: incorrect plaintext");
324            }
325        }
326    }
327}