├── rustfmt.toml ├── .gitattributes ├── .github ├── triage-labeler.yaml ├── auto_assign.yml ├── pull_request_template.md ├── labeler.yml ├── workflows │ ├── issues.yaml │ ├── pr-assign.yaml │ ├── copyright.yaml │ ├── pr.yaml │ └── ci.yaml ├── CODEOWNERS ├── dependabot.yaml └── settings.yml ├── rust-toolchain.toml ├── img └── mobilecoin_logo.png ├── clippy.toml ├── mc-oblivious-traits ├── README.md ├── Cargo.toml └── src │ ├── linear_scanning.rs │ ├── naive_storage.rs │ ├── creators.rs │ └── testing.rs ├── mc-oblivious-ram ├── README.md ├── Cargo.toml └── src │ ├── position_map │ └── mod.rs │ └── path_oram │ └── mod.rs ├── .gitignore ├── test-helper ├── Cargo.toml └── src │ └── lib.rs ├── .markdownlint-cli2.jsonc ├── tools ├── install_git_settings.sh └── lint.sh ├── Cargo.toml ├── mc-oblivious-map ├── src │ └── build_hasher.rs ├── README.md ├── Cargo.toml └── benches │ ├── ingest.rs │ └── view.rs ├── no-asm-tests ├── Cargo.toml ├── README.md ├── src │ ├── insecure_position_map.rs │ └── main.rs └── Cargo.lock ├── .cargo └── config ├── balanced-tree-index ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── aligned-cmov ├── Cargo.toml ├── src │ ├── cmov_impl_no_asm.rs │ ├── cmov_impl_asm.rs │ └── lib.rs ├── README.md └── benches │ └── large_cmov.rs ├── .cruft.json ├── deny.toml ├── CONTRIBUTING.md ├── hooks └── pre-commit ├── CODE_OF_CONDUCT.md ├── CLA.md └── README.md /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | Cargo.lock merge=theirs 2 | -------------------------------------------------------------------------------- /.github/triage-labeler.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | needs-triage: 3 | - '.*' 4 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | --- 2 | addAssignees: author 3 | runOnDraft: true 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = [ "clippy", "rustfmt" ] 4 | -------------------------------------------------------------------------------- /img/mobilecoin_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobilecoinfoundation/mc-oblivious/HEAD/img/mobilecoin_logo.png -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | cognitive-complexity-threshold = 100 2 | type-complexity-threshold = 10000 3 | too-many-arguments-threshold = 13 4 | enum-variant-size-threshold = 10000 5 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Motivation 4 | 5 | 6 | -------------------------------------------------------------------------------- /mc-oblivious-traits/README.md: -------------------------------------------------------------------------------- 1 | # mc-oblivious-traits 2 | 3 | Traits defining interfaces for Oblivious RAM, a backing storage supporting 4 | Oblivious RAM, and Oblivious Maps, in terms of fixed-size chunks of aligned bytes. 5 | -------------------------------------------------------------------------------- /mc-oblivious-ram/README.md: -------------------------------------------------------------------------------- 1 | # mc-oblivious-ram 2 | 3 | This crate provides implementations of Oblivious RAM data structures, suitable 4 | for use in an Intel SGX environment. 5 | 6 | In crate right now: 7 | 8 | - Adaptation of Path ORAM 9 | - Adaptation of Circuit ORAM 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | **/target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | **/Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /test-helper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-helper" 3 | version = "2.3.0" 4 | authors = ["Chris Beck "] 5 | edition = "2018" 6 | license = "GPL-3.0" 7 | 8 | [dependencies] 9 | aligned-array = { version = "1", features = ["subtle"] } 10 | aligned-cmov = { path = "../aligned-cmov", version = "2.3" } 11 | rand_core = { version = "0.6", default-features = false } 12 | rand_hc = "0.3" 13 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rust: 3 | - '**.rs' 4 | - '**/Cargo.lock' 5 | - '**/Cargo.toml' 6 | 7 | github_actions: 8 | - '.github/workflows/**' 9 | - '.github/labeler.yaml' 10 | - '.github/triage-labeler.yaml' 11 | - '.github/auto_assign.yaml' 12 | 13 | python: 14 | - '**.py' 15 | - '**/poetry.lock' 16 | - '**/pyproject.toml' 17 | - '**/requirements.txt' 18 | 19 | javascript: 20 | - '**.js' 21 | -------------------------------------------------------------------------------- /.markdownlint-cli2.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | // Disable some built-in rules 3 | "config": { 4 | "line-length": false, 5 | "no-duplicate-header": { 6 | // Allow multiple "Added" sections in the changelog 7 | "siblings_only": true 8 | } 9 | }, 10 | 11 | // Define glob expressions to use (only valid at root) 12 | "globs": ["**/*.md"], 13 | "ignores": [".github/pull_request_template.md", "target/"] 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/issues.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: issues 3 | 4 | "on": 5 | issues: 6 | types: 7 | - opened 8 | - transferred 9 | 10 | jobs: 11 | triage: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: github/issue-labeler@v3.1 15 | with: 16 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 17 | configuration-path: .github/triage-labeler.yaml 18 | enable-versioned-regex: 0 19 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These lines impact repository security 2 | /.github/CODEOWNERS @cbeck88 @jcape @wjuan-mob 3 | /.github/settings.yml @cbeck88 @jcape @wjuan-mob 4 | 5 | # These lines prevent reviews of trivial changes blocking on particular users 6 | /.gitattributes 7 | /.gitconfig 8 | /.gitignore 9 | /.markdownlint-cli2.jsonrc 10 | /CHANGELOG.md 11 | /Cargo.toml 12 | /Cargo.lock 13 | /LICENSE 14 | /README.md 15 | /deny.toml 16 | /rust-toolchain.toml 17 | /rustfmt.toml 18 | -------------------------------------------------------------------------------- /tools/install_git_settings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -x 4 | 5 | ROOT=`git rev-parse --show-toplevel` 6 | 7 | # symlink the precommit hook into .git/hooks 8 | # see https://stackoverflow.com/questions/4592838/symbolic-link-to-a-hook-in-git 9 | cd "$ROOT/.git/hooks" && ln -s -f "../../hooks/pre-commit" . 10 | 11 | # define a 'theirs' merge driver which uses unix false utility to always choose 12 | # theirs. In .gitattributes we apply this to Cargo.lock files 13 | git config --local merge.theirs.driver false 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "aligned-cmov", 4 | "balanced-tree-index", 5 | "mc-oblivious-map", 6 | "mc-oblivious-ram", 7 | "mc-oblivious-traits", 8 | "test-helper", 9 | ] 10 | 11 | exclude = [ 12 | "no-asm-tests", 13 | ] 14 | 15 | # This is done to enable running larger tests and get better test coverage, 16 | # oblivious RAM is very slow in an opt-level = 0 build. 17 | [profile.test.package.mc-oblivious-ram] 18 | opt-level = 2 19 | debug = 2 20 | debug-assertions = true 21 | -------------------------------------------------------------------------------- /.github/workflows/pr-assign.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: pr-assign 3 | 4 | "on": 5 | pull_request: 6 | types: [opened, ready_for_review] 7 | 8 | jobs: 9 | auto-assign: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | pull-requests: write 13 | steps: 14 | - uses: kentaro-m/auto-assign-action@v1.2.5 15 | - uses: rowi1de/auto-assign-review-teams@v1.1.3 16 | with: 17 | repo-token: ${{ secrets.MEOWBLECOIN_PAT }} 18 | org: "mobilecoinfoundation" 19 | teams: "crypto-eng" 20 | -------------------------------------------------------------------------------- /mc-oblivious-map/src/build_hasher.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | use core::hash::BuildHasher; 4 | use rand_core::{CryptoRng, RngCore}; 5 | use siphasher::sip::SipHasher13; 6 | 7 | pub struct SipBuildHasher { 8 | k0: u64, 9 | k1: u64, 10 | } 11 | 12 | impl BuildHasher for SipBuildHasher { 13 | type Hasher = SipHasher13; 14 | fn build_hasher(&self) -> Self::Hasher { 15 | SipHasher13::new_with_keys(self.k0, self.k1) 16 | } 17 | } 18 | 19 | impl SipBuildHasher { 20 | pub fn from_rng(rng: &mut R) -> Self { 21 | Self { 22 | k0: rng.next_u64(), 23 | k1: rng.next_u64(), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/copyright.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: copyright 3 | 4 | "on": 5 | schedule: 6 | - cron: '0 3 1 1 *' # 03:00 AM on January 1 7 | 8 | jobs: 9 | update-license-year: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: FantasticFiasco/action-update-license-year@v3 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | path: | 20 | **/*.rs 21 | **/*.proto 22 | assignees: '@mobilecoinfoundation/crypto-eng' 23 | labels: "copyright" 24 | transform: (?<=^\/\/ Copyright \(c\) )(?\d{4})?-?(\d{4})? 25 | -------------------------------------------------------------------------------- /no-asm-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "no-asm-tests" 3 | version = "2.3.0" 4 | description = "A crate in another workspace which sets the no_asm_insecure flag and runs tests that way" 5 | authors = ["MobileCoin"] 6 | edition = "2018" 7 | readme = "README.md" 8 | 9 | [workspace] 10 | 11 | [dependencies] 12 | aligned-cmov = { path = "../aligned-cmov", features = ["no_asm_insecure"] } 13 | balanced-tree-index = { path = "../balanced-tree-index" } 14 | mc-oblivious-ram = { path = "../mc-oblivious-ram", features = ["no_asm_insecure"] } 15 | mc-oblivious-traits = { path = "../mc-oblivious-traits", features = ["no_asm_insecure"] } 16 | test-helper = { path = "../test-helper" } 17 | 18 | [dev-dependencies] 19 | GSL = "6.0.0" 20 | -------------------------------------------------------------------------------- /.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-D", "warnings"] 3 | 4 | # Using 'cfg` is broken, see https://github.com/rust-lang/cargo/issues/6858 5 | # [target.'cfg(target_arch = "x86_64")'] 6 | # rustflags = ["-D", "warnings", "-C", "target-cpu=skylake"] 7 | 8 | # ...so instead we list all target triples (Tier 1 64-bit platforms) 9 | 10 | [target.x86_64-unknown-linux-gnu] 11 | rustflags = ["-D", "warnings", "-C", "target-cpu=skylake"] 12 | 13 | [target.x86_64-pc-windows-gnu] 14 | rustflags = ["-D", "warnings", "-C", "target-cpu=skylake"] 15 | 16 | [target.x86_64-pc-windows-msvc] 17 | rustflags = ["-D", "warnings", "-C", "target-cpu=skylake"] 18 | 19 | [target.x86_64-apple-darwin] 20 | rustflags = ["-D", "warnings", "-C", "target-cpu=skylake"] 21 | -------------------------------------------------------------------------------- /balanced-tree-index/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "balanced-tree-index" 3 | version = "2.3.0" 4 | description = "Utilities for constant-time manipulation of a complete binary tree with a flat in-memory representation." 5 | authors = ["MobileCoin"] 6 | license = "GPL-3.0" 7 | edition = "2018" 8 | readme = "README.md" 9 | repository = "https://github.com/mobilecoinofficial/mc-oblivious" 10 | keywords = ["binary-tree", "constant-time", "utilities", "no_std"] 11 | categories = ["cryptography", "data-structures"] 12 | 13 | [dependencies] 14 | aligned-cmov = { path = "../aligned-cmov", version = "2.3" } 15 | 16 | rand_core = { version = "0.6", default-features = false } 17 | 18 | [dev-dependencies] 19 | test-helper = { path = "../test-helper" } 20 | -------------------------------------------------------------------------------- /aligned-cmov/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aligned-cmov" 3 | version = "2.3.0" 4 | description = "Fast constant-time conditional moves of aligned bytes" 5 | authors = ["MobileCoin"] 6 | license = "GPL-3.0" 7 | edition = "2018" 8 | readme = "README.md" 9 | repository = "https://github.com/mobilecoinofficial/mc-oblivious" 10 | keywords = ["aligned", "alignment", "cryptography", "constant-time", "utilities"] 11 | categories = ["embedded", "memory-management", "cryptography", "no-std"] 12 | 13 | [features] 14 | no_asm_insecure = [] 15 | 16 | [dependencies] 17 | aligned-array = { version = "1", features = ["subtle"] } 18 | generic-array = { version = "0.14", default-features = false } 19 | 20 | [dev-dependencies] 21 | criterion = "0.5" 22 | 23 | [[bench]] 24 | name = "large_cmov" 25 | harness = false 26 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: pr 3 | 4 | "on": 5 | pull_request: 6 | 7 | jobs: 8 | update-metadata: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | pull-requests: write 13 | steps: 14 | - uses: pascalgn/size-label-action@v0.5.0 15 | env: 16 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 17 | IGNORED: "Cargo.lock" 18 | INPUT_SIZES: > 19 | { 20 | "0": "XS", 21 | "30": "S", 22 | "100": "M", 23 | "250": "L", 24 | "500": "XL", 25 | "1000": "XXL", 26 | "1500": "OHLAWDHECOMIN" 27 | } 28 | - uses: actions/labeler@v4 29 | with: 30 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 31 | -------------------------------------------------------------------------------- /tools/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2018-2022 The MobileCoin Foundation 4 | 5 | set -e # exit on error 6 | 7 | if [[ ! -z "$1" ]]; then 8 | cd "$1" 9 | fi 10 | 11 | # We want to check with --all-targets since it checks test code, but that flag 12 | # leads to build errors in enclave workspaces, so check it here. 13 | cargo clippy --all --all-features --all-targets 14 | 15 | cargo install cargo-sort 16 | 17 | for toml in $(grep --exclude-dir cargo --exclude-dir rust-mbedtls --include=Cargo.toml -r . -e '\[workspace\]' | cut -d: -f1); do 18 | pushd $(dirname $toml) >/dev/null 19 | echo "Linting in $PWD" 20 | cargo sort --workspace --grouped --check 21 | cargo fmt -- --check 22 | cargo clippy --all --all-features 23 | echo "Linting in $PWD complete." 24 | popd >/dev/null 25 | done 26 | -------------------------------------------------------------------------------- /mc-oblivious-traits/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mc-oblivious-traits" 3 | version = "2.3.0" 4 | description = "Traits and interfaces for components related to Oblivious data structures" 5 | authors = ["MobileCoin"] 6 | license = "GPL-3.0" 7 | edition = "2018" 8 | readme = "README.md" 9 | repository = "https://github.com/mobilecoinofficial/mc-oblivious" 10 | keywords = ["cryptography", "crypto", "constant-time", "oblivious-ram"] 11 | categories = ["cryptography", "data-structures", "no-std"] 12 | 13 | [features] 14 | no_asm_insecure = ["aligned-cmov/no_asm_insecure"] 15 | 16 | [dependencies] 17 | aligned-cmov = { path = "../aligned-cmov", version = "2.3" } 18 | balanced-tree-index = { path = "../balanced-tree-index", version = "2.3" } 19 | 20 | # third party 21 | rand_core = { version = "0.6", default-features = false } 22 | -------------------------------------------------------------------------------- /mc-oblivious-ram/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mc-oblivious-ram" 3 | version = "2.3.0" 4 | description = "Implementations of Oblivious RAM data structures" 5 | authors = ["MobileCoin"] 6 | license = "GPL-3.0" 7 | edition = "2018" 8 | readme = "README.md" 9 | repository = "https://github.com/mobilecoinofficial/mc-oblivious" 10 | keywords = ["cryptography", "crypto", "constant-time", "oblivious-ram"] 11 | categories = ["cryptography", "data-structures", "no-std"] 12 | 13 | [features] 14 | no_asm_insecure = ["aligned-cmov/no_asm_insecure"] 15 | 16 | [dependencies] 17 | aligned-cmov = { path = "../aligned-cmov", version = "2.3" } 18 | balanced-tree-index = { path = "../balanced-tree-index", version = "2.3" } 19 | mc-oblivious-traits = { path = "../mc-oblivious-traits", version = "2.3" } 20 | 21 | # third party 22 | rand_core = { version = "0.6", default-features = false } 23 | 24 | [dev-dependencies] 25 | test-helper = { path = "../test-helper" } 26 | yare = "1.0.2" 27 | -------------------------------------------------------------------------------- /no-asm-tests/README.md: -------------------------------------------------------------------------------- 1 | no-asm-tests 2 | ============ 3 | 4 | Tests that should only run with the `no-asm-insecure` feature on. 5 | 6 | This crate must not be part of the rest of the workspace, to prevent feature 7 | unification. 8 | 9 | - Tests related to measuring when stash overflow occurs in an ORAM implementation. 10 | This is needed to know how to tune the stash size. 11 | - Tests that would be much too slow to run even in release mode. 12 | By turning off the cmov-related asm for some tests, we allow for greater test coverage, 13 | which is still testing the logic of the data structure itself. 14 | 15 | TODO: Right now, stash-overflow tests are based on driving the ORAM with small stash sizes 16 | until it overflows, and cathing the panic. A better way to collect data would be to 17 | introduce an API that allows to monitor the stash size. This API should never be used 18 | in production, but it would be okay for purposes of these diagnostics, so it could 19 | be gated on the `no-asm-insecure` flag. 20 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: cargo 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | commit-message: 9 | prefix: "chore(deps)" 10 | reviewers: 11 | - "mobilecoinfoundation/crypto-eng" 12 | 13 | - package-ecosystem: cargo 14 | directory: "/no-asm-tests/" 15 | schedule: 16 | interval: daily 17 | commit-message: 18 | prefix: "chore(deps)" 19 | reviewers: 20 | - "mobilecoinfoundation/crypto-eng" 21 | 22 | - package-ecosystem: github-actions 23 | directory: "/" 24 | schedule: 25 | interval: daily 26 | commit-message: 27 | prefix: "chore(deps)" 28 | reviewers: 29 | - "mobilecoinfoundation/crypto-eng" 30 | ignore: 31 | # See https://github.com/dtolnay/rust-toolchain/issues/45 tags aren't used 32 | # by rust-toolchain. Using a branch name will cause dependabot to suggest 33 | # which ever branch happens to be the newest 34 | - dependency-name: "rust-toolchain" 35 | -------------------------------------------------------------------------------- /mc-oblivious-map/README.md: -------------------------------------------------------------------------------- 1 | # mc-oblivious-map 2 | 3 | This crate provides an implementation of an oblivious hashmap on top of oblivious RAM, 4 | meeting the requirements in the trait described in `mc-oblivious-traits`. 5 | 6 | In crate right now: 7 | 8 | - An implementation of Cuckoo hashing with buckets, using Oblivious RAM as the 9 | cuckoo hashing arena. 10 | See [wikipedia](https://en.wikipedia.org/wiki/Cuckoo_hashing) for background. 11 | This is close to or the same as CUCKOO-DISJOINT algorithm described by 12 | [this paper](https://arxiv.org/pdf/1104.5400.pdf), except for the use of Oblivious RAM. 13 | The `access-or-insert` method is novel in this work, see code comments for discussion. 14 | 15 | For more background, see also "power of two choices" hashing (ABKU99, Mitzenmacher). 16 | And [wikipedia](https://en.wikipedia.org/wiki/2-choice_hashing) for additional background. 17 | This is conceptually an ancestor of cuckoo hashing. The main reason to use this, or cuckoo 18 | hashing, in our context, is that it guarantees that reads make exactly two accesses to the 19 | table, which makes the constant-time property easy to verify. Cuckoo hashing achieves good 20 | memory utilization, better than "power of two choices", which is what we tried first. 21 | -------------------------------------------------------------------------------- /mc-oblivious-traits/src/linear_scanning.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | //! This module defines a naive, linear-scanning ORAM 4 | 5 | use super::*; 6 | use alloc::vec; 7 | 8 | pub struct LinearScanningORAM> { 9 | data: Vec>, 10 | } 11 | 12 | impl> LinearScanningORAM { 13 | pub fn new(size: u64) -> Self { 14 | Self { 15 | data: vec![Default::default(); size as usize], 16 | } 17 | } 18 | } 19 | 20 | impl> ORAM for LinearScanningORAM { 21 | fn len(&self) -> u64 { 22 | self.data.len() as u64 23 | } 24 | 25 | fn stash_size(&self) -> usize { 26 | 0 27 | } 28 | 29 | fn access) -> T>(&mut self, query: u64, f: F) -> T { 30 | let mut temp: A64Bytes = Default::default(); 31 | for idx in 0..self.data.len() { 32 | temp.cmov((idx as u64).ct_eq(&query), &self.data[idx]); 33 | } 34 | let result = f(&mut temp); 35 | for idx in 0..self.data.len() { 36 | self.data[idx].cmov((idx as u64).ct_eq(&query), &temp); 37 | } 38 | result 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /mc-oblivious-map/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mc-oblivious-map" 3 | version = "2.3.0" 4 | description = "Implementation of Oblivious Hash Map data structures on top of Oblivious RAM" 5 | authors = ["MobileCoin"] 6 | license = "GPL-3.0" 7 | edition = "2018" 8 | readme = "README.md" 9 | repository = "https://github.com/mobilecoinofficial/mc-oblivious" 10 | keywords = ["cryptography", "crypto", "constant-time", "oblivious-ram"] 11 | categories = ["cryptography", "data-structures", "no-std"] 12 | 13 | [features] 14 | no_asm_insecure = ["aligned-cmov/no_asm_insecure"] 15 | 16 | [dependencies] 17 | aligned-array = { version = "1", features = ["subtle"] } 18 | aligned-cmov = { path = "../aligned-cmov", version = "2.3" } 19 | mc-oblivious-traits = { path = "../mc-oblivious-traits", version = "2.3" } 20 | 21 | #third party 22 | generic-array = { version = "0.14", default-features = false } 23 | rand_core = { version = "0.6", default-features = false } 24 | siphasher = "0.3" 25 | 26 | [dev-dependencies] 27 | criterion = { version = "0.5", features = ["html_reports"] } 28 | mc-oblivious-ram = { path = "../mc-oblivious-ram", version = "2.3" } 29 | mc-rand = { version = "1" } 30 | test-helper = { path = "../test-helper" } 31 | 32 | [[bench]] 33 | name = "ingest" 34 | harness = false 35 | 36 | [[bench]] 37 | name = "view" 38 | harness = false 39 | -------------------------------------------------------------------------------- /.cruft.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "https://github.com/mobilecoinfoundation/cookiecutters", 3 | "commit": "0c90944d70114c1b74944a935e800aef2f6c9f09", 4 | "checkout": null, 5 | "context": { 6 | "cookiecutter": { 7 | "_copy_without_render": [ 8 | ".github/workflows/*" 9 | ], 10 | "repo_name": "mc-oblivious", 11 | "github_username": "@cbeck88", 12 | "owners": "@cbeck88 @jcape @wjuan-mob", 13 | "type": "workspace", 14 | "license": "GPL-3.0-only", 15 | "arch": "x86_64", 16 | "workspace_description": "ORAM and related for Intel SGX enclaves", 17 | "workspace_readme_title": "mc-oblivious: Traits and implementations for Oblivious RAM inside of Intel SGX enclaves.", 18 | "crate_name": "mc-oblivious-ram", 19 | "crate_namespace_prefix": "mc-", 20 | "crate_description": "Implementations of Oblivious RAM data structures", 21 | "crate_readme_title": "This crate provides implementations of Oblivious RAM data structures, suitable", 22 | "crate_sub_dir": "for use in an Intel SGX environment.", 23 | "version": "2.2.0", 24 | "crate_keywords": "\"cryptography\", \"crypto\", \"constant-time\", \"oblivious-ram\"", 25 | "crate_categories": "\"cryptography\", \"data-structures\", \"no-std\"", 26 | "_template": "https://github.com/mobilecoinfoundation/cookiecutters" 27 | } 28 | }, 29 | "directory": "rust/repo" 30 | } 31 | -------------------------------------------------------------------------------- /aligned-cmov/src/cmov_impl_no_asm.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | //! Naive implementation of cmov using a branch 4 | //! This is not secure, and is meant for testing the *correctness* of large 5 | //! orams quickly. 6 | 7 | use super::{A64Bytes, A8Bytes, ArrayLength}; 8 | 9 | #[inline] 10 | pub fn cmov_u32(condition: bool, src: &u32, dest: &mut u32) { 11 | if condition { 12 | *dest = *src 13 | } 14 | } 15 | 16 | #[inline] 17 | pub fn cmov_u64(condition: bool, src: &u64, dest: &mut u64) { 18 | if condition { 19 | *dest = *src 20 | } 21 | } 22 | 23 | #[inline] 24 | pub fn cmov_i32(condition: bool, src: &i32, dest: &mut i32) { 25 | if condition { 26 | *dest = *src 27 | } 28 | } 29 | 30 | #[inline] 31 | pub fn cmov_i64(condition: bool, src: &i64, dest: &mut i64) { 32 | if condition { 33 | *dest = *src 34 | } 35 | } 36 | 37 | #[inline] 38 | pub fn cmov_usize(condition: bool, src: &usize, dest: &mut usize) { 39 | if condition { 40 | *dest = *src 41 | } 42 | } 43 | 44 | #[inline] 45 | pub fn cmov_a8_bytes>(condition: bool, src: &A8Bytes, dest: &mut A8Bytes) { 46 | if condition { 47 | *dest = src.clone() 48 | } 49 | } 50 | 51 | #[inline] 52 | pub fn cmov_a64_bytes>( 53 | condition: bool, 54 | src: &A64Bytes, 55 | dest: &mut A64Bytes, 56 | ) { 57 | if condition { 58 | *dest = src.clone() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | targets = [] 2 | 3 | [advisories] 4 | db-path = "~/.cargo/advisory-db" 5 | db-urls = ["https://github.com/rustsec/advisory-db"] 6 | vulnerability = "deny" 7 | unmaintained = "deny" 8 | unsound = "deny" 9 | yanked = "deny" 10 | notice = "warn" 11 | # criterion depends on atty, which is unmaintained, but criterion is just for tests 12 | ignore = ["RUSTSEC-2021-0145"] 13 | 14 | [licenses] 15 | unlicensed = "deny" 16 | allow = [ 17 | "Apache-2.0", 18 | "Apache-2.0 WITH LLVM-exception", 19 | "BSD-3-Clause", 20 | "ISC", 21 | "MIT", 22 | "Unicode-DFS-2016", 23 | "GPL-3.0", 24 | "LGPL-3.0", 25 | ] 26 | deny = [] 27 | copyleft = "allow" 28 | allow-osi-fsf-free = "both" 29 | default = "deny" 30 | confidence-threshold = 0.8 31 | exceptions = [] 32 | 33 | [bans] 34 | multiple-versions = "warn" 35 | # Lint level for when a crate version requirement is `*` 36 | wildcards = "warn" 37 | highlight = "all" 38 | allow = [] 39 | deny = [ 40 | # https://github.com/briansmith/ring/issues/774 41 | { name = "ring" }, 42 | ] 43 | skip = [ 44 | # Workaround for path only dependencies, 45 | # https://github.com/EmbarkStudios/cargo-deny/issues/241 46 | { name = "test-helper" }, 47 | ] 48 | skip-tree = [ 49 | { name = "test-helper" }, 50 | ] 51 | 52 | [sources] 53 | unknown-registry = "warn" 54 | unknown-git = "warn" 55 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 56 | allow-git = [] 57 | 58 | [sources.allow-org] 59 | github = [] 60 | gitlab = [] 61 | bitbucket = [] 62 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Contributing Bug Reports 4 | 5 | MobileCoin is a prototype that is being actively developed. 6 | 7 | `mc-oblivious` was created to meet the needs of *MobileCoin Fog*, to be used in SGX enclaves in fog servers. 8 | 9 | `mc-oblivious` is, as far as we know, the *first* oblivious RAM implementation world-wide 10 | that has deployed to production and serve real user requests. 11 | 12 | Please report issues to [mc-oblivious/issues](https://github.com/mobilecoinfoundation/mc-oblivious/issues). 13 | 14 | 1. Search both open and closed tickets to make sure your bug report is not a duplicate. 15 | 1. Do not use github issues as a forum. To participate in community discussions, please use the community forum at [community.mobilecoin.foundation](https://community.mobilecoin.foundation), or join us at discord. 16 | 17 | ## Pull Requests (PRs) 18 | 19 | Pull requests are welcome! 20 | 21 | Oblivious RAM is complex and we are actively working to improve the quality of our implementation. 22 | There are many exciting avenues of research especially to improve performance. 23 | 24 | If you plan to open a pull request, please install the code formatting git hooks, so that your code will be formatted when you open the PR. 25 | 26 | Also, note the `.cargo/config` file which sets `target-cpu=skylake`. If you have an older CPU you may need to change or override this 27 | when you develop locally. 28 | 29 | Coming soon: Code Style Guidelines 30 | 31 | ## Sign the Contributor License Agreement (CLA) 32 | 33 | You will need to sign [our CLA](./CLA.md) before your pull request can be merged. Please email [cla@mobilecoin.com](mailto://cla@mobilecoin.com) and we will send you a copy. 34 | 35 | ## Get in Touch 36 | 37 | We're friendly. Feel free to [ping us](mailto://oram@mobilecoin.com)! 38 | -------------------------------------------------------------------------------- /test-helper/src/lib.rs: -------------------------------------------------------------------------------- 1 | use aligned_cmov::{A64Bytes, A8Bytes, ArrayLength}; 2 | pub use rand_core::{CryptoRng, RngCore, SeedableRng}; 3 | use rand_hc::Hc128Rng; 4 | type Seed = ::Seed; 5 | 6 | const NUM_TRIALS: usize = 3; 7 | 8 | // Sometimes you need to have the type in scope to call trait functions 9 | pub type RngType = Hc128Rng; 10 | 11 | // Helper for running a unit test that requires randomness, but doing it 12 | // seeded and deterministically 13 | pub fn run_with_several_seeds(mut f: F) { 14 | for seed in &get_seeds() { 15 | f(RngType::from_seed(*seed)); 16 | } 17 | } 18 | 19 | pub fn run_with_one_seed(f: F) { 20 | f(get_seeded_rng()); 21 | } 22 | 23 | // TODO(chris): Can we store the result of this function in a const somehow? 24 | fn get_seeds() -> [Seed; NUM_TRIALS] { 25 | let mut rng = get_seeded_rng(); 26 | 27 | let mut result = [[0u8; 32]; NUM_TRIALS]; 28 | for bytes in &mut result[..] { 29 | rng.fill_bytes(bytes) 30 | } 31 | result 32 | } 33 | 34 | pub fn get_seeded_rng() -> RngType { 35 | RngType::from_seed([7u8; 32]) 36 | } 37 | 38 | /// Make a8-bytes that are initialized to a particular byte value 39 | /// This makes tests shorter to write 40 | pub fn a8_8>(src: u8) -> A8Bytes { 41 | let mut result = A8Bytes::::default(); 42 | for byte in result.as_mut_slice() { 43 | *byte = src; 44 | } 45 | result 46 | } 47 | 48 | /// Make a64-bytes that are initialized to a particular byte value 49 | /// This makes tests shorter to write 50 | pub fn a64_8>(src: u8) -> A64Bytes { 51 | let mut result = A64Bytes::::default(); 52 | for byte in result.iter_mut() { 53 | *byte = src; 54 | } 55 | result 56 | } 57 | -------------------------------------------------------------------------------- /no-asm-tests/src/insecure_position_map.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | use std::vec; 4 | 5 | use balanced_tree_index::TreeIndex; 6 | use mc_oblivious_traits::{PositionMap, PositionMapCreator}; 7 | use std::vec::Vec; 8 | use test_helper::{CryptoRng, RngCore}; 9 | 10 | /// An insecure position map implemented via direct lookup 11 | /// Positions are represented as 32 bytes inside a page. 12 | pub struct InsecurePositionMap { 13 | data: Vec, 14 | height: u32, 15 | rng: R, 16 | } 17 | 18 | impl InsecurePositionMap { 19 | /// Create trivial position map 20 | pub fn new(size: u64, height: u32, rng_maker: &mut impl FnMut() -> R) -> Self { 21 | assert!( 22 | height < 32, 23 | "Can't use u32 position map when height of tree exceeds 31" 24 | ); 25 | Self { 26 | data: vec![0u32; size as usize], 27 | height, 28 | rng: rng_maker(), 29 | } 30 | } 31 | } 32 | 33 | impl PositionMap for InsecurePositionMap { 34 | fn len(&self) -> u64 { 35 | self.data.len() as u64 36 | } 37 | fn write(&mut self, key: &u64, new_val: &u64) -> u64 { 38 | let old_val = self.data[*key as usize]; 39 | self.data[*key as usize] = *new_val as u32; 40 | 41 | (if old_val == 0 { 42 | 1u32.random_child_at_height(self.height, &mut self.rng) 43 | } else { 44 | old_val 45 | }) as u64 46 | } 47 | } 48 | 49 | pub struct InsecurePositionMapCreator { 50 | _r: core::marker::PhantomData R>, 51 | } 52 | 53 | impl PositionMapCreator 54 | for InsecurePositionMapCreator 55 | { 56 | fn create R>( 57 | size: u64, 58 | height: u32, 59 | _stash_size: usize, 60 | rng_maker: &mut M, 61 | ) -> Box { 62 | Box::new(InsecurePositionMap::::new(size, height, rng_maker)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /mc-oblivious-map/benches/ingest.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | use aligned_cmov::{typenum, A8Bytes}; 4 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 5 | use mc_oblivious_map::{CuckooHashTable, CuckooHashTableCreator}; 6 | use mc_oblivious_ram::PathORAM4096Z4Creator; 7 | use mc_oblivious_traits::{HeapORAMStorageCreator, OMapCreator, ORAMCreator, ObliviousHashMap}; 8 | use mc_rand::McRng; 9 | use std::time::Duration; 10 | use test_helper::a8_8; 11 | use typenum::{U1024, U32}; 12 | 13 | type ORAMCreatorZ4 = PathORAM4096Z4Creator; 14 | type PathORAMZ4 = >::Output; 15 | type Table = CuckooHashTable; 16 | type CuckooCreatorZ4 = CuckooHashTableCreator; 17 | 18 | fn make_omap(capacity: u64) -> Table { 19 | CuckooCreatorZ4::create(capacity, 32, || McRng {}) 20 | } 21 | 22 | pub fn path_oram_4096_z4_1mil_ingest_write(c: &mut Criterion) { 23 | let mut omap = make_omap(1024u64 * 1024u64); 24 | 25 | let key: A8Bytes = a8_8(1); 26 | let val: A8Bytes = a8_8(2); 27 | 28 | c.bench_function("capacity 1 million vartime write", |b| { 29 | b.iter(|| omap.vartime_write(&key, &val, 1.into())) 30 | }); 31 | } 32 | 33 | pub fn path_oram_4096_z4_1mil_ingest_write_progressive(c: &mut Criterion) { 34 | let mut omap = make_omap(1024u64 * 1024u64); 35 | 36 | let mut key: A8Bytes = a8_8(1); 37 | let val: A8Bytes = a8_8(2); 38 | 39 | let mut temp = 0u64; 40 | 41 | c.bench_function("capacity 1 million vartime write progressive", |b| { 42 | b.iter(|| { 43 | key[0..8].copy_from_slice(&black_box(temp).to_le_bytes()); 44 | temp += 1; 45 | omap.vartime_write(&key, &val, 1.into()) 46 | }) 47 | }); 48 | } 49 | 50 | pub fn path_oram_4096_z4_16mil_ingest_write(c: &mut Criterion) { 51 | let mut omap = make_omap(16 * 1024u64 * 1024u64); 52 | 53 | let key: A8Bytes = a8_8(1); 54 | let val: A8Bytes = a8_8(2); 55 | 56 | c.bench_function("capacity 16 million vartime write", |b| { 57 | b.iter(|| omap.vartime_write(&key, &val, 1.into())) 58 | }); 59 | } 60 | 61 | criterion_group! { 62 | name = path_oram_4096_z4; 63 | config = Criterion::default().measurement_time(Duration::new(10, 0)); 64 | targets = path_oram_4096_z4_1mil_ingest_write, path_oram_4096_z4_1mil_ingest_write_progressive, path_oram_4096_z4_16mil_ingest_write 65 | } 66 | criterion_main!(path_oram_4096_z4); 67 | -------------------------------------------------------------------------------- /mc-oblivious-map/benches/view.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | use aligned_cmov::{typenum, A8Bytes}; 4 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 5 | use mc_oblivious_map::{CuckooHashTable, CuckooHashTableCreator}; 6 | use mc_oblivious_ram::PathORAM4096Z4Creator; 7 | use mc_oblivious_traits::{HeapORAMStorageCreator, OMapCreator, ORAMCreator, ObliviousHashMap}; 8 | use mc_rand::McRng; 9 | use std::time::Duration; 10 | use test_helper::a8_8; 11 | use typenum::{U1024, U16, U240}; 12 | 13 | type ORAMCreatorZ4 = PathORAM4096Z4Creator; 14 | type PathORAMZ4 = >::Output; 15 | type Table = CuckooHashTable; 16 | type CuckooCreatorZ4 = CuckooHashTableCreator; 17 | 18 | fn make_omap(capacity: u64) -> Table { 19 | CuckooCreatorZ4::create(capacity, 32, || McRng {}) 20 | } 21 | 22 | pub fn path_oram_4096_z4_1mil_view_write(c: &mut Criterion) { 23 | let mut omap = make_omap(1024u64 * 1024u64); 24 | 25 | let key: A8Bytes = a8_8(1); 26 | let val: A8Bytes = a8_8(2); 27 | 28 | c.bench_function("capacity 1 million vartime write", |b| { 29 | b.iter(|| omap.vartime_write(&key, &val, 1.into())) 30 | }); 31 | } 32 | 33 | pub fn path_oram_4096_z4_1mil_view_write_progressive(c: &mut Criterion) { 34 | let mut omap = make_omap(1024u64 * 1024u64); 35 | 36 | let mut key: A8Bytes = a8_8(1); 37 | let val: A8Bytes = a8_8(2); 38 | 39 | let mut temp = 0u64; 40 | 41 | c.bench_function("capacity 1 million vartime write progressive", |b| { 42 | b.iter(|| { 43 | key[0..8].copy_from_slice(&black_box(temp).to_le_bytes()); 44 | temp += 1; 45 | omap.vartime_write(&key, &val, 1.into()) 46 | }) 47 | }); 48 | } 49 | 50 | // This is too expensive to run on my laptop for now, the OS kills it 51 | pub fn path_oram_4096_z4_16mil_view_write(c: &mut Criterion) { 52 | let mut omap = make_omap(16 * 1024u64 * 1024u64); 53 | 54 | let key: A8Bytes = a8_8(1); 55 | let val: A8Bytes = a8_8(2); 56 | 57 | c.bench_function("capacity 16 million vartime write", |b| { 58 | b.iter(|| omap.vartime_write(&key, &val, 1.into())) 59 | }); 60 | } 61 | 62 | criterion_group! { 63 | name = path_oram_4096_z4; 64 | config = Criterion::default().measurement_time(Duration::new(10, 0)); 65 | targets = path_oram_4096_z4_1mil_view_write, path_oram_4096_z4_1mil_view_write_progressive, //path_oram_4096_z4_16mil_view_write 66 | } 67 | criterion_main!(path_oram_4096_z4); 68 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | repository: 3 | name: "mc-oblivious" 4 | description: "ORAM and related for Intel SGX enclaves" 5 | homepage: https://mobilecoin.foundation/ 6 | topics: "cryptography, no-std" 7 | private: false 8 | has_issues: true 9 | has_projects: false 10 | has_wiki: true 11 | has_downloads: false 12 | default_branch: master 13 | allow_squash_merge: true 14 | allow_merge_commit: false 15 | allow_rebase_merge: false 16 | allow_auto_merge: true 17 | delete_branch_on_merge: true 18 | use_squash_pr_title_as_default: true 19 | enable_automated_security_fixes: true 20 | enable_vulnerability_alerts: true 21 | 22 | labels: 23 | - name: dependencies 24 | color: '#0366d6' 25 | description: Pull requests that update a dependency file 26 | 27 | - name: github_actions 28 | color: '#000000' 29 | description: Pull requests that update github actions 30 | - name: go 31 | color: '#29beb0' 32 | description: Pull requests that update golang code 33 | - name: javascript 34 | color: '#f0db4f' 35 | description: Pull requests that update javascript code 36 | - name: python 37 | color: '#4584b6' 38 | description: Pull requests that update python code 39 | - name: rust 40 | color: '#f74c00' 41 | description: Pull requests that update rust code 42 | 43 | - name: size/XS 44 | color: '#00ed01' 45 | description: Extra-Small PRs 46 | - name: size/S 47 | color: '#3af901' 48 | description: Small PRs 49 | - name: size/M 50 | color: '#cefb02' 51 | description: Medium-sized PRs 52 | - name: size/L 53 | color: '#ffde40' 54 | description: Large PRs 55 | - name: size/XL 56 | color: '#ff9100' 57 | description: Extra-Large PRs 58 | - name: size/XXL 59 | color: '#f24d11' 60 | description: Double-wide PRs 61 | - name: size/OHLAWDHECOMIN 62 | color: '#ed1717' 63 | description: PRs that should get broken down 64 | 65 | collaborators: 66 | - username: meowblecoinbot 67 | permission: triage 68 | - username: cbeck88 69 | permission: admin 70 | - username: jcape 71 | permission: admin 72 | - username: wjuan-mob 73 | permission: maintain 74 | - username: nick-mobilecoin 75 | permission: maintain 76 | 77 | teams: 78 | - name: coredev 79 | permission: push 80 | - name: crypto-eng 81 | permission: push 82 | 83 | branches: 84 | - name: master 85 | # https://docs.github.com/en/rest/branches/branch-protection?apiVersion=2022-11-28 86 | protection: 87 | required_pull_request_reviews: 88 | require_code_owner_reviews: true 89 | required_approving_review_count: 1 90 | require_last_push_approval: false 91 | required_conversation_resolution: false 92 | required_status_checks: 93 | strict: false 94 | contexts: 95 | - lint 96 | - "deny (bans licenses sources)" 97 | - sort 98 | - "clippy (stable)" 99 | - "build (nightly-2023-03-23)" 100 | - "build (nightly-2023-03-23, --release)" 101 | - "test (nightly-2023-03-23)" 102 | - "test (nightly-2023-03-23, --release)" 103 | - coverage 104 | enforce_admins: false 105 | required_linear_history: true 106 | restrictions: null 107 | -------------------------------------------------------------------------------- /balanced-tree-index/README.md: -------------------------------------------------------------------------------- 1 | # balanced-tree-index 2 | 3 | This crate holds a very small amount of code related to the following fundamental idea: 4 | 5 | In "conventional" data-structures, a binary tree is created using pointers, and is 6 | kept "approximately" balanced using e.g. red-black tree self-balancing strategies. 7 | 8 | However, when the binary tree has fixed size and will not be dynamically added to 9 | or restructured, there is a simpler way of mapping the nodes to memory, which is 10 | more compact and has better locality. 11 | 12 | In this mapping, the internal nodes and leaves of the binary tree are numbered 13 | in order of height, starting from 1, up to 2^n. 14 | 15 | ```rust 16 | 1 17 | 2 3 18 | 4 5 6 7 19 | 8 9 10 11 12 13 14 15 20 | ``` 21 | 22 | Then, the data for the nodes are stored in an array of 2^n elements. 23 | This means that you don't use `malloc` on a per-node basis and can directly 24 | use a block storage interface to access the node members. 25 | 26 | This works very well for ORAM because ORAM always uses a complete balanced binary tree. 27 | 28 | In this scheme, it is easy to find the parent, left, or right child of a nodes, 29 | using bit manipulations, which are fast and constant-time. 30 | 31 | ```rust 32 | parent(x) := x >> 1 33 | left(x) := x << 1 34 | right(x) := (x << 1) + 1 35 | ``` 36 | 37 | In this scheme, the `0` value is unused, so it can be used as a sentinel value. 38 | 39 | As an alternative way to understand the scheme, consider the binary representation of 40 | a number. 41 | 42 | ```rust 43 | 0 0 0 1 0 1 1 0 1 44 | ``` 45 | 46 | The position of the highest-order 1 digit indicates the height of the node in the tree. 47 | Since there are 5 digits after it in this example, this node is exactly 5 steps away 48 | from the root. 49 | 50 | By reading off the remaining digits, we can read off the path to reach this number: 51 | First go left, then right, then right, then left, then right. 52 | 53 | ## Additional Operations 54 | 55 | There are a few additional handy operations that we can do easily in constant time 56 | with this scheme, that we need to do in ORAM. 57 | 58 | - Height of node in the tree can be computed by counting leading zeros of its index. Intel provides constant-time operations for this. 59 | - For two nodes at the same height, we can compute the height of their common ancestor by finding the left-most bit position in which they differ. 60 | This corresponds exactly to the first time that their paths from the root departed. 61 | - For two nodes at a different height, we can still compute the height of their common ancestor, but we first need to take the parent of the 62 | node that is deeper until we get to the same level, or, pad the one that is higher in tree with random bits until they match. 63 | Taking a random child of a node at a particular height is useful for a lot of reasons anyways. 64 | 65 | The scope of this crate is to provide a trait and implementations of these functionalities and related, 66 | on u32 and u64 integer types, for use in ORAM implementations. This is a separate crate so that the 67 | code can be shared, and also because it might be used in the ORAM memory engine. 68 | 69 | There are some other nice properties of the scheme: 70 | 71 | - If a level is added to the tree, the old indices don't become invalid, they just continue on. 72 | - Promoting from a u32 to a u64 doesn't break anything and the bit operations continue to work as before basically. 73 | 74 | ## Constant-time 75 | 76 | This code is meant to be used to implement tree-based ORAM algorithms like Path ORAM. In some cases it is important 77 | that one of these operations is constant-time. When it is necessary that the code provides this property, we document this. 78 | Since the scope of `mc-oblivious` is to focus on SGX enclaves, we only care about x86-64 architecture for this. 79 | -------------------------------------------------------------------------------- /no-asm-tests/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "GSL" 7 | version = "6.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "c9becaf6d7d1ba36a457288e661fa6a0472e8328629276f45369eafcd48ef1ce" 10 | dependencies = [ 11 | "GSL-sys", 12 | "paste", 13 | ] 14 | 15 | [[package]] 16 | name = "GSL-sys" 17 | version = "3.0.0" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "4577670dcc0720995dc39f04c438595eaae8ccc27f4aafd3e572dd408d01bd9d" 20 | dependencies = [ 21 | "libc", 22 | "pkg-config", 23 | ] 24 | 25 | [[package]] 26 | name = "aligned-array" 27 | version = "1.0.1" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "e05c92d086290f52938013f6242ac62bf7d401fab8ad36798a609faa65c3fd2c" 30 | dependencies = [ 31 | "generic-array", 32 | "subtle", 33 | ] 34 | 35 | [[package]] 36 | name = "aligned-cmov" 37 | version = "2.3.0" 38 | dependencies = [ 39 | "aligned-array", 40 | "generic-array", 41 | ] 42 | 43 | [[package]] 44 | name = "balanced-tree-index" 45 | version = "2.3.0" 46 | dependencies = [ 47 | "aligned-cmov", 48 | "rand_core", 49 | ] 50 | 51 | [[package]] 52 | name = "generic-array" 53 | version = "0.14.7" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 56 | dependencies = [ 57 | "typenum", 58 | "version_check", 59 | ] 60 | 61 | [[package]] 62 | name = "libc" 63 | version = "0.2.140" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" 66 | 67 | [[package]] 68 | name = "mc-oblivious-ram" 69 | version = "2.3.0" 70 | dependencies = [ 71 | "aligned-cmov", 72 | "balanced-tree-index", 73 | "mc-oblivious-traits", 74 | "rand_core", 75 | ] 76 | 77 | [[package]] 78 | name = "mc-oblivious-traits" 79 | version = "2.3.0" 80 | dependencies = [ 81 | "aligned-cmov", 82 | "balanced-tree-index", 83 | "rand_core", 84 | ] 85 | 86 | [[package]] 87 | name = "no-asm-tests" 88 | version = "2.3.0" 89 | dependencies = [ 90 | "GSL", 91 | "aligned-cmov", 92 | "balanced-tree-index", 93 | "mc-oblivious-ram", 94 | "mc-oblivious-traits", 95 | "test-helper", 96 | ] 97 | 98 | [[package]] 99 | name = "paste" 100 | version = "1.0.12" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" 103 | 104 | [[package]] 105 | name = "pkg-config" 106 | version = "0.3.26" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 109 | 110 | [[package]] 111 | name = "rand_core" 112 | version = "0.6.4" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 115 | 116 | [[package]] 117 | name = "rand_hc" 118 | version = "0.3.2" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "7b363d4f6370f88d62bf586c80405657bde0f0e1b8945d47d2ad59b906cb4f54" 121 | dependencies = [ 122 | "rand_core", 123 | ] 124 | 125 | [[package]] 126 | name = "subtle" 127 | version = "2.5.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 130 | 131 | [[package]] 132 | name = "test-helper" 133 | version = "2.3.0" 134 | dependencies = [ 135 | "aligned-array", 136 | "aligned-cmov", 137 | "rand_core", 138 | "rand_hc", 139 | ] 140 | 141 | [[package]] 142 | name = "typenum" 143 | version = "1.16.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 146 | 147 | [[package]] 148 | name = "version_check" 149 | version = "0.9.4" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 152 | -------------------------------------------------------------------------------- /mc-oblivious-traits/src/naive_storage.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | //! HeapORAMStorage just uses a Vec to provide access to block storage in the 4 | //! simplest way possible. It does not do any memory encryption or talk to 5 | //! untrusted. It does not have any oblivious properties itself. 6 | //! This is suitable for tests, or ORAMs that fit entirely in the enclave. 7 | 8 | use super::*; 9 | 10 | use alloc::vec; 11 | use balanced_tree_index::TreeIndex; 12 | 13 | /// The HeapORAMStorage is simply vector 14 | pub struct HeapORAMStorage, MetaSize: ArrayLength> { 15 | /// The storage for the blocks 16 | data: Vec>, 17 | /// The storage for the metadata 18 | metadata: Vec>, 19 | /// This is here so that we can provide good debug asserts in tests, 20 | /// it wouldn't be needed necessarily in a production version. 21 | checkout_index: Option, 22 | } 23 | 24 | impl, MetaSize: ArrayLength> HeapORAMStorage { 25 | pub fn new(size: u64) -> Self { 26 | Self { 27 | data: vec![Default::default(); size as usize], 28 | metadata: vec![Default::default(); size as usize], 29 | checkout_index: None, 30 | } 31 | } 32 | } 33 | 34 | impl, MetaSize: ArrayLength> ORAMStorage 35 | for HeapORAMStorage 36 | { 37 | fn len(&self) -> u64 { 38 | self.data.len() as u64 39 | } 40 | fn checkout( 41 | &mut self, 42 | leaf_index: u64, 43 | dest: &mut [A64Bytes], 44 | dest_meta: &mut [A8Bytes], 45 | ) { 46 | debug_assert!(self.checkout_index.is_none(), "double checkout"); 47 | debug_assert!(dest.len() == dest_meta.len(), "buffer size mismatch"); 48 | debug_assert!( 49 | leaf_index.parents().count() == dest.len(), 50 | "leaf height doesn't match buffer sizes" 51 | ); 52 | for (n, tree_index) in leaf_index.parents().enumerate() { 53 | dest[n] = self.data[tree_index as usize].clone(); 54 | dest_meta[n] = self.metadata[tree_index as usize].clone(); 55 | } 56 | self.checkout_index = Some(leaf_index); 57 | } 58 | fn checkin( 59 | &mut self, 60 | leaf_index: u64, 61 | src: &mut [A64Bytes], 62 | src_meta: &mut [A8Bytes], 63 | ) { 64 | debug_assert!(self.checkout_index.is_some(), "checkin without checkout"); 65 | debug_assert!( 66 | self.checkout_index == Some(leaf_index), 67 | "unexpected checkin" 68 | ); 69 | debug_assert!(src.len() == src_meta.len(), "buffer size mismatch"); 70 | debug_assert!( 71 | leaf_index.parents().count() == src.len(), 72 | "leaf height doesn't match buffer sizes" 73 | ); 74 | for (n, tree_index) in leaf_index.parents().enumerate() { 75 | self.data[tree_index as usize] = src[n].clone(); 76 | self.metadata[tree_index as usize] = src_meta[n].clone(); 77 | } 78 | self.checkout_index = None; 79 | } 80 | } 81 | 82 | /// HeapORAMStorage simply allocates a vector, and requires no special 83 | /// initialization support 84 | pub struct HeapORAMStorageCreator {} 85 | 86 | impl + 'static, MetaSize: ArrayLength + 'static> 87 | ORAMStorageCreator for HeapORAMStorageCreator 88 | { 89 | type Output = HeapORAMStorage; 90 | type Error = HeapORAMStorageCreatorError; 91 | 92 | fn create( 93 | size: u64, 94 | _rng: &mut R, 95 | ) -> Result { 96 | Ok(Self::Output::new(size)) 97 | } 98 | } 99 | 100 | /// There are not actually any failure modes 101 | #[derive(Debug)] 102 | pub enum HeapORAMStorageCreatorError {} 103 | 104 | impl core::fmt::Display for HeapORAMStorageCreatorError { 105 | fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result { 106 | unreachable!() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Inspired by https://github.com/google/tarpc/blob/master/hooks/pre-commit 4 | # 5 | # Pre-commit hook for the mobilenode repository. To use this hook, copy it or 6 | # add a symlink to it in .git/hooks in your repository root 7 | # 8 | # This precommit checks the following: 9 | # 1. All filenames are ascii 10 | # 2. There is no bad whitespace 11 | # 3. rustfmt is installed 12 | # 4. rustfmt is a noop on files that are in the index 13 | # 14 | # Options: 15 | # 16 | # - SKIP_RUSTFMT, default = 0 17 | # 18 | # Set this to 1 to skip running rustfmt 19 | # 20 | # Note that these options are most useful for testing the hooks themselves. Use git commit 21 | # --no-verify to skip the pre-commit hook altogether. 22 | 23 | BOLD='\033[33;1m' 24 | RED='\033[0;31m' 25 | GREEN='\033[0;32m' 26 | YELLOW='\033[0;33m' 27 | NC='\033[0m' # No Color 28 | 29 | PREFIX="${GREEN}[PRECOMMIT]${NC}" 30 | FAILURE="${RED}FAILED${NC}" 31 | WARNING="${RED}[WARNING]${NC}" 32 | SKIPPED="${YELLOW}SKIPPED${NC}" 33 | SUCCESS="${GREEN}ok${NC}" 34 | 35 | GITROOT=$(git rev-parse --show-toplevel) 36 | pushd "$GITROOT" >/dev/null 2>&1 37 | 38 | if git rev-parse --verify HEAD &>/dev/null 39 | then 40 | against=HEAD 41 | else 42 | # Initial commit: diff against an empty tree object 43 | against=$(git hash-object -t tree /dev/null) 44 | fi 45 | 46 | FAILED=0 47 | 48 | printf "${PREFIX} Checking that all filenames are ascii ... " 49 | # Note that the use of brackets around a tr range is ok here, (it's 50 | # even required, for portability to Solaris 10's /usr/bin/tr), since 51 | # the square bracket bytes happen to fall in the designated range. 52 | if test $(git diff --cached --name-only --diff-filter=A -z $against | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 53 | then 54 | FAILED=1 55 | printf "${FAILURE}\n" 56 | else 57 | printf "${SUCCESS}\n" 58 | fi 59 | 60 | printf "${PREFIX} Checking for bad whitespace ... " 61 | WHITESPACE_OUTPUT=$(git diff-index --color=always --check --cached $against -- 2>&1 | grep -v '^[^a-zA-Z]' | egrep -v '^public/sgx/sgx_(tcrypto|urts|types)' | grep -v '^src/sdk_json_interface/.*\.swift' | grep -v '.*.patch' ) 62 | if [[ -n "$WHITESPACE_OUTPUT" ]]; then 63 | FAILED=1 64 | printf "${FAILURE}\n" 65 | 66 | echo -e "$WHITESPACE_OUTPUT" 67 | else 68 | printf "${SUCCESS}\n" 69 | fi 70 | 71 | printf "${PREFIX} Checking for rustfmt ... " 72 | cargo fmt --version 73 | if [ $? == 0 ]; then 74 | printf "${SUCCESS}\n" 75 | else 76 | printf "${FAILURE}\n" 77 | popd >/dev/null 2>&1 78 | exit 1 79 | fi 80 | 81 | CARGOFMT="cargo fmt --" 82 | 83 | # Just check that running rustfmt doesn't do anything to the file. I do this instead of 84 | # modifying the file because I don't want to mess with the developer's index, which may 85 | # not only contain discrete files. 86 | printf "${PREFIX} Checking formatting ... " 87 | FMTRESULT=0 88 | diff="" 89 | for file in $(git diff --name-only --cached --diff-filter=d | grep -v '^public/thirdparty/.*' | grep -v '^thirdparty/.*' | egrep -v '^public/sgx/sgx_(tcrypto|urts|types)'); 90 | do 91 | if [[ ${file: -3} == ".rs" ]]; then 92 | newdiff=$($CARGOFMT --check $file | grep '^Diff in ' | awk -F' ' '{print $3;}') 93 | if [[ -n "$newdiff" ]]; then 94 | for filename in $newdiff; do 95 | diff="$filename\n$diff" 96 | done 97 | fi 98 | fi 99 | done 100 | 101 | if [[ "${SKIP_RUSTFMT}" == 1 ]]; then 102 | printf "${SKIPPED}\n"$? 103 | elif [[ -n "$diff" ]]; then 104 | FAILED=1 105 | printf "${FAILURE}\n" 106 | echo -e "\033[33;1mFiles Needing Rustfmt:\033[0m" 107 | echo -e "$diff" | sort -u 108 | if [[ -n "$(which tty)" ]] && [[ -n "$(tty)" ]]; then 109 | exec < /dev/tty 110 | echo "Do you want to fix all these files automatically? (y/N) " 111 | read YESNO 112 | if [[ -n "$YESNO" ]] && [[ "$(echo "${YESNO:0:1}" | tr '[[:lower:]]' '[[:upper:]]')" = "Y" ]]; then 113 | echo -e "$diff" | while read file; do 114 | $CARGOFMT $file 115 | done 116 | echo "You should attempt this commit again to pick up these changes." 117 | else 118 | echo -e "Run ${BOLD}$CARGOFMT -- ${NC} to format the files you have staged." 119 | fi 120 | exec <&- 121 | else 122 | echo -e "Run ${BOLD}$CARGOFMT -- ${NC} to format the files you have staged." 123 | fi 124 | else 125 | printf "${SUCCESS}\n" 126 | fi 127 | 128 | popd >/dev/null 2>&1 129 | exit ${FAILED} 130 | -------------------------------------------------------------------------------- /mc-oblivious-traits/src/creators.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | //! This module defines several "factory traits" that e.g. create recursive 4 | //! ORAMS and control the configuration of recursive children etc. 5 | //! 6 | //! Factories are useful, as opposed to "new_from_..." traits, because a single 7 | //! generic implementation may be often configured in one of several ways. 8 | //! The factory is thus a configuration strategy and they naturally may chain 9 | //! together to create an easy-to-use interface to get an ORAM. 10 | //! 11 | //! This is an alternative to hard-coding constants such as the block size or 12 | //! number of buckets in an ORAM into the implementation code itself, and may 13 | //! make it easier to create automated benchmarks that compare the effects of 14 | //! different settings. 15 | 16 | use super::*; 17 | 18 | use alloc::boxed::Box; 19 | use rand_core::SeedableRng; 20 | 21 | /// A factory which creates an ORAM of arbitrary size using recursive strategy. 22 | /// The result is required to have the 'static lifetime, and not be tied to the 23 | /// factory. 24 | pub trait ORAMCreator, RngType: RngCore + CryptoRng> { 25 | type Output: ORAM + Send + Sync + 'static; 26 | 27 | fn create RngType>( 28 | size: u64, 29 | stash_size: usize, 30 | rng_maker: &mut M, 31 | ) -> Self::Output; 32 | } 33 | 34 | /// A factory which creates a PositionMap 35 | pub trait PositionMapCreator { 36 | fn create RngType>( 37 | size: u64, 38 | height: u32, 39 | stash_size: usize, 40 | rng_maker: &mut M, 41 | ) -> Box; 42 | } 43 | 44 | /// A factory which makes ORAMStorage objects of some type 45 | /// 46 | /// In case of tests, it may simply create Vec objects. 47 | /// In production, it likely calls out to untrusted and asks to allocate 48 | /// block storage for an ORAM. 49 | /// 50 | /// The result is required to have the 'static lifetime, there is no in-enclave 51 | /// "manager" object which these objects can refer to. Instead they are either 52 | /// wrapping a vector, or e.g. they hold integer handles which they use when 53 | /// they make OCALL's to untrusted. 54 | /// So there is no manager object in the enclave which they cannot outlive. 55 | pub trait ORAMStorageCreator, MetaSize: ArrayLength> { 56 | /// The storage type produced 57 | type Output: ORAMStorage + Send + Sync + 'static; 58 | /// The error type produced 59 | type Error: Display + Debug; 60 | 61 | /// Create OramStorage, giving it a size and a CSPRNG for initialization. 62 | /// This should usually be RDRAND but in tests it might have a seed. 63 | /// 64 | /// It is expected that all storage will be zeroed from the caller's point 65 | /// of view, the first time that they access any of it. 66 | fn create( 67 | size: u64, 68 | csprng: &mut R, 69 | ) -> Result; 70 | } 71 | 72 | /// A factory which makes ObliviousMap objects of some type, based on an ORAM 73 | pub trait OMapCreator, ValueSize: ArrayLength, R: RngCore + CryptoRng> 74 | { 75 | /// The storage type produced 76 | type Output: ObliviousHashMap + Send + Sync + 'static; 77 | 78 | /// Create an oblivious map, with at least capacity specified. 79 | /// The stash size will be used by ORAMs underlying it. 80 | fn create R>( 81 | desired_capacity: u64, 82 | stash_size: usize, 83 | rng_maker: M, 84 | ) -> Self::Output; 85 | } 86 | 87 | /// A helper which takes an Rng implementing SeedableRng and returns a lambda 88 | /// which returns newly seeded Rng's with seeds derived from this one. 89 | /// This matches the `rng_maker` constraints in the above traits, and can be 90 | /// used in tests when we want all the Rng's to be seeded. 91 | pub fn rng_maker( 92 | mut source: R, 93 | ) -> impl FnMut() -> R + 'static { 94 | move || { 95 | let mut seed = R::Seed::default(); 96 | source.fill_bytes(seed.as_mut()); 97 | R::from_seed(seed) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement on [our Discord](https://discord.gg/mobilecoin). 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 125 | [https://www.contributor-covenant.org/translations][translations]. 126 | 127 | [homepage]: https://www.contributor-covenant.org 128 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 129 | [Mozilla CoC]: https://github.com/mozilla/diversity 130 | [FAQ]: https://www.contributor-covenant.org/faq 131 | [translations]: https://www.contributor-covenant.org/translations 132 | -------------------------------------------------------------------------------- /CLA.md: -------------------------------------------------------------------------------- 1 | # Contributor License Agreement 2 | 3 | Thank you for your contribution to the MobileCoin project from MoblieCoin Inc. and the MobileCoin Foundation (“MobileCoin”). 4 | 5 | This contributor license agreement documents the rights granted by contributors to MobileCoin. This license is for your protection as a Contributor as well as the protection of MobileCoin, its users, and its licensees; you may still license your own Contributions under other terms. 6 | 7 | In exchange for the ability to participate in the MobileCoin community and for other good consideration, the receipt of which is hereby acknowledged, you accept and agree to the following terms and conditions for Your present and future Contributions submitted to MobileCoin. Except for the license granted herein to MobileCoin and recipients of software distributed by MobileCoin, You reserve all right, title, and interest in and to Your Contributions. 8 | 9 | 1. Definitions. 10 | 11 | “You” (or “Your”) shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with MobileCoin. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, “control” means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 12 | 13 | “Contribution” shall mean any original work of authorship or invention, including any modifications or additions to an existing work, that is intentionally submitted by You to MobileCoin for inclusion in, or documentation of, any of the products owned or managed by MobileCoin (the “Work”). For the purposes of this definition, “submitted” means any form of electronic, verbal, or written communication sent to MobileCoin or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, MobileCoin for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as “Not a Contribution.” 14 | 15 | 1. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to MobileCoin and to recipients of software distributed by MobileCoin a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, and distribute Your Contributions and such derivative works, as well as the right to sublicense and have sublicensed all of the foregoing rights, through multiple tiers of sublicensees, provided that in all cases, MobileCoin will make Your Contributions available under an open source license. 16 | 17 | a. Moral Rights. If moral rights apply to the Contribution, to the maximum extent permitted by law, You waive and agree not to assert such moral rights against MobileCoin or its successors in interest, or any of MobileCoin’s licensees, either direct or indirect. 18 | 19 | 1. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to MobileCoin and to recipients of software distributed by MobileCoin a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. 20 | 21 | 1. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your contributions to MobileCoin, or that your employer has executed with MobileCoin a separate contributor license agreement substantially similar to this Agreement. 22 | 23 | 1. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. 24 | 25 | 1. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of title, non-infringement, merchantability, or fitness for a particular purpose. 26 | 27 | 1. Should You wish to submit work that is not Your original creation, You may submit it to MobileCoin separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as “Not a Contribution”. Third-party materials licensed pursuant to: [license name(s) here]” (substituting the bracketed text with the appropriate license name(s)). 28 | 29 | 1. You agree to notify MobileCoin of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. 30 | -------------------------------------------------------------------------------- /aligned-cmov/README.md: -------------------------------------------------------------------------------- 1 | # aligned-cmov 2 | 3 | `cmov` is an abbreviation of *conditional move*. A conditional operation which takes 4 | a source value, a destination value, and a boolean, and overwrites the destination with 5 | the source if the flag is true. `CMOV` is the name of an x86 CPU instruction which does this 6 | for two registers. 7 | 8 | `CMOV` is mainly interesting in cryptographic code because `CMOV` 9 | are not "predicted" by any x86 hardware, and conform to Intel's constant-time coding principles 10 | even when the condition value for the move is supposed to be a secret. 11 | 12 | This functionality is a crticial building block for ORAM. 13 | ORAM requires performing many conditional move operations on large (~4k sized) blocks 14 | of memory repeatedly. This is expected to be the performance bottleneck if not done well. 15 | The security requirement is that these operations should not be predicted by the CPU, and 16 | should be conducted in a "side-channel resistant way" -- an attacker in the SGX threat model 17 | should not be able to observe if the move happened or not, if it happened inside an enclave. 18 | 19 | This crate provides a trait called `CMov` which is meant to implement this operation, 20 | and to provide it on several simple datatypes. 21 | 22 | Because the scope of `mc-oblivious` is only to support *Intel x86-64 inside of SGX*, 23 | on relatively recent (>= skylake) CPUs, 24 | we provide inline assembly which does the optimal thing for the datatypes that we care about. 25 | 26 | ## Comparison to `subtle` 27 | 28 | The [subtle crate](https://github.com/dalek-cryptography/subtle) is the most obvious other crate in the same genre. 29 | 30 | This crate builds on subtle, but it introduces a new trait for conditional assignment: 31 | 32 | ```rust 33 | pub trait CMov { 34 | fn cmov(&mut self, condition: subtle::Choice, src: &self); 35 | } 36 | ``` 37 | 38 | We chose not to use `subtle::ConditionallySelectable` for this because that trait defines its functionality in terms of 39 | 40 | ```rust 41 | fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self; 42 | ``` 43 | 44 | an API which necessitates a copy. Since we need to do CMOVs of very large values, implementing it this way 45 | would create a lot of extra copies on the stack. Even if those are eliminated in release mode, they won't be eliminated 46 | in debug mode, and we usually test in debug mode when iterating locally. Making things faster in debug mode allows us to get 47 | better test coverage without hurting iteration times. 48 | 49 | Besides this, aesthetically we feel that `CMov` API is better, because it lines up more naturally with how the hardware 50 | actually works, which makes it easier to reason about performance. 51 | 52 | The other main difference between us and subtle is that subtle uses no assembly, builds on stable rust, and is portable. 53 | For our purpose, we only care about x86-64 targets that are relatively recent and support SGX, and we don't mind using the nightly compiler. 54 | We specifically want to use assembly to go faster. 55 | 56 | Additionally, using assembly may improve the security, in the sense that, the compiler 57 | explicitly promises not to modify or introspect on "volatile" inline assembly blocks. 58 | But future optimization passes introduced into llvm may in principle enable optimizations 59 | such that the indirection in "rust timing shield" and subtle doesn't work anymore. So there is some trade-off 60 | happening here between portability of the code and correct assembly generation. 61 | 62 | That said, we still rely on subtle for a "shielded boolean" type that we need to be the argument of cmov. 63 | 64 | ## Future directions 65 | 66 | In the long run, it might be nice to get functionality like this in subtle crate itself. 67 | 68 | For example, the rust-crypto `aes` [crate](https://docs.rs/aes/0.6.0/aes/) uses platform detection in its Cargo.toml 69 | to select at compile-time between: 70 | 71 | - A portable implementation of aes (`aes-soft`) 72 | - A hardware-accelerated implementation of aes using x86 aesni instructions (`aesni`) 73 | 74 | If rust inline assembly is stabilized, we could imagine that there is a version of `subtle` using platform-specific assembly 75 | for `x86`, and the software-based version is the fallback. Then code like in this crate could belong there. 76 | 77 | It's possible that `subtle` maintainers don't want to maintain the `aligned-cmov` code with `subtle` though, because, 78 | there are not really any applications of "fast 4096 byte conditional moves" besides oblivious RAM. There are no other cryptographic 79 | primitives that require that AFAIK. Since `subtle` is dependend on by ALOT of cryptographic implementations now, adding this kind 80 | of functionality may be scope creep and it's not clear it's desirable to have this extra stuff in the dependency tree of many other 81 | cryptographic libraries. 82 | 83 | ## References 84 | 85 | Constant-time code and side-channel resistance: 86 | 87 | - Intel's [Guidelines for mitigating timing side-channels against cryptographic implementations](https://software.intel.com/security-software-guidance/insights/guidelines-mitigating-timing-side-channels-against-cryptographic-implementations) 88 | - Tim McLean's [Rust-timing-shield](https://www.chosenplaintext.ca/open-source/rust-timing-shield/security) 89 | - isis agora lovecruft's [subtle](https://github.com/dalek-cryptography/subtle) 90 | - Chandler Carruth on [Spectre](https://www.youtube.com/watch?v=_f7O3IfIR2k) 91 | 92 | Using AVX instructions for cryptographic implementations 93 | 94 | - Samuel Neves and Jean-Philippe Aumasson [Implementing BLAKE with AVX, AVX2, and XOP](https://131002.net/data/papers/NA12a.pdf) 95 | - Henry DeValence on [AVX512 backend in curve25519-dalek](https://medium.com/@hdevalence/even-faster-edwards-curves-with-ifma-8b1e576a00e9) 96 | - David Wong on [SIMD Instructions in Crypto](https://www.cryptologie.net/article/405/simd-instructions-in-crypto/) 97 | 98 | x86-64 assembly: 99 | 100 | - Felix Cloutier's [x86-64 instruction reference](https://www.felixcloutier.com/x86/) 101 | See also his links to official Intel documentation. 102 | - Intel's [x86-64 intrinsics guide](https://software.intel.com/sites/landingpage/IntrinsicsGuide/) 103 | - Agner Fog's [instruction table timings](https://www.agner.org/optimize/instruction_tables.pdf) 104 | (See especially skylake CPUs.) 105 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ci 3 | 4 | "on": 5 | push: 6 | branches: 7 | - 'master' 8 | pull_request: 9 | 10 | env: 11 | CARGO_INCREMENTAL: 0 12 | 13 | jobs: 14 | # TODO: Fix automatically 15 | lint: 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: dtolnay/rust-toolchain@stable 20 | with: 21 | components: rustfmt 22 | - uses: r7kamura/rust-problem-matchers@v1 23 | - run: cargo fmt --all -- --check 24 | - uses: xt0rted/markdownlint-problem-matcher@v2 25 | - uses: DavidAnson/markdownlint-cli2-action@v10 26 | with: 27 | globs: "**/*.md" 28 | # FIXME: Add yamllint problem matcher 29 | - run: yamllint -s . 30 | 31 | deny: 32 | runs-on: ubuntu-22.04 33 | needs: 34 | - lint 35 | strategy: 36 | matrix: 37 | checks: 38 | - advisories 39 | - bans licenses sources 40 | fail-fast: false 41 | steps: 42 | - uses: actions/checkout@v3 43 | - uses: dtolnay/rust-toolchain@stable 44 | - uses: EmbarkStudios/cargo-deny-action@v1 45 | with: 46 | command: check ${{ matrix.checks }} 47 | 48 | sort: 49 | runs-on: ubuntu-22.04 50 | needs: 51 | - lint 52 | steps: 53 | - uses: actions/checkout@v3 54 | - uses: dtolnay/rust-toolchain@stable 55 | - uses: taiki-e/install-action@v2 56 | with: 57 | tool: cargo-sort 58 | - run: cargo sort --workspace --grouped --check >/dev/null 59 | # TODO: Fix automatically 60 | 61 | clippy: 62 | runs-on: ubuntu-22.04 63 | needs: 64 | - lint 65 | strategy: 66 | matrix: 67 | rust: 68 | - stable 69 | fail-fast: false 70 | steps: 71 | - uses: actions/checkout@v3 72 | - uses: dtolnay/rust-toolchain@master 73 | with: 74 | toolchain: ${{ matrix.rust }} 75 | components: clippy 76 | - uses: r7kamura/rust-problem-matchers@v1 77 | - run: | 78 | cargo +${{ matrix.rust }} clippy --all --all-features -- -D warnings 79 | # TODO: Fix automatically, or post GH-suggestions comments 80 | 81 | build: 82 | runs-on: ubuntu-22.04 83 | needs: 84 | - lint 85 | strategy: 86 | matrix: 87 | rust: 88 | - stable 89 | - nightly-2023-03-23 90 | flags: 91 | - "" 92 | - "--release" 93 | steps: 94 | - uses: actions/checkout@v3 95 | - uses: dtolnay/rust-toolchain@master 96 | with: 97 | toolchain: ${{ matrix.rust }} 98 | - uses: r7kamura/rust-problem-matchers@v1 99 | - run: cargo +${{ matrix.rust }} build ${{ matrix.flags }} 100 | 101 | test: 102 | runs-on: ubuntu-22.04 103 | needs: 104 | - lint 105 | strategy: 106 | matrix: 107 | rust: 108 | - stable 109 | - nightly-2023-03-23 110 | flags: 111 | - "" 112 | - "--release" 113 | steps: 114 | - uses: actions/checkout@v3 115 | - uses: dtolnay/rust-toolchain@master 116 | with: 117 | toolchain: ${{ matrix.rust }} 118 | - uses: r7kamura/rust-problem-matchers@v1 119 | - run: cargo +${{ matrix.rust }} test ${{ matrix.flags }} 120 | 121 | bench: 122 | runs-on: ubuntu-22.04 123 | needs: 124 | - lint 125 | strategy: 126 | matrix: 127 | rust: 128 | - nightly-2023-03-23 129 | steps: 130 | - uses: actions/checkout@v3 131 | - uses: dtolnay/rust-toolchain@master 132 | with: 133 | toolchain: ${{ matrix.rust }} 134 | - uses: r7kamura/rust-problem-matchers@v1 135 | - run: cargo +${{ matrix.rust }} bench 136 | 137 | no-asm-tests: 138 | runs-on: ubuntu-22.04 139 | needs: 140 | - lint 141 | strategy: 142 | matrix: 143 | rust: 144 | - stable 145 | steps: 146 | - uses: actions/checkout@v3 147 | - uses: dtolnay/rust-toolchain@master 148 | with: 149 | toolchain: ${{ matrix.rust }} 150 | - uses: r7kamura/rust-problem-matchers@v1 151 | - run: | 152 | sudo apt-get update && \ 153 | sudo apt-get install -y libgsl-dev && \ 154 | cd no-asm-tests && \ 155 | cargo +${{ matrix.rust }} test --release 156 | 157 | doc: 158 | runs-on: ubuntu-22.04 159 | needs: 160 | - lint 161 | strategy: 162 | matrix: 163 | rust: 164 | - stable 165 | - beta 166 | # Prevent beta docs warnings from causing CI failure 167 | continue-on-error: ${{ matrix.rust == 'beta' }} 168 | steps: 169 | - uses: actions/checkout@v3 170 | - uses: dtolnay/rust-toolchain@master 171 | with: 172 | toolchain: ${{ matrix.rust }} 173 | - uses: r7kamura/rust-problem-matchers@v1 174 | - run: cargo +${{ matrix.rust }} doc --release --no-deps 175 | 176 | coverage: 177 | runs-on: ubuntu-22.04 178 | needs: 179 | - lint 180 | steps: 181 | - uses: actions/checkout@v3 182 | - uses: dtolnay/rust-toolchain@stable 183 | with: 184 | components: llvm-tools-preview 185 | - uses: taiki-e/install-action@v2 186 | with: 187 | tool: cargo-llvm-cov 188 | - run: cargo llvm-cov --workspace --lcov --output-path lcov.info 189 | - uses: codecov/codecov-action@v3 190 | with: 191 | files: lcov.info 192 | 193 | notify: 194 | runs-on: ubuntu-latest 195 | if: github.event_name == 'push' && failure() 196 | needs: 197 | - lint 198 | - deny 199 | - sort 200 | - clippy 201 | - build 202 | - test 203 | - doc 204 | - coverage 205 | steps: 206 | - name: Notify Discord on failure 207 | uses: sarisia/actions-status-discord@v1 208 | with: 209 | webhook: ${{ secrets.DISCORD_WEBHOOK }} 210 | username: "Github Actions" 211 | status: Failure 212 | color: 0xff0000 213 | nodetail: true 214 | # yamllint disable rule:line-length 215 | title: "${{ github.repository }} ${{ github.workflow }} has failed on ${{ github.event_name }} to ${{ github.ref_name }}" 216 | url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 217 | description: > 218 | [`@${{ github.actor }}`](${{ github.server_url }}/${{ github.actor }}) 219 | was the last one to touch 220 | [that repository](${{ github.server_url }}/${{ github.repository }}), 221 | is all I'm saying. 222 | avatar_url: "https://media0.giphy.com/media/oe33xf3B50fsc/200.gif" 223 | # yamllint enable rule:line-length 224 | -------------------------------------------------------------------------------- /mc-oblivious-ram/src/position_map/mod.rs: -------------------------------------------------------------------------------- 1 | //! Defines oblivious position map on top of a generic ORAM, 2 | //! using strategy as described in PathORAM 3 | //! In our representation of tree-index, a value of 0 represents 4 | //! a "vacant" / uninitialized value. 5 | //! 6 | //! For correctness we must ensure that the position map appears in 7 | //! a randomly initialized state, so we replace zeros with a random leaf 8 | //! at the correct height before returning to caller. 9 | 10 | use alloc::vec; 11 | 12 | use aligned_cmov::{ 13 | subtle::ConstantTimeEq, 14 | typenum::{PartialDiv, U8}, 15 | ArrayLength, AsNeSlice, CMov, 16 | }; 17 | use alloc::{boxed::Box, vec::Vec}; 18 | use balanced_tree_index::TreeIndex; 19 | use core::marker::PhantomData; 20 | use mc_oblivious_traits::{log2_ceil, ORAMCreator, PositionMap, PositionMapCreator, ORAM}; 21 | use rand_core::{CryptoRng, RngCore}; 22 | 23 | /// A trivial position map implemented via linear scanning. 24 | /// Positions are represented as 32 bytes inside a page. 25 | pub struct TrivialPositionMap { 26 | data: Vec, 27 | height: u32, 28 | rng: R, 29 | } 30 | 31 | impl TrivialPositionMap { 32 | /// Create trivial position map 33 | pub fn new(size: u64, height: u32, rng_maker: &mut impl FnMut() -> R) -> Self { 34 | assert!( 35 | height < 32, 36 | "Can't use u32 position map when height of tree exceeds 31" 37 | ); 38 | Self { 39 | data: vec![0u32; size as usize], 40 | height, 41 | rng: rng_maker(), 42 | } 43 | } 44 | } 45 | 46 | impl PositionMap for TrivialPositionMap { 47 | fn len(&self) -> u64 { 48 | self.data.len() as u64 49 | } 50 | fn write(&mut self, key: &u64, new_val: &u64) -> u64 { 51 | debug_assert!(*key < self.data.len() as u64, "key was out of bounds"); 52 | let key = *key as u32; 53 | let new_val = *new_val as u32; 54 | let mut old_val = 0u32; 55 | for idx in 0..self.data.len() { 56 | let test = (idx as u32).ct_eq(&key); 57 | old_val.cmov(test, &self.data[idx]); 58 | self.data[idx].cmov(test, &new_val); 59 | } 60 | // if old_val is zero, sample a random leaf 61 | old_val.cmov( 62 | old_val.ct_eq(&0), 63 | &1u32.random_child_at_height(self.height, &mut self.rng), 64 | ); 65 | old_val as u64 66 | } 67 | } 68 | 69 | /// A position map implemented on top of an ORAM 70 | /// Positions are represented as 32 bytes inside a page in an ORAM. 71 | /// 72 | /// Value size represents the chunk of 32 byte values that we scan across. 73 | pub struct ORAMU32PositionMap< 74 | ValueSize: ArrayLength + PartialDiv, 75 | O: ORAM + Send + Sync + 'static, 76 | R: RngCore + CryptoRng + Send + Sync + 'static, 77 | > { 78 | oram: O, 79 | height: u32, 80 | rng: R, 81 | _value_size: PhantomData ValueSize>, 82 | } 83 | 84 | impl ORAMU32PositionMap 85 | where 86 | ValueSize: ArrayLength + PartialDiv, 87 | O: ORAM + Send + Sync + 'static, 88 | R: RngCore + CryptoRng + Send + Sync + 'static, 89 | { 90 | // We subtract 2 over ValueSize because u32 is 4 bytes 91 | const L: u32 = log2_ceil(ValueSize::U64) - 2; 92 | 93 | /// Create position map where all positions appear random, lazily 94 | pub fn new, M: 'static + FnMut() -> R>( 95 | size: u64, 96 | height: u32, 97 | stash_size: usize, 98 | rng_maker: &mut M, 99 | ) -> Self { 100 | assert!( 101 | height < 32, 102 | "Can't use U32 position map when height of tree exceeds 31" 103 | ); 104 | let rng = rng_maker(); 105 | Self { 106 | oram: OC::create(size >> Self::L, stash_size, rng_maker), 107 | height, 108 | rng, 109 | _value_size: Default::default(), 110 | } 111 | } 112 | } 113 | 114 | impl PositionMap for ORAMU32PositionMap 115 | where 116 | ValueSize: ArrayLength + PartialDiv, 117 | O: ORAM + Send + Sync + 'static, 118 | R: RngCore + CryptoRng + Send + Sync + 'static, 119 | { 120 | fn len(&self) -> u64 { 121 | self.oram.len() << Self::L 122 | } 123 | fn write(&mut self, key: &u64, new_val: &u64) -> u64 { 124 | let new_val = *new_val as u32; 125 | let upper_key = *key >> Self::L; 126 | let lower_key = (*key as u32) & ((1u32 << Self::L) - 1); 127 | 128 | let mut old_val = self.oram.access(upper_key, |block| -> u32 { 129 | let mut old_val = 0u32; 130 | let u32_slice = block.as_mut_ne_u32_slice(); 131 | for idx in 0..(1u32 << Self::L) { 132 | old_val.cmov(idx.ct_eq(&lower_key), &u32_slice[idx as usize]); 133 | u32_slice[idx as usize].cmov(idx.ct_eq(&lower_key), &new_val); 134 | } 135 | old_val 136 | }); 137 | // if old_val is zero, sample a random leaf 138 | old_val.cmov( 139 | old_val.ct_eq(&0), 140 | &1u32.random_child_at_height(self.height, &mut self.rng), 141 | ); 142 | old_val as u64 143 | } 144 | } 145 | 146 | /// Creates U32 Position Maps, either the trivial one or recursively on top of 147 | /// ORAMs. The value size times the Z value determines the size of an ORAM 148 | /// bucket 149 | pub struct U32PositionMapCreator< 150 | ValueSize: ArrayLength + PartialDiv + 'static, 151 | R: RngCore + CryptoRng + Send + Sync + 'static, 152 | OC: ORAMCreator, 153 | > { 154 | _value: PhantomData ValueSize>, 155 | _rng: PhantomData R>, 156 | _oc: PhantomData OC>, 157 | } 158 | 159 | impl< 160 | ValueSize: ArrayLength + PartialDiv + 'static, 161 | R: RngCore + CryptoRng + Send + Sync + 'static, 162 | OC: ORAMCreator, 163 | > PositionMapCreator for U32PositionMapCreator 164 | { 165 | fn create R>( 166 | size: u64, 167 | height: u32, 168 | stash_size: usize, 169 | rng_maker: &mut M, 170 | ) -> Box { 171 | // This threshold is a total guess, this corresponds to four pages 172 | if size <= 4096 { 173 | Box::new(TrivialPositionMap::::new(size, height, rng_maker)) 174 | } else if height <= 31 { 175 | Box::new( 176 | ORAMU32PositionMap::::new::( 177 | size, height, stash_size, rng_maker, 178 | ), 179 | ) 180 | } else { 181 | panic!( 182 | "height = {}, but we didn't implement u64 position map yet", 183 | height 184 | ) 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /aligned-cmov/benches/large_cmov.rs: -------------------------------------------------------------------------------- 1 | use aligned_cmov::{typenum, A64Bytes, A8Bytes, ArrayLength, CMov}; 2 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 3 | use typenum::{U1024, U2048, U256, U4096}; 4 | 5 | /// Make a8-bytes that are initialized to a particular byte value 6 | /// This makes tests shorter to write 7 | fn a8_8>(src: u8) -> A8Bytes { 8 | let mut result = A8Bytes::::default(); 9 | for byte in result.as_mut_slice() { 10 | *byte = src; 11 | } 12 | result 13 | } 14 | 15 | fn a64_8>(src: u8) -> A64Bytes { 16 | let mut result = A64Bytes::::default(); 17 | for byte in result.as_mut_slice() { 18 | *byte = src; 19 | } 20 | result 21 | } 22 | 23 | pub fn cmov_a8_256_true(c: &mut Criterion) { 24 | type N = U256; 25 | let mut dest: A8Bytes = a8_8(20); 26 | let src: A8Bytes = a8_8(40); 27 | 28 | c.bench_function("cmov a8 256 true", |b| { 29 | b.iter(|| { 30 | dest.cmov(black_box(1.into()), &src); 31 | dest[0] 32 | }) 33 | }); 34 | } 35 | 36 | pub fn cmov_a8_256_false(c: &mut Criterion) { 37 | type N = U256; 38 | let mut dest: A8Bytes = a8_8(20); 39 | let src: A8Bytes = a8_8(40); 40 | 41 | c.bench_function("cmov a8 256 false", |b| { 42 | b.iter(|| { 43 | dest.cmov(black_box(0.into()), &src); 44 | dest[0] 45 | }) 46 | }); 47 | } 48 | 49 | pub fn cmov_a8_1024_true(c: &mut Criterion) { 50 | type N = U1024; 51 | let mut dest: A8Bytes = a8_8(20); 52 | let src: A8Bytes = a8_8(40); 53 | 54 | c.bench_function("cmov a8 1024 true", |b| { 55 | b.iter(|| { 56 | dest.cmov(black_box(1.into()), &src); 57 | dest[0] 58 | }) 59 | }); 60 | } 61 | 62 | pub fn cmov_a8_1024_false(c: &mut Criterion) { 63 | type N = U1024; 64 | let mut dest: A8Bytes = a8_8(20); 65 | let src: A8Bytes = a8_8(40); 66 | 67 | c.bench_function("cmov a8 1024 false", |b| { 68 | b.iter(|| { 69 | dest.cmov(black_box(0.into()), &src); 70 | dest[0] 71 | }) 72 | }); 73 | } 74 | 75 | pub fn cmov_a8_2048_true(c: &mut Criterion) { 76 | type N = U2048; 77 | let mut dest: A8Bytes = a8_8(20); 78 | let src: A8Bytes = a8_8(40); 79 | 80 | c.bench_function("cmov a8 2048 true", |b| { 81 | b.iter(|| { 82 | dest.cmov(black_box(1.into()), &src); 83 | dest[0] 84 | }) 85 | }); 86 | } 87 | 88 | pub fn cmov_a8_2048_false(c: &mut Criterion) { 89 | type N = U2048; 90 | let mut dest: A8Bytes = a8_8(20); 91 | let src: A8Bytes = a8_8(40); 92 | 93 | c.bench_function("cmov a8 2048 false", |b| { 94 | b.iter(|| { 95 | dest.cmov(black_box(0.into()), &src); 96 | dest[0] 97 | }) 98 | }); 99 | } 100 | 101 | pub fn cmov_a8_4096_true(c: &mut Criterion) { 102 | type N = U4096; 103 | let mut dest: A8Bytes = a8_8(20); 104 | let src: A8Bytes = a8_8(40); 105 | 106 | c.bench_function("cmov a8 4096 true", |b| { 107 | b.iter(|| { 108 | dest.cmov(black_box(1.into()), &src); 109 | dest[0] 110 | }) 111 | }); 112 | } 113 | 114 | pub fn cmov_a8_4096_false(c: &mut Criterion) { 115 | type N = U4096; 116 | let mut dest: A8Bytes = a8_8(20); 117 | let src: A8Bytes = a8_8(40); 118 | 119 | c.bench_function("cmov a8 4096 false", |b| { 120 | b.iter(|| { 121 | dest.cmov(black_box(0.into()), &src); 122 | dest[0] 123 | }) 124 | }); 125 | } 126 | 127 | criterion_group!( 128 | benches_a8, 129 | cmov_a8_256_true, 130 | cmov_a8_256_false, 131 | cmov_a8_1024_true, 132 | cmov_a8_1024_false, 133 | cmov_a8_2048_true, 134 | cmov_a8_2048_false, 135 | cmov_a8_4096_true, 136 | cmov_a8_4096_false 137 | ); 138 | 139 | pub fn cmov_a64_256_true(c: &mut Criterion) { 140 | type N = U256; 141 | let mut dest: A64Bytes = a64_8(20); 142 | let src: A64Bytes = a64_8(40); 143 | 144 | c.bench_function("cmov a64 256 true", |b| { 145 | b.iter(|| { 146 | dest.cmov(black_box(1.into()), &src); 147 | dest[0] 148 | }) 149 | }); 150 | } 151 | 152 | pub fn cmov_a64_256_false(c: &mut Criterion) { 153 | type N = U256; 154 | let mut dest: A64Bytes = a64_8(20); 155 | let src: A64Bytes = a64_8(40); 156 | 157 | c.bench_function("cmov a64 256 false", |b| { 158 | b.iter(|| { 159 | dest.cmov(black_box(0.into()), &src); 160 | dest[0] 161 | }) 162 | }); 163 | } 164 | 165 | pub fn cmov_a64_1024_true(c: &mut Criterion) { 166 | type N = U1024; 167 | let mut dest: A64Bytes = a64_8(20); 168 | let src: A64Bytes = a64_8(40); 169 | 170 | c.bench_function("cmov a64 1024 true", |b| { 171 | b.iter(|| { 172 | dest.cmov(black_box(1.into()), &src); 173 | dest[0] 174 | }) 175 | }); 176 | } 177 | 178 | pub fn cmov_a64_1024_false(c: &mut Criterion) { 179 | type N = U1024; 180 | let mut dest: A64Bytes = a64_8(20); 181 | let src: A64Bytes = a64_8(40); 182 | 183 | c.bench_function("cmov a64 1024 false", |b| { 184 | b.iter(|| { 185 | dest.cmov(black_box(0.into()), &src); 186 | dest[0] 187 | }) 188 | }); 189 | } 190 | 191 | pub fn cmov_a64_2048_true(c: &mut Criterion) { 192 | type N = U2048; 193 | let mut dest: A64Bytes = a64_8(20); 194 | let src: A64Bytes = a64_8(40); 195 | 196 | c.bench_function("cmov a64 2048 true", |b| { 197 | b.iter(|| { 198 | dest.cmov(black_box(1.into()), &src); 199 | dest[0] 200 | }) 201 | }); 202 | } 203 | 204 | pub fn cmov_a64_2048_false(c: &mut Criterion) { 205 | type N = U2048; 206 | let mut dest: A64Bytes = a64_8(20); 207 | let src: A64Bytes = a64_8(40); 208 | 209 | c.bench_function("cmov a64 2048 false", |b| { 210 | b.iter(|| { 211 | dest.cmov(black_box(0.into()), &src); 212 | dest[0] 213 | }) 214 | }); 215 | } 216 | 217 | pub fn cmov_a64_4096_true(c: &mut Criterion) { 218 | type N = U4096; 219 | let mut dest: A64Bytes = a64_8(20); 220 | let src: A64Bytes = a64_8(40); 221 | 222 | c.bench_function("cmov a64 4096 true", |b| { 223 | b.iter(|| { 224 | dest.cmov(black_box(1.into()), &src); 225 | dest[0] 226 | }) 227 | }); 228 | } 229 | 230 | pub fn cmov_a64_4096_false(c: &mut Criterion) { 231 | type N = U4096; 232 | let mut dest: A64Bytes = a64_8(20); 233 | let src: A64Bytes = a64_8(40); 234 | 235 | c.bench_function("cmov a64 4096 false", |b| { 236 | b.iter(|| { 237 | dest.cmov(black_box(0.into()), &src); 238 | dest[0] 239 | }) 240 | }); 241 | } 242 | 243 | criterion_group!( 244 | benches_a64, 245 | cmov_a64_256_true, 246 | cmov_a64_256_false, 247 | cmov_a64_1024_true, 248 | cmov_a64_1024_false, 249 | cmov_a64_2048_true, 250 | cmov_a64_2048_false, 251 | cmov_a64_4096_true, 252 | cmov_a64_4096_false 253 | ); 254 | criterion_main!(benches_a8, benches_a64); 255 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mc-oblivious ![mobilecoin](./img/mobilecoin_logo.png) 2 | 3 | [![Project Chat][chat-image]][chat-link]![License][license-image][![Dependency Status][deps-image]][deps-link][![CodeCov Status][codecov-image]][codecov-link][![GitHub Workflow Status][gha-image]][gha-link][![Contributor Covenant][conduct-image]][conduct-link] 9 | 10 | Traits and implementations for Oblivious RAM inside of Intel SGX enclaves. 11 | 12 | The scope of this repository is: 13 | 14 | - Traits for fast constant-time conditional moves of aligned memory in x86-64 15 | - Traits for "untrusted block storage" and "memory encryption engine" to support a backing store that exceeds enclave memory limits 16 | - Traits for Oblivious RAM, and implementations 17 | - Traits for Oblivious Hash Tables, and implementations 18 | - Other oblivious data structures and algorithms, such as shuffling or sorting. 19 | 20 | The code in this repo is expected to run on an x86-64 CPU inside SGX. It is out of scope 21 | to support other platforms. (However, we still abstract things in a reasonable way. 22 | Only the `aligned-cmov` crate contains x86-64-specific code.) 23 | 24 | The code in this repo is expected to require the nightly compiler, 25 | so that we can use inline assembly if needed, to ensure that we get CMOV etc., 26 | because obliviously moving large blocks of memory is expected to be a bottleneck. 27 | If and when inline assembly is stabilized in rust, we expect not to need nightly anymore. 28 | 29 | ## What is oblivious RAM? 30 | 31 | Oblivious RAM is a class of data structures designed to avoid information leaks 32 | over memory access pattern side-channels, introduced in [Goldreich '96]. 33 | 34 | Tree-based ORAM was introduced in a seminal paper [Shi, Chan, Stefanov, Li '11]. 35 | Tree-based ORAM algorithms arrange their data in a complete balanced binary tree, 36 | and are the first and only class of algorithms to have good (poly-log) worst-case performance. 37 | 38 | The first oblivious RAM algorithm that attracted significant interest from practicioners was 39 | Path ORAM [Shi, Stefanov, Li '13]. Circuit ORAM appeared in [Wang, Chan, Shi '16]. 40 | 41 | ORAM can in principle be used in several ways, and many papers in ORAM consider several of the application modes: 42 | 43 | - A user can use it to interact with (untrusted) cloud storage and make use of storage without leaking access patterns. 44 | - It can be implemented in hardware in the "secure processor" setting, such that the "ORAM controller / client" is 45 | implemented in silicon, and the main memory corresponds to the "server". 46 | - It can be implemented in software in a "secure enclave", such that the "ORAM controller / client" is the enclave, 47 | and the main memory corresponds to the "server". 48 | - It can be implemented in a compiler pass that transforms arbitrary code into code that leaks nothing via its memory access patterns, 49 | but runs more slowly. 50 | 51 | As explained, in this repository we are focused on the SGX-based approach, which was first described in the ZeroTrace paper [Sasy, Gorbunuv, Fletcher '17]. 52 | 53 | ## What is oblivious / constant-time? 54 | 55 | A great exposition from Intel appears in [Guidelines for Mitigating Timing Side Channels Against Cryptographic Implementations](https://software.intel.com/security-software-guidance/secure-coding/guidelines-mitigating-timing-side-channels-against-cryptographic-implementations). 56 | 57 | > Most traditional side channels—regardless of technique—can be mitigated by applying all three of the following general "constant time"[2] principles, listed here at a high level. We discuss details and examples of these principles later. 58 | > 59 | > - Ensure runtime is independent of secret values. 60 | > - Ensure code access patterns[3] are independent of secret values. 61 | > - Ensure data access patterns[4] are independent of secret values. 62 | > 63 | > ... 64 | > 65 | > [2] Use of the term “constant time” is a legacy term that is ingrained in literature and used here for consistency. In modern processors, the time to execute a given set of instructions may vary depending on many factors. The key is to ensure that none of these factors are related to the manipulation of secret data values. Modern algorithm research uses the more inclusive term "data oblivious algorithm." 66 | > [3] A program's code access pattern is the order and address of instructions that it executes. 67 | > [4] A program's data access pattern is the order and address of memory operands that it loads and stores. 68 | 69 | These crates provide functions and data structures that have the "data-oblivious" property. 70 | 71 | A function is completely constant-time / data-oblivious if for any two sets of arguments you might pass it, the code and data access patterns are: 72 | 73 | - the same, or 74 | - identically distributed, or 75 | - distributed according to distributions that are computationally indistinguishable. 76 | 77 | For example, the implementations of the `CMov` trait in the `aligned-cmov` crate are completely constant-time, because the code and data access patterns 78 | are exactly the same no matter what the inputs are. 79 | 80 | The implementation of `access` in PathORAM is completely constant-time, because the code and data access patterns are identically distributed 81 | regardless of what memory position is accessed. 82 | 83 | In some more complex cases, a function may be oblivious with respect to some of its inputs, but not all of them. 84 | We follow the convention that those functions are labelled with `vartime` in their name, and explain to what extent if any they are oblivious in documentation. 85 | Sometimes such functions are completely oblivious with respect to some of the arguments but not all of them. 86 | Examples include `vartime_write` in the `ObliviousMap` trait. 87 | 88 | In some cases, it is obvious that the function will not be oblivious. For example the ORAM and ObliviousMap creator functions take a capacity as an argument. 89 | Increasing the capacity will require using more memory, so we are not oblivious with respect to that parameter. We nevertheless don't call the function `vartime_create`. 90 | 91 | As another example, the `access` function in PathORAM implementation takes a closure to which the accessed data is passed. 92 | This closure includes a function pointer -- if two different closures are passed, the code access patterns will be different. Additionally, 93 | if the code in the closure is not itself constant-time with respect to the query then we won't be constant-time. We don't bother documenting this since it should be clear to the user of the API. 94 | 95 | [chat-image]: https://img.shields.io/discord/844353360348971068?style=flat-square 96 | [chat-link]: https://discord.gg/mobilecoin 97 | [license-image]: https://img.shields.io/crates/l/aligned-cmov?style=flat-square 98 | [deps-image]: https://deps.rs/repo/github/mobilecoinfoundation/mc-oblivious/status.svg?style=flat-square 99 | [deps-link]: https://deps.rs/repo/github/mobilecoinfoundation/mc-oblivious 100 | [codecov-image]: https://img.shields.io/codecov/c/github/mobilecoinfoundation/mc-oblivious/master?style=flat-square 101 | [codecov-link]: https://codecov.io/gh/mobilecoinfoundation/mc-oblivious 102 | [gha-image]: https://img.shields.io/github/actions/workflow/status/mobilecoinfoundation/mc-oblivious/ci.yaml?branch=master&style=flat-square 103 | [gha-link]: https://github.com/mobilecoinfoundation/mc-oblivious/actions/workflows/ci.yaml?query=branch%master 104 | [conduct-link]: CODE_OF_CONDUCT.md 105 | [conduct-image]: https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?style=flat-square 106 | -------------------------------------------------------------------------------- /aligned-cmov/src/cmov_impl_asm.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | //! Implementation of cmov on x86-64 using inline assembly. 4 | //! 5 | //! Right now we have cmov of u32, u64, A8Bytes, and A64Bytes. 6 | //! 7 | //! This should be the most performant implementation that we know how to do 8 | //! inside a skylake+ x86-64 CPU in the SGX enclave, while meeting the security 9 | //! requirement that we don't leak "condition" over side-channels. 10 | //! The perf-critical case is expected to be A64Bytes of size 1024, 2048 or so, 11 | //! either 2 or 4 times less than page size. 12 | //! 13 | //! The u32, u64, and A8Bytes versions all use some form of CMOV instruction, 14 | //! and the 64-byte alignment version uses AVX2 VPMASKMOV instruction. 15 | //! 16 | //! We could possibly do the AVX2 stuff using intrinsics instead of inline 17 | //! assembly, but AFAIK we cannot get the CMOV instruction without inline 18 | //! assembly, because there are no intrinsics for that. 19 | //! For now it seems simplest to use inline assembly for all of it. 20 | 21 | use super::{A64Bytes, A8Bytes, ArrayLength}; 22 | use core::arch::asm; 23 | 24 | // CMov for u32 values 25 | #[inline] 26 | pub fn cmov_u32(condition: bool, src: &u32, dest: &mut u32) { 27 | unsafe { 28 | asm!( 29 | // Set ZF if cond==0 30 | "test {0}, {0}", 31 | // Conditionally move src into temp (based on ZF) 32 | // `:e` formats to the 32-bit register. 33 | "cmovnz {2:e}, {1:e}", 34 | in(reg_byte) condition as u8, 35 | in(reg) *src, 36 | // inout since we might not write, so we need the existing value. 37 | inout(reg) *dest, 38 | ); 39 | } 40 | } 41 | 42 | // CMov for u64 values 43 | #[inline] 44 | pub fn cmov_u64(condition: bool, src: &u64, dest: &mut u64) { 45 | unsafe { 46 | asm!( 47 | // Set ZF if cond==0 48 | "test {0}, {0}", 49 | // Conditionally move src into dest (based on ZF) 50 | "cmovnz {2}, {1}", 51 | in(reg_byte) condition as u8, 52 | in(reg) *src, 53 | // inout since we might not write, so we need the existing value. 54 | inout(reg) *dest, 55 | ); 56 | } 57 | } 58 | 59 | // CMov for usize on 64 bit pointer size architecture 60 | #[inline] 61 | #[cfg(target_pointer_width = "64")] 62 | pub fn cmov_usize(condition: bool, src: &usize, dest: &mut usize) { 63 | let src_transmuted = unsafe { core::mem::transmute::<&usize, &u64>(src) }; 64 | let dest_transmuted = unsafe { core::mem::transmute::<&mut usize, &mut u64>(dest) }; 65 | 66 | cmov_u64(condition, src_transmuted, dest_transmuted); 67 | } 68 | 69 | // CMov for usize on 32 bit pointer size architecture 70 | #[inline] 71 | #[cfg(target_pointer_width = "32")] 72 | pub fn cmov_usize(condition: bool, src: &usize, dest: &mut usize) { 73 | let src_transmuted = unsafe { core::mem::transmute::<&usize, &u32>(src) }; 74 | let dest_transmuted = unsafe { core::mem::transmute::<&mut usize, &mut u32>(dest) }; 75 | 76 | cmov_u32(condition, src_transmuted, dest_transmuted); 77 | } 78 | // CMov for i32 values 79 | #[inline] 80 | pub fn cmov_i32(condition: bool, src: &i32, dest: &mut i32) { 81 | let src_transmuted = unsafe { core::mem::transmute::<&i32, &u32>(src) }; 82 | let dest_transmuted = unsafe { core::mem::transmute::<&mut i32, &mut u32>(dest) }; 83 | 84 | cmov_u32(condition, src_transmuted, dest_transmuted); 85 | } 86 | 87 | // CMov for u64 values 88 | #[inline] 89 | pub fn cmov_i64(condition: bool, src: &i64, dest: &mut i64) { 90 | let src_transmuted = unsafe { core::mem::transmute::<&i64, &u64>(src) }; 91 | let dest_transmuted = unsafe { core::mem::transmute::<&mut i64, &mut u64>(dest) }; 92 | 93 | cmov_u64(condition, src_transmuted, dest_transmuted); 94 | } 95 | 96 | // CMov for blocks aligned to 8-byte boundary 97 | #[inline] 98 | pub fn cmov_a8_bytes>(condition: bool, src: &A8Bytes, dest: &mut A8Bytes) { 99 | // This test is expected to be optimized away but avoids UB if N == 0. 100 | if N::USIZE != 0 { 101 | // View the A8Bytes ref as pointers to u64. 102 | // Note that rust code is never actually dereferencing this pointer, 103 | // it is being fed to asm which will use it in assembly operands directly. 104 | // So no type-based alias analysis rules, or similar, can be relevant here. 105 | let count = (N::USIZE / 8) + (if 0 == N::USIZE % 8 { 0 } else { 1 }); 106 | unsafe { 107 | cmov_byte_slice_a8( 108 | condition, 109 | src as *const A8Bytes as *const u64, 110 | dest as *mut A8Bytes as *mut u64, 111 | count, 112 | ) 113 | }; 114 | } 115 | } 116 | 117 | // CMov for blocks aligned to 64-byte boundary 118 | // Without avx2, fallback to cmov_byte_slice_a8 119 | #[cfg(not(target_feature = "avx2"))] 120 | #[inline] 121 | pub fn cmov_a64_bytes>( 122 | condition: bool, 123 | src: &A64Bytes, 124 | dest: &mut A64Bytes, 125 | ) { 126 | if N::USIZE != 0 { 127 | let count = (N::USIZE / 8) + (if 0 == N::USIZE % 8 { 0 } else { 1 }); 128 | unsafe { 129 | cmov_byte_slice_a8( 130 | condition, 131 | src as *const A64Bytes as *const u64, 132 | dest as *mut A64Bytes as *mut u64, 133 | count, 134 | ) 135 | }; 136 | } 137 | } 138 | 139 | // If we have avx2 then we can use cmov_byte_slice_a64 implementation 140 | #[cfg(target_feature = "avx2")] 141 | #[inline] 142 | pub fn cmov_a64_bytes>( 143 | condition: bool, 144 | src: &A64Bytes, 145 | dest: &mut A64Bytes, 146 | ) { 147 | if N::USIZE != 0 { 148 | let count = (N::USIZE / 64) + (if 0 == N::USIZE % 64 { 0 } else { 1 }); 149 | unsafe { 150 | // Note! API for count is different from with cmov_byte_slice_a8, 151 | // because in the `[BASE + INDEX * SCALE + DISPLACEMENT]` syntax, 152 | // scale > 8 not allowed in x86-64, here the scale would need to be 64. 153 | // In cmov_byte_slice_a64, the 4th parameter is the number of bytes to copy. 154 | cmov_byte_slice_a64( 155 | condition, 156 | src as *const A64Bytes as *const u64, 157 | dest as *mut A64Bytes as *mut u64, 158 | count * 64, 159 | ) 160 | }; 161 | } 162 | } 163 | 164 | // Should be a constant time function equivalent to: 165 | // if condition { memcpy(dest, src, count * 8) } 166 | // for pointers aligned to 8 byte boundary. Assumes count > 0 167 | #[inline] 168 | unsafe fn cmov_byte_slice_a8(condition: bool, src: *const u64, dest: *mut u64, count: usize) { 169 | debug_assert!(count > 0, "count cannot be 0"); 170 | // The idea here is to test once before we enter the loop and reuse the test 171 | // result for every cmov. 172 | // The loop is a dec, jnz loop. 173 | // Because the semantics of x86 loops are, decrement, then test for 0, and 174 | // not test for 0 and then decrement, the values of the loop variable of 175 | // 1..count and not 0..count - 1. We adjust for this by subtracting 8 when 176 | // indexing. Because dec will clobber ZF, we can't use cmovnz. We store 177 | // the result of outer test in CF which is not clobbered by dec. 178 | // cmovc is contingent on CF 179 | // neg is used to set CF to 1 iff the condition value was 1. 180 | // Pity, we cannot cmov directly to memory 181 | // 182 | // count is modified by the assembly -- for this reason it has to be labelled 183 | // as an input and an output, otherwise its illegal to modify it. 184 | // 185 | // The condition is passed in through cond, then neg moves the value to CF. 186 | // After that we don't need condition in a register, so cond register can be 187 | // reused. 188 | asm!( 189 | // Sets CF=0 iff cond==0, 1 otherwise. 190 | "neg {0}", 191 | // Must use numeric local labels, since this block may be inlined multiple times. 192 | // https://doc.rust-lang.org/nightly/rust-by-example/unsafe/asm.html#labels 193 | "42:", 194 | // Copy into temp register from dest (for NOP default). 195 | "mov {0}, [{3} + 8*{1} - 8]", 196 | // Conditionally copy into temp register from src (based on CF). 197 | "cmovc {0}, [{2} + 8*{1} - 8]", 198 | // Copy from temp register into dest. 199 | "mov [{3} + 8*{1} - 8], {0}", 200 | // Decrement count. Sets ZF=1 when we reach 0. 201 | "dec {1}", 202 | // If ZF is not set, loop. 203 | "jnz 42b", 204 | // Discard outputs; the memory side effects are what's desired. 205 | inout(reg) (condition as u64) => _, 206 | inout(reg) count => _, 207 | in(reg) src, 208 | in(reg) dest, 209 | ); 210 | } 211 | 212 | // Should be a constant time function equivalent to: 213 | // if condition { memcpy(dest, src, num_bytes) } 214 | // for pointers aligned to *64* byte boundary. Will fault if that is not the 215 | // case. Requires num_bytes > 0, and num_bytes divisible by 64. 216 | // This version uses AVX2 256-bit moves 217 | #[cfg(target_feature = "avx2")] 218 | #[inline] 219 | unsafe fn cmov_byte_slice_a64(condition: bool, src: *const u64, dest: *mut u64, num_bytes: usize) { 220 | debug_assert!(num_bytes > 0, "num_bytes cannot be 0"); 221 | debug_assert!(num_bytes % 64 == 0, "num_bytes must be divisible by 64"); 222 | 223 | // Notes: 224 | // temp = {0} is the scratch register 225 | // 226 | // The values of {0} in the loop are num_bytes, num_bytes - 64, ... 64, 227 | // rather than num_bytes - 64 ... 0. This is because the semantics of the loop 228 | // are subtract 64, then test for 0, rather than test for 0, then subtract. 229 | // So when we index using {0}, we subtract 64 to compensate. 230 | // TODO: Does unrolling the loop more help? 231 | asm!( 232 | // Similarly to cmov_byte_slice_a8, we want to test once and use the 233 | // result for the whole loop. 234 | // Before we enter the loop, we want to set ymm1 to all 0s or all 1s, 235 | // depending on condition. We use neg to make all 64 bits 0s or all 1s 236 | // (since neg(0) = 0 and neg(1) = -1 = 11111111b in two's complement), 237 | // then vmovq to move that to xmm2, then vbroadcastsd to fill ymm1 238 | // with ones or zeros. 239 | "neg {0}", 240 | "vmovq xmm2, {0}", 241 | "vbroadcastsd ymm1, xmm2", 242 | // Once we have the mask in ymm1 we don't need the condition in 243 | // a register anymore. Then, {0} is the loop variable, counting down 244 | // from num_bytes in steps of 64 245 | "mov {0}, {3}", 246 | // Must use numeric local labels, since this block may be inlined multiple times. 247 | // https://doc.rust-lang.org/nightly/rust-by-example/unsafe/asm.html#labels 248 | "42:", 249 | // This time the cmov mechanism is: 250 | // - VMOVDQA to load the source into a ymm register, 251 | // - VPMASKMOVQ to move that to memory, after masking it with ymm1. 252 | // An alternative approach which I didn't test is, MASKMOV to 253 | // another ymm register, then move that register to memory. 254 | "vmovdqa ymm2, [{1} + {0} - 64]", 255 | "vpmaskmovq [{2} + {0} - 64], ymm1, ymm2", 256 | // We unroll the loop once because we can assume 64 byte alignment, 257 | // but ymm register holds 32 bytes. So one round of 258 | // vmovdqa+vpmaskmovq moves 32 bytes. 259 | // So we do this twice in one pass through the loop. 260 | // 261 | // TODO: In AVX512 zmm registers we could move 64 bytes at once... 262 | "vmovdqa ymm3, [{1} + {0} - 32]", 263 | "vpmaskmovq [{2} + {0} - 32], ymm1, ymm3", 264 | // Decrement num_bytes. Sets ZF=1 when we reach 0. 265 | "sub {0}, 64", 266 | // If ZF is not set, loop. 267 | "jnz 42b", 268 | // Discard output; the memory side effects are what's desired. 269 | inout(reg) condition as u64 => _, 270 | in(reg) src, 271 | in(reg) dest, 272 | in(reg) num_bytes, 273 | // Scratch/Temp registers. 274 | out("ymm1") _, 275 | out("ymm2") _, 276 | out("ymm3") _, 277 | ); 278 | } 279 | 280 | // TODO: In avx512 there is vmovdqa which takes a mask register, and kmovq to 281 | // move register to mask register which seems like a good candidate to speed 282 | // this up for 64-byte aligned chunks 283 | -------------------------------------------------------------------------------- /no-asm-tests/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | use aligned_cmov::{ 4 | typenum::{U1024, U2, U2048, U32, U4, U4096, U64}, 5 | ArrayLength, 6 | }; 7 | use core::marker::PhantomData; 8 | use mc_oblivious_ram::{ 9 | CircuitOramDeterministicEvictor, CircuitOramDeterministicEvictorCreator, PathORAM, 10 | PathOramDeterministicEvictor, PathOramDeterministicEvictorCreator, 11 | }; 12 | use mc_oblivious_traits::{ 13 | rng_maker, HeapORAMStorageCreator, ORAMCreator, ORAMStorageCreator, ORAM, 14 | }; 15 | use std::panic::{catch_unwind, AssertUnwindSafe}; 16 | use test_helper::{CryptoRng, RngCore, RngType, SeedableRng}; 17 | extern crate alloc; 18 | extern crate std; 19 | mod insecure_position_map; 20 | use insecure_position_map::InsecurePositionMapCreator; 21 | 22 | /// Create an ORAM and drive it until the stash overflows, then return the 23 | /// number of operations. Should be driven by a seeded Rng 24 | /// 25 | /// Arguments: 26 | /// * size: Size of ORAM. Must be a power of two. 27 | /// * stash_size: Size of ORAM stash. 28 | /// * rng: Rng to use to construct the ORAM and to drive the random accesses. 29 | /// * limit: The maximum number of accesses to do until we bail out of the loop 30 | /// 31 | /// Returns: 32 | /// * Some(n) if we overflowed after the n'th access 33 | /// * None if we hit the limit without overflowing and stopped testing 34 | pub fn measure_stash_overflow( 35 | size: u64, 36 | stash_size: usize, 37 | rng: Rng, 38 | limit: usize, 39 | ) -> Option 40 | where 41 | ValueSize: ArrayLength, 42 | Rng: RngCore + CryptoRng + SeedableRng + 'static, 43 | OC: ORAMCreator, 44 | { 45 | let mut oram = OC::create(size, stash_size, &mut rng_maker(rng)); 46 | 47 | for rep in 0..limit { 48 | if let Err(_err) = catch_unwind(AssertUnwindSafe(|| oram.access(rep as u64 % size, |_| {}))) 49 | { 50 | return Some(rep); 51 | } 52 | } 53 | 54 | None 55 | } 56 | 57 | pub fn main() { 58 | const SIZES: &[u64] = &[1024, 8192, 16384, 32768]; 59 | const STASH_SIZES: &[usize] = &[4, 6, 8, 10, 12, 14, 16, 17]; 60 | const REPS: usize = 100; 61 | const LIMIT: usize = 500_000; 62 | 63 | println!("PathORAM4096Z4:"); 64 | for size in SIZES { 65 | let mut maker = rng_maker(test_helper::get_seeded_rng()); 66 | for stash_size in STASH_SIZES { 67 | let mut least_overflow: Option = None; 68 | for _ in 0..REPS { 69 | let result = measure_stash_overflow::< 70 | InsecurePathORAM4096Z4Creator, 71 | U1024, 72 | RngType, 73 | >( 74 | *size, *stash_size, maker(), least_overflow.unwrap_or(LIMIT) 75 | ); 76 | 77 | if let Some(val) = result { 78 | let least = least_overflow.get_or_insert(val); 79 | if val < *least { 80 | *least = val 81 | } 82 | } 83 | } 84 | 85 | let desc = match least_overflow { 86 | Some(num) => format!("= {}", num), 87 | None => format!(">= {}", LIMIT), 88 | }; 89 | 90 | println!( 91 | "{{ size = {}, stash = {}, repetitions = {}, least overflow {} }}", 92 | size, stash_size, REPS, desc 93 | ); 94 | } 95 | } 96 | } 97 | 98 | /// Creator for PathORAM based on 4096-sized blocks of storage and bucket size 99 | /// (Z) of 4, and the insecure position map implementation. 100 | /// This is used to determine how to calibrate stash size appropriately via 101 | /// stress tests. 102 | pub struct InsecurePathORAM4096Z4Creator> { 103 | _sc: PhantomData SC>, 104 | } 105 | 106 | impl> ORAMCreator 107 | for InsecurePathORAM4096Z4Creator 108 | { 109 | type Output = PathORAM; 110 | 111 | fn create RngType>( 112 | size: u64, 113 | stash_size: usize, 114 | rng_maker: &mut M, 115 | ) -> Self::Output { 116 | let evictor_factory = PathOramDeterministicEvictorCreator::new(0); 117 | PathORAM::new::< 118 | InsecurePositionMapCreator, 119 | SC, 120 | M, 121 | PathOramDeterministicEvictorCreator, 122 | >(size, stash_size, rng_maker, evictor_factory) 123 | } 124 | } 125 | 126 | /// Creator for CircuitOram based on 4096-sized blocks of storage 127 | /// and bucket size (Z) of 2, and the insecure position map implementation. 128 | /// This is used to determine calibrate circuit oram 129 | pub struct InsecureCircuitORAM4096Z2Creator> 130 | where 131 | R: RngCore + CryptoRng + 'static, 132 | { 133 | _sc: PhantomData SC>, 134 | _rng: PhantomData R>, 135 | } 136 | 137 | impl> ORAMCreator 138 | for InsecureCircuitORAM4096Z2Creator 139 | where 140 | R: RngCore + CryptoRng + Send + Sync + 'static, 141 | SC: ORAMStorageCreator, 142 | { 143 | type Output = PathORAM; 144 | 145 | fn create R>( 146 | size: u64, 147 | stash_size: usize, 148 | rng_maker: &mut M, 149 | ) -> Self::Output { 150 | let evictor_factory = CircuitOramDeterministicEvictorCreator::new(2); 151 | PathORAM::new::, SC, M, CircuitOramDeterministicEvictorCreator>( 152 | size, 153 | stash_size, 154 | rng_maker, 155 | evictor_factory, 156 | ) 157 | } 158 | } 159 | 160 | #[cfg(test)] 161 | mod tests { 162 | use super::*; 163 | use alloc::collections::BTreeMap; 164 | use core::convert::TryInto; 165 | use mc_oblivious_traits::{rng_maker, testing, HeapORAMStorageCreator, ORAMCreator}; 166 | use std::vec; 167 | use test_helper::{run_with_one_seed, run_with_several_seeds}; 168 | 169 | // Run the exercise oram tests for 200,000 rounds in 131072 sized z4 oram 170 | #[test] 171 | fn exercise_path_oram_z4_131072() { 172 | run_with_several_seeds(|rng| { 173 | let mut maker = rng_maker(rng); 174 | let mut rng = maker(); 175 | let stash_size = 16; 176 | let mut oram = InsecurePathORAM4096Z4Creator::::create( 177 | 131072, stash_size, &mut maker, 178 | ); 179 | testing::exercise_oram(200_000, &mut oram, &mut rng); 180 | }); 181 | } 182 | 183 | // Run the exercise oram tests for 400,000 rounds in 262144 sized z4 oram 184 | #[test] 185 | fn exercise_path_oram_z4_262144() { 186 | run_with_several_seeds(|rng| { 187 | let mut maker = rng_maker(rng); 188 | let mut rng = maker(); 189 | let stash_size = 16; 190 | let mut oram = InsecurePathORAM4096Z4Creator::::create( 191 | 262144, stash_size, &mut maker, 192 | ); 193 | testing::exercise_oram(400_000, &mut oram, &mut rng); 194 | }); 195 | } 196 | 197 | // Run the analysis oram tests similar to CircuitOram section 5. Warm up with 198 | // 2^10 accesses, then run for 2^20 accesses cycling through all N logical 199 | // addresses. N=2^10. This choice is arbitrary because stash size should not 200 | // depend on N. Measure the number of times that the stash is above any 201 | // given size. 202 | #[test] 203 | fn analyse_path_oram_z4_8192() { 204 | const STASH_SIZE: usize = 32; 205 | const CORRELATION_THRESHOLD: f64 = 0.85; 206 | run_with_several_seeds(|rng| { 207 | let base: u64 = 2; 208 | let num_prerounds: u64 = base.pow(10); 209 | let num_rounds: u64 = base.pow(20); 210 | let mut maker = rng_maker(rng); 211 | let mut rng = maker(); 212 | let mut oram = InsecurePathORAM4096Z4Creator::::create( 213 | base.pow(10), 214 | STASH_SIZE, 215 | &mut maker, 216 | ); 217 | let stash_stats = testing::measure_oram_stash_size_distribution( 218 | num_prerounds.try_into().unwrap(), 219 | num_rounds.try_into().unwrap(), 220 | &mut oram, 221 | &mut rng, 222 | ); 223 | let mut x_axis: vec::Vec = vec::Vec::new(); 224 | let mut y_axis: vec::Vec = vec::Vec::new(); 225 | #[cfg(debug_assertions)] 226 | dbg!(stash_stats.get(&0).unwrap_or(&0)); 227 | for stash_count in 1..STASH_SIZE { 228 | if let Some(stash_count_probability) = stash_stats.get(&stash_count) { 229 | #[cfg(debug_assertions)] 230 | dbg!(stash_count, stash_count_probability); 231 | y_axis.push((num_rounds as f64 / *stash_count_probability as f64).log2()); 232 | x_axis.push(stash_count as f64); 233 | } else { 234 | #[cfg(debug_assertions)] 235 | dbg!(stash_count); 236 | } 237 | } 238 | 239 | let correlation = rgsl::statistics::correlation(&x_axis, 1, &y_axis, 1, x_axis.len()); 240 | #[cfg(debug_assertions)] 241 | dbg!(correlation); 242 | assert!(correlation > CORRELATION_THRESHOLD); 243 | }); 244 | } 245 | 246 | // Test for stash performance independence for changing N (Oram size) without 247 | // changing number of calls. 248 | #[test] 249 | fn analyse_oram_n_independence() { 250 | const STASH_SIZE: usize = 32; 251 | const BASE: u64 = 2; 252 | const NUM_ROUNDS: u64 = BASE.pow(20); 253 | const NUM_PREROUNDS: u64 = BASE.pow(10); 254 | const VARIANCE_THRESHOLD: f64 = 0.15; 255 | 256 | run_with_one_seed(|rng| { 257 | let mut oram_size_to_stash_size_by_count = 258 | BTreeMap::>::default(); 259 | let mut maker = rng_maker(rng); 260 | for oram_power in (10..24).step_by(2) { 261 | let mut rng = maker(); 262 | let oram_size = BASE.pow(oram_power); 263 | let mut oram = InsecurePathORAM4096Z4Creator::::create( 264 | oram_size, STASH_SIZE, &mut maker, 265 | ); 266 | let stash_stats = testing::measure_oram_stash_size_distribution( 267 | NUM_PREROUNDS.try_into().unwrap(), 268 | NUM_ROUNDS.try_into().unwrap(), 269 | &mut oram, 270 | &mut rng, 271 | ); 272 | oram_size_to_stash_size_by_count.insert(oram_power, stash_stats); 273 | } 274 | for stash_num in 1..6 { 275 | let mut probability_of_stash_size = vec::Vec::new(); 276 | for (_oram_power, stash_size_by_count) in &oram_size_to_stash_size_by_count { 277 | if let Some(stash_count) = stash_size_by_count.get(&stash_num) { 278 | let stash_count_probability = 279 | (NUM_ROUNDS as f64 / *stash_count as f64).log2(); 280 | probability_of_stash_size.push(stash_count_probability); 281 | #[cfg(debug_assertions)] 282 | dbg!(stash_num, stash_count, _oram_power); 283 | #[cfg(debug_assertions)] 284 | dbg!(stash_num, stash_count_probability, _oram_power); 285 | } else { 286 | #[cfg(debug_assertions)] 287 | dbg!(stash_num, _oram_power); 288 | } 289 | } 290 | let data_variance = rgsl::statistics::variance( 291 | &probability_of_stash_size, 292 | 1, 293 | probability_of_stash_size.len(), 294 | ); 295 | #[cfg(debug_assertions)] 296 | dbg!(stash_num, data_variance); 297 | assert!(data_variance < VARIANCE_THRESHOLD); 298 | } 299 | }); 300 | } 301 | 302 | // Run the analysis oram tests similar to CircuitOram section 5. Warm up with 303 | // 2^10 accesses, then run for 2^10 accesses cycling through all N logical 304 | // addresses. N=2^5. This choice is arbitrary because stash size should not 305 | // depend on N. Measure the number of times that the stash is above any 306 | // given size. 307 | #[test] 308 | fn analyse_obliviouscircuit_oram_z2_8192() { 309 | const STASH_SIZE: usize = 32; 310 | const CORRELATION_THRESHOLD: f64 = 0.85; 311 | run_with_several_seeds(|rng| { 312 | let base: u64 = 2; 313 | let num_prerounds: u64 = base.pow(10); 314 | let num_rounds: u64 = base.pow(10); 315 | let mut maker = rng_maker(rng); 316 | let mut rng = maker(); 317 | let mut oram = 318 | InsecureCircuitORAM4096Z2Creator::::create( 319 | base.pow(5), 320 | STASH_SIZE, 321 | &mut maker, 322 | ); 323 | let stash_stats = testing::measure_oram_stash_size_distribution( 324 | num_prerounds.try_into().unwrap(), 325 | num_rounds.try_into().unwrap(), 326 | &mut oram, 327 | &mut rng, 328 | ); 329 | let mut x_axis: vec::Vec = vec::Vec::new(); 330 | let mut y_axis: vec::Vec = vec::Vec::new(); 331 | #[cfg(debug_assertions)] 332 | dbg!(stash_stats.get(&0).unwrap_or(&0)); 333 | for stash_count in 1..STASH_SIZE { 334 | if let Some(stash_count_probability) = stash_stats.get(&stash_count) { 335 | #[cfg(debug_assertions)] 336 | dbg!(stash_count, stash_count_probability); 337 | y_axis.push((num_rounds as f64 / *stash_count_probability as f64).log2()); 338 | x_axis.push(stash_count as f64); 339 | } else { 340 | #[cfg(debug_assertions)] 341 | dbg!(stash_count); 342 | } 343 | } 344 | // We do not check the correlation if we keep the stash count sufficiently 345 | // small, because otherwise it is not enough elements for correlation to be 346 | // reliable. If the oram is packing very efficiently, that is 347 | // sufficient for our use case. 348 | if x_axis.len() > 5 { 349 | let correlation = 350 | rgsl::statistics::correlation(&x_axis, 1, &y_axis, 1, x_axis.len()); 351 | #[cfg(debug_assertions)] 352 | dbg!(correlation); 353 | assert!(correlation > CORRELATION_THRESHOLD); 354 | } 355 | }); 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /mc-oblivious-traits/src/testing.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | //! Some generic tests that exercise objects implementing these traits 4 | 5 | use crate::{ObliviousHashMap, OMAP_FOUND, OMAP_INVALID_KEY, OMAP_NOT_FOUND, OMAP_OVERFLOW, ORAM}; 6 | use aligned_cmov::{subtle::Choice, typenum::U8, A64Bytes, A8Bytes, Aligned, ArrayLength}; 7 | use alloc::{ 8 | collections::{btree_map::Entry, BTreeMap}, 9 | vec::Vec, 10 | }; 11 | use rand_core::{CryptoRng, RngCore}; 12 | 13 | /// Exercise an ORAM by writing, reading, and rewriting, a progressively larger 14 | /// set of random locations 15 | pub fn exercise_oram(mut num_rounds: usize, oram: &mut O, rng: &mut R) 16 | where 17 | BlockSize: ArrayLength, 18 | O: ORAM, 19 | R: RngCore + CryptoRng, 20 | { 21 | let len = oram.len(); 22 | assert!(len != 0, "len is zero"); 23 | assert_eq!(len & (len - 1), 0, "len is not a power of two"); 24 | let mut expected = BTreeMap::>::default(); 25 | let mut probe_positions = Vec::::new(); 26 | let mut probe_idx = 0usize; 27 | 28 | while num_rounds > 0 { 29 | if probe_idx >= probe_positions.len() { 30 | probe_positions.push(rng.next_u64() & (len - 1)); 31 | probe_idx = 0; 32 | } 33 | let query = probe_positions[probe_idx]; 34 | 35 | query_oram_and_randomize(&mut expected, query, oram, rng); 36 | 37 | probe_idx += 1; 38 | num_rounds -= 1; 39 | } 40 | } 41 | 42 | /// Exercise an ORAM by writing, reading, and rewriting, all locations 43 | /// consecutively 44 | pub fn exercise_oram_consecutive(mut num_rounds: usize, oram: &mut O, rng: &mut R) 45 | where 46 | BlockSize: ArrayLength, 47 | O: ORAM, 48 | R: RngCore + CryptoRng, 49 | { 50 | let len = oram.len(); 51 | assert!(len != 0, "len is zero"); 52 | assert_eq!(len & (len - 1), 0, "len is not a power of two"); 53 | let mut expected = BTreeMap::>::default(); 54 | 55 | while num_rounds > 0 { 56 | let query = num_rounds as u64 & (len - 1); 57 | query_oram_and_randomize(&mut expected, query, oram, rng); 58 | 59 | num_rounds -= 1; 60 | } 61 | } 62 | 63 | fn query_oram_and_randomize( 64 | expected: &mut BTreeMap< 65 | u64, 66 | Aligned>, 67 | >, 68 | query: u64, 69 | oram: &mut O, 70 | rng: &mut R, 71 | ) where 72 | BlockSize: ArrayLength, 73 | O: ORAM, 74 | R: RngCore + CryptoRng, 75 | { 76 | let expected_ent = expected.entry(query).or_default(); 77 | oram.access(query, |val| { 78 | assert_eq!(val, expected_ent); 79 | rng.fill_bytes(val); 80 | expected_ent.clone_from_slice(val.as_slice()); 81 | }); 82 | } 83 | 84 | /// Exercise an ORAM by writing, reading, and rewriting, first cycling through 85 | /// all N locations num_pre_rounds times to warm up the oram, then repeatedly 86 | /// cycling through all N locations a total of num_rounds times as a worst case 87 | /// access sequence and measuring the stash size. 88 | pub fn measure_oram_stash_size_distribution( 89 | mut num_pre_rounds: usize, 90 | mut num_rounds: usize, 91 | oram: &mut O, 92 | rng: &mut R, 93 | ) -> BTreeMap 94 | where 95 | BlockSize: ArrayLength, 96 | O: ORAM, 97 | R: RngCore + CryptoRng, 98 | { 99 | let len = oram.len(); 100 | assert!(len != 0, "len is zero"); 101 | assert_eq!(len & (len - 1), 0, "len is not a power of two"); 102 | 103 | let mut expected = BTreeMap::>::default(); 104 | let mut probe_idx = 0u64; 105 | let mut stash_size_by_count = BTreeMap::::default(); 106 | 107 | while num_pre_rounds > 0 { 108 | query_oram_and_randomize(&mut expected, probe_idx, oram, rng); 109 | probe_idx = (probe_idx + 1) & (len - 1); 110 | num_pre_rounds -= 1; 111 | } 112 | 113 | while num_rounds > 0 { 114 | query_oram_and_randomize(&mut expected, probe_idx, oram, rng); 115 | *stash_size_by_count.entry(oram.stash_size()).or_default() += 1; 116 | probe_idx = (probe_idx + 1) & (len - 1); 117 | num_rounds -= 1; 118 | } 119 | stash_size_by_count 120 | } 121 | 122 | /// Exercise an OMAP by writing, reading, accessing, and removing a 123 | /// progressively larger set of random locations 124 | pub fn exercise_omap(mut num_rounds: usize, omap: &mut O, rng: &mut R) 125 | where 126 | KeySize: ArrayLength, 127 | ValSize: ArrayLength, 128 | O: ObliviousHashMap, 129 | R: RngCore + CryptoRng, 130 | { 131 | let mut expected = BTreeMap::, A8Bytes>::default(); 132 | let mut probe_positions = Vec::>::new(); 133 | let mut probe_idx = 0usize; 134 | 135 | while num_rounds > 0 { 136 | if probe_idx >= probe_positions.len() { 137 | let mut bytes = A8Bytes::::default(); 138 | rng.fill_bytes(&mut bytes); 139 | probe_positions.push(bytes); 140 | probe_idx = 0; 141 | } 142 | 143 | // In one round, do a query from the sequence and a random query 144 | let query1 = probe_positions[probe_idx].clone(); 145 | let query2 = { 146 | let mut bytes = A8Bytes::::default(); 147 | rng.fill_bytes(&mut bytes); 148 | bytes 149 | }; 150 | 151 | for query in &[query1, query2] { 152 | // First, read at query and sanity check it 153 | { 154 | let mut output = A8Bytes::::default(); 155 | let result_code = omap.read(query, &mut output); 156 | 157 | let expected_ent = expected.entry(query.clone()); 158 | match expected_ent { 159 | Entry::Vacant(_) => { 160 | assert_eq!(result_code, OMAP_NOT_FOUND); 161 | } 162 | Entry::Occupied(occ) => { 163 | assert_eq!(result_code, OMAP_FOUND); 164 | assert_eq!(&output, occ.get()); 165 | } 166 | }; 167 | } 168 | 169 | // decide what random action to take that modifies the map 170 | let action = rng.next_u32() % 7; 171 | match action { 172 | // In this case we only READ and continue through the loop 173 | 0 => { 174 | continue; 175 | } 176 | 1 | 2 => { 177 | // In this case we WRITE to the omap, allowing overwrite 178 | let mut new_val = A8Bytes::::default(); 179 | rng.fill_bytes(new_val.as_mut_slice()); 180 | let result_code = omap.vartime_write(query, &new_val, Choice::from(1)); 181 | 182 | if expected.contains_key(query) { 183 | assert_eq!(result_code, OMAP_FOUND); 184 | } else { 185 | assert_eq!(result_code, OMAP_NOT_FOUND); 186 | } 187 | 188 | expected 189 | .entry(query.clone()) 190 | .or_default() 191 | .copy_from_slice(new_val.as_slice()); 192 | } 193 | 3 | 4 => { 194 | // In this case we WRITE to the omap, not allowing overwrite 195 | let mut new_val = A8Bytes::::default(); 196 | rng.fill_bytes(new_val.as_mut_slice()); 197 | let result_code = omap.vartime_write(query, &new_val, Choice::from(0)); 198 | 199 | if expected.contains_key(query) { 200 | assert_eq!(result_code, OMAP_FOUND); 201 | } else { 202 | assert_eq!(result_code, OMAP_NOT_FOUND); 203 | } 204 | 205 | expected.entry(query.clone()).or_insert(new_val); 206 | } 207 | 5 => { 208 | // In this case we ACCESS the omap 209 | omap.access(query, |result_code, val| { 210 | match expected.entry(query.clone()) { 211 | Entry::Vacant(_) => { 212 | assert_eq!(result_code, OMAP_NOT_FOUND); 213 | } 214 | Entry::Occupied(mut occ) => { 215 | assert_eq!(result_code, OMAP_FOUND); 216 | rng.fill_bytes(val.as_mut_slice()); 217 | *occ.get_mut() = val.clone(); 218 | } 219 | } 220 | }) 221 | } 222 | _ => { 223 | // In this case we REMOVE from the omap 224 | let result_code = omap.remove(query); 225 | 226 | if expected.remove(query).is_some() { 227 | assert_eq!(result_code, OMAP_FOUND); 228 | } else { 229 | assert_eq!(result_code, OMAP_NOT_FOUND); 230 | } 231 | } 232 | }; 233 | 234 | // Finally read from the position again as an extra check 235 | { 236 | // In this case we READ from omap 237 | let mut output = A8Bytes::::default(); 238 | let result_code = omap.read(query, &mut output); 239 | 240 | let expected_ent = expected.entry(query.clone()); 241 | match expected_ent { 242 | Entry::Vacant(_) => { 243 | assert_eq!(result_code, OMAP_NOT_FOUND); 244 | } 245 | Entry::Occupied(occ) => { 246 | assert_eq!(result_code, OMAP_FOUND); 247 | assert_eq!(&output, occ.get(),); 248 | } 249 | }; 250 | } 251 | } 252 | 253 | probe_idx += 1; 254 | num_rounds -= 1; 255 | } 256 | } 257 | 258 | /// Take an empty omap and add items consecutively to it until it overflows. 259 | /// Then test that on overflow we have rollback semantics, and we can still find 260 | /// all of the items that we added. 261 | pub fn test_omap_overflow(omap: &mut O) -> u64 262 | where 263 | KeySize: ArrayLength, 264 | ValSize: ArrayLength, 265 | O: ObliviousHashMap, 266 | { 267 | // count from 1 because 0 is an invalid key 268 | let mut idx = 1u64; 269 | let mut key = A8Bytes::::default(); 270 | let mut val = A8Bytes::::default(); 271 | 272 | loop { 273 | assert_eq!(omap.len(), idx - 1, "unexpected omap.len()"); 274 | key[0..8].copy_from_slice(&idx.to_le_bytes()); 275 | val[0..8].copy_from_slice(&idx.to_le_bytes()); 276 | let result_code = omap.vartime_write(&key, &val, Choice::from(0)); 277 | 278 | if result_code == OMAP_FOUND { 279 | panic!("unexpectedly found item idx = {}", idx); 280 | } else if result_code == OMAP_INVALID_KEY { 281 | panic!("unexpectedly recieved OMAP_INVALID_KEY, idx = {}", idx); 282 | } else if result_code == OMAP_OVERFLOW { 283 | // Now that we got an overflow, lets test if rollback semantics worked. 284 | assert_eq!( 285 | omap.len(), 286 | idx - 1, 287 | "omap.len() unexpected value after overflow" 288 | ); 289 | let mut temp = A8Bytes::::default(); 290 | for idx2 in 1u64..idx { 291 | key[0..8].copy_from_slice(&idx2.to_le_bytes()); 292 | val[0..8].copy_from_slice(&idx2.to_le_bytes()); 293 | let result_code = omap.read(&key, &mut temp); 294 | assert_eq!( 295 | result_code, OMAP_FOUND, 296 | "Failed to find an item that should be in the map: idx2 = {}", 297 | idx2 298 | ); 299 | assert_eq!( 300 | temp, val, 301 | "Value that was stored in the map was wrong after overflow: idx2 = {}", 302 | idx2 303 | ); 304 | } 305 | return omap.len(); 306 | } else if result_code != OMAP_NOT_FOUND { 307 | panic!("unexpected result code: {}", result_code); 308 | } 309 | 310 | idx += 1; 311 | } 312 | } 313 | 314 | /// Exercise an OMAP used as an oblivious counter table via the 315 | /// access_and_insert operation 316 | pub fn exercise_omap_counter_table(mut num_rounds: usize, omap: &mut O, rng: &mut R) 317 | where 318 | KeySize: ArrayLength, 319 | O: ObliviousHashMap, 320 | R: RngCore + CryptoRng, 321 | { 322 | type ValSize = U8; 323 | let zero: A8Bytes = Default::default(); 324 | 325 | let mut expected = BTreeMap::, A8Bytes>::default(); 326 | let mut probe_positions = Vec::>::new(); 327 | let mut probe_idx = 0usize; 328 | 329 | while num_rounds > 0 { 330 | if probe_idx >= probe_positions.len() { 331 | let mut bytes = A8Bytes::::default(); 332 | rng.fill_bytes(&mut bytes); 333 | probe_positions.push(bytes); 334 | probe_idx = 0; 335 | } 336 | 337 | // In one round, do a query from the sequence and a random query 338 | let query1 = probe_positions[probe_idx].clone(); 339 | let query2 = { 340 | let mut bytes = A8Bytes::::default(); 341 | rng.fill_bytes(&mut bytes); 342 | bytes 343 | }; 344 | 345 | for query in &[query1, query2] { 346 | // First, read at query and sanity check it 347 | { 348 | let mut output = A8Bytes::::default(); 349 | let result_code = omap.read(query, &mut output); 350 | 351 | let expected_ent = expected.entry(query.clone()); 352 | match expected_ent { 353 | Entry::Vacant(_) => { 354 | // Value should be absent or 0 (0's are created by the random inserts) 355 | assert!( 356 | result_code == OMAP_NOT_FOUND 357 | || (result_code == OMAP_FOUND && output == zero), 358 | "Expected no value but omap found nonzero value: result_code {}", 359 | result_code 360 | ); 361 | } 362 | Entry::Occupied(occ) => { 363 | assert!( 364 | result_code == OMAP_FOUND 365 | || (result_code == OMAP_NOT_FOUND && occ.get() == &zero), 366 | "Expected a value but OMAP found none: result_code: {}", 367 | result_code 368 | ); 369 | assert_eq!(&output, occ.get()); 370 | } 371 | }; 372 | } 373 | 374 | // Next, use access_and_insert to increment it 375 | let result_code = omap.access_and_insert(query, &zero, rng, |_status_code, buffer| { 376 | let num = u64::from_ne_bytes(*buffer.as_ref()) + 1; 377 | *buffer = Aligned(num.to_ne_bytes().into()); 378 | 379 | expected 380 | .entry(query.clone()) 381 | .or_default() 382 | .copy_from_slice(buffer); 383 | }); 384 | assert!(result_code != OMAP_INVALID_KEY, "Invalid key"); 385 | if result_code == OMAP_OVERFLOW { 386 | // When overflow occurs, we don't know if the change was rolled back or not, 387 | // so we have to read the map to figure it out, if we want to continue the test. 388 | let mut buffer = A8Bytes::::default(); 389 | let _result_code = omap.read(query, &mut buffer); 390 | 391 | let map_num = u64::from_ne_bytes(*buffer.as_ref()); 392 | let expected_buf = expected.get(query).unwrap().clone(); 393 | let expected_num = u64::from_ne_bytes(*expected_buf.as_ref()); 394 | assert!( 395 | map_num == expected_num || map_num + 1 == expected_num, 396 | "Unexpected value in omap: map_num {}, expected_num = {}", 397 | map_num, 398 | expected_num 399 | ); 400 | expected 401 | .entry(query.clone()) 402 | .or_default() 403 | .copy_from_slice(&buffer); 404 | } 405 | 406 | // Finally read from the position again as an extra check 407 | { 408 | // In this case we READ from omap 409 | let mut output = A8Bytes::::default(); 410 | let result_code = omap.read(query, &mut output); 411 | 412 | let expected_ent = expected.entry(query.clone()); 413 | match expected_ent { 414 | Entry::Vacant(_) => { 415 | // Value should be absent or 0 (0's are created by the random inserts) 416 | assert!( 417 | result_code == OMAP_NOT_FOUND 418 | || (result_code == OMAP_FOUND && output == zero), 419 | "Expected no value but omap found nonzero value: result_code {}", 420 | result_code 421 | ); 422 | } 423 | Entry::Occupied(occ) => { 424 | assert!( 425 | result_code == OMAP_FOUND 426 | || (result_code == OMAP_NOT_FOUND && occ.get() == &zero), 427 | "Expected a value but OMAP found none: result_code: {}", 428 | result_code 429 | ); 430 | assert_eq!(&output, occ.get()); 431 | } 432 | }; 433 | } 434 | } 435 | 436 | probe_idx += 1; 437 | num_rounds -= 1; 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /aligned-cmov/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | #![no_std] 4 | 5 | pub use aligned_array::{subtle, Aligned, AsAlignedChunks, AsNeSlice, A64, A8}; 6 | pub use generic_array::{arr, typenum, ArrayLength, GenericArray}; 7 | use subtle::Choice; 8 | 9 | /// An alias representing 8-byte aligned bytes, mainly to save typing 10 | pub type A8Bytes = Aligned>; 11 | /// An alias representing 64-byte aligned bytes, mainly to save typing 12 | pub type A64Bytes = Aligned>; 13 | 14 | /// CMov represents types that can be (obliviously) conditionally moved. 15 | /// 16 | /// "Conditional move" means: `if condition { *dest = *src }` 17 | /// The interesting case is when this must be side-channel resistant and 18 | /// the condition cannot be leaked over CPU side-channels. 19 | /// 20 | /// This API is object-oriented, and we take self = dest. 21 | /// This is good in rust because then you don't have to name the type 22 | /// to call the function. 23 | /// 24 | /// These are the types that we can hope to support with ORAM, 25 | /// and this API allows ORAM to be written in a generic way. 26 | /// 27 | /// Note: Types that own dynamic memory cannot be CMov by design. 28 | /// They also cannot be in an ORAM, by design. 29 | /// If your type has nontrivial `Drop` it is likely not reasonable to `CMov` it 30 | /// or put it in an ORAM. 31 | pub trait CMov: Sized { 32 | fn cmov(&mut self, condition: Choice, src: &Self); 33 | } 34 | 35 | impl CMov for u32 { 36 | #[inline] 37 | fn cmov(&mut self, condition: Choice, src: &u32) { 38 | cmov_impl::cmov_u32(condition.unwrap_u8() != 0, src, self) 39 | } 40 | } 41 | 42 | impl CMov for u64 { 43 | #[inline] 44 | fn cmov(&mut self, condition: Choice, src: &u64) { 45 | cmov_impl::cmov_u64(condition.unwrap_u8() != 0, src, self) 46 | } 47 | } 48 | 49 | impl CMov for i32 { 50 | #[inline] 51 | fn cmov(&mut self, condition: Choice, src: &i32) { 52 | cmov_impl::cmov_i32(condition.unwrap_u8() != 0, src, self) 53 | } 54 | } 55 | 56 | impl CMov for i64 { 57 | #[inline] 58 | fn cmov(&mut self, condition: Choice, src: &i64) { 59 | cmov_impl::cmov_i64(condition.unwrap_u8() != 0, src, self) 60 | } 61 | } 62 | 63 | impl CMov for usize { 64 | #[inline] 65 | fn cmov(&mut self, condition: Choice, src: &usize) { 66 | cmov_impl::cmov_usize(condition.unwrap_u8() != 0, src, self) 67 | } 68 | } 69 | 70 | impl CMov for bool { 71 | #[inline] 72 | fn cmov(&mut self, condition: Choice, src: &bool) { 73 | let mut temp = *self as u32; 74 | temp.cmov(condition, &(*src as u32)); 75 | *self = temp != 0; 76 | } 77 | } 78 | 79 | impl> CMov for A8Bytes { 80 | #[inline] 81 | fn cmov(&mut self, condition: Choice, src: &A8Bytes) { 82 | cmov_impl::cmov_a8_bytes(condition.unwrap_u8() != 0, src, self) 83 | } 84 | } 85 | 86 | impl> CMov for A64Bytes { 87 | #[inline] 88 | fn cmov(&mut self, condition: Choice, src: &A64Bytes) { 89 | cmov_impl::cmov_a64_bytes(condition.unwrap_u8() != 0, src, self) 90 | } 91 | } 92 | 93 | #[inline] 94 | pub fn cswap(condition: Choice, a: &mut T, b: &mut T) { 95 | let mut temp = T::default(); 96 | temp.cmov(condition, a); 97 | a.cmov(condition, b); 98 | b.cmov(condition, &temp); 99 | } 100 | 101 | #[cfg_attr(not(feature = "no_asm_insecure"), path = "cmov_impl_asm.rs")] 102 | #[cfg_attr(feature = "no_asm_insecure", path = "cmov_impl_no_asm.rs")] 103 | mod cmov_impl; 104 | 105 | #[cfg(test)] 106 | mod testing { 107 | use super::*; 108 | use typenum::{U128, U3, U320, U448, U64, U72, U8, U96}; 109 | 110 | // Helper to reduce boilerplate. 111 | // This panics if the slice is not the right length, so it's not a good API 112 | // outside of tests. 113 | fn to_a8_bytes>(src: &[u8]) -> A8Bytes { 114 | Aligned(GenericArray::from_slice(src).clone()) 115 | } 116 | 117 | fn to_a64_bytes>(src: &[u8]) -> A64Bytes { 118 | Aligned(GenericArray::from_slice(src).clone()) 119 | } 120 | 121 | #[test] 122 | fn test_cmov_u32() { 123 | let ctrue: Choice = Choice::from(1u8); 124 | let cfalse: Choice = Choice::from(0u8); 125 | 126 | let mut a = 0u32; 127 | a.cmov(ctrue, &1); 128 | assert_eq!(a, 1); 129 | 130 | a.cmov(ctrue, &2); 131 | assert_eq!(a, 2); 132 | 133 | a.cmov(cfalse, &0); 134 | assert_eq!(a, 2); 135 | 136 | a.cmov(ctrue, &0); 137 | assert_eq!(a, 0); 138 | } 139 | 140 | #[test] 141 | fn test_cmov_u64() { 142 | let ctrue: Choice = Choice::from(1u8); 143 | let cfalse: Choice = Choice::from(0u8); 144 | 145 | let mut a = 0u64; 146 | a.cmov(ctrue, &1); 147 | assert_eq!(a, 1); 148 | 149 | a.cmov(ctrue, &2); 150 | assert_eq!(a, 2); 151 | 152 | a.cmov(cfalse, &0); 153 | assert_eq!(a, 2); 154 | 155 | a.cmov(ctrue, &0); 156 | assert_eq!(a, 0); 157 | } 158 | 159 | #[test] 160 | fn test_cmov_i32() { 161 | let ctrue: Choice = Choice::from(1u8); 162 | let cfalse: Choice = Choice::from(0u8); 163 | 164 | let mut a = 0i32; 165 | a.cmov(ctrue, &1); 166 | assert_eq!(a, 1); 167 | 168 | a.cmov(ctrue, &2); 169 | assert_eq!(a, 2); 170 | 171 | a.cmov(cfalse, &0); 172 | assert_eq!(a, 2); 173 | 174 | a.cmov(ctrue, &0); 175 | assert_eq!(a, 0); 176 | } 177 | 178 | #[test] 179 | fn test_cmov_i64() { 180 | let ctrue: Choice = Choice::from(1u8); 181 | let cfalse: Choice = Choice::from(0u8); 182 | 183 | let mut a = 0i64; 184 | a.cmov(ctrue, &1); 185 | assert_eq!(a, 1); 186 | 187 | a.cmov(ctrue, &2); 188 | assert_eq!(a, 2); 189 | 190 | a.cmov(cfalse, &0); 191 | assert_eq!(a, 2); 192 | 193 | a.cmov(ctrue, &0); 194 | assert_eq!(a, 0); 195 | } 196 | 197 | #[test] 198 | fn test_cmov_usize() { 199 | let ctrue: Choice = Choice::from(1u8); 200 | let cfalse: Choice = Choice::from(0u8); 201 | 202 | let mut a = 0usize; 203 | a.cmov(ctrue, &1usize); 204 | assert_eq!(a, 1); 205 | 206 | a.cmov(ctrue, &2usize); 207 | assert_eq!(a, 2); 208 | 209 | a.cmov(cfalse, &0usize); 210 | assert_eq!(a, 2); 211 | 212 | a.cmov(ctrue, &0usize); 213 | assert_eq!(a, 0); 214 | } 215 | 216 | #[test] 217 | fn test_cmov_64bytes() { 218 | let ctrue: Choice = Choice::from(1u8); 219 | let cfalse: Choice = Choice::from(0u8); 220 | 221 | let mut a: A8Bytes = to_a8_bytes(&[0u8; 64]); 222 | a.cmov(ctrue, &to_a8_bytes(&[1u8; 64])); 223 | assert_eq!(*a, *to_a8_bytes(&[1u8; 64])); 224 | 225 | a.cmov(cfalse, &to_a8_bytes(&[0u8; 64])); 226 | assert_eq!(*a, *to_a8_bytes(&[1u8; 64])); 227 | 228 | a.cmov(ctrue, &to_a8_bytes(&[2u8; 64])); 229 | assert_eq!(*a, *to_a8_bytes(&[2u8; 64])); 230 | 231 | a.cmov(cfalse, &to_a8_bytes(&[1u8; 64])); 232 | assert_eq!(*a, *to_a8_bytes(&[2u8; 64])); 233 | 234 | a.cmov(cfalse, &to_a8_bytes(&[0u8; 64])); 235 | assert_eq!(*a, *to_a8_bytes(&[2u8; 64])); 236 | 237 | a.cmov(ctrue, &to_a8_bytes(&[0u8; 64])); 238 | assert_eq!(*a, *to_a8_bytes(&[0u8; 64])); 239 | 240 | a.cmov(ctrue, &to_a8_bytes(&[3u8; 64])); 241 | assert_eq!(*a, *to_a8_bytes(&[3u8; 64])); 242 | 243 | a.cmov(cfalse, &to_a8_bytes(&[0u8; 64])); 244 | assert_eq!(*a, *to_a8_bytes(&[3u8; 64])); 245 | } 246 | 247 | #[test] 248 | fn test_cmov_96bytes() { 249 | let ctrue: Choice = Choice::from(1u8); 250 | let cfalse: Choice = Choice::from(0u8); 251 | 252 | let mut a: A8Bytes = to_a8_bytes(&[0u8; 96]); 253 | a.cmov(ctrue, &to_a8_bytes(&[1u8; 96])); 254 | assert_eq!(*a, *to_a8_bytes(&[1u8; 96])); 255 | 256 | a.cmov(cfalse, &to_a8_bytes(&[0u8; 96])); 257 | assert_eq!(*a, *to_a8_bytes(&[1u8; 96])); 258 | 259 | a.cmov(ctrue, &to_a8_bytes(&[2u8; 96])); 260 | assert_eq!(*a, *to_a8_bytes(&[2u8; 96])); 261 | 262 | a.cmov(cfalse, &to_a8_bytes(&[1u8; 96])); 263 | assert_eq!(*a, *to_a8_bytes(&[2u8; 96])); 264 | 265 | a.cmov(cfalse, &to_a8_bytes(&[0u8; 96])); 266 | assert_eq!(*a, *to_a8_bytes(&[2u8; 96])); 267 | 268 | a.cmov(ctrue, &to_a8_bytes(&[0u8; 96])); 269 | assert_eq!(*a, *to_a8_bytes(&[0u8; 96])); 270 | 271 | a.cmov(ctrue, &to_a8_bytes(&[3u8; 96])); 272 | assert_eq!(*a, *to_a8_bytes(&[3u8; 96])); 273 | 274 | a.cmov(cfalse, &to_a8_bytes(&[0u8; 96])); 275 | assert_eq!(*a, *to_a8_bytes(&[3u8; 96])); 276 | } 277 | 278 | #[test] 279 | fn test_cmov_72bytes() { 280 | let ctrue: Choice = Choice::from(1u8); 281 | let cfalse: Choice = Choice::from(0u8); 282 | 283 | let mut a: A8Bytes = to_a8_bytes(&[0u8; 72]); 284 | a.cmov(ctrue, &to_a8_bytes(&[1u8; 72])); 285 | assert_eq!(*a, *to_a8_bytes(&[1u8; 72])); 286 | 287 | a.cmov(cfalse, &to_a8_bytes(&[0u8; 72])); 288 | assert_eq!(*a, *to_a8_bytes(&[1u8; 72])); 289 | 290 | a.cmov(ctrue, &to_a8_bytes(&[2u8; 72])); 291 | assert_eq!(*a, *to_a8_bytes(&[2u8; 72])); 292 | 293 | a.cmov(cfalse, &to_a8_bytes(&[1u8; 72])); 294 | assert_eq!(*a, *to_a8_bytes(&[2u8; 72])); 295 | 296 | a.cmov(cfalse, &to_a8_bytes(&[0u8; 72])); 297 | assert_eq!(*a, *to_a8_bytes(&[2u8; 72])); 298 | 299 | a.cmov(ctrue, &to_a8_bytes(&[0u8; 72])); 300 | assert_eq!(*a, *to_a8_bytes(&[0u8; 72])); 301 | 302 | a.cmov(ctrue, &to_a8_bytes(&[3u8; 72])); 303 | assert_eq!(*a, *to_a8_bytes(&[3u8; 72])); 304 | 305 | a.cmov(cfalse, &to_a8_bytes(&[0u8; 72])); 306 | assert_eq!(*a, *to_a8_bytes(&[3u8; 72])); 307 | } 308 | 309 | #[test] 310 | fn test_cmov_8bytes() { 311 | let ctrue: Choice = Choice::from(1u8); 312 | let cfalse: Choice = Choice::from(0u8); 313 | 314 | let mut a: A8Bytes = to_a8_bytes(&[0u8; 8]); 315 | a.cmov(ctrue, &to_a8_bytes(&[1u8; 8])); 316 | assert_eq!(*a, *to_a8_bytes(&[1u8; 8])); 317 | 318 | a.cmov(cfalse, &to_a8_bytes(&[0u8; 8])); 319 | assert_eq!(*a, *to_a8_bytes(&[1u8; 8])); 320 | 321 | a.cmov(ctrue, &to_a8_bytes(&[2u8; 8])); 322 | assert_eq!(*a, *to_a8_bytes(&[2u8; 8])); 323 | 324 | a.cmov(cfalse, &to_a8_bytes(&[1u8; 8])); 325 | assert_eq!(*a, *to_a8_bytes(&[2u8; 8])); 326 | 327 | a.cmov(cfalse, &to_a8_bytes(&[0u8; 8])); 328 | assert_eq!(*a, *to_a8_bytes(&[2u8; 8])); 329 | 330 | a.cmov(ctrue, &to_a8_bytes(&[0u8; 8])); 331 | assert_eq!(*a, *to_a8_bytes(&[0u8; 8])); 332 | 333 | a.cmov(ctrue, &to_a8_bytes(&[3u8; 8])); 334 | assert_eq!(*a, *to_a8_bytes(&[3u8; 8])); 335 | 336 | a.cmov(cfalse, &to_a8_bytes(&[0u8; 8])); 337 | assert_eq!(*a, *to_a8_bytes(&[3u8; 8])); 338 | } 339 | 340 | #[test] 341 | fn test_cmov_a64_64bytes() { 342 | let ctrue: Choice = Choice::from(1u8); 343 | let cfalse: Choice = Choice::from(0u8); 344 | 345 | let mut a: A64Bytes = to_a64_bytes(&[0u8; 64]); 346 | a.cmov(ctrue, &to_a64_bytes(&[1u8; 64])); 347 | assert_eq!(*a, *to_a64_bytes(&[1u8; 64])); 348 | 349 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 64])); 350 | assert_eq!(*a, *to_a64_bytes(&[1u8; 64])); 351 | 352 | a.cmov(ctrue, &to_a64_bytes(&[2u8; 64])); 353 | assert_eq!(*a, *to_a64_bytes(&[2u8; 64])); 354 | 355 | a.cmov(cfalse, &to_a64_bytes(&[1u8; 64])); 356 | assert_eq!(*a, *to_a64_bytes(&[2u8; 64])); 357 | 358 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 64])); 359 | assert_eq!(*a, *to_a64_bytes(&[2u8; 64])); 360 | 361 | a.cmov(ctrue, &to_a64_bytes(&[0u8; 64])); 362 | assert_eq!(*a, *to_a64_bytes(&[0u8; 64])); 363 | 364 | a.cmov(ctrue, &to_a64_bytes(&[3u8; 64])); 365 | assert_eq!(*a, *to_a64_bytes(&[3u8; 64])); 366 | 367 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 64])); 368 | assert_eq!(*a, *to_a64_bytes(&[3u8; 64])); 369 | } 370 | 371 | #[test] 372 | fn test_cmov_a64_128bytes() { 373 | let ctrue: Choice = Choice::from(1u8); 374 | let cfalse: Choice = Choice::from(0u8); 375 | 376 | let mut a: A64Bytes = to_a64_bytes(&[0u8; 128]); 377 | a.cmov(ctrue, &to_a64_bytes(&[1u8; 128])); 378 | assert_eq!(*a, *to_a64_bytes(&[1u8; 128])); 379 | 380 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 128])); 381 | assert_eq!(*a, *to_a64_bytes(&[1u8; 128])); 382 | 383 | a.cmov(ctrue, &to_a64_bytes(&[2u8; 128])); 384 | assert_eq!(*a, *to_a64_bytes(&[2u8; 128])); 385 | 386 | a.cmov(cfalse, &to_a64_bytes(&[1u8; 128])); 387 | assert_eq!(*a, *to_a64_bytes(&[2u8; 128])); 388 | 389 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 128])); 390 | assert_eq!(*a, *to_a64_bytes(&[2u8; 128])); 391 | 392 | a.cmov(ctrue, &to_a64_bytes(&[0u8; 128])); 393 | assert_eq!(*a, *to_a64_bytes(&[0u8; 128])); 394 | 395 | a.cmov(ctrue, &to_a64_bytes(&[3u8; 128])); 396 | assert_eq!(*a, *to_a64_bytes(&[3u8; 128])); 397 | 398 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 128])); 399 | assert_eq!(*a, *to_a64_bytes(&[3u8; 128])); 400 | } 401 | 402 | #[test] 403 | fn test_cmov_a64_320bytes() { 404 | let ctrue: Choice = Choice::from(1u8); 405 | let cfalse: Choice = Choice::from(0u8); 406 | 407 | let mut a: A64Bytes = to_a64_bytes(&[0u8; 320]); 408 | a.cmov(ctrue, &to_a64_bytes(&[1u8; 320])); 409 | assert_eq!(*a, *to_a64_bytes(&[1u8; 320])); 410 | 411 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 320])); 412 | assert_eq!(*a, *to_a64_bytes(&[1u8; 320])); 413 | 414 | a.cmov(ctrue, &to_a64_bytes(&[2u8; 320])); 415 | assert_eq!(*a, *to_a64_bytes(&[2u8; 320])); 416 | 417 | a.cmov(cfalse, &to_a64_bytes(&[1u8; 320])); 418 | assert_eq!(*a, *to_a64_bytes(&[2u8; 320])); 419 | 420 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 320])); 421 | assert_eq!(*a, *to_a64_bytes(&[2u8; 320])); 422 | 423 | a.cmov(ctrue, &to_a64_bytes(&[0u8; 320])); 424 | assert_eq!(*a, *to_a64_bytes(&[0u8; 320])); 425 | 426 | a.cmov(ctrue, &to_a64_bytes(&[3u8; 320])); 427 | assert_eq!(*a, *to_a64_bytes(&[3u8; 320])); 428 | 429 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 320])); 430 | assert_eq!(*a, *to_a64_bytes(&[3u8; 320])); 431 | } 432 | 433 | #[test] 434 | fn test_cmov_a64_448bytes() { 435 | let ctrue: Choice = Choice::from(1u8); 436 | let cfalse: Choice = Choice::from(0u8); 437 | 438 | let mut a: A64Bytes = to_a64_bytes(&[0u8; 448]); 439 | a.cmov(ctrue, &to_a64_bytes(&[1u8; 448])); 440 | assert_eq!(*a, *to_a64_bytes(&[1u8; 448])); 441 | 442 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 448])); 443 | assert_eq!(*a, *to_a64_bytes(&[1u8; 448])); 444 | 445 | a.cmov(ctrue, &to_a64_bytes(&[2u8; 448])); 446 | assert_eq!(*a, *to_a64_bytes(&[2u8; 448])); 447 | 448 | a.cmov(cfalse, &to_a64_bytes(&[1u8; 448])); 449 | assert_eq!(*a, *to_a64_bytes(&[2u8; 448])); 450 | 451 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 448])); 452 | assert_eq!(*a, *to_a64_bytes(&[2u8; 448])); 453 | 454 | a.cmov(ctrue, &to_a64_bytes(&[0u8; 448])); 455 | assert_eq!(*a, *to_a64_bytes(&[0u8; 448])); 456 | 457 | a.cmov(ctrue, &to_a64_bytes(&[3u8; 448])); 458 | assert_eq!(*a, *to_a64_bytes(&[3u8; 448])); 459 | 460 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 448])); 461 | assert_eq!(*a, *to_a64_bytes(&[3u8; 448])); 462 | } 463 | 464 | #[test] 465 | fn test_cmov_a64_96bytes() { 466 | let ctrue: Choice = Choice::from(1u8); 467 | let cfalse: Choice = Choice::from(0u8); 468 | 469 | let mut a: A64Bytes = to_a64_bytes(&[0u8; 96]); 470 | a.cmov(ctrue, &to_a64_bytes(&[1u8; 96])); 471 | assert_eq!(*a, *to_a64_bytes(&[1u8; 96])); 472 | 473 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 96])); 474 | assert_eq!(*a, *to_a64_bytes(&[1u8; 96])); 475 | 476 | a.cmov(ctrue, &to_a64_bytes(&[2u8; 96])); 477 | assert_eq!(*a, *to_a64_bytes(&[2u8; 96])); 478 | 479 | a.cmov(cfalse, &to_a64_bytes(&[1u8; 96])); 480 | assert_eq!(*a, *to_a64_bytes(&[2u8; 96])); 481 | 482 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 96])); 483 | assert_eq!(*a, *to_a64_bytes(&[2u8; 96])); 484 | 485 | a.cmov(ctrue, &to_a64_bytes(&[0u8; 96])); 486 | assert_eq!(*a, *to_a64_bytes(&[0u8; 96])); 487 | 488 | a.cmov(ctrue, &to_a64_bytes(&[3u8; 96])); 489 | assert_eq!(*a, *to_a64_bytes(&[3u8; 96])); 490 | 491 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 96])); 492 | assert_eq!(*a, *to_a64_bytes(&[3u8; 96])); 493 | } 494 | 495 | #[test] 496 | fn test_cmov_a64_3bytes() { 497 | let ctrue: Choice = Choice::from(1u8); 498 | let cfalse: Choice = Choice::from(0u8); 499 | 500 | let mut a: A64Bytes = to_a64_bytes(&[0u8; 3]); 501 | a.cmov(ctrue, &to_a64_bytes(&[1u8; 3])); 502 | assert_eq!(*a, *to_a64_bytes(&[1u8; 3])); 503 | 504 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 3])); 505 | assert_eq!(*a, *to_a64_bytes(&[1u8; 3])); 506 | 507 | a.cmov(ctrue, &to_a64_bytes(&[2u8; 3])); 508 | assert_eq!(*a, *to_a64_bytes(&[2u8; 3])); 509 | 510 | a.cmov(cfalse, &to_a64_bytes(&[1u8; 3])); 511 | assert_eq!(*a, *to_a64_bytes(&[2u8; 3])); 512 | 513 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 3])); 514 | assert_eq!(*a, *to_a64_bytes(&[2u8; 3])); 515 | 516 | a.cmov(ctrue, &to_a64_bytes(&[0u8; 3])); 517 | assert_eq!(*a, *to_a64_bytes(&[0u8; 3])); 518 | 519 | a.cmov(ctrue, &to_a64_bytes(&[3u8; 3])); 520 | assert_eq!(*a, *to_a64_bytes(&[3u8; 3])); 521 | 522 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 3])); 523 | assert_eq!(*a, *to_a64_bytes(&[3u8; 3])); 524 | } 525 | 526 | #[test] 527 | fn test_cmov_a64_72bytes() { 528 | let ctrue: Choice = Choice::from(1u8); 529 | let cfalse: Choice = Choice::from(0u8); 530 | 531 | let mut a: A64Bytes = to_a64_bytes(&[0u8; 72]); 532 | a.cmov(ctrue, &to_a64_bytes(&[1u8; 72])); 533 | assert_eq!(*a, *to_a64_bytes(&[1u8; 72])); 534 | 535 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 72])); 536 | assert_eq!(*a, *to_a64_bytes(&[1u8; 72])); 537 | 538 | a.cmov(ctrue, &to_a64_bytes(&[2u8; 72])); 539 | assert_eq!(*a, *to_a64_bytes(&[2u8; 72])); 540 | 541 | a.cmov(cfalse, &to_a64_bytes(&[1u8; 72])); 542 | assert_eq!(*a, *to_a64_bytes(&[2u8; 72])); 543 | 544 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 72])); 545 | assert_eq!(*a, *to_a64_bytes(&[2u8; 72])); 546 | 547 | a.cmov(ctrue, &to_a64_bytes(&[0u8; 72])); 548 | assert_eq!(*a, *to_a64_bytes(&[0u8; 72])); 549 | 550 | a.cmov(ctrue, &to_a64_bytes(&[3u8; 72])); 551 | assert_eq!(*a, *to_a64_bytes(&[3u8; 72])); 552 | 553 | a.cmov(cfalse, &to_a64_bytes(&[0u8; 72])); 554 | assert_eq!(*a, *to_a64_bytes(&[3u8; 72])); 555 | } 556 | } 557 | -------------------------------------------------------------------------------- /balanced-tree-index/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2023 The MobileCoin Foundation 2 | 3 | #![no_std] 4 | #![deny(missing_docs)] 5 | #![deny(unsafe_code)] 6 | 7 | //! Defines an interface for a type that represents an index into a 8 | //! memory-mapped complete balanced binary tree. 9 | //! 10 | //! The operations that we define mostly help with finding parents or common 11 | //! ancestors in the tree structure. 12 | //! 13 | //! This type is usually u32 or u64, and these operations are usually performed 14 | //! using bit-twiddling tricks. Coding against this API means that people 15 | //! reading ORAM code don't necessarily have to understand all the bit-twiddling 16 | //! tricks. 17 | 18 | use aligned_cmov::{ 19 | subtle::{ConstantTimeEq, ConstantTimeLess}, 20 | CMov, 21 | }; 22 | use rand_core::RngCore; 23 | 24 | /// Trait representing a type that can represent a tree index in a balanced 25 | /// binary tree, using the numbering where the root is 1, and nodes are labelled 26 | /// consecutively level by level, using lexicographic order within a level. 27 | /// 28 | /// All operations here should be constant time, leaking nothing about the input 29 | /// and &self, unless otherwise stated. 30 | pub trait TreeIndex: Copy + Eq + PartialEq + CMov { 31 | /// The Zero index that is unused and does not actually refer to a node in 32 | /// the tree. 33 | const NONE: Self; 34 | 35 | /// The index of the root of the tree, logically 1. 36 | /// The parent of ROOT is NONE. 37 | const ROOT: Self; 38 | 39 | /// Find the i'th parent of a node. 40 | fn parent(&self, i: u32) -> Self; 41 | 42 | /// Find the height of a node. 43 | /// This returns u32 because rust's count_leading_zeros does. 44 | /// It is illegal to call this when self is the NONE value. 45 | fn height(&self) -> u32; 46 | 47 | /// For two nodes promised to be "peers" i.e. at the same height, 48 | /// compute the distance from (either) to their common ancestor in the tree. 49 | /// This is the number of times you have to compute "parent" before they are 50 | /// equal. It is illegal to call this if the height of the two arguments 51 | /// is not the same. Should not reveal anything else about the 52 | /// arguments. 53 | fn common_ancestor_distance_of_peers(&self, other: &Self) -> u32; 54 | 55 | /// Compute the height of the common ancestor of any two nodes. 56 | /// It is illegal to call this when either of the inputs is the NONE value. 57 | fn common_ancestor_height(&self, other: &Self) -> u32 { 58 | let ht_self = self.height(); 59 | let ht_other = other.height(); 60 | 61 | // Take the min in constant time of the two heights 62 | let ht_min = { 63 | let mut ht_min = ht_self; 64 | ht_min.cmov(ht_other.ct_lt(&ht_self), &ht_other); 65 | ht_min 66 | }; 67 | 68 | let adjusted_self = self.parent(ht_self.wrapping_sub(ht_min)); 69 | let adjusted_other = other.parent(ht_other.wrapping_sub(ht_min)); 70 | 71 | debug_assert!(adjusted_self.height() == ht_min); 72 | debug_assert!(adjusted_other.height() == ht_min); 73 | 74 | let dist = adjusted_self.common_ancestor_distance_of_peers(&adjusted_other); 75 | debug_assert!(dist <= ht_min); 76 | 77 | ht_min.wrapping_sub(dist) 78 | } 79 | 80 | /// Random child at a given height. 81 | /// This height must be the same or less than the height of the given node, 82 | /// otherwise the call is illegal. 83 | /// It is legal to call this on the NONE value, it will be as if ROOT was 84 | /// passed. 85 | fn random_child_at_height(&self, height: u32, rng: &mut R) -> Self; 86 | 87 | /// Iterate over the parents of this node, including self. 88 | /// Access patterns when evaluating this iterator reveal the height of self, 89 | /// but not more than that. 90 | fn parents(&self) -> ParentsIterator { 91 | ParentsIterator::from(*self) 92 | } 93 | } 94 | 95 | /// Iterator type over the sequence of parents of a TreeIndex 96 | pub struct ParentsIterator { 97 | internal: I, 98 | } 99 | 100 | impl Iterator for ParentsIterator { 101 | type Item = I; 102 | 103 | fn next(&mut self) -> Option { 104 | if self.internal == I::NONE { 105 | None 106 | } else { 107 | let temp = self.internal; 108 | self.internal = self.internal.parent(1); 109 | Some(temp) 110 | } 111 | } 112 | } 113 | 114 | impl From for ParentsIterator { 115 | fn from(internal: I) -> Self { 116 | Self { internal } 117 | } 118 | } 119 | 120 | // Implements TreeIndex for a type like u32 or u64 121 | // Because we need things like count_leading_ones and ::MAX and there are no 122 | // traits in the language for this, it is painful to do without macros. 123 | macro_rules! implement_tree_index_for_primitive { 124 | ($uint:ty) => { 125 | impl TreeIndex for $uint { 126 | const NONE: $uint = 0; 127 | const ROOT: $uint = 1; 128 | fn parent(&self, i: u32) -> Self { 129 | self >> i 130 | } 131 | fn height(&self) -> u32 { 132 | debug_assert!(*self != 0); 133 | const DIGITS_MINUS_ONE: u32 = <$uint>::MAX.leading_ones() - 1; 134 | // Wrapping sub is used to avoid panics 135 | // Note: We assume that leading_zeroes is compiling down to ctlz 136 | // and is constant time. 137 | DIGITS_MINUS_ONE.wrapping_sub(self.leading_zeros()) 138 | } 139 | fn common_ancestor_distance_of_peers(&self, other: &Self) -> u32 { 140 | debug_assert!(self.height() == other.height()); 141 | const DIGITS: u32 = <$uint>::MAX.leading_ones(); 142 | // Wrapping sub is used to avoid panics 143 | // Note: We assume that leading_zeroes is compiling down to ctlz 144 | // and is constant time. 145 | DIGITS.wrapping_sub((self ^ other).leading_zeros()) 146 | } 147 | fn random_child_at_height(&self, height: u32, rng: &mut R) -> Self { 148 | // Make a copy of self that we can conditionally overwrite in case of none 149 | let mut myself = *self; 150 | myself.cmov(myself.ct_eq(&Self::NONE), &Self::ROOT); 151 | 152 | // Wrapping sub is used to avoid panic, branching, in production 153 | debug_assert!(height >= myself.height()); 154 | let num_bits_needed = height.wrapping_sub(myself.height()); 155 | 156 | // Note: Would be nice to use mc_util_from_random here instead of (next_u64 as 157 | // $uint) Here we are taking the u64, casting to self, then masking it 158 | // with bit mask for low order bits equal to number of random bits 159 | // needed. 160 | let randomness = 161 | (rng.next_u64() as $uint) & (((1 as $uint) << num_bits_needed) - 1); 162 | 163 | // We shift myself over and xor in the random bits. 164 | (myself << num_bits_needed) ^ randomness 165 | } 166 | } 167 | }; 168 | } 169 | 170 | implement_tree_index_for_primitive!(u32); 171 | implement_tree_index_for_primitive!(u64); 172 | 173 | #[cfg(test)] 174 | mod testing { 175 | use super::*; 176 | extern crate alloc; 177 | use alloc::vec; 178 | 179 | use alloc::vec::Vec; 180 | 181 | // Helper that takes a ParentsIterator and returns a Vec 182 | fn collect_to_vec(it: ParentsIterator) -> Vec { 183 | it.collect() 184 | } 185 | 186 | // Test height calculations 187 | #[test] 188 | fn test_height_u64() { 189 | assert_eq!(1u64.height(), 0); 190 | assert_eq!(2u64.height(), 1); 191 | assert_eq!(3u64.height(), 1); 192 | assert_eq!(4u64.height(), 2); 193 | assert_eq!(5u64.height(), 2); 194 | assert_eq!(6u64.height(), 2); 195 | assert_eq!(7u64.height(), 2); 196 | assert_eq!(8u64.height(), 3); 197 | assert_eq!(9u64.height(), 3); 198 | assert_eq!(10u64.height(), 3); 199 | assert_eq!(11u64.height(), 3); 200 | assert_eq!(12u64.height(), 3); 201 | assert_eq!(13u64.height(), 3); 202 | assert_eq!(14u64.height(), 3); 203 | assert_eq!(15u64.height(), 3); 204 | assert_eq!(16u64.height(), 4); 205 | } 206 | 207 | // Test height calculations 208 | #[test] 209 | fn test_height_u32() { 210 | assert_eq!(1u32.height(), 0); 211 | assert_eq!(2u32.height(), 1); 212 | assert_eq!(3u32.height(), 1); 213 | assert_eq!(4u32.height(), 2); 214 | assert_eq!(5u32.height(), 2); 215 | assert_eq!(6u32.height(), 2); 216 | assert_eq!(7u32.height(), 2); 217 | assert_eq!(8u32.height(), 3); 218 | assert_eq!(9u32.height(), 3); 219 | assert_eq!(10u32.height(), 3); 220 | assert_eq!(11u32.height(), 3); 221 | assert_eq!(12u32.height(), 3); 222 | assert_eq!(13u32.height(), 3); 223 | assert_eq!(14u32.height(), 3); 224 | assert_eq!(15u32.height(), 3); 225 | assert_eq!(16u32.height(), 4); 226 | } 227 | 228 | // Test random_child_at_height 229 | #[test] 230 | fn test_random_child_at_height_u64() { 231 | test_helper::run_with_several_seeds(|mut rng| { 232 | for ht in 0..40 { 233 | for _ in 0..10 { 234 | let node = 1u64.random_child_at_height(ht, &mut rng); 235 | assert_eq!(node.height(), ht); 236 | } 237 | } 238 | 239 | for ht in 20..40 { 240 | for _ in 0..10 { 241 | let node = 10u64.random_child_at_height(ht, &mut rng); 242 | assert_eq!(node.height(), ht); 243 | assert!(node.parents().any(|x| x == 10u64)) 244 | } 245 | } 246 | }) 247 | } 248 | 249 | // Test random_child_at_height 250 | #[test] 251 | fn test_random_child_at_height_u32() { 252 | test_helper::run_with_several_seeds(|mut rng| { 253 | for ht in 0..30 { 254 | for _ in 0..10 { 255 | let node = 1u32.random_child_at_height(ht, &mut rng); 256 | assert_eq!(node.height(), ht); 257 | } 258 | } 259 | 260 | for ht in 20..30 { 261 | for _ in 0..10 { 262 | let node = 10u64.random_child_at_height(ht, &mut rng); 263 | assert_eq!(node.height(), ht); 264 | assert!(node.parents().any(|x| x == 10u64)) 265 | } 266 | } 267 | }) 268 | } 269 | 270 | // Test that parents iterator is giving expected outputs 271 | #[test] 272 | fn test_parents_iterator_u64() { 273 | assert_eq!(collect_to_vec(1u64.parents()), vec![0b1]); 274 | assert_eq!(collect_to_vec(2u64.parents()), vec![0b10, 0b1]); 275 | assert_eq!(collect_to_vec(3u64.parents()), vec![0b11, 0b1]); 276 | assert_eq!(collect_to_vec(4u64.parents()), vec![0b100, 0b10, 0b1]); 277 | assert_eq!(collect_to_vec(5u64.parents()), vec![0b101, 0b10, 0b1]); 278 | assert_eq!(collect_to_vec(6u64.parents()), vec![0b110, 0b11, 0b1]); 279 | assert_eq!(collect_to_vec(7u64.parents()), vec![0b111, 0b11, 0b1]); 280 | assert_eq!( 281 | collect_to_vec(8u64.parents()), 282 | vec![0b1000, 0b100, 0b10, 0b1] 283 | ); 284 | assert_eq!( 285 | collect_to_vec(9u64.parents()), 286 | vec![0b1001, 0b100, 0b10, 0b1] 287 | ); 288 | assert_eq!( 289 | collect_to_vec(10u64.parents()), 290 | vec![0b1010, 0b101, 0b10, 0b1] 291 | ); 292 | assert_eq!( 293 | collect_to_vec(11u64.parents()), 294 | vec![0b1011, 0b101, 0b10, 0b1] 295 | ); 296 | assert_eq!( 297 | collect_to_vec(12u64.parents()), 298 | vec![0b1100, 0b110, 0b11, 0b1] 299 | ); 300 | assert_eq!( 301 | collect_to_vec(13u64.parents()), 302 | vec![0b1101, 0b110, 0b11, 0b1] 303 | ); 304 | assert_eq!( 305 | collect_to_vec(14u64.parents()), 306 | vec![0b1110, 0b111, 0b11, 0b1] 307 | ); 308 | assert_eq!( 309 | collect_to_vec(15u64.parents()), 310 | vec![0b1111, 0b111, 0b11, 0b1] 311 | ); 312 | assert_eq!( 313 | collect_to_vec(16u64.parents()), 314 | vec![0b10000, 0b1000, 0b100, 0b10, 0b1] 315 | ); 316 | assert_eq!( 317 | collect_to_vec(17u64.parents()), 318 | vec![0b10001, 0b1000, 0b100, 0b10, 0b1] 319 | ); 320 | assert_eq!( 321 | collect_to_vec(18u64.parents()), 322 | vec![0b10010, 0b1001, 0b100, 0b10, 0b1] 323 | ); 324 | assert_eq!( 325 | collect_to_vec(19u64.parents()), 326 | vec![0b10011, 0b1001, 0b100, 0b10, 0b1] 327 | ); 328 | } 329 | 330 | // Test that parents iterator is giving expected outputs 331 | #[test] 332 | fn test_parents_iterator_u32() { 333 | assert_eq!(collect_to_vec(1u32.parents()), vec![0b1]); 334 | assert_eq!(collect_to_vec(2u32.parents()), vec![0b10, 0b1]); 335 | assert_eq!(collect_to_vec(3u32.parents()), vec![0b11, 0b1]); 336 | assert_eq!(collect_to_vec(4u32.parents()), vec![0b100, 0b10, 0b1]); 337 | assert_eq!(collect_to_vec(5u32.parents()), vec![0b101, 0b10, 0b1]); 338 | assert_eq!(collect_to_vec(6u32.parents()), vec![0b110, 0b11, 0b1]); 339 | assert_eq!(collect_to_vec(7u32.parents()), vec![0b111, 0b11, 0b1]); 340 | assert_eq!( 341 | collect_to_vec(8u32.parents()), 342 | vec![0b1000, 0b100, 0b10, 0b1] 343 | ); 344 | assert_eq!( 345 | collect_to_vec(9u32.parents()), 346 | vec![0b1001, 0b100, 0b10, 0b1] 347 | ); 348 | assert_eq!( 349 | collect_to_vec(10u32.parents()), 350 | vec![0b1010, 0b101, 0b10, 0b1] 351 | ); 352 | assert_eq!( 353 | collect_to_vec(11u32.parents()), 354 | vec![0b1011, 0b101, 0b10, 0b1] 355 | ); 356 | assert_eq!( 357 | collect_to_vec(12u32.parents()), 358 | vec![0b1100, 0b110, 0b11, 0b1] 359 | ); 360 | assert_eq!( 361 | collect_to_vec(13u32.parents()), 362 | vec![0b1101, 0b110, 0b11, 0b1] 363 | ); 364 | assert_eq!( 365 | collect_to_vec(14u32.parents()), 366 | vec![0b1110, 0b111, 0b11, 0b1] 367 | ); 368 | assert_eq!( 369 | collect_to_vec(15u32.parents()), 370 | vec![0b1111, 0b111, 0b11, 0b1] 371 | ); 372 | assert_eq!( 373 | collect_to_vec(16u32.parents()), 374 | vec![0b10000, 0b1000, 0b100, 0b10, 0b1] 375 | ); 376 | assert_eq!( 377 | collect_to_vec(17u32.parents()), 378 | vec![0b10001, 0b1000, 0b100, 0b10, 0b1] 379 | ); 380 | assert_eq!( 381 | collect_to_vec(18u32.parents()), 382 | vec![0b10010, 0b1001, 0b100, 0b10, 0b1] 383 | ); 384 | assert_eq!( 385 | collect_to_vec(19u32.parents()), 386 | vec![0b10011, 0b1001, 0b100, 0b10, 0b1] 387 | ); 388 | } 389 | 390 | // Test that common_ancestor_distance_of_peers is giving expected outputs 391 | #[test] 392 | fn test_common_ancestor_u64() { 393 | assert_eq!(1u64.common_ancestor_distance_of_peers(&1u64), 0); 394 | assert_eq!(2u64.common_ancestor_distance_of_peers(&2u64), 0); 395 | assert_eq!(2u64.common_ancestor_distance_of_peers(&3u64), 1); 396 | assert_eq!(3u64.common_ancestor_distance_of_peers(&3u64), 0); 397 | assert_eq!(4u64.common_ancestor_distance_of_peers(&7u64), 2); 398 | assert_eq!(4u64.common_ancestor_distance_of_peers(&5u64), 1); 399 | assert_eq!(4u64.common_ancestor_distance_of_peers(&6u64), 2); 400 | assert_eq!(7u64.common_ancestor_distance_of_peers(&7u64), 0); 401 | assert_eq!(7u64.common_ancestor_distance_of_peers(&6u64), 1); 402 | assert_eq!(7u64.common_ancestor_distance_of_peers(&5u64), 2); 403 | assert_eq!(17u64.common_ancestor_distance_of_peers(&31u64), 4); 404 | assert_eq!(17u64.common_ancestor_distance_of_peers(&23u64), 3); 405 | assert_eq!(17u64.common_ancestor_distance_of_peers(&19u64), 2); 406 | } 407 | 408 | // Test that common_ancestor_distance_of_peers is giving expected outputs 409 | #[test] 410 | fn test_common_ancestor_u32() { 411 | assert_eq!(1u32.common_ancestor_distance_of_peers(&1u32), 0); 412 | assert_eq!(2u32.common_ancestor_distance_of_peers(&2u32), 0); 413 | assert_eq!(2u32.common_ancestor_distance_of_peers(&3u32), 1); 414 | assert_eq!(3u32.common_ancestor_distance_of_peers(&3u32), 0); 415 | assert_eq!(4u32.common_ancestor_distance_of_peers(&7u32), 2); 416 | assert_eq!(4u32.common_ancestor_distance_of_peers(&5u32), 1); 417 | assert_eq!(4u32.common_ancestor_distance_of_peers(&6u32), 2); 418 | assert_eq!(7u32.common_ancestor_distance_of_peers(&7u32), 0); 419 | assert_eq!(7u32.common_ancestor_distance_of_peers(&6u32), 1); 420 | assert_eq!(7u32.common_ancestor_distance_of_peers(&5u32), 2); 421 | assert_eq!(17u32.common_ancestor_distance_of_peers(&31u32), 4); 422 | assert_eq!(17u32.common_ancestor_distance_of_peers(&23u32), 3); 423 | assert_eq!(17u32.common_ancestor_distance_of_peers(&19u32), 2); 424 | } 425 | 426 | // Naive implementation of common_ancestor_distance_of_peers 427 | fn naive_common_ancestor_distance_of_peers(lhs: &I, rhs: &I) -> u32 { 428 | let mut counter = 0u32; 429 | let mut it1 = lhs.parents(); 430 | let mut it2 = rhs.parents(); 431 | while it1.next().unwrap() != it2.next().unwrap() { 432 | counter += 1; 433 | } 434 | counter 435 | } 436 | 437 | // Test that common_ancestor_distance_of_peers agrees with the naive 438 | // implementation 439 | #[test] 440 | fn common_ancestor_distance_conformance_u64() { 441 | test_helper::run_with_several_seeds(|mut rng| { 442 | for ht in 0..30 { 443 | for _ in 0..10 { 444 | let node = 1u64.random_child_at_height(ht, &mut rng); 445 | let node2 = 1u64.random_child_at_height(ht, &mut rng); 446 | assert_eq!( 447 | node.common_ancestor_distance_of_peers(&node2), 448 | naive_common_ancestor_distance_of_peers(&node, &node2) 449 | ); 450 | } 451 | } 452 | 453 | for ht in 20..30 { 454 | for _ in 0..10 { 455 | let node = 16u64.random_child_at_height(ht, &mut rng); 456 | let node2 = 16u64.random_child_at_height(ht, &mut rng); 457 | assert_eq!( 458 | node.common_ancestor_distance_of_peers(&node2), 459 | naive_common_ancestor_distance_of_peers(&node, &node2) 460 | ); 461 | } 462 | } 463 | }) 464 | } 465 | 466 | // Test that common_ancestor_distance_of_peers agrees with the naive 467 | // implementation 468 | #[test] 469 | fn common_ancestor_distance_conformance_u32() { 470 | test_helper::run_with_several_seeds(|mut rng| { 471 | for ht in 0..30 { 472 | for _ in 0..10 { 473 | let node = 1u32.random_child_at_height(ht, &mut rng); 474 | let node2 = 1u32.random_child_at_height(ht, &mut rng); 475 | assert_eq!( 476 | node.common_ancestor_distance_of_peers(&node2), 477 | naive_common_ancestor_distance_of_peers(&node, &node2) 478 | ); 479 | } 480 | } 481 | 482 | for ht in 20..30 { 483 | for _ in 0..10 { 484 | let node = 16u32.random_child_at_height(ht, &mut rng); 485 | let node2 = 16u32.random_child_at_height(ht, &mut rng); 486 | assert_eq!( 487 | node.common_ancestor_distance_of_peers(&node2), 488 | naive_common_ancestor_distance_of_peers(&node, &node2) 489 | ); 490 | } 491 | } 492 | }) 493 | } 494 | 495 | // Test that common_ancestor_height is giving expected results for nodes 496 | // at different heights. 497 | #[test] 498 | fn common_ancestor_height_u64() { 499 | assert_eq!(1u64.common_ancestor_height(&1u64), 0); 500 | assert_eq!(2u64.common_ancestor_height(&2u64), 1); 501 | assert_eq!(4u64.common_ancestor_height(&4u64), 2); 502 | assert_eq!(8u64.common_ancestor_height(&8u64), 3); 503 | assert_eq!(8u64.common_ancestor_height(&4u64), 2); 504 | assert_eq!(8u64.common_ancestor_height(&7u64), 0); 505 | assert_eq!(8u64.common_ancestor_height(&3u64), 0); 506 | assert_eq!(8u64.common_ancestor_height(&9u64), 2); 507 | assert_eq!(8u64.common_ancestor_height(&11u64), 1); 508 | assert_eq!(8u64.common_ancestor_height(&13u64), 0); 509 | assert_eq!(16u64.common_ancestor_height(&8u64), 3); 510 | assert_eq!(16u64.common_ancestor_height(&4u64), 2); 511 | assert_eq!(16u64.common_ancestor_height(&7u64), 0); 512 | assert_eq!(16u64.common_ancestor_height(&3u64), 0); 513 | assert_eq!(16u64.common_ancestor_height(&9u64), 2); 514 | assert_eq!(16u64.common_ancestor_height(&11u64), 1); 515 | assert_eq!(16u64.common_ancestor_height(&13u64), 0); 516 | assert_eq!(17u64.common_ancestor_height(&15u64), 0); 517 | assert_eq!(17u64.common_ancestor_height(&19u64), 2); 518 | assert_eq!(17u64.common_ancestor_height(&21u64), 1); 519 | assert_eq!(17u64.common_ancestor_height(&31u64), 0); 520 | assert_eq!(17u64.common_ancestor_height(&63u64), 0); 521 | assert_eq!(17u64.common_ancestor_height(&127u64), 0); 522 | } 523 | 524 | // Test that common_ancestor_height is giving expected results for nodes 525 | // at different heights. 526 | #[test] 527 | fn common_ancestor_height_u32() { 528 | assert_eq!(1u32.common_ancestor_height(&1u32), 0); 529 | assert_eq!(2u32.common_ancestor_height(&2u32), 1); 530 | assert_eq!(4u32.common_ancestor_height(&4u32), 2); 531 | assert_eq!(8u32.common_ancestor_height(&8u32), 3); 532 | assert_eq!(8u32.common_ancestor_height(&4u32), 2); 533 | assert_eq!(8u32.common_ancestor_height(&7u32), 0); 534 | assert_eq!(8u32.common_ancestor_height(&3u32), 0); 535 | assert_eq!(8u32.common_ancestor_height(&9u32), 2); 536 | assert_eq!(8u32.common_ancestor_height(&11u32), 1); 537 | assert_eq!(8u32.common_ancestor_height(&13u32), 0); 538 | assert_eq!(16u32.common_ancestor_height(&8u32), 3); 539 | assert_eq!(16u32.common_ancestor_height(&4u32), 2); 540 | assert_eq!(16u32.common_ancestor_height(&7u32), 0); 541 | assert_eq!(16u32.common_ancestor_height(&3u32), 0); 542 | assert_eq!(16u32.common_ancestor_height(&9u32), 2); 543 | assert_eq!(16u32.common_ancestor_height(&11u32), 1); 544 | assert_eq!(16u32.common_ancestor_height(&13u32), 0); 545 | assert_eq!(17u32.common_ancestor_height(&15u32), 0); 546 | assert_eq!(17u32.common_ancestor_height(&19u32), 2); 547 | assert_eq!(17u32.common_ancestor_height(&21u32), 1); 548 | assert_eq!(17u32.common_ancestor_height(&31u32), 0); 549 | assert_eq!(17u32.common_ancestor_height(&63u32), 0); 550 | assert_eq!(17u32.common_ancestor_height(&127u32), 0); 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /mc-oblivious-ram/src/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 | 16 | use alloc::vec; 17 | 18 | use crate::evictor::{BranchSelector, EvictionStrategy, EvictorCreator}; 19 | use aligned_cmov::{ 20 | subtle::{Choice, ConstantTimeEq, ConstantTimeLess}, 21 | typenum::{PartialDiv, Prod, Unsigned, U16, U64, U8}, 22 | A64Bytes, A8Bytes, ArrayLength, AsAlignedChunks, AsNeSlice, CMov, 23 | }; 24 | use alloc::{boxed::Box, vec::Vec}; 25 | use balanced_tree_index::TreeIndex; 26 | use core::{marker::PhantomData, ops::Mul}; 27 | use mc_oblivious_traits::{ 28 | log2_ceil, ORAMStorage, ORAMStorageCreator, PositionMap, PositionMapCreator, ORAM, 29 | }; 30 | use 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. 38 | pub(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 61 | pub(crate) fn meta_leaf_num(src: &A8Bytes) -> &u64 { 62 | &src.as_ne_u64_slice()[0] 63 | } 64 | /// Get the leaf num of a mutable metadata 65 | pub(crate) fn meta_leaf_num_mut(src: &mut A8Bytes) -> &mut u64 { 66 | &mut src.as_mut_ne_u64_slice()[0] 67 | } 68 | /// Get the block num of a metadata 69 | pub(crate) fn meta_block_num(src: &A8Bytes) -> &u64 { 70 | &src.as_ne_u64_slice()[1] 71 | } 72 | /// Get the block num of a mutable metadata 73 | pub(crate) fn meta_block_num_mut(src: &mut A8Bytes) -> &mut u64 { 74 | &mut src.as_mut_ne_u64_slice()[1] 75 | } 76 | /// Test if a metadata is "vacant" 77 | pub(crate) fn meta_is_vacant(src: &A8Bytes) -> Choice { 78 | meta_leaf_num(src).ct_eq(&0) 79 | } 80 | /// Set a metadata to vacant, obliviously, if a condition is true 81 | pub(crate) fn meta_set_vacant(condition: Choice, src: &mut A8Bytes) { 82 | meta_leaf_num_mut(src).cmov(condition, &0); 83 | } 84 | 85 | /// An implementation of PathORAM, using u64 to represent leaves in metadata. 86 | pub struct PathORAM 87 | where 88 | ValueSize: ArrayLength + PartialDiv + PartialDiv, 89 | Z: Unsigned + Mul + Mul, 90 | RngType: RngCore + CryptoRng + Send + Sync + 'static, 91 | StorageType: ORAMStorage, Prod> + Send + Sync + 'static, 92 | EvictorType: EvictionStrategy + BranchSelector + Send + Sync + 'static, 93 | Prod: ArrayLength + PartialDiv, 94 | Prod: ArrayLength + PartialDiv, 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, 102 | /// The rng 103 | rng: RngType, 104 | /// The stashed values 105 | stash_data: Vec>, 106 | /// The stashed metadata 107 | stash_meta: Vec>, 108 | /// Our currently checked-out branch if any 109 | branch: BranchCheckout, 110 | /// Eviction strategy 111 | evictor: EvictorType, 112 | } 113 | 114 | impl 115 | PathORAM 116 | where 117 | ValueSize: ArrayLength + PartialDiv + PartialDiv, 118 | Z: Unsigned + Mul + Mul, 119 | RngType: RngCore + CryptoRng + Send + Sync + 'static, 120 | StorageType: ORAMStorage, Prod> + Send + Sync + 'static, 121 | EvictorType: EvictionStrategy + BranchSelector + Send + Sync + 'static, 122 | Prod: ArrayLength + PartialDiv, 123 | Prod: ArrayLength + PartialDiv, 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, 133 | SC: ORAMStorageCreator, Prod, Output = StorageType>, 134 | F: FnMut() -> RngType + 'static, 135 | EVC: EvictorCreator, 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 | 166 | impl ORAM 167 | for PathORAM 168 | where 169 | ValueSize: ArrayLength + PartialDiv + PartialDiv, 170 | Z: Unsigned + Mul + Mul, 171 | RngType: RngCore + CryptoRng + Send + Sync + 'static, 172 | StorageType: ORAMStorage, Prod> + Send + Sync + 'static, 173 | EvictorType: EvictionStrategy + BranchSelector + Send + Sync + 'static, 174 | Prod: ArrayLength + PartialDiv, 175 | Prod: ArrayLength + PartialDiv, 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>(&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::::default(); 212 | let mut data = A64Bytes::::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. 284 | pub struct BranchCheckout 285 | where 286 | ValueSize: ArrayLength + PartialDiv + PartialDiv, 287 | Z: Unsigned + Mul + Mul, 288 | Prod: ArrayLength + PartialDiv, 289 | Prod: ArrayLength + PartialDiv, 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>>, 296 | /// The scratch-space for checked-out branch metadata 297 | pub(crate) meta: Vec>>, 298 | /// Phantom data for ValueSize 299 | _value_size: PhantomData ValueSize>, 300 | } 301 | 302 | impl Default for BranchCheckout 303 | where 304 | ValueSize: ArrayLength + PartialDiv + PartialDiv, 305 | Z: Unsigned + Mul + Mul, 306 | Prod: ArrayLength + PartialDiv, 307 | Prod: ArrayLength + PartialDiv, 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 | 319 | impl BranchCheckout 320 | where 321 | ValueSize: ArrayLength + PartialDiv + PartialDiv, 322 | Z: Unsigned + Mul + Mul, 323 | Prod: ArrayLength + PartialDiv, 324 | Prod: ArrayLength + PartialDiv, 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, 332 | dest_meta: &mut A8Bytes, 333 | ) { 334 | debug_assert!(self.data.len() == self.meta.len()); 335 | for idx in 0..self.data.len() { 336 | let bucket_data: &mut [A64Bytes] = self.data[idx].as_mut_aligned_chunks(); 337 | let bucket_meta: &mut [A8Bytes] = 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, 358 | src_meta: &mut A8Bytes, 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] = upper_data[0].as_mut_aligned_chunks(); 383 | let bucket_meta: &mut [A8Bytes] = 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 = &mut bucket_data[idx]; 388 | let src_meta: &mut A8Bytes = &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>, 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>, 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, 475 | src_meta: &mut A8Bytes, 476 | insert_after_index: usize, 477 | dest_data: &mut [A64Bytes>], 478 | dest_meta: &mut [A8Bytes>], 479 | ) { 480 | debug_assert!(dest_data.len() == dest_meta.len()); 481 | for idx in 0..dest_data.len() { 482 | details::ct_insert::( 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 494 | pub(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>( 513 | mut condition: Choice, 514 | query: &u64, 515 | dest_data: &mut A64Bytes, 516 | dest_meta: &mut A8Bytes, 517 | src_data: &mut [A64Bytes], 518 | src_meta: &mut [A8Bytes], 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>( 550 | mut condition: Choice, 551 | src_data: &A64Bytes, 552 | src_meta: &mut A8Bytes, 553 | dest_data: &mut [A64Bytes], 554 | dest_meta: &mut [A8Bytes], 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 | } 569 | --------------------------------------------------------------------------------