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
7pub const CHACHA20_POLY1305_KEY_LEN: usize = 32;
8pub const CHACHA20_POLY1305_NONCE_LEN: usize = 8;
9pub const CHACHA20_POLY1305_TAG_LEN: usize = 16;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum CryptoError {
13    InvalidKey,
14    InvalidNonce,
15    CiphertextTooShort,
16    AuthenticationFailed,
17}
18
19impl std::fmt::Display for CryptoError {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            Self::InvalidKey => write!(f, "invalid key"),
23            Self::InvalidNonce => write!(f, "invalid nonce"),
24            Self::CiphertextTooShort => write!(f, "ciphertext is shorter than authentication tag"),
25            Self::AuthenticationFailed => write!(f, "authentication failed"),
26        }
27    }
28}
29
30impl std::error::Error for CryptoError {}
31
32pub fn decrypt_chacha20poly1305_legacy(
33    key: &[u8],
34    nonce: &[u8],
35    aad: &[u8],
36    ciphertext_and_tag: &[u8],
37) -> Result<Vec<u8>, CryptoError> {
38    if ciphertext_and_tag.len() < CHACHA20_POLY1305_TAG_LEN {
39        return Err(CryptoError::CiphertextTooShort);
40    }
41    let ciphertext_len = ciphertext_and_tag.len() - CHACHA20_POLY1305_TAG_LEN;
42    let ciphertext = &ciphertext_and_tag[..ciphertext_len];
43    let expected_tag = &ciphertext_and_tag[ciphertext_len..];
44    let tag = chacha20poly1305_legacy_tag(key, nonce, aad, ciphertext)?;
45    if tag.ct_eq(expected_tag).unwrap_u8() != 1 {
46        return Err(CryptoError::AuthenticationFailed);
47    }
48
49    let mut plaintext = ciphertext.to_vec();
50    apply_chacha20_legacy_keystream(key, nonce, 64, &mut plaintext)?;
51    Ok(plaintext)
52}
53
54pub fn encrypt_chacha20poly1305_legacy(
55    key: &[u8],
56    nonce: &[u8],
57    aad: &[u8],
58    plaintext: &[u8],
59) -> Result<Vec<u8>, CryptoError> {
60    let mut ciphertext = plaintext.to_vec();
61    apply_chacha20_legacy_keystream(key, nonce, 64, &mut ciphertext)?;
62    let tag = chacha20poly1305_legacy_tag(key, nonce, aad, &ciphertext)?;
63    ciphertext.extend_from_slice(&tag);
64    Ok(ciphertext)
65}
66
67fn chacha20poly1305_legacy_tag(
68    key: &[u8],
69    nonce: &[u8],
70    aad: &[u8],
71    ciphertext: &[u8],
72) -> Result<[u8; CHACHA20_POLY1305_TAG_LEN], CryptoError> {
73    let mut block0 = [0; 64];
74    apply_chacha20_legacy_keystream(key, nonce, 0, &mut block0)?;
75    let poly_key: [u8; 32] = block0[..32]
76        .try_into()
77        .map_err(|_| CryptoError::InvalidKey)?;
78
79    let mut mac_data = Vec::with_capacity(
80        aad.len() + pad16_len(aad.len()) + ciphertext.len() + pad16_len(ciphertext.len()) + 16,
81    );
82    mac_data.extend_from_slice(aad);
83    mac_data.extend(std::iter::repeat_n(0, pad16_len(aad.len())));
84    mac_data.extend_from_slice(ciphertext);
85    mac_data.extend(std::iter::repeat_n(0, pad16_len(ciphertext.len())));
86    mac_data.extend_from_slice(&(aad.len() as u64).to_le_bytes());
87    mac_data.extend_from_slice(&(ciphertext.len() as u64).to_le_bytes());
88
89    let tag = Poly1305::new((&poly_key).into()).compute_unpadded(&mac_data);
90    let mut out = [0; CHACHA20_POLY1305_TAG_LEN];
91    out.copy_from_slice(&tag);
92    Ok(out)
93}
94
95fn apply_chacha20_legacy_keystream(
96    key: &[u8],
97    nonce: &[u8],
98    offset: u32,
99    data: &mut [u8],
100) -> Result<(), CryptoError> {
101    let key: [u8; CHACHA20_POLY1305_KEY_LEN] =
102        key.try_into().map_err(|_| CryptoError::InvalidKey)?;
103    let nonce: [u8; CHACHA20_POLY1305_NONCE_LEN] =
104        nonce.try_into().map_err(|_| CryptoError::InvalidNonce)?;
105    let mut cipher = ChaCha20Legacy::new(&key.into(), &nonce.into());
106    cipher.seek(offset);
107    cipher.apply_keystream(data);
108    Ok(())
109}
110
111const fn pad16_len(len: usize) -> usize {
112    (16 - (len % 16)) % 16
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn legacy_aead_roundtrips_with_aad() {
121        let key = [7; 32];
122        let nonce = [9; 8];
123        let aad = b"wfb block header";
124        let plaintext = b"rtp payload bytes";
125
126        let encrypted = encrypt_chacha20poly1305_legacy(&key, &nonce, aad, plaintext).unwrap();
127        assert_ne!(&encrypted[..plaintext.len()], plaintext);
128        let decrypted = decrypt_chacha20poly1305_legacy(&key, &nonce, aad, &encrypted).unwrap();
129        assert_eq!(decrypted, plaintext);
130    }
131
132    #[test]
133    fn legacy_aead_rejects_modified_tag() {
134        let key = [7; 32];
135        let nonce = [9; 8];
136        let mut encrypted =
137            encrypt_chacha20poly1305_legacy(&key, &nonce, b"aad", b"payload").unwrap();
138        let last = encrypted.len() - 1;
139        encrypted[last] ^= 0x80;
140
141        assert_eq!(
142            decrypt_chacha20poly1305_legacy(&key, &nonce, b"aad", &encrypted).unwrap_err(),
143            CryptoError::AuthenticationFailed
144        );
145    }
146}