simploxide-client 0.10.0

SimpleX-Chat API client
Documentation
use base64::{Engine as _, engine::general_purpose::URL_SAFE};
use rand::Rng as _;
use simploxide_api_types::CryptoFileArgs as SxcCryptoFileArgs;
use zeroize::{Zeroize as _, ZeroizeOnDrop, Zeroizing};

use crate::crypto::InvalidCryptoArgs;

use super::{InvalidAuthTag, Poly1305Tag, SimplexSecretBox, XSalsa20Key, XSalsa20Nonce};

pub mod std;
pub mod tokio;

#[cfg(feature = "native_crypto")]
pub type StdEncryptedFile = std::EncryptedFile<super::native::SecretBox>;

#[cfg(feature = "native_crypto")]
pub type TokioEncryptedFile = tokio::EncryptedFile<super::native::SecretBox>;

#[cfg(feature = "native_crypto")]
pub type StdMaybeCryptoFile = std::StdMaybeCryptoFile<super::native::SecretBox>;

#[cfg(feature = "native_crypto")]
pub type TokioMaybeCryptoFile = tokio::TokioMaybeCryptoFile<super::native::SecretBox>;

#[derive(ZeroizeOnDrop)]
pub struct FileCryptoArgs {
    key: XSalsa20Key,
    nonce: XSalsa20Nonce,
}

impl FileCryptoArgs {
    fn new(key: &XSalsa20Key, nonce: &XSalsa20Nonce) -> Self {
        Self {
            key: *key,
            nonce: *nonce,
        }
    }

    pub fn try_from_base64(mut key: String, mut nonce: String) -> Result<Self, InvalidCryptoArgs> {
        fn try_decode(key_str: &str, nonce_str: &str) -> Result<FileCryptoArgs, InvalidCryptoArgs> {
            let mut key = Zeroizing::new([0u8; ::std::mem::size_of::<XSalsa20Key>()]);
            let mut nonce = Zeroizing::new([0u8; ::std::mem::size_of::<XSalsa20Nonce>()]);

            decode_base64_arg(key_str, key.as_mut())?;
            decode_base64_arg(nonce_str, nonce.as_mut())?;

            Ok(FileCryptoArgs::new(&key, &nonce))
        }

        let result = try_decode(&key, &nonce);

        key.zeroize();
        nonce.zeroize();

        result
    }

    pub fn expose(&self) -> SxcCryptoFileArgs {
        SxcCryptoFileArgs {
            file_key: URL_SAFE.encode(self.key),
            file_nonce: URL_SAFE.encode(self.nonce),
            undocumented: Default::default(),
        }
    }
}

impl TryFrom<SxcCryptoFileArgs> for FileCryptoArgs {
    type Error = InvalidCryptoArgs;

    fn try_from(args: SxcCryptoFileArgs) -> Result<Self, Self::Error> {
        Self::try_from_base64(args.file_key, args.file_nonce)
    }
}

struct EncryptedFileState<S> {
    crypto_args: FileCryptoArgs,
    secret_box: S,
    buf: Zeroizing<Vec<u8>>,
    mode: Mode,
    remaining_data_len: usize,
}

impl<S> EncryptedFileState<S> {
    const DEFAULT_BUFSIZE: usize = 65536;
}

impl<S: SimplexSecretBox> EncryptedFileState<S> {
    fn new() -> Self {
        let mut rng = rand::rng();

        let mut key = [0u8; ::std::mem::size_of::<XSalsa20Key>()];
        let mut nonce = [0u8; ::std::mem::size_of::<XSalsa20Nonce>()];

        rng.fill_bytes(&mut key);
        rng.fill_bytes(&mut nonce);

        let crypto_args = FileCryptoArgs::new(&key, &nonce);
        let secret_box = SimplexSecretBox::init(&key, &nonce);

        key.zeroize();
        nonce.zeroize();

        Self {
            crypto_args,
            secret_box,
            buf: Zeroizing::new(Vec::new()),
            mode: Mode::Write,
            remaining_data_len: 0,
        }
    }

    fn from_args(crypto_args: FileCryptoArgs) -> Self {
        let secret_box = SimplexSecretBox::init(&crypto_args.key, &crypto_args.nonce);

        Self {
            crypto_args,
            secret_box,
            buf: Zeroizing::new(Vec::new()),
            mode: Mode::Write,
            remaining_data_len: 0,
        }
    }

    fn from_size_and_args(
        file_size: usize,
        crypto_args: FileCryptoArgs,
    ) -> ::std::io::Result<Self> {
        let mut state = Self::from_args(crypto_args);
        if file_size < ::std::mem::size_of::<Poly1305Tag>() {
            return Err(InvalidAuthTag::io_error());
        } else if file_size == ::std::mem::size_of::<Poly1305Tag>() {
            state.switch_to_auth_mode();
        } else {
            state.remaining_data_len = file_size - ::std::mem::size_of::<Poly1305Tag>();
            state.mode = Mode::Read;
        }

        Ok(state)
    }

    fn crypto_args(&self) -> &FileCryptoArgs {
        &self.crypto_args
    }

    fn reset(&mut self) {
        let mut rng = rand::rng();
        let mut key = [0u8; ::std::mem::size_of::<XSalsa20Key>()];
        let mut nonce = [0u8; ::std::mem::size_of::<XSalsa20Nonce>()];

        rng.fill_bytes(&mut key);
        rng.fill_bytes(&mut nonce);

        self.crypto_args = FileCryptoArgs::new(&key, &nonce);
        self.secret_box = SimplexSecretBox::init(&key, &nonce);
        self.remaining_data_len = 0;

        key.zeroize();
        nonce.zeroize();
    }

    fn encrypt_chunk(&mut self, chunk: &[u8]) -> &[u8] {
        self.buf
            .reserve_exact(::std::cmp::max(Self::DEFAULT_BUFSIZE, chunk.len()));
        self.buf.resize(chunk.len(), 0);

        self.secret_box.encrypt_chunk(chunk, &mut self.buf);
        self.remaining_data_len += chunk.len();
        &self.buf
    }

    fn encrypted_buf(&self) -> &[u8] {
        let offset = self
            .buf
            .len()
            .checked_sub(self.remaining_data_len)
            .expect("encrypted_buf: no overflows");

        &self.buf[offset..]
    }

    fn consume_encrypted_bytes(&mut self, bytes: usize) {
        self.remaining_data_len = self
            .remaining_data_len
            .checked_sub(bytes)
            .expect("consume_encrypted_bytes: no overflows");
    }

    fn is_encrypted_buf_consumed(&self) -> bool {
        self.is_all_data_read()
    }

    fn prep_read_buf(&mut self, bytes: usize) -> &mut [u8] {
        let corrected_bytes = ::std::cmp::min(bytes, self.remaining_data_len);
        let buf_size = ::std::cmp::max(self.optimal_buf_size(), corrected_bytes);
        self.buf.reserve_exact(buf_size);

        self.buf.resize(corrected_bytes, 0);
        &mut self.buf
    }

    fn decrypt_read_buf(&mut self, bytes_read: usize, out_chunk: &mut [u8]) {
        self.remaining_data_len = self
            .remaining_data_len
            .checked_sub(bytes_read)
            .expect("decrypt_read_buf: no overflows");

        self.secret_box
            .decrypt_chunk(&mut self.buf[..bytes_read], out_chunk);
    }

    fn is_all_data_read(&self) -> bool {
        self.remaining_data_len == 0
    }

    fn optimal_buf_size(&self) -> usize {
        if self.mode == Mode::Auth {
            ::std::mem::size_of::<Poly1305Tag>()
        } else if self.mode == Mode::Read && self.remaining_data_len < Self::DEFAULT_BUFSIZE {
            self.remaining_data_len
        } else {
            Self::DEFAULT_BUFSIZE
        }
    }

    fn plaintext_size_hint(&self) -> usize {
        match self.mode {
            Mode::Read => self.remaining_data_len,
            Mode::Write => EncryptedFileState::<S>::DEFAULT_BUFSIZE,
            Mode::Auth | Mode::Shutdown | Mode::AuthFailure => 0,
        }
    }

    fn assert_writable(&self) -> ::std::io::Result<()> {
        if self.mode == Mode::Write {
            Ok(())
        } else {
            Err(::std::io::Error::other("Trying to write non-writable file"))
        }
    }

    fn assert_readable(&self) -> ::std::io::Result<()> {
        if self.mode == Mode::Read {
            Ok(())
        } else {
            Err(::std::io::Error::other("Trying to read non-readable file"))
        }
    }

    fn switch_to_auth_mode(&mut self) {
        self.mode = Mode::Auth;
        self.buf.resize(::std::mem::size_of::<Poly1305Tag>(), 0);
        self.remaining_data_len = ::std::mem::size_of::<Poly1305Tag>();
    }

    fn write_auth_tag_in_buf(&mut self) {
        self.switch_to_auth_mode();
        let file_tag = self.secret_box.auth_tag();
        self.buf.copy_from_slice(&file_tag);
    }

    fn auth_tag_buf(&mut self) -> &mut [u8] {
        let offset = self
            .buf
            .len()
            .checked_sub(self.remaining_data_len)
            .expect("auth_tag_buf: no overflows");

        self.buf[offset..].as_mut()
    }

    fn consume_auth_tag_bytes(&mut self, bytes: usize) {
        self.consume_encrypted_bytes(bytes);
    }

    fn authenticate(&mut self) -> ::std::io::Result<()> {
        let file_tag: &Poly1305Tag = self
            .buf
            .as_slice()
            .try_into()
            .map_err(|_| InvalidAuthTag::io_error())?;

        let result = if self.secret_box.verify_tag(file_tag) {
            Ok(())
        } else {
            self.mode = Mode::AuthFailure;
            Err(InvalidAuthTag::io_error())
        };

        self.buf.truncate(0);
        self.remaining_data_len = 0;

        result
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
    Read,
    Write,
    Auth,
    Shutdown,
    AuthFailure,
}

fn decode_base64_arg(b64str: &str, buf: &mut [u8]) -> Result<(), InvalidCryptoArgs> {
    let len = URL_SAFE
        .decode_slice(b64str, buf)
        .map_err(|_| InvalidCryptoArgs)?;

    if len == buf.len() {
        Ok(())
    } else {
        Err(InvalidCryptoArgs)
    }
}