Skip to main content

chacha20/
xchacha.rs

1//! XChaCha is an extended nonce variant of ChaCha
2
3use crate::{
4    CONSTANTS, ChaChaCore, Key, R8, R12, R20, Rounds, STATE_WORDS, quarter_round, variants::Ietf,
5};
6use cipher::{
7    BlockSizeUser, IvSizeUser, KeyIvInit, KeySizeUser, StreamCipherClosure, StreamCipherCore,
8    StreamCipherCoreWrapper, StreamCipherSeekCore,
9    array::Array,
10    consts::{U4, U16, U24, U32, U64},
11};
12
13#[cfg(feature = "zeroize")]
14use zeroize::ZeroizeOnDrop;
15
16/// Nonce type used by XChaCha variants.
17pub type XNonce = Array<u8, U24>;
18
19/// XChaCha is a ChaCha20 variant with an extended 192-bit (24-byte) nonce.
20///
21/// The construction is an adaptation of the same techniques used by
22/// XChaCha as described in the paper "Extending the Salsa20 Nonce",
23/// applied to the 96-bit nonce variant of ChaCha20, and derive a
24/// separate subkey/nonce for each extended nonce:
25///
26/// <https://cr.yp.to/snuffle/xsalsa-20081128.pdf>
27///
28/// No authoritative specification exists for XChaCha20, however the
29/// construction has "rough consensus and running code" in the form of
30/// several interoperable libraries and protocols (e.g. libsodium, WireGuard)
31/// and is documented in an (expired) IETF draft:
32///
33/// <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha>
34pub type XChaCha20 = StreamCipherCoreWrapper<XChaChaCore<R20>>;
35/// XChaCha12 stream cipher (reduced-round variant of [`XChaCha20`] with 12 rounds)
36pub type XChaCha12 = StreamCipherCoreWrapper<XChaChaCore<R12>>;
37/// XChaCha8 stream cipher (reduced-round variant of [`XChaCha20`] with 8 rounds)
38pub type XChaCha8 = StreamCipherCoreWrapper<XChaChaCore<R8>>;
39
40/// The XChaCha core function.
41#[derive(Debug)]
42pub struct XChaChaCore<R: Rounds>(ChaChaCore<R, Ietf>);
43
44impl<R: Rounds> KeySizeUser for XChaChaCore<R> {
45    type KeySize = U32;
46}
47
48impl<R: Rounds> IvSizeUser for XChaChaCore<R> {
49    type IvSize = U24;
50}
51
52impl<R: Rounds> BlockSizeUser for XChaChaCore<R> {
53    type BlockSize = U64;
54}
55
56impl<R: Rounds> KeyIvInit for XChaChaCore<R> {
57    fn new(key: &Key, iv: &XNonce) -> Self {
58        #[allow(clippy::unwrap_used)]
59        let subkey = hchacha::<R>(key, iv[..16].as_ref().try_into().unwrap());
60
61        let mut nonce = [0u8; 12];
62        // first 4 bytes are 0, last 8 bytes are last 8 from the iv
63        // according to draft-arciszewski-xchacha-03
64        nonce[4..].copy_from_slice(&iv[16..]);
65        Self(ChaChaCore::<R, Ietf>::new_internal(subkey.as_ref(), &nonce))
66    }
67}
68
69impl<R: Rounds> StreamCipherCore for XChaChaCore<R> {
70    #[inline(always)]
71    fn remaining_blocks(&self) -> Option<usize> {
72        self.0.remaining_blocks()
73    }
74
75    #[inline(always)]
76    fn process_with_backend(&mut self, f: impl StreamCipherClosure<BlockSize = Self::BlockSize>) {
77        self.0.process_with_backend(f);
78    }
79}
80
81impl<R: Rounds> StreamCipherSeekCore for XChaChaCore<R> {
82    type Counter = u32;
83
84    #[inline(always)]
85    fn get_block_pos(&self) -> u32 {
86        self.0.get_block_pos()
87    }
88
89    #[inline(always)]
90    fn set_block_pos(&mut self, pos: u32) {
91        self.0.set_block_pos(pos);
92    }
93}
94
95#[cfg(feature = "zeroize")]
96impl<R: Rounds> ZeroizeOnDrop for XChaChaCore<R> {}
97
98/// The HChaCha function: adapts the ChaCha core function in the same
99/// manner that HSalsa adapts the Salsa function.
100///
101/// HChaCha takes 512-bits of input:
102///
103/// - Constants: `u32` x 4
104/// - Key: `u32` x 8
105/// - Nonce: `u32` x 4
106///
107/// It produces 256-bits of output suitable for use as a ChaCha key
108///
109/// For more information on HSalsa on which HChaCha is based, see:
110///
111/// <http://cr.yp.to/snuffle/xsalsa-20110204.pdf>
112#[must_use]
113pub fn hchacha<R: Rounds>(key: &Key, input: &Array<u8, U16>) -> Array<u8, U32> {
114    let mut state = [0u32; STATE_WORDS];
115    state[..4].copy_from_slice(&CONSTANTS);
116
117    // TODO(tarcieri): use `[T]::as_chunks` when MSRV 1.88
118    let key_chunks = Array::<u8, U4>::slice_as_chunks(key).0;
119    for (v, chunk) in state[4..12].iter_mut().zip(key_chunks) {
120        *v = u32::from_le_bytes(chunk.0);
121    }
122    let input_chunks = Array::<u8, U4>::slice_as_chunks(input).0;
123    for (v, chunk) in state[12..16].iter_mut().zip(input_chunks) {
124        *v = u32::from_le_bytes(chunk.0);
125    }
126
127    // R rounds consisting of R/2 column rounds and R/2 diagonal rounds
128    for _ in 0..R::COUNT {
129        // column rounds
130        quarter_round(0, 4, 8, 12, &mut state);
131        quarter_round(1, 5, 9, 13, &mut state);
132        quarter_round(2, 6, 10, 14, &mut state);
133        quarter_round(3, 7, 11, 15, &mut state);
134
135        // diagonal rounds
136        quarter_round(0, 5, 10, 15, &mut state);
137        quarter_round(1, 6, 11, 12, &mut state);
138        quarter_round(2, 7, 8, 13, &mut state);
139        quarter_round(3, 4, 9, 14, &mut state);
140    }
141
142    let mut output = Array::default();
143
144    for (chunk, val) in output[..16].chunks_exact_mut(4).zip(&state[..4]) {
145        chunk.copy_from_slice(&val.to_le_bytes());
146    }
147
148    for (chunk, val) in output[16..].chunks_exact_mut(4).zip(&state[12..]) {
149        chunk.copy_from_slice(&val.to_le_bytes());
150    }
151
152    output
153}
154
155#[cfg(test)]
156mod hchacha20_tests {
157    use super::*;
158    use hex_literal::hex;
159
160    /// Test vectors from:
161    /// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#section-2.2.1
162    #[test]
163    fn test_vector() {
164        const KEY: [u8; 32] = hex!(
165            "000102030405060708090a0b0c0d0e0f"
166            "101112131415161718191a1b1c1d1e1f"
167        );
168
169        const INPUT: [u8; 16] = hex!("000000090000004a0000000031415927");
170
171        const OUTPUT: [u8; 32] = hex!(
172            "82413b4227b27bfed30e42508a877d73"
173            "a0f9e4d58a74a853c12ec41326d3ecdc"
174        );
175
176        let actual = hchacha::<R20>(&KEY.into(), &INPUT.into());
177        assert_eq!(actual.as_slice(), &OUTPUT);
178    }
179}