Skip to main content

openipc_core/
crypto.rs

1use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
2use chacha20::ChaCha20Legacy;
3use poly1305::universal_hash::KeyInit;
4use poly1305::Poly1305;
5use subtle::ConstantTimeEq;
6
7/// Key length used by the legacy WFB ChaCha20-Poly1305 construction.
8pub const CHACHA20_POLY1305_KEY_LEN: usize = 32;
9/// Nonce length used by the legacy WFB ChaCha20-Poly1305 construction.
10pub const CHACHA20_POLY1305_NONCE_LEN: usize = 8;
11/// Authentication tag length used by the legacy WFB construction.
12pub const CHACHA20_POLY1305_TAG_LEN: usize = 16;
13
14/// Error from legacy WFB ChaCha20-Poly1305 helpers.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum CryptoError {
17    /// Key slice was not 32 bytes.
18    InvalidKey,
19    /// Nonce slice was not 8 bytes.
20    InvalidNonce,
21    /// Ciphertext did not include a full authentication tag.
22    CiphertextTooShort,
23    /// Authentication tag did not verify.
24    AuthenticationFailed,
25}
26
27impl std::fmt::Display for CryptoError {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            Self::InvalidKey => write!(f, "invalid key"),
31            Self::InvalidNonce => write!(f, "invalid nonce"),
32            Self::CiphertextTooShort => write!(f, "ciphertext is shorter than authentication tag"),
33            Self::AuthenticationFailed => write!(f, "authentication failed"),
34        }
35    }
36}
37
38impl std::error::Error for CryptoError {}
39
40/// Verify and decrypt the legacy WFB ChaCha20-Poly1305 payload shape.
41pub fn decrypt_chacha20poly1305_legacy(
42    key: &[u8],
43    nonce: &[u8],
44    aad: &[u8],
45    ciphertext_and_tag: &[u8],
46) -> Result<Vec<u8>, CryptoError> {
47    let mut plaintext = Vec::with_capacity(ciphertext_and_tag.len());
48    decrypt_chacha20poly1305_legacy_into(key, nonce, aad, ciphertext_and_tag, &mut plaintext)?;
49    Ok(plaintext)
50}
51
52/// Verify and decrypt into a reusable caller-owned buffer.
53///
54/// The legacy WFB packet format is fixed-size and is received at high packet
55/// rates. Reusing this buffer avoids one allocation for every authenticated
56/// data fragment while preserving the allocating convenience API above.
57pub fn decrypt_chacha20poly1305_legacy_into(
58    key: &[u8],
59    nonce: &[u8],
60    aad: &[u8],
61    ciphertext_and_tag: &[u8],
62    plaintext: &mut Vec<u8>,
63) -> Result<(), CryptoError> {
64    if ciphertext_and_tag.len() < CHACHA20_POLY1305_TAG_LEN {
65        return Err(CryptoError::CiphertextTooShort);
66    }
67    let ciphertext_len = ciphertext_and_tag.len() - CHACHA20_POLY1305_TAG_LEN;
68    let ciphertext = &ciphertext_and_tag[..ciphertext_len];
69    let expected_tag = &ciphertext_and_tag[ciphertext_len..];
70    let tag = chacha20poly1305_legacy_tag(key, nonce, aad, ciphertext)?;
71    if tag.ct_eq(expected_tag).unwrap_u8() != 1 {
72        return Err(CryptoError::AuthenticationFailed);
73    }
74
75    plaintext.clear();
76    plaintext.extend_from_slice(ciphertext);
77    apply_chacha20_legacy_keystream(key, nonce, 64, plaintext.as_mut_slice())?;
78    Ok(())
79}
80
81/// Encrypt and authenticate using the legacy WFB ChaCha20-Poly1305 shape.
82pub fn encrypt_chacha20poly1305_legacy(
83    key: &[u8],
84    nonce: &[u8],
85    aad: &[u8],
86    plaintext: &[u8],
87) -> Result<Vec<u8>, CryptoError> {
88    let mut ciphertext = plaintext.to_vec();
89    apply_chacha20_legacy_keystream(key, nonce, 64, &mut ciphertext)?;
90    let tag = chacha20poly1305_legacy_tag(key, nonce, aad, &ciphertext)?;
91    ciphertext.extend_from_slice(&tag);
92    Ok(ciphertext)
93}
94
95fn chacha20poly1305_legacy_tag(
96    key: &[u8],
97    nonce: &[u8],
98    aad: &[u8],
99    ciphertext: &[u8],
100) -> Result<[u8; CHACHA20_POLY1305_TAG_LEN], CryptoError> {
101    let mut block0 = [0; 64];
102    apply_chacha20_legacy_keystream(key, nonce, 0, &mut block0)?;
103    let poly_key: [u8; 32] = block0[..32]
104        .try_into()
105        .map_err(|_| CryptoError::InvalidKey)?;
106
107    let mut mac_data = Vec::with_capacity(
108        aad.len() + pad16_len(aad.len()) + ciphertext.len() + pad16_len(ciphertext.len()) + 16,
109    );
110    mac_data.extend_from_slice(aad);
111    mac_data.extend(std::iter::repeat_n(0, pad16_len(aad.len())));
112    mac_data.extend_from_slice(ciphertext);
113    mac_data.extend(std::iter::repeat_n(0, pad16_len(ciphertext.len())));
114    mac_data.extend_from_slice(&(aad.len() as u64).to_le_bytes());
115    mac_data.extend_from_slice(&(ciphertext.len() as u64).to_le_bytes());
116
117    let tag = Poly1305::new((&poly_key).into()).compute_unpadded(&mac_data);
118    let mut out = [0; CHACHA20_POLY1305_TAG_LEN];
119    out.copy_from_slice(&tag);
120    Ok(out)
121}
122
123fn apply_chacha20_legacy_keystream(
124    key: &[u8],
125    nonce: &[u8],
126    offset: u32,
127    data: &mut [u8],
128) -> Result<(), CryptoError> {
129    let key: [u8; CHACHA20_POLY1305_KEY_LEN] =
130        key.try_into().map_err(|_| CryptoError::InvalidKey)?;
131    let nonce: [u8; CHACHA20_POLY1305_NONCE_LEN] =
132        nonce.try_into().map_err(|_| CryptoError::InvalidNonce)?;
133    let mut cipher = ChaCha20Legacy::new(&key.into(), &nonce.into());
134    cipher.seek(offset);
135    cipher.apply_keystream(data);
136    Ok(())
137}
138
139const fn pad16_len(len: usize) -> usize {
140    (16 - (len % 16)) % 16
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn legacy_aead_roundtrips_with_aad() {
149        let key = [7; 32];
150        let nonce = [9; 8];
151        let aad = b"wfb block header";
152        let plaintext = b"rtp payload bytes";
153
154        let encrypted = encrypt_chacha20poly1305_legacy(&key, &nonce, aad, plaintext).unwrap();
155        assert_ne!(&encrypted[..plaintext.len()], plaintext);
156        let decrypted = decrypt_chacha20poly1305_legacy(&key, &nonce, aad, &encrypted).unwrap();
157        assert_eq!(decrypted, plaintext);
158    }
159
160    #[test]
161    fn legacy_aead_rejects_modified_tag() {
162        let key = [7; 32];
163        let nonce = [9; 8];
164        let mut encrypted =
165            encrypt_chacha20poly1305_legacy(&key, &nonce, b"aad", b"payload").unwrap();
166        let last = encrypted.len() - 1;
167        encrypted[last] ^= 0x80;
168
169        assert_eq!(
170            decrypt_chacha20poly1305_legacy(&key, &nonce, b"aad", &encrypted).unwrap_err(),
171            CryptoError::AuthenticationFailed
172        );
173    }
174}