reticulum-rs-core 0.5.1

Core packet, identity, ratchet, and cryptographic primitives for the Reticulum network stack.
Documentation
use crate::{
    error::RnsError,
    identity::{PrivateIdentity, PUBLIC_KEY_LENGTH},
    ratchets::decrypt_with_private_key,
};
use alloc::vec::Vec;
#[cfg(feature = "std")]
use ed25519_dalek::Signature;
use rand_core::OsRng;
#[cfg(feature = "std")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
use serde_bytes::ByteBuf;
#[cfg(feature = "std")]
use std::path::{Path, PathBuf};
use x25519_dalek::{PublicKey, StaticSecret};

pub const RATCHET_LENGTH: usize = PUBLIC_KEY_LENGTH;
const DEFAULT_RATCHET_INTERVAL_SECS: u64 = 30 * 60;
const DEFAULT_RETAINED_RATCHETS: usize = 512;

#[derive(Clone)]
pub(crate) struct RatchetState {
    pub(crate) enabled: bool,
    pub(crate) ratchets: Vec<[u8; RATCHET_LENGTH]>,
    #[cfg(feature = "std")]
    pub(crate) ratchets_path: Option<PathBuf>,
    pub(crate) ratchet_interval_secs: u64,
    pub(crate) retained_ratchets: usize,
    pub(crate) latest_ratchet_time: Option<f64>,
    pub(crate) enforce_ratchets: bool,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct PersistedRatchets {
    signature: ByteBuf,
    ratchets: ByteBuf,
}

impl Default for RatchetState {
    fn default() -> Self {
        Self {
            enabled: false,
            ratchets: Vec::new(),
            #[cfg(feature = "std")]
            ratchets_path: None,
            ratchet_interval_secs: DEFAULT_RATCHET_INTERVAL_SECS,
            retained_ratchets: DEFAULT_RETAINED_RATCHETS,
            latest_ratchet_time: None,
            enforce_ratchets: false,
        }
    }
}

impl RatchetState {
    #[cfg(feature = "std")]
    pub(crate) fn enable(
        &mut self,
        identity: &PrivateIdentity,
        path: PathBuf,
    ) -> Result<(), RnsError> {
        self.latest_ratchet_time = Some(0.0);
        self.reload(identity, &path)?;
        self.enabled = true;
        self.ratchets_path = Some(path);
        Ok(())
    }

    #[cfg(feature = "std")]
    pub(crate) fn reload(
        &mut self,
        identity: &PrivateIdentity,
        path: &Path,
    ) -> Result<(), RnsError> {
        if path.exists() {
            let data = std::fs::read(path).map_err(|_| RnsError::PacketError)?;
            let persisted: PersistedRatchets =
                rmp_serde::from_slice(&data).map_err(|_| RnsError::PacketError)?;
            let signature = Signature::from_slice(persisted.signature.as_ref())
                .map_err(|_| RnsError::CryptoError)?;
            identity
                .verify(persisted.ratchets.as_ref(), &signature)
                .map_err(|_| RnsError::IncorrectSignature)?;
            let decoded: Vec<ByteBuf> = rmp_serde::from_slice(persisted.ratchets.as_ref())
                .map_err(|_| RnsError::PacketError)?;
            let mut ratchets = Vec::new();
            for ratchet in decoded {
                if ratchet.len() == RATCHET_LENGTH {
                    let mut bytes = [0u8; RATCHET_LENGTH];
                    bytes.copy_from_slice(ratchet.as_ref());
                    ratchets.push(bytes);
                }
            }
            self.ratchets = ratchets;
            return Ok(());
        }

        self.ratchets = Vec::new();
        self.persist(identity, path)?;
        Ok(())
    }

    #[cfg(feature = "std")]
    fn persist(&self, identity: &PrivateIdentity, path: &Path) -> Result<(), RnsError> {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).map_err(|_| RnsError::PacketError)?;
        }
        let packed = pack_ratchets(&self.ratchets)?;
        let signature = identity.sign(&packed).to_bytes();
        let persisted = PersistedRatchets {
            signature: ByteBuf::from(signature.to_vec()),
            ratchets: ByteBuf::from(packed),
        };
        let encoded = rmp_serde::to_vec(&persisted).map_err(|_| RnsError::PacketError)?;
        let tmp_path = path.with_extension("tmp");
        std::fs::write(&tmp_path, encoded).map_err(|_| RnsError::PacketError)?;
        if path.exists() {
            let _ = std::fs::remove_file(path);
        }
        std::fs::rename(&tmp_path, path).map_err(|_| RnsError::PacketError)?;
        Ok(())
    }

    pub(crate) fn rotate_if_needed(
        &mut self,
        identity: &PrivateIdentity,
        now: f64,
    ) -> Result<(), RnsError> {
        #[cfg(not(feature = "std"))]
        let _ = identity;
        if !self.enabled {
            return Ok(());
        }
        let last = self.latest_ratchet_time.unwrap_or(0.0);
        if self.ratchets.is_empty() || now > last + self.ratchet_interval_secs as f64 {
            let secret = StaticSecret::random_from_rng(OsRng);
            self.ratchets.insert(0, secret.to_bytes());
            self.latest_ratchet_time = Some(now);
            if self.ratchets.len() > self.retained_ratchets {
                self.ratchets.truncate(self.retained_ratchets);
            }
            #[cfg(feature = "std")]
            if let Some(path) = self.ratchets_path.clone() {
                self.persist(identity, &path)?;
            }
        }
        Ok(())
    }

    pub(crate) fn current_ratchet_public(&self) -> Option<[u8; RATCHET_LENGTH]> {
        let ratchet = self.ratchets.first()?;
        let secret = StaticSecret::from(*ratchet);
        let public = PublicKey::from(&secret);
        let mut bytes = [0u8; RATCHET_LENGTH];
        bytes.copy_from_slice(public.as_bytes());
        Some(bytes)
    }
}

#[cfg(feature = "std")]
fn pack_ratchets(ratchets: &[[u8; RATCHET_LENGTH]]) -> Result<Vec<u8>, RnsError> {
    let list: Vec<ByteBuf> = ratchets.iter().map(|bytes| ByteBuf::from(bytes.to_vec())).collect();
    rmp_serde::to_vec(&list).map_err(|_| RnsError::PacketError)
}

pub(crate) fn try_decrypt_with_ratchets(
    state: &RatchetState,
    salt: &[u8],
    ciphertext: &[u8],
) -> Option<Vec<u8>> {
    for ratchet in &state.ratchets {
        let secret = StaticSecret::from(*ratchet);
        if let Ok(plaintext) = decrypt_with_private_key(&secret, salt, ciphertext) {
            return Some(plaintext);
        }
    }
    None
}