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    if ciphertext_and_tag.len() < CHACHA20_POLY1305_TAG_LEN {
48        return Err(CryptoError::CiphertextTooShort);
49    }
50    let ciphertext_len = ciphertext_and_tag.len() - CHACHA20_POLY1305_TAG_LEN;
51    let ciphertext = &ciphertext_and_tag[..ciphertext_len];
52    let expected_tag = &ciphertext_and_tag[ciphertext_len..];
53    let tag = chacha20poly1305_legacy_tag(key, nonce, aad, ciphertext)?;
54    if tag.ct_eq(expected_tag).unwrap_u8() != 1 {
55        return Err(CryptoError::AuthenticationFailed);
56    }
57
58    let mut plaintext = ciphertext.to_vec();
59    apply_chacha20_legacy_keystream(key, nonce, 64, &mut plaintext)?;
60    Ok(plaintext)
61}
62
63/// Encrypt and authenticate using the legacy WFB ChaCha20-Poly1305 shape.
64pub fn encrypt_chacha20poly1305_legacy(
65    key: &[u8],
66    nonce: &[u8],
67    aad: &[u8],
68    plaintext: &[u8],
69) -> Result<Vec<u8>, CryptoError> {
70    let mut ciphertext = plaintext.to_vec();
71    apply_chacha20_legacy_keystream(key, nonce, 64, &mut ciphertext)?;
72    let tag = chacha20poly1305_legacy_tag(key, nonce, aad, &ciphertext)?;
73    ciphertext.extend_from_slice(&tag);
74    Ok(ciphertext)
75}
76
77fn chacha20poly1305_legacy_tag(
78    key: &[u8],
79    nonce: &[u8],
80    aad: &[u8],
81    ciphertext: &[u8],
82) -> Result<[u8; CHACHA20_POLY1305_TAG_LEN], CryptoError> {
83    let mut block0 = [0; 64];
84    apply_chacha20_legacy_keystream(key, nonce, 0, &mut block0)?;
85    let poly_key: [u8; 32] = block0[..32]
86        .try_into()
87        .map_err(|_| CryptoError::InvalidKey)?;
88
89    let mut mac_data = Vec::with_capacity(
90        aad.len() + pad16_len(aad.len()) + ciphertext.len() + pad16_len(ciphertext.len()) + 16,
91    );
92    mac_data.extend_from_slice(aad);
93    mac_data.extend(std::iter::repeat_n(0, pad16_len(aad.len())));
94    mac_data.extend_from_slice(ciphertext);
95    mac_data.extend(std::iter::repeat_n(0, pad16_len(ciphertext.len())));
96    mac_data.extend_from_slice(&(aad.len() as u64).to_le_bytes());
97    mac_data.extend_from_slice(&(ciphertext.len() as u64).to_le_bytes());
98
99    let tag = Poly1305::new((&poly_key).into()).compute_unpadded(&mac_data);
100    let mut out = [0; CHACHA20_POLY1305_TAG_LEN];
101    out.copy_from_slice(&tag);
102    Ok(out)
103}
104
105fn apply_chacha20_legacy_keystream(
106    key: &[u8],
107    nonce: &[u8],
108    offset: u32,
109    data: &mut [u8],
110) -> Result<(), CryptoError> {
111    let key: [u8; CHACHA20_POLY1305_KEY_LEN] =
112        key.try_into().map_err(|_| CryptoError::InvalidKey)?;
113    let nonce: [u8; CHACHA20_POLY1305_NONCE_LEN] =
114        nonce.try_into().map_err(|_| CryptoError::InvalidNonce)?;
115    let mut cipher = ChaCha20Legacy::new(&key.into(), &nonce.into());
116    cipher.seek(offset);
117    cipher.apply_keystream(data);
118    Ok(())
119}
120
121const fn pad16_len(len: usize) -> usize {
122    (16 - (len % 16)) % 16
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn legacy_aead_roundtrips_with_aad() {
131        let key = [7; 32];
132        let nonce = [9; 8];
133        let aad = b"wfb block header";
134        let plaintext = b"rtp payload bytes";
135
136        let encrypted = encrypt_chacha20poly1305_legacy(&key, &nonce, aad, plaintext).unwrap();
137        assert_ne!(&encrypted[..plaintext.len()], plaintext);
138        let decrypted = decrypt_chacha20poly1305_legacy(&key, &nonce, aad, &encrypted).unwrap();
139        assert_eq!(decrypted, plaintext);
140    }
141
142    #[test]
143    fn legacy_aead_rejects_modified_tag() {
144        let key = [7; 32];
145        let nonce = [9; 8];
146        let mut encrypted =
147            encrypt_chacha20poly1305_legacy(&key, &nonce, b"aad", b"payload").unwrap();
148        let last = encrypted.len() - 1;
149        encrypted[last] ^= 0x80;
150
151        assert_eq!(
152            decrypt_chacha20poly1305_legacy(&key, &nonce, b"aad", &encrypted).unwrap_err(),
153            CryptoError::AuthenticationFailed
154        );
155    }
156}