mc_oblivious_ram/path_oram/
mod.rs

1//! Implements PathORAM on top of a generic ORAMStorage and a generic
2//! PositionMap.
3//!
4//! In this implementation, the bucket size (Z in paper) is configurable.
5//!
6//! The storage will hold blocks of size ValueSize * Z for the data, and
7//! MetaSize * Z for the metadata.
8//!
9//! Most papers suggest Z = 2 or Z = 4, Z = 1 probably won't work.
10//!
11//! It is expected that you want the block size to be 4096 (one linux page)
12//!
13//! Height of storage tree is set as log size - log bucket_size
14//! This is informed by Gentry et al.
15
16use alloc::vec;
17
18use crate::evictor::{BranchSelector, EvictionStrategy, EvictorCreator};
19use aligned_cmov::{
20    subtle::{Choice, ConstantTimeEq, ConstantTimeLess},
21    typenum::{PartialDiv, Prod, Unsigned, U16, U64, U8},
22    A64Bytes, A8Bytes, ArrayLength, AsAlignedChunks, AsNeSlice, CMov,
23};
24use alloc::{boxed::Box, vec::Vec};
25use balanced_tree_index::TreeIndex;
26use core::{marker::PhantomData, ops::Mul};
27use mc_oblivious_traits::{
28    log2_ceil, ORAMStorage, ORAMStorageCreator, PositionMap, PositionMapCreator, ORAM,
29};
30use rand_core::{CryptoRng, RngCore};
31
32/// In this implementation, a value is expected to be an aligned 4096 byte page.
33/// The metadata associated to a value is two u64's (block num and leaf), so 16
34/// bytes. It is stored separately from the value so as not to break alignment.
35/// In many cases block-num and leaf can be u32's. But I suspect that there will
36/// be other stuff in this metadata as well in the end so the savings isn't
37/// much.
38pub(crate) type MetaSize = U16;
39
40// A metadata object is always associated to any Value in the PathORAM
41// structure. A metadata consists of two fields: leaf_num and block_num
42// A metadata has the status of being "vacant" or "not vacant".
43//
44// The block_num is the number in range 0..len that corresponds to the user's
45// query. every block of data in the ORAM has an associated block number.
46// There should be only one non-vacant data with a given block number at a time,
47// if none is found then it will be initialized lazily on first query.
48//
49// The leaf_num is the "target" of this data in the tree, according to Path ORAM
50// algorithm. It represents a TreeIndex value. In particular it is not zero.
51//
52// The leaf_num attached to a block_num should match pos[block_num], it is a
53// cache of that value, which enables us to perform efficient eviction and
54// packing in a branch.
55//
56// A metadata is defined to be "vacant" if leaf_num IS zero.
57// This indicates that the metadata and its corresponding value can be
58// overwritten with a real item.
59
60/// Get the leaf num of a metadata
61pub(crate) fn meta_leaf_num(src: &A8Bytes<MetaSize>) -> &u64 {
62    &src.as_ne_u64_slice()[0]
63}
64/// Get the leaf num of a mutable metadata
65pub(crate) fn meta_leaf_num_mut(src: &mut A8Bytes<MetaSize>) -> &mut u64 {
66    &mut src.as_mut_ne_u64_slice()[0]
67}
68/// Get the block num of a metadata
69pub(crate) fn meta_block_num(src: &A8Bytes<MetaSize>) -> &u64 {
70    &src.as_ne_u64_slice()[1]
71}
72/// Get the block num of a mutable metadata
73pub(crate) fn meta_block_num_mut(src: &mut A8Bytes<MetaSize>) -> &mut u64 {
74    &mut src.as_mut_ne_u64_slice()[1]
75}
76/// Test if a metadata is "vacant"
77pub(crate) fn meta_is_vacant(src: &A8Bytes<MetaSize>) -> Choice {
78    meta_leaf_num(src).ct_eq(&0)
79}
80/// Set a metadata to vacant, obliviously, if a condition is true
81pub(crate) fn meta_set_vacant(condition: Choice, src: &mut A8Bytes<MetaSize>) {
82    meta_leaf_num_mut(src).cmov(condition, &0);
83}
84
85/// An implementation of PathORAM, using u64 to represent leaves in metadata.
86pub struct PathORAM<ValueSize, Z, StorageType, RngType, EvictorType>
87where
88    ValueSize: ArrayLength<u8> + PartialDiv<U8> + PartialDiv<U64>,
89    Z: Unsigned + Mul<ValueSize> + Mul<MetaSize>,
90    RngType: RngCore + CryptoRng + Send + Sync + 'static,
91    StorageType: ORAMStorage<Prod<Z, ValueSize>, Prod<Z, MetaSize>> + Send + Sync + 'static,
92    EvictorType: EvictionStrategy<ValueSize, Z> + BranchSelector + Send + Sync + 'static,
93    Prod<Z, ValueSize>: ArrayLength<u8> + PartialDiv<U8>,
94    Prod<Z, MetaSize>: ArrayLength<u8> + PartialDiv<U8>,
95{
96    /// The height of the binary tree used for storage
97    height: u32,
98    /// The storage itself
99    storage: StorageType,
100    /// The position map
101    pos: Box<dyn PositionMap + Send + Sync + 'static>,
102    /// The rng
103    rng: RngType,
104    /// The stashed values
105    stash_data: Vec<A64Bytes<ValueSize>>,
106    /// The stashed metadata
107    stash_meta: Vec<A8Bytes<MetaSize>>,
108    /// Our currently checked-out branch if any
109    branch: BranchCheckout<ValueSize, Z>,
110    /// Eviction strategy
111    evictor: EvictorType,
112}
113
114impl<ValueSize, Z, StorageType, RngType, EvictorType>
115    PathORAM<ValueSize, Z, StorageType, RngType, EvictorType>
116where
117    ValueSize: ArrayLength<u8> + PartialDiv<U8> + PartialDiv<U64>,
118    Z: Unsigned + Mul<ValueSize> + Mul<MetaSize>,
119    RngType: RngCore + CryptoRng + Send + Sync + 'static,
120    StorageType: ORAMStorage<Prod<Z, ValueSize>, Prod<Z, MetaSize>> + Send + Sync + 'static,
121    EvictorType: EvictionStrategy<ValueSize, Z> + BranchSelector + Send + Sync + 'static,
122    Prod<Z, ValueSize>: ArrayLength<u8> + PartialDiv<U8>,
123    Prod<Z, MetaSize>: ArrayLength<u8> + PartialDiv<U8>,
124{
125    /// New function creates this ORAM given a position map creator and a
126    /// storage type creator and an Rng creator.
127    /// The main thing that is going on here is, given the size, we are
128    /// determining what the height will be, which will be like log(size) -
129    /// log(bucket_size) Then we are making sure that all the various
130    /// creators use this number.
131    pub fn new<
132        PMC: PositionMapCreator<RngType>,
133        SC: ORAMStorageCreator<Prod<Z, ValueSize>, Prod<Z, MetaSize>, Output = StorageType>,
134        F: FnMut() -> RngType + 'static,
135        EVC: EvictorCreator<ValueSize, Z, Output = EvictorType>,
136    >(
137        size: u64,
138        stash_size: usize,
139        rng_maker: &mut F,
140        evictor_factory: EVC,
141    ) -> Self {
142        assert!(size != 0, "size cannot be zero");
143        assert!(size & (size - 1) == 0, "size must be a power of two");
144        // saturating_sub is used so that creating an ORAM of size 1 or 2 doesn't fail
145        let height = log2_ceil(size).saturating_sub(log2_ceil(Z::U64));
146        // This is 2u64 << height because it must be 2^{h+1}, we have defined
147        // the height of the root to be 0, so in a tree where the lowest level
148        // is h, there are 2^{h+1} nodes.
149        let mut rng = rng_maker();
150        let evictor = evictor_factory.create(height);
151        let storage = SC::create(2u64 << height, &mut rng).expect("Storage failed");
152        let pos = PMC::create(size, height, stash_size, rng_maker);
153        Self {
154            height,
155            storage,
156            pos,
157            rng,
158            stash_data: vec![Default::default(); stash_size],
159            stash_meta: vec![Default::default(); stash_size],
160            branch: Default::default(),
161            evictor,
162        }
163    }
164}
165
166impl<ValueSize, Z, StorageType, RngType, EvictorType> ORAM<ValueSize>
167    for PathORAM<ValueSize, Z, StorageType, RngType, EvictorType>
168where
169    ValueSize: ArrayLength<u8> + PartialDiv<U8> + PartialDiv<U64>,
170    Z: Unsigned + Mul<ValueSize> + Mul<MetaSize>,
171    RngType: RngCore + CryptoRng + Send + Sync + 'static,
172    StorageType: ORAMStorage<Prod<Z, ValueSize>, Prod<Z, MetaSize>> + Send + Sync + 'static,
173    EvictorType: EvictionStrategy<ValueSize, Z> + BranchSelector + Send + Sync + 'static,
174    Prod<Z, ValueSize>: ArrayLength<u8> + PartialDiv<U8>,
175    Prod<Z, MetaSize>: ArrayLength<u8> + PartialDiv<U8>,
176{
177    fn len(&self) -> u64 {
178        self.pos.len()
179    }
180
181    fn stash_size(&self) -> usize {
182        let mut stash_count = 0u64;
183        for idx in 0..self.stash_data.len() {
184            let stash_count_incremented = stash_count + 1;
185            stash_count.cmov(
186                !meta_is_vacant(&self.stash_meta[idx]),
187                &stash_count_incremented,
188            );
189        }
190        stash_count as usize
191    }
192
193    // TODO: We should try implementing a circuit-ORAM like approach also
194    fn access<T, F: FnOnce(&mut A64Bytes<ValueSize>) -> T>(&mut self, key: u64, f: F) -> T {
195        let result: T;
196        // Choose what will be the next (secret) position of this item
197        let new_pos = 1u64.random_child_at_height(self.height, &mut self.rng);
198        // Set the new value and recover the old (current) position.
199        let current_pos = self.pos.write(&key, &new_pos);
200        debug_assert!(current_pos != 0, "position map told us the item is at 0");
201        // Get the branch where we expect to find the item.
202        // NOTE: If we move to a scheme where the tree can be resized dynamically,
203        // then we should checkout at `current_pos.random_child_at_height(self.height)`.
204        debug_assert!(self.branch.leaf == 0);
205        self.branch.checkout(&mut self.storage, current_pos);
206
207        // Fetch the item from branch and then from stash.
208        // Visit it and then insert it into the stash.
209        {
210            debug_assert!(self.branch.leaf == current_pos);
211            let mut meta = A8Bytes::<MetaSize>::default();
212            let mut data = A64Bytes::<ValueSize>::default();
213
214            self.branch
215                .ct_find_and_remove(1.into(), &key, &mut data, &mut meta);
216            details::ct_find_and_remove(
217                1.into(),
218                &key,
219                &mut data,
220                &mut meta,
221                &mut self.stash_data,
222                &mut self.stash_meta,
223            );
224            debug_assert!(
225                meta_block_num(&meta) == &key || meta_is_vacant(&meta).into(),
226                "Hmm, we didn't find the expected item something else"
227            );
228            debug_assert!(self.branch.leaf == current_pos);
229
230            // Call the callback, then store the result
231            result = f(&mut data);
232
233            // Set the block_num in case the item was not initialized yet
234            *meta_block_num_mut(&mut meta) = key;
235            // Set the new leaf destination for the item
236            *meta_leaf_num_mut(&mut meta) = new_pos;
237
238            // Stash the item
239            details::ct_insert(
240                1.into(),
241                &data,
242                &mut meta,
243                &mut self.stash_data,
244                &mut self.stash_meta,
245            );
246            assert!(bool::from(meta_is_vacant(&meta)), "Stash overflow!");
247        }
248
249        // Now do cleanup / eviction on this branch, before checking out
250        self.evictor.evict_from_stash_to_branch(
251            &mut self.stash_data,
252            &mut self.stash_meta,
253            &mut self.branch,
254        );
255
256        debug_assert!(self.branch.leaf == current_pos);
257        self.branch.checkin(&mut self.storage);
258        debug_assert!(self.branch.leaf == 0);
259
260        for _ in 0..self.evictor.get_number_of_additional_branches_to_evict() {
261            let leaf = self.evictor.get_next_branch_to_evict();
262            debug_assert!(leaf != 0);
263            self.branch.checkout(&mut self.storage, leaf);
264            self.evictor.evict_from_stash_to_branch(
265                &mut self.stash_data,
266                &mut self.stash_meta,
267                &mut self.branch,
268            );
269            self.branch.checkin(&mut self.storage);
270            debug_assert!(self.branch.leaf == 0);
271        }
272
273        result
274    }
275}
276
277/// Struct which represents a branch which we have checked out, including its
278/// leaf and the associated data.
279///
280/// This struct is a member of PathORAM and is long lived, so that we don't
281/// call malloc with every checkout.
282///
283/// This is mainly just an organizational thing.
284pub struct BranchCheckout<ValueSize, Z>
285where
286    ValueSize: ArrayLength<u8> + PartialDiv<U8> + PartialDiv<U64>,
287    Z: Unsigned + Mul<ValueSize> + Mul<MetaSize>,
288    Prod<Z, ValueSize>: ArrayLength<u8> + PartialDiv<U8>,
289    Prod<Z, MetaSize>: ArrayLength<u8> + PartialDiv<U8>,
290{
291    /// The leaf of branch that is currently checked-out. 0 if no existing
292    /// checkout.
293    pub(crate) leaf: u64,
294    /// The scratch-space for checked-out branch data
295    pub(crate) data: Vec<A64Bytes<Prod<Z, ValueSize>>>,
296    /// The scratch-space for checked-out branch metadata
297    pub(crate) meta: Vec<A8Bytes<Prod<Z, MetaSize>>>,
298    /// Phantom data for ValueSize
299    _value_size: PhantomData<fn() -> ValueSize>,
300}
301
302impl<ValueSize, Z> Default for BranchCheckout<ValueSize, Z>
303where
304    ValueSize: ArrayLength<u8> + PartialDiv<U8> + PartialDiv<U64>,
305    Z: Unsigned + Mul<ValueSize> + Mul<MetaSize>,
306    Prod<Z, ValueSize>: ArrayLength<u8> + PartialDiv<U8>,
307    Prod<Z, MetaSize>: ArrayLength<u8> + PartialDiv<U8>,
308{
309    fn default() -> Self {
310        Self {
311            leaf: 0,
312            data: Default::default(),
313            meta: Default::default(),
314            _value_size: Default::default(),
315        }
316    }
317}
318
319impl<ValueSize, Z> BranchCheckout<ValueSize, Z>
320where
321    ValueSize: ArrayLength<u8> + PartialDiv<U8> + PartialDiv<U64>,
322    Z: Unsigned + Mul<ValueSize> + Mul<MetaSize>,
323    Prod<Z, ValueSize>: ArrayLength<u8> + PartialDiv<U8>,
324    Prod<Z, MetaSize>: ArrayLength<u8> + PartialDiv<U8>,
325{
326    /// Try to extract an item from the branch
327    pub fn ct_find_and_remove(
328        &mut self,
329        condition: Choice,
330        query: &u64,
331        dest_data: &mut A64Bytes<ValueSize>,
332        dest_meta: &mut A8Bytes<MetaSize>,
333    ) {
334        debug_assert!(self.data.len() == self.meta.len());
335        for idx in 0..self.data.len() {
336            let bucket_data: &mut [A64Bytes<ValueSize>] = self.data[idx].as_mut_aligned_chunks();
337            let bucket_meta: &mut [A8Bytes<MetaSize>] = self.meta[idx].as_mut_aligned_chunks();
338            debug_assert!(bucket_data.len() == Z::USIZE);
339            debug_assert!(bucket_meta.len() == Z::USIZE);
340
341            details::ct_find_and_remove(
342                condition,
343                query,
344                dest_data,
345                dest_meta,
346                bucket_data,
347                bucket_meta,
348            );
349        }
350    }
351
352    /// Try to insert an item into the branch, as low as it can go, consistent
353    /// with the invariant.
354    pub fn ct_insert(
355        &mut self,
356        mut condition: Choice,
357        src_data: &A64Bytes<ValueSize>,
358        src_meta: &mut A8Bytes<MetaSize>,
359    ) {
360        condition &= !meta_is_vacant(src_meta);
361        let lowest_height_legal_index = self.lowest_height_legal_index(*meta_leaf_num(src_meta));
362        Self::insert_into_branch_suffix(
363            condition,
364            src_data,
365            src_meta,
366            lowest_height_legal_index,
367            &mut self.data,
368            &mut self.meta,
369        );
370    }
371
372    /// This is the Path ORAM branch packing procedure, which we implement
373    /// obliviously in a naive way.
374    pub fn pack(&mut self) {
375        debug_assert!(self.leaf != 0);
376        debug_assert!(self.data.len() == self.meta.len());
377        let data_len = self.data.len();
378        for bucket_num in 1..self.data.len() {
379            let (lower_data, upper_data) = self.data.split_at_mut(bucket_num);
380            let (lower_meta, upper_meta) = self.meta.split_at_mut(bucket_num);
381
382            let bucket_data: &mut [A64Bytes<ValueSize>] = upper_data[0].as_mut_aligned_chunks();
383            let bucket_meta: &mut [A8Bytes<MetaSize>] = upper_meta[0].as_mut_aligned_chunks();
384
385            debug_assert!(bucket_data.len() == bucket_meta.len());
386            for idx in 0..bucket_data.len() {
387                let src_data: &mut A64Bytes<ValueSize> = &mut bucket_data[idx];
388                let src_meta: &mut A8Bytes<MetaSize> = &mut bucket_meta[idx];
389
390                // We use the _impl version here because we cannot borrow self
391                // while self.data and self.meta are borrowed
392                let lowest_height_legal_index = Self::lowest_height_legal_index_impl(
393                    *meta_leaf_num(src_meta),
394                    self.leaf,
395                    data_len,
396                );
397                Self::insert_into_branch_suffix(
398                    1.into(),
399                    src_data,
400                    src_meta,
401                    lowest_height_legal_index,
402                    lower_data,
403                    lower_meta,
404                );
405            }
406        }
407        debug_assert!(self.leaf != 0);
408    }
409
410    /// Checkout a branch from storage into ourself
411    pub fn checkout(
412        &mut self,
413        storage: &mut impl ORAMStorage<Prod<Z, ValueSize>, Prod<Z, MetaSize>>,
414        leaf: u64,
415    ) {
416        debug_assert!(self.leaf == 0);
417        self.data
418            .resize_with(leaf.height() as usize + 1, Default::default);
419        self.meta
420            .resize_with(leaf.height() as usize + 1, Default::default);
421        storage.checkout(leaf, &mut self.data, &mut self.meta);
422        self.leaf = leaf;
423    }
424
425    /// Checkin our branch to storage and clear our checkout_leaf
426    pub fn checkin(
427        &mut self,
428        storage: &mut impl ORAMStorage<Prod<Z, ValueSize>, Prod<Z, MetaSize>>,
429    ) {
430        debug_assert!(self.leaf != 0);
431        storage.checkin(self.leaf, &mut self.data, &mut self.meta);
432        self.leaf = 0;
433    }
434
435    /// Given a tree-index value (a node in the tree)
436    /// Compute the lowest height (closest to the leaf) legal index of a bucket
437    /// in this branch into which it can be placed. This depends on the
438    /// common ancestor height of tree_index and self.leaf.
439    ///
440    /// This is required to give well-defined output even if tree_index is 0.
441    /// It is not required to give well-defined output if self.leaf is 0.
442    pub(crate) fn lowest_height_legal_index(&self, query: u64) -> usize {
443        Self::lowest_height_legal_index_impl(query, self.leaf, self.data.len())
444    }
445
446    /// The internal logic of lowest_height_legal_index.
447    /// This stand-alone version is needed to get around the borrow checker,
448    /// because we cannot call functions that take &self as a parameter
449    /// while data or meta are mutably borrowed.
450    pub(crate) fn lowest_height_legal_index_impl(
451        mut query: u64,
452        leaf: u64,
453        data_len: usize,
454    ) -> usize {
455        // Set query to point to root (1) if it is currently 0 (none / vacant)
456        query.cmov(query.ct_eq(&0), &1);
457        debug_assert!(
458            leaf != 0,
459            "this should not be called when there is not currently a checkout"
460        );
461
462        let common_ancestor_height = leaf.common_ancestor_height(&query) as usize;
463        debug_assert!(data_len > common_ancestor_height);
464        data_len - 1 - common_ancestor_height
465    }
466
467    /// Low-level helper function: Insert an item into (a portion of) the branch
468    /// - No inspection of the src_meta is performed
469    /// - The first free spot in a bucket of index >= insert_after_index is used
470    /// - The destination slices need not be the whole branch, they could be a
471    ///   prefix
472    pub(crate) fn insert_into_branch_suffix(
473        condition: Choice,
474        src_data: &A64Bytes<ValueSize>,
475        src_meta: &mut A8Bytes<MetaSize>,
476        insert_after_index: usize,
477        dest_data: &mut [A64Bytes<Prod<Z, ValueSize>>],
478        dest_meta: &mut [A8Bytes<Prod<Z, MetaSize>>],
479    ) {
480        debug_assert!(dest_data.len() == dest_meta.len());
481        for idx in 0..dest_data.len() {
482            details::ct_insert::<ValueSize>(
483                condition & !(idx as u64).ct_lt(&(insert_after_index as u64)),
484                src_data,
485                src_meta,
486                dest_data[idx].as_mut_aligned_chunks(),
487                dest_meta[idx].as_mut_aligned_chunks(),
488            )
489        }
490    }
491}
492
493/// Constant time helper functions
494pub(crate) mod details {
495    use super::*;
496
497    /// ct_find_and_remove tries to find and remove an item with a particular
498    /// block num from a mutable sequence, and store it in dest_data and
499    /// dest_meta.
500    ///
501    /// The condition value that is passed must be true or no move will actually
502    /// happen. When the operation succeeds in finding an item, dest_meta
503    /// will not be vacant and will have the desired block_num, and that
504    /// item will be set vacant in the mutable sequence.
505    ///
506    /// Semantics: If dest is vacant, and condition is true,
507    ///            scan across src and find the first non-vacant item with
508    ///            desired block_num then cmov that to dest.
509    ///            Also set source to vacant.
510    ///
511    /// The whole operation must be constant time.
512    pub fn ct_find_and_remove<ValueSize: ArrayLength<u8>>(
513        mut condition: Choice,
514        query: &u64,
515        dest_data: &mut A64Bytes<ValueSize>,
516        dest_meta: &mut A8Bytes<MetaSize>,
517        src_data: &mut [A64Bytes<ValueSize>],
518        src_meta: &mut [A8Bytes<MetaSize>],
519    ) {
520        debug_assert!(src_data.len() == src_meta.len());
521        for idx in 0..src_meta.len() {
522            // XXX: Must be constant time and not optimized, may need a better barrier here
523            // Maybe just use subtle::Choice
524            let test = condition
525                & (query.ct_eq(meta_block_num(&src_meta[idx])))
526                & !meta_is_vacant(&src_meta[idx]);
527            dest_meta.cmov(test, &src_meta[idx]);
528            dest_data.cmov(test, &src_data[idx]);
529            // Zero out the src[meta] if we moved it
530            meta_set_vacant(test, &mut src_meta[idx]);
531            condition &= !test;
532        }
533    }
534
535    /// ct_insert tries to insert an item into a mutable sequence
536    ///
537    /// It takes the source data and source metadata, (the item being inserted),
538    /// and slices corresponding to the destination data and metadata.
539    /// It also takes a boolean "condition", if the condition is false,
540    /// then all the memory accesses will be done but no side-effects will
541    /// occur.
542    ///
543    /// Semantics: If source is not vacant, and condition is true,
544    ///            scan across destination and find the first vacant slot,
545    ///            then cmov the source to the slot.
546    ///            Also set source to vacant.
547    ///
548    /// The whole operation must be constant time.
549    pub fn ct_insert<ValueSize: ArrayLength<u8>>(
550        mut condition: Choice,
551        src_data: &A64Bytes<ValueSize>,
552        src_meta: &mut A8Bytes<MetaSize>,
553        dest_data: &mut [A64Bytes<ValueSize>],
554        dest_meta: &mut [A8Bytes<MetaSize>],
555    ) {
556        debug_assert!(dest_data.len() == dest_meta.len());
557        condition &= !meta_is_vacant(src_meta);
558        for idx in 0..dest_meta.len() {
559            // XXX: Must be constant time and not optimized, may need a better barrier here
560            // Maybe just use subtle::Choice
561            let test = condition & meta_is_vacant(&dest_meta[idx]);
562            dest_meta[idx].cmov(test, src_meta);
563            dest_data[idx].cmov(test, src_data);
564            meta_set_vacant(test, src_meta);
565            condition &= !test;
566        }
567    }
568}