citadel_crypt/
toolset.rs

1//! # Cryptographic Toolset Management
2//!
3//! This module provides a management layer for cryptographic ratchets, handling version control,
4//! synchronization, and lifecycle management of encryption keys. It maintains a rolling window
5//! of active ratchets while ensuring secure key evolution.
6//!
7//! ## Features
8//! - Manages multiple versions of cryptographic ratchets
9//! - Provides automatic version control and synchronization
10//! - Implements memory-bounded storage with configurable limits
11//! - Supports static auxiliary ratchet for persistent encryption
12//! - Handles secure ratchet updates and deregistration
13//! - Ensures thread-safe access to cryptographic primitives
14//!
15//! ## Important Notes
16//! - Maximum number of ratchets in memory is configurable and environment-dependent
17//! - Static auxiliary ratchet provides persistent encryption for stored data
18//! - Version synchronization is required when maximum capacity is reached
19//! - Thread-safe operations for concurrent access
20//!
21//! ## Related Components
22//! - [`StackedRatchet`](crate::ratchets::stacked::ratchet::StackedRatchet): Core ratchet implementation
23//! - [`EntropyBank`](crate::ratchets::entropy_bank::EntropyBank): Entropy source for ratchets
24//! - [`CryptError`](crate::misc::CryptError): Error handling for cryptographic operations
25//! - [`ClientNetworkAccount`]: High-level account management
26
27use std::collections::VecDeque;
28
29use serde::{Deserialize, Serialize};
30
31use crate::misc::CryptError;
32use crate::ratchets::stacked::ratchet::StackedRatchet;
33use crate::ratchets::Ratchet;
34use std::ops::RangeInclusive;
35
36/// The maximum number of ratchets to store in memory. Note that, most of the time, the true number in memory
37/// will be the max - 1, since the max is only reached when the most recent ratchet is added and the toolset
38/// is in the state of pending synchronization/truncation
39#[cfg(debug_assertions)]
40pub const MAX_RATCHETS_IN_MEMORY: usize = 6;
41#[cfg(not(debug_assertions))]
42pub const MAX_RATCHETS_IN_MEMORY: usize = 32;
43
44/// The reserved version for the static aux ratchet
45pub const STATIC_AUX_VERSION: u32 = 0;
46
47/// The [Toolset] is the layer of abstraction between a [ClientNetworkAccount] and the
48/// inner hyper ratchets.
49#[derive(Serialize, Deserialize)]
50pub struct Toolset<R: Ratchet> {
51    /// the CID of the owner
52    pub cid: u64,
53    most_recent_ratchet_version: u32,
54    oldest_ratchet_version: u32,
55    #[serde(bound = "")]
56    map: VecDeque<R>,
57    /// The static auxiliary entropy_bank was made to cover a unique situation that is consequence of dropping-off the back of the VecDeque upon upgrade:
58    /// As the back gets dropped, any data encrypted using that version now becomes undecipherable forever. The solution to this is having a static entropy_bank, but this
59    /// does indeed compromise safety. This should NEVER be used for network data transmission (except for first packets), and should only
60    /// really be used when encrypting data which is stored under the local filesystem via HyxeFiles. Since a HyxeFile, for example, hides revealing data
61    /// with a complex file path, any possible hacker wouldn't necessarily be able to correlate the HyxeFile with the correct CID unless additional work was done.
62    /// Local filesystems should be encrypted anyways (otherwise voids warranty), but, having the HyxeFile layer is really just a "weak" layer of protection
63    /// designed to derail any currently existing or historical viruses that may look for conventional means of breaking-through data
64    #[serde(bound = "")]
65    static_auxiliary_ratchet: R,
66}
67
68// This clone should only be called in the middle of a session
69impl<R: Ratchet> Clone for Toolset<R> {
70    fn clone(&self) -> Self {
71        Self {
72            cid: self.cid,
73            most_recent_ratchet_version: self.most_recent_ratchet_version,
74            oldest_ratchet_version: self.oldest_ratchet_version,
75            map: self.map.clone(),
76            static_auxiliary_ratchet: self.static_auxiliary_ratchet.clone(),
77        }
78    }
79}
80
81#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Debug)]
82pub enum ToolsetUpdateStatus {
83    // new version has been committed, and the number of HRs is still less than the total max. No E2E synchronization required
84    Committed {
85        new_version: u32,
86    },
87    // The maximum number of acceptable HR's have been stored in memory, but will not be removed until both endpoints can agree
88    // to removing the version
89    CommittedNeedsSynchronization {
90        new_version: u32,
91        oldest_version: u32,
92    },
93}
94
95impl<R: Ratchet> Toolset<R> {
96    /// Creates a new [Toolset]. Designates the `ratchet` as the static auxiliary
97    /// ratchet should be version 0
98    pub fn new(cid: u64, ratchet: R) -> Self {
99        let mut map = VecDeque::with_capacity(MAX_RATCHETS_IN_MEMORY);
100        map.push_front(ratchet.clone());
101        Toolset {
102            cid,
103            most_recent_ratchet_version: 0,
104            oldest_ratchet_version: 0,
105            map,
106            static_auxiliary_ratchet: ratchet,
107        }
108    }
109
110    pub fn new_debug(
111        cid: u64,
112        ratchet: R,
113        most_recent_ratchet_version: u32,
114        oldest_ratchet_version: u32,
115    ) -> Self {
116        let mut map = VecDeque::with_capacity(MAX_RATCHETS_IN_MEMORY);
117        map.push_front(ratchet.clone());
118        Toolset {
119            cid,
120            most_recent_ratchet_version,
121            oldest_ratchet_version,
122            map,
123            static_auxiliary_ratchet: ratchet,
124        }
125    }
126
127    /// Updates from an inbound DrillUpdateObject. Returns the new Drill
128    pub fn update_from(&mut self, new_ratchet: R) -> Option<ToolsetUpdateStatus> {
129        let latest_hr_version = self.get_most_recent_ratchet_version();
130
131        if new_ratchet.get_cid() != self.cid {
132            log::error!(target: "citadel", "The supplied hyper ratchet does not belong to the expected CID (expected: {}, obtained: {})", self.cid, new_ratchet.get_cid());
133            return None;
134        }
135
136        if latest_hr_version != new_ratchet.version().wrapping_sub(1) {
137            log::error!(target: "citadel", "The supplied hyper ratchet is not precedent to the entropy_bank update object (expected: {}, obtained: {})", latest_hr_version + 1, new_ratchet.version());
138            return None;
139        }
140
141        let update_status = self.append_ratchet(new_ratchet);
142        let cur_version = match &update_status {
143            ToolsetUpdateStatus::Committed { new_version }
144            | ToolsetUpdateStatus::CommittedNeedsSynchronization { new_version, .. } => {
145                *new_version
146            }
147        };
148
149        self.most_recent_ratchet_version = cur_version;
150
151        let prev_version = self.most_recent_ratchet_version.wrapping_sub(1);
152        log::trace!(target: "citadel", "[{}] Upgraded {} to {} for cid={}. Adjusted index of current: {}. Adjusted index of (current - 1): {} || OLDEST: {} || LEN: {}", MAX_RATCHETS_IN_MEMORY, prev_version, cur_version, self.cid, self.get_adjusted_index(cur_version), self.get_adjusted_index(prev_version), self.get_oldest_ratchet_version(), self.map.len());
153        Some(update_status)
154    }
155
156    #[allow(unused_results)]
157    ///Replacing entropy_banks is not allowed, and is why this subroutine returns an error when a collision is detected
158    ///
159    /// Returns the new hyper ratchet version
160    fn append_ratchet(&mut self, ratchet: R) -> ToolsetUpdateStatus {
161        //debug_assert!(self.map.len() <= MAX_HYPER_RATCHETS_IN_MEMORY);
162        let new_version = ratchet.version();
163        //println!("max hypers: {} @ {} bytes ea", MAX_HYPER_RATCHETS_IN_MEMORY, get_approx_bytes_per_ratchet());
164        self.map.push_front(ratchet);
165        if self.map.len() >= MAX_RATCHETS_IN_MEMORY {
166            let oldest_version = self.get_oldest_ratchet_version();
167            log::trace!(target: "citadel", "[Toolset Update] Needs Truncation. Oldest version: {}", oldest_version);
168            ToolsetUpdateStatus::CommittedNeedsSynchronization {
169                new_version,
170                oldest_version,
171            }
172        } else {
173            ToolsetUpdateStatus::Committed { new_version }
174        }
175    }
176
177    /// When append_ratchet returns CommittedNeedsSynchronization on Bob's side, Bob should first
178    /// send a packet to Alice telling her that capacity has been reached and that version V should be dropped.
179    /// Alice will then prevent herself from sending any more packets using version V, and will locally run this
180    /// function. Next, Alice should alert Bob telling him that it's now safe to remove version V. Bob then runs
181    /// this function last. By doing this, Alice no longer sends packets that may be no longer be valid
182    #[allow(unused_results)]
183    pub fn deregister_oldest_ratchet(&mut self, version: u32) -> Result<(), CryptError> {
184        if self.map.len() < MAX_RATCHETS_IN_MEMORY {
185            return Err(CryptError::RekeyUpdateError(
186                "Cannot call for deregistration unless the map len is maxed out".to_string(),
187            ));
188        }
189
190        let oldest = self.get_oldest_ratchet_version();
191        if oldest != version {
192            Err(CryptError::RekeyUpdateError(format!(
193                "Unable to deregister. Provided version: {version}, expected version: {oldest}",
194            )))
195        } else {
196            self.map.pop_back().ok_or(CryptError::OutOfBoundsError)?;
197            self.oldest_ratchet_version = self.oldest_ratchet_version.wrapping_add(1);
198            log::trace!(target: "citadel", "[Toolset] Deregistered version {} for cid={}. New oldest: {} | LEN: {}", version, self.cid, self.oldest_ratchet_version, self.len());
199            Ok(())
200        }
201    }
202
203    /// Returns the number of StackedRatchets internally
204    #[allow(clippy::len_without_is_empty)]
205    pub fn len(&self) -> usize {
206        self.map.len()
207    }
208
209    /// Returns the latest entropy_bank version
210    pub fn get_most_recent_ratchet(&self) -> Option<&R> {
211        self.map.front()
212    }
213
214    /// Returns the oldest entropy_bank in the VecDeque
215    pub fn get_oldest_ratchet(&self) -> Option<&R> {
216        self.map.back()
217    }
218
219    /// Gets the oldest entropy_bank version
220    pub fn get_oldest_ratchet_version(&self) -> u32 {
221        self.oldest_ratchet_version
222    }
223
224    /// Returns the most recent entropy_bank
225    pub fn get_most_recent_ratchet_version(&self) -> u32 {
226        self.most_recent_ratchet_version
227    }
228
229    /// Returns the static auxiliary entropy_bank. There is no "set" function, because this really
230    /// shouldn't be changing internally as this is depended upon by datasets which require a fixed encryption
231    /// version which would otherwise normally get dropped from the VecDeque semi-actively.
232    ///
233    /// This panics if the internal map is empty
234    ///
235    /// The static auxilliary entropy_bank is used for RECOVERY MODE. I.e., if the version are out
236    /// of sync, then the static auxiliary entropy_bank is used to obtain the nonce for the AES GCM
237    /// mode of encryption
238    pub fn get_static_auxiliary_ratchet(&self) -> &R {
239        &self.static_auxiliary_ratchet
240    }
241
242    /// The index within the vec deque does not necessarily track the entropy_bank versions.
243    /// This function adjusts for that
244    #[inline]
245    fn get_adjusted_index(&self, version: u32) -> usize {
246        self.most_recent_ratchet_version.wrapping_sub(version) as usize
247    }
248
249    /// Returns a specific entropy_bank version
250    pub fn get_ratchet(&self, version: u32) -> Option<&R> {
251        let idx = self.get_adjusted_index(version);
252
253        let res = self.map.get(idx);
254        if res.is_none() {
255            log::error!(target: "citadel", "Attempted to get ratchet v{} for cid={}, but does not exist! len: {}. Oldest: {}. Newest: {}", version, self.cid, self.map.len(), self.oldest_ratchet_version, self.most_recent_ratchet_version);
256        }
257
258        res
259    }
260
261    /// Returns a range of entropy_banks. Returns None if any entropy_bank in the range is missing
262    pub fn get_ratchets(&self, versions: RangeInclusive<u32>) -> Option<Vec<&R>> {
263        let mut ret = Vec::with_capacity((*versions.end() - *versions.start() + 1) as usize);
264        for version in versions {
265            if let Some(entropy_bank) = self.get_ratchet(version) {
266                ret.push(entropy_bank);
267            } else {
268                return None;
269            }
270        }
271
272        Some(ret)
273    }
274
275    /// Serializes the toolset to a buffer
276    pub fn serialize_to_vec(&self) -> Result<Vec<u8>, CryptError<String>> {
277        bincode::serialize(self).map_err(|err| CryptError::RekeyUpdateError(err.to_string()))
278    }
279
280    /// Deserializes from a slice of bytes
281    pub fn deserialize_from_bytes<T: AsRef<[u8]>>(input: T) -> Result<Self, CryptError<String>> {
282        bincode::deserialize(input.as_ref())
283            .map_err(|err| CryptError::RekeyUpdateError(err.to_string()))
284    }
285
286    /// Resets the internal state to the default, if necessary. At the beginning of each session, this should be called
287    pub fn verify_init_state(&self) -> Option<()> {
288        self.static_auxiliary_ratchet.reset_ara();
289        Some(())
290    }
291}
292
293/// Makes replacing/synchronizing toolsets easier
294/// input: (static_aux_ratchet, f(0))
295pub type StaticAuxRatchet = StackedRatchet;
296impl<R: Ratchet> From<(R, R)> for Toolset<R> {
297    fn from(entropy_bank: (R, R)) -> Self {
298        let most_recent_ratchet_version = entropy_bank.1.version();
299        let oldest_ratchet_version = most_recent_ratchet_version; // for init, just like in the normal constructor
300        let mut map = VecDeque::with_capacity(MAX_RATCHETS_IN_MEMORY);
301        map.insert(0, entropy_bank.1);
302        Self {
303            cid: entropy_bank.0.get_cid(),
304            oldest_ratchet_version,
305            most_recent_ratchet_version,
306            map,
307            static_auxiliary_ratchet: entropy_bank.0,
308        }
309    }
310}