enigma-protocol 0.1.0

High-level orchestrator that composes the Enigma crates into a production-ready messaging protocol
Documentation
use std::collections::HashMap;

use enigma_packet::{Message, MessageMeta};
use uuid::Uuid;

use crate::crypto::{conversation_ad_bytes, SessionRatchet};
use crate::error::{EnigmaProtocolError, Result};
use crate::types::SessionBootstrap;

pub struct Session {
    local_id: String,
    remote_id: String,
    ratchet: SessionRatchet,
    attachments: HashMap<Uuid, AttachmentTracker>,
}

struct AttachmentTracker {
    meta: enigma_packet::AttachmentMeta,
    received_chunks: u32,
    received_size: u64,
}

pub enum AttachmentUpdate {
    Init {
        attachment_id: Uuid,
        total_chunks: u32,
    },
    Chunk {
        attachment_id: Uuid,
        received_chunks: u32,
        total_chunks: u32,
    },
    End {
        attachment_id: Uuid,
        total_size: u64,
    },
}

impl Session {
    pub fn new(local: &str, remote: &str, bootstrap: &SessionBootstrap) -> Result<Self> {
        let ratchet = SessionRatchet::from_bootstrap(bootstrap, local, remote)?;
        Ok(Self {
            local_id: local.to_string(),
            remote_id: remote.to_string(),
            ratchet,
            attachments: HashMap::new(),
        })
    }

    pub fn conversation_id(&self) -> Uuid {
        self.ratchet.conversation_id()
    }

    pub fn local_id(&self) -> &str {
        &self.local_id
    }

    pub fn remote_id(&self) -> &str {
        &self.remote_id
    }

    pub fn encrypt_message(&mut self, message: &Message) -> Result<Vec<u8>> {
        let encoded = enigma_packet::encode_message(message)?;
        let key = self.ratchet.next_send_key()?;
        let box_ = enigma_aead::AeadBox::new(key);
        let ad = conversation_ad_bytes(&self.conversation_id(), &self.local_id, &self.remote_id);
        let ciphertext = box_.encrypt(&encoded, &ad)?;
        Ok(ciphertext)
    }

    pub fn decrypt_packet(&mut self, packet: &[u8]) -> Result<Message> {
        let key = self.ratchet.next_recv_key()?;
        let box_ = enigma_aead::AeadBox::new(key);
        let ad = conversation_ad_bytes(&self.conversation_id(), &self.remote_id, &self.local_id);
        let plaintext = box_.decrypt(packet, &ad)?;
        let message = enigma_packet::decode_message(&plaintext)?;
        Ok(message)
    }

    pub fn handle_attachment_message(
        &mut self,
        message: &Message,
    ) -> Result<Option<AttachmentUpdate>> {
        match &message.meta {
            MessageMeta::AttachmentInit(meta) => {
                let tracker = AttachmentTracker {
                    meta: meta.clone(),
                    received_chunks: 0,
                    received_size: 0,
                };
                self.attachments.insert(meta.attachment_id, tracker);
                Ok(Some(AttachmentUpdate::Init {
                    attachment_id: meta.attachment_id,
                    total_chunks: meta.chunk_count,
                }))
            }
            MessageMeta::AttachmentChunk(chunk) => {
                let tracker = self
                    .attachments
                    .get_mut(&chunk.attachment_id)
                    .ok_or(EnigmaProtocolError::Attachment)?;
                if chunk.index != tracker.received_chunks {
                    return Err(EnigmaProtocolError::Attachment);
                }
                tracker.received_chunks = tracker
                    .received_chunks
                    .checked_add(1)
                    .ok_or(EnigmaProtocolError::Attachment)?;
                tracker.received_size = tracker
                    .received_size
                    .checked_add(chunk.chunk_size as u64)
                    .ok_or(EnigmaProtocolError::Attachment)?;
                Ok(Some(AttachmentUpdate::Chunk {
                    attachment_id: chunk.attachment_id,
                    received_chunks: tracker.received_chunks,
                    total_chunks: tracker.meta.chunk_count,
                }))
            }
            MessageMeta::AttachmentEnd {
                attachment_id,
                total_size,
                chunk_count,
                ..
            } => {
                let tracker = self
                    .attachments
                    .remove(attachment_id)
                    .ok_or(EnigmaProtocolError::Attachment)?;
                if &tracker.meta.chunk_count != chunk_count {
                    return Err(EnigmaProtocolError::Attachment);
                }
                if &tracker.meta.total_size != total_size {
                    return Err(EnigmaProtocolError::Attachment);
                }
                Ok(Some(AttachmentUpdate::End {
                    attachment_id: *attachment_id,
                    total_size: *total_size,
                }))
            }
            _ => Ok(None),
        }
    }
}