use aead::{AeadCore, AeadInPlace, Buffer, KeyInit, Result};
use chacha20::{
cipher::{
consts::{U10, U16},
generic_array::{
functional::FunctionalSequence,
sequence::{Concat, Split},
GenericArray,
},
KeyIvInit, StreamCipher, StreamCipherSeek,
},
hchacha, ChaCha20,
};
use core::{mem, slice};
use poly1305::{
universal_hash::{
crypto_common::{BlockSizeUser, IvSizeUser},
UniversalHash,
},
Poly1305,
};
use rand_core::{CryptoRng, RngCore};
use subtle::ConstantTimeEq;
use super::nonce::Nonce;
use crate::{header::Header, Key, Tag};
const MAC_BLOCK_SIZE: usize = 16;
const TAG_BLOCK_SIZE: usize = 16 * 4;
pub struct Stream {
key: chacha20::Key,
nonce: Nonce,
counter: u32,
}
impl Stream {
pub const ABYTES: usize = MAC_BLOCK_SIZE + 1;
pub fn init(key: &Key, header: Header) -> Self {
let (hchacha20_nonce, nonce) = header.split();
Self {
key: hchacha::<U10>(key.as_array(), &hchacha20_nonce),
nonce,
counter: 1,
}
}
fn get_cipher_and_mac(
&self,
cipher_nonce: &aead::Nonce<Self>,
) -> Result<(ChaCha20, poly1305::Key)> {
let mut cipher = ChaCha20::new(&self.key, cipher_nonce);
let mut mac_key = poly1305::Key::from([0u8; 32]);
cipher
.try_apply_keystream(mac_key.as_mut())
.map_err(|_| aead::Error)?;
Ok((cipher, mac_key))
}
fn update_state(&mut self, mac_output: GenericArray<u8, U16>, tag: Tag) -> Result<()> {
let (reduced_mac, _) = mac_output.split();
self.nonce = self.nonce.zip(reduced_mac, |l, r| l ^ r);
let incremented_counter = self.counter.checked_add(1);
match incremented_counter {
Some(increment) if tag != Tag::Rekey => self.counter = increment,
_ => {
self.counter = incremented_counter.unwrap_or(0); self.rekey()?;
}
}
Ok(())
}
fn compute_mac(
mac_key: &poly1305::Key,
associated_data: &[u8],
tag_block: [u8; TAG_BLOCK_SIZE],
ciphertext: &[u8],
) -> poly1305::Tag {
let mut mac = Poly1305::new(mac_key);
let mac_padding_error_size = ((0x10 - 64 + ciphertext.len() as i64) & 0xf) as usize;
let mut size_block = [0u8; MAC_BLOCK_SIZE];
size_block[..8].copy_from_slice(&(associated_data.len() as u64).to_le_bytes());
size_block[8..]
.copy_from_slice(&(TAG_BLOCK_SIZE as u64 + ciphertext.len() as u64).to_le_bytes());
mac.update_padded(associated_data); mac.update_padded(&tag_block);
let chunks = ciphertext.chunks_exact(MAC_BLOCK_SIZE);
let remaining_ciphertext = chunks.remainder();
for block in chunks {
mac.update(slice::from_ref(poly1305::Block::from_slice(block)))
}
let mut last_blocks = [0u8; 3 * MAC_BLOCK_SIZE];
last_blocks[..remaining_ciphertext.len()].clone_from_slice(remaining_ciphertext);
let size_block_offset = remaining_ciphertext.len() + mac_padding_error_size;
last_blocks[size_block_offset..size_block_offset + MAC_BLOCK_SIZE]
.copy_from_slice(&size_block);
mac.compute_unpadded(&last_blocks[..size_block_offset + MAC_BLOCK_SIZE])
}
fn rekey(&mut self) -> Result<()> {
let cipher_nonce = self.get_cipher_nonce();
let mut cipher = ChaCha20::new(&self.key, &cipher_nonce);
cipher
.try_apply_keystream(&mut self.key)
.map_err(|_| aead::Error)?;
cipher
.try_apply_keystream(&mut self.nonce)
.map_err(|_| aead::Error)?;
self.counter = 1;
Ok(())
}
fn get_cipher_nonce(&self) -> chacha20::Nonce {
GenericArray::from(self.counter.to_le_bytes()).concat(self.nonce)
}
}
pub struct PushStream(Stream);
impl PushStream {
pub fn init(csprng: impl RngCore + CryptoRng, key: &Key) -> (Header, Self) {
let header = Header::generate(csprng);
let stream = Stream::init(key, header);
(header, Self(stream))
}
pub fn push(
&mut self,
buffer: &mut impl aead::Buffer,
associated_data: &[u8],
tag: Tag,
) -> Result<()> {
let cipher_nonce = self.0.get_cipher_nonce();
buffer
.extend_from_slice(&[tag as u8])
.map_err(|_| aead::Error)?;
buffer.as_mut().rotate_right(1);
let mac =
self.0
.encrypt_in_place_detached(&cipher_nonce, associated_data, buffer.as_mut())?;
buffer.extend_from_slice(&mac).map_err(|_| aead::Error)?;
self.0.update_state(mac, tag)?;
Ok(())
}
}
pub struct PullStream(Stream);
impl PullStream {
pub fn init(header: Header, key: &Key) -> Self {
Self(Stream::init(key, header))
}
pub fn pull(&mut self, buffer: &mut impl Buffer, associated_data: &[u8]) -> Result<Tag> {
let cipher_nonce = self.0.get_cipher_nonce();
if buffer.len() < MAC_BLOCK_SIZE + 1 {
return Err(aead::Error);
}
let mac = *poly1305::Block::from_slice(&buffer.as_ref()[buffer.len() - MAC_BLOCK_SIZE..]);
buffer.truncate(buffer.len() - MAC_BLOCK_SIZE);
self.0
.decrypt_in_place_detached(&cipher_nonce, associated_data, buffer.as_mut(), &mac)?;
let tag = Tag::try_from(buffer.as_ref()[0]).map_err(|_| aead::Error)?;
buffer.as_mut().rotate_left(1);
buffer.truncate(buffer.len() - 1);
self.0.update_state(mac, tag)?;
Ok(tag)
}
}
impl AeadCore for Stream {
type NonceSize = <ChaCha20 as IvSizeUser>::IvSize;
type TagSize = <Poly1305 as BlockSizeUser>::BlockSize;
type CiphertextOverhead = <Poly1305 as BlockSizeUser>::BlockSize;
}
impl AeadInPlace for Stream {
fn encrypt_in_place_detached(
&self,
nonce: &aead::Nonce<Self>,
associated_data: &[u8],
buffer: &mut [u8],
) -> Result<aead::Tag<Self>> {
if buffer.is_empty() {
return Err(aead::Error);
}
let tag = Tag::try_from(buffer[0]).map_err(|_| aead::Error)?;
if buffer.len() as u64 > u64::MAX - (TAG_BLOCK_SIZE as u64) {
return Err(aead::Error);
}
let (mut cipher, mac_key) = self.get_cipher_and_mac(nonce)?;
let mut tag_block = [0u8; TAG_BLOCK_SIZE];
tag_block[0] = tag as u8;
cipher.try_seek(64).map_err(|_| aead::Error)?;
cipher
.try_apply_keystream(&mut tag_block)
.map_err(|_| aead::Error)?;
buffer[0] = tag_block[0];
cipher.try_seek(128).map_err(|_| aead::Error)?;
cipher
.try_apply_keystream(&mut buffer[1..]) .map_err(|_| aead::Error)?;
let mac_output = Stream::compute_mac(&mac_key, associated_data, tag_block, &buffer[1..]);
Ok(mac_output)
}
fn decrypt_in_place_detached(
&self,
nonce: &aead::Nonce<Self>,
associated_data: &[u8],
buffer: &mut [u8],
mac: &aead::Tag<Self>,
) -> Result<()> {
let (mut cipher, mac_key) = self.get_cipher_and_mac(nonce)?;
if buffer.is_empty() {
return Err(aead::Error);
}
let mut tag_block = [0u8; TAG_BLOCK_SIZE];
tag_block[0] = buffer[0];
cipher.try_seek(64).map_err(|_| aead::Error)?;
cipher
.try_apply_keystream(&mut tag_block)
.map_err(|_| aead::Error)?;
mem::swap(&mut tag_block[0], &mut buffer[0]);
let mac_output = Stream::compute_mac(&mac_key, associated_data, tag_block, &buffer[1..]);
if bool::from(!mac_output.ct_eq(mac)) {
return Err(aead::Error);
}
cipher.try_seek(128).map_err(|_| aead::Error)?;
cipher
.try_apply_keystream(&mut buffer[1..])
.map_err(|_| aead::Error)?;
Ok(())
}
}