use crate::{Result, SodiumError};
use libc;
use std::convert::TryFrom;
pub const KEYBYTES: usize = libsodium_sys::crypto_secretstream_xchacha20poly1305_KEYBYTES as usize;
pub const HEADERBYTES: usize =
libsodium_sys::crypto_secretstream_xchacha20poly1305_HEADERBYTES as usize;
pub const ABYTES: usize = libsodium_sys::crypto_secretstream_xchacha20poly1305_ABYTES as usize;
pub const TAG_MESSAGE: u8 = libsodium_sys::crypto_secretstream_xchacha20poly1305_TAG_MESSAGE as u8;
pub const TAG_FINAL: u8 = libsodium_sys::crypto_secretstream_xchacha20poly1305_TAG_FINAL as u8;
pub const TAG_PUSH: u8 = libsodium_sys::crypto_secretstream_xchacha20poly1305_TAG_PUSH as u8;
pub const TAG_REKEY: u8 = libsodium_sys::crypto_secretstream_xchacha20poly1305_TAG_REKEY as u8;
#[derive(Debug, Clone, Eq, PartialEq, zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
pub struct Key([u8; KEYBYTES]);
impl Key {
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() != KEYBYTES {
return Err(SodiumError::InvalidInput(format!(
"key must be exactly {KEYBYTES} bytes"
)));
}
let mut key = [0u8; KEYBYTES];
key.copy_from_slice(bytes);
Ok(Key(key))
}
pub fn generate() -> Self {
let mut key = [0u8; KEYBYTES];
unsafe {
libsodium_sys::crypto_secretstream_xchacha20poly1305_keygen(key.as_mut_ptr());
}
Key(key)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
impl AsRef<[u8]> for Key {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl TryFrom<&[u8]> for Key {
type Error = SodiumError;
fn try_from(slice: &[u8]) -> std::result::Result<Self, Self::Error> {
Self::from_bytes(slice)
}
}
impl From<[u8; KEYBYTES]> for Key {
fn from(bytes: [u8; KEYBYTES]) -> Self {
Self(bytes)
}
}
impl From<Key> for [u8; KEYBYTES] {
fn from(key: Key) -> [u8; KEYBYTES] {
key.0
}
}
pub struct State {
state: Box<libsodium_sys::crypto_secretstream_xchacha20poly1305_state>,
}
impl Drop for State {
fn drop(&mut self) {
unsafe {
libsodium_sys::sodium_memzero(
self.state.as_mut() as *mut _ as *mut libc::c_void,
std::mem::size_of::<libsodium_sys::crypto_secretstream_xchacha20poly1305_state>(),
);
}
}
}
impl State {
fn new() -> Self {
State {
state: Box::new(unsafe { std::mem::zeroed() }),
}
}
}
impl AsMut<libsodium_sys::crypto_secretstream_xchacha20poly1305_state> for State {
fn as_mut(&mut self) -> &mut libsodium_sys::crypto_secretstream_xchacha20poly1305_state {
self.state.as_mut()
}
}
pub struct PushState(State);
impl PushState {
pub fn init_push(key: &Key) -> Result<(PushState, [u8; HEADERBYTES])> {
let mut state = State::new();
let mut header = [0u8; HEADERBYTES];
unsafe {
let result = libsodium_sys::crypto_secretstream_xchacha20poly1305_init_push(
state.as_mut(),
header.as_mut_ptr(),
key.as_bytes().as_ptr(),
);
if result != 0 {
return Err(SodiumError::OperationError(
"failed to initialize push state".into(),
));
}
}
Ok((PushState(state), header))
}
pub fn push(&mut self, m: &[u8], ad: Option<&[u8]>, tag: u8) -> Result<Vec<u8>> {
let c_len = m.len() + ABYTES;
let mut c = vec![0u8; c_len];
let mut c_len: u64 = 0;
unsafe {
let result = libsodium_sys::crypto_secretstream_xchacha20poly1305_push(
self.0.state.as_mut(),
c.as_mut_ptr(),
&mut c_len,
m.as_ptr(),
m.len() as u64,
ad.map_or(std::ptr::null(), |ad| ad.as_ptr()),
ad.map_or(0, |ad| ad.len()) as u64,
tag,
);
if result != 0 {
return Err(SodiumError::OperationError("encryption failed".into()));
}
}
c.truncate(c_len as usize);
Ok(c)
}
pub fn rekey(&mut self) -> Result<()> {
unsafe {
libsodium_sys::crypto_secretstream_xchacha20poly1305_rekey(self.0.state.as_mut());
}
Ok(())
}
}
pub struct PullState(State);
impl PullState {
pub fn init_pull(header: &[u8; HEADERBYTES], key: &Key) -> Result<PullState> {
let mut state = State::new();
let result = unsafe {
libsodium_sys::crypto_secretstream_xchacha20poly1305_init_pull(
state.as_mut(),
header.as_ptr(),
key.as_bytes().as_ptr(),
)
};
if result != 0 {
return Err(SodiumError::OperationError(
"failed to initialize pull state".into(),
));
}
Ok(PullState(state))
}
pub fn pull(&mut self, c: &[u8], ad: Option<&[u8]>) -> Result<(Vec<u8>, u8)> {
if c.len() < ABYTES {
return Err(SodiumError::InvalidInput("ciphertext too short".into()));
}
let m_len = c.len() - ABYTES;
let mut m = vec![0u8; m_len];
let mut m_len_out: u64 = 0;
let mut tag: u8 = 0;
unsafe {
let result = libsodium_sys::crypto_secretstream_xchacha20poly1305_pull(
self.0.state.as_mut(),
m.as_mut_ptr(),
&mut m_len_out,
&mut tag,
c.as_ptr(),
c.len() as u64,
ad.map_or(std::ptr::null(), |ad| ad.as_ptr()),
ad.map_or(0, |ad| ad.len()) as u64,
);
if result != 0 {
return Err(SodiumError::OperationError("decryption failed".into()));
}
}
m.truncate(m_len_out as usize);
Ok((m, tag))
}
pub fn rekey(&mut self) -> Result<()> {
unsafe {
libsodium_sys::crypto_secretstream_xchacha20poly1305_rekey(self.0.state.as_mut());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_secretstream() {
let key = Key::generate();
let message1 = b"Hello, ";
let message2 = b"World!";
let message3 = b"This is the final message.";
let ad = b"Additional data";
let (mut push_state, header) = PushState::init_push(&key).unwrap();
let ciphertext1 = push_state.push(message1, Some(ad), TAG_MESSAGE).unwrap();
let ciphertext2 = push_state.push(message2, Some(ad), TAG_MESSAGE).unwrap();
let ciphertext3 = push_state.push(message3, Some(ad), TAG_FINAL).unwrap();
let mut pull_state = PullState::init_pull(&header, &key).unwrap();
let (decrypted1, tag1) = pull_state.pull(&ciphertext1, Some(ad)).unwrap();
let (decrypted2, tag2) = pull_state.pull(&ciphertext2, Some(ad)).unwrap();
let (decrypted3, tag3) = pull_state.pull(&ciphertext3, Some(ad)).unwrap();
assert_eq!(&decrypted1, message1);
assert_eq!(&decrypted2, message2);
assert_eq!(&decrypted3, message3);
assert_eq!(tag1, TAG_MESSAGE);
assert_eq!(tag2, TAG_MESSAGE);
assert_eq!(tag3, TAG_FINAL);
}
#[test]
fn test_secretstream_rekey() {
let key = Key::generate();
let message1 = b"Message before rekey";
let message2 = b"Message after rekey";
let (mut push_state, header) = PushState::init_push(&key).unwrap();
let ciphertext1 = push_state.push(message1, None, TAG_MESSAGE).unwrap();
push_state.rekey().unwrap();
let ciphertext2 = push_state.push(message2, None, TAG_MESSAGE).unwrap();
let mut pull_state = PullState::init_pull(&header, &key).unwrap();
let (decrypted1, _) = pull_state.pull(&ciphertext1, None).unwrap();
pull_state.rekey().unwrap();
let (decrypted2, _) = pull_state.pull(&ciphertext2, None).unwrap();
assert_eq!(&decrypted1, message1);
assert_eq!(&decrypted2, message2);
}
#[test]
fn test_secretstream_tag_push() {
let key = Key::generate();
let message = b"Message with key material";
let (mut push_state, header) = PushState::init_push(&key).unwrap();
let ciphertext = push_state.push(message, None, TAG_PUSH).unwrap();
let mut pull_state = PullState::init_pull(&header, &key).unwrap();
let (decrypted, tag) = pull_state.pull(&ciphertext, None).unwrap();
assert_eq!(&decrypted, message);
assert_eq!(tag, TAG_PUSH);
}
#[test]
fn test_key_traits() {
let bytes = [0x42; KEYBYTES];
let key = Key::try_from(&bytes[..]).unwrap();
assert_eq!(key.as_bytes(), &bytes);
let invalid_bytes = [0x42; KEYBYTES - 1];
assert!(Key::try_from(&invalid_bytes[..]).is_err());
let bytes = [0x43; KEYBYTES];
let key2 = Key::from(bytes);
assert_eq!(key2.as_bytes(), &bytes);
let extracted: [u8; KEYBYTES] = key2.into();
assert_eq!(extracted, bytes);
let key3 = Key::generate();
let slice_ref: &[u8] = key3.as_ref();
assert_eq!(slice_ref.len(), KEYBYTES);
}
}