diff --git a/Cargo.toml b/Cargo.toml index dca9d3a..36bbb6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,13 @@ -[package] -name = "ore-rs" -version = "0.7.0" -authors = ["Dan Draper "] -edition = "2018" -homepage = "https://cipherstash.com" -description = "Order-revealing encryption library used by the CipherStash searchable encryption platform" -license-file = "LICENCE" +[workspace] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dev-dependencies] -criterion = "0.3.5" -quickcheck = "1.0.3" +members = [ + "core", + "formats", + "primitives", + "ore-rs-5bit", +] -[dependencies] +[workspace.dependencies] aes = { version = "0.8.2", features = ["zeroize"]} block-modes = "0.8.1" byteorder = "1.4.3" @@ -25,10 +20,4 @@ subtle-ng = "2.5.0" zeroize = { version = "1.5.7", features = [ "zeroize_derive", "alloc" ] } lazy_static = "1.4.0" thiserror = "1.0.38" - -[[bench]] -name = "oreaes128" -harness = false - -[[example]] -name = "encrypt" +quickcheck = "1.0.3" diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..6dea96b --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "ore-rs" +version = "0.7.0" +authors = ["Dan Draper "] +edition = "2018" +homepage = "https://cipherstash.com" +description = "Order-revealing encryption library used by the CipherStash searchable encryption platform" +license-file = "LICENCE" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dev-dependencies] +criterion = "0.3.5" +quickcheck = "1.0.3" + +[dependencies] +primitives = { path = "../primitives" } +aes = { version = "0.8.2", features = ["zeroize"]} +block-modes = "0.8.1" +byteorder = "1.4.3" +hex-literal = "0.3.2" +rand = "0.8.5" +rand_chacha = "0.3.1" +num = "0.4.0" +hex = "0.4.3" +subtle-ng = "2.5.0" +zeroize = { version = "1.5.7", features = [ "zeroize_derive", "alloc" ] } +lazy_static = "1.4.0" +thiserror = "1.0.38" + + + +[[bench]] +name = "oreaes128" +harness = false + +[[example]] +name = "encrypt" diff --git a/benches/oreaes128.rs b/core/benches/oreaes128.rs similarity index 100% rename from benches/oreaes128.rs rename to core/benches/oreaes128.rs diff --git a/examples/encrypt.rs b/core/examples/encrypt.rs similarity index 100% rename from examples/encrypt.rs rename to core/examples/encrypt.rs diff --git a/src/ciphertext.rs b/core/src/ciphertext.rs similarity index 99% rename from src/ciphertext.rs rename to core/src/ciphertext.rs index 43c93f3..782b5a5 100644 --- a/src/ciphertext.rs +++ b/core/src/ciphertext.rs @@ -1,6 +1,6 @@ use thiserror::Error; -use crate::primitives::NONCE_SIZE; +use primitives::NONCE_SIZE; pub use crate::OreCipher; /// The trait of any encryption output (either Left, Right or combined). diff --git a/src/convert.rs b/core/src/convert.rs similarity index 100% rename from src/convert.rs rename to core/src/convert.rs diff --git a/src/encrypt.rs b/core/src/encrypt.rs similarity index 100% rename from src/encrypt.rs rename to core/src/encrypt.rs diff --git a/src/lib.rs b/core/src/lib.rs similarity index 99% rename from src/lib.rs rename to core/src/lib.rs index 1dd3814..ed2b16a 100644 --- a/src/lib.rs +++ b/core/src/lib.rs @@ -137,7 +137,7 @@ mod ciphertext; mod convert; mod encrypt; -mod primitives; +//mod primitives; pub mod scheme; pub use crate::ciphertext::*; pub use crate::encrypt::OreEncrypt; diff --git a/src/scheme.rs b/core/src/scheme.rs similarity index 100% rename from src/scheme.rs rename to core/src/scheme.rs diff --git a/src/scheme/bit2.rs b/core/src/scheme/bit2.rs similarity index 99% rename from src/scheme/bit2.rs rename to core/src/scheme/bit2.rs index ef5c87f..e08defa 100644 --- a/src/scheme/bit2.rs +++ b/core/src/scheme/bit2.rs @@ -2,12 +2,12 @@ * Block ORE Implemenation using a 2-bit indicator function */ +use primitives::{ + hash::Aes128Z2Hash, prf::Aes128Prf, prp::KnuthShufflePRP, AesBlock, Hash, HashKey, Prf, + Prp, NONCE_SIZE, +}; use crate::{ ciphertext::*, - primitives::{ - hash::Aes128Z2Hash, prf::Aes128Prf, prp::KnuthShufflePRP, AesBlock, Hash, HashKey, Prf, - Prp, NONCE_SIZE, - }, OreCipher, OreError, PlainText, }; diff --git a/src/scheme/bit2/block_types.rs b/core/src/scheme/bit2/block_types.rs similarity index 98% rename from src/scheme/bit2/block_types.rs rename to core/src/scheme/bit2/block_types.rs index 8811003..ae06b99 100644 --- a/src/scheme/bit2/block_types.rs +++ b/core/src/scheme/bit2/block_types.rs @@ -1,7 +1,7 @@ use zeroize::Zeroize; use crate::ciphertext::{CipherTextBlock, ParseError}; -use crate::primitives::AesBlock; +use primitives::AesBlock; pub type LeftBlock16 = AesBlock; diff --git a/formats/Cargo.toml b/formats/Cargo.toml new file mode 100644 index 0000000..e770597 --- /dev/null +++ b/formats/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "formats" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +subtle-ng = { workspace = true } diff --git a/formats/src/ciphertext/combined.rs b/formats/src/ciphertext/combined.rs new file mode 100644 index 0000000..85e4fa0 --- /dev/null +++ b/formats/src/ciphertext/combined.rs @@ -0,0 +1,145 @@ +use std::{marker::PhantomData, cmp::Ordering}; +use crate::{data_with_header::{CtType, DataWithHeader}, header::Header, ParseError, LeftBlockEq, LeftCipherTextBlock, OreBlockOrd}; +use super::{CipherTextBlock, CipherText, left::LeftCiphertext, right::RightCiphertext, RightCipherTextBlock}; + +#[derive(Debug)] +pub struct CombinedBlock<'a, L: CipherTextBlock<'a>, R: CipherTextBlock<'a>> { + pub left: L, + pub right: R, + _phantom: PhantomData<&'a L> +} + +/// A combined ciphertext block also implements Right Block +impl<'a, L, R> RightCipherTextBlock<'a> for CombinedBlock<'a, L, R> +where L: LeftCipherTextBlock<'a>, + R: RightCipherTextBlock<'a> +{} + +impl <'a, L: CipherTextBlock<'a>, R: CipherTextBlock<'a>> From<&'a [u8]> for CombinedBlock<'a, L, R> { + fn from(value: &'a [u8]) -> Self { + let left = L::from(&value[..L::byte_size()]); + let right = R::from(&value[L::byte_size()..]); + Self { left, right, _phantom: PhantomData } + } +} + +impl<'a, L: CipherTextBlock<'a>, R: CipherTextBlock<'a>> CipherTextBlock<'a> for CombinedBlock<'a, L, R> { + fn byte_size() -> usize { + L::byte_size() + R::byte_size() + } + + fn extend_into(&self, out: &mut DataWithHeader) { + todo!() + } +} + +pub struct CombinedCiphertext<'a, L: LeftCipherTextBlock<'a>, R: RightCipherTextBlock<'a>> { + data: DataWithHeader, + _phantom: (PhantomData<&'a L>, PhantomData<&'a R>), +} + + +impl<'a, L, R> CombinedCiphertext<'a, L, R> +where + L: LeftCipherTextBlock<'a>, + R: RightCipherTextBlock<'a> +{ + pub fn new(num_blocks: usize, nonce: &[u8; 16]) -> Self { + let hdr = Header::new(CtType::Combined, num_blocks); + let mut data = DataWithHeader::new( + hdr, + RightCiphertext::<'a, R>::NONCE_SIZE + (num_blocks * ::Block::byte_size()) + ); + data.extend_from_slice(nonce); + Self { data, _phantom: (PhantomData, PhantomData) } + } + + // TODO: We should probably pass the args as references (same for left and right impls) + pub fn add_block(&mut self, left: L, right: R) { + left.extend_into(&mut self.data); + right.extend_into(&mut self.data); + } + + pub fn nonce(&self) -> &[u8] { + &self.data.body()[..RightCiphertext::<'a, R>::NONCE_SIZE] + } +} + +impl<'a, L, R> CipherText<'a> for CombinedCiphertext<'a, L, R> +where + L: LeftCipherTextBlock<'a>, + R: RightCipherTextBlock<'a> +{ + type Block = CombinedBlock<'a, L, R>; + + fn len(&self) -> usize { + self.data.len() + } + + fn header(&self) -> Header { + self.data.header() + } + + // TODO: This can go into the trait if we add a body method + // Right is different though because we have the nonce! + fn blocks(&'a self) -> Box + 'a> { + Box::new( + self.data.body()[RightCiphertext::<'a, R>::NONCE_SIZE..] + .chunks(Self::Block::byte_size()) + .map(|bytes| Self::Block::from(bytes)) + ) + } +} + + +impl<'a, L, R> TryFrom<&'a [u8]> for CombinedCiphertext<'a, L, R> +where + L: LeftCipherTextBlock<'a>, + R: RightCipherTextBlock<'a> +{ + type Error = ParseError; + + fn try_from(data: &'a [u8]) -> Result { + let hdr = Header::from_slice(data); + if matches!(hdr.ct_type, CtType::Combined) { + Ok(Self { data: data.into(), _phantom: (PhantomData, PhantomData) }) + } else { + Err(ParseError { }) + } + } +} + +impl<'a, L, R> AsRef<[u8]> for CombinedCiphertext<'a, L, R> +where + L: LeftCipherTextBlock<'a>, + R: RightCipherTextBlock<'a> +{ + fn as_ref(&self) -> &[u8] { + self.data.as_ref() + } +} + +/// Blanket implementation to compare a left block to the left of any combined block +impl<'a, L, R> LeftBlockEq<'a, CombinedBlock<'a, L, R>> for L +where + L: LeftCipherTextBlock<'a>, + R: RightCipherTextBlock<'a> +{ + fn constant_eq(&self, other: &CombinedBlock<'a, L, R>) -> subtle_ng::Choice { + self.constant_eq(&other.left) + } +} + +/// Blanket implementation for a left block to Ore compare to the right block +/// of a combined block. +impl<'a, L, R> OreBlockOrd<'a, CombinedBlock<'a, L, R>> for L +where + L: LeftCipherTextBlock<'a>, + R: RightCipherTextBlock<'a>, + L: OreBlockOrd<'a, R> +{ + fn ore_compare(&self, nonce: &[u8], other: &CombinedBlock<'a, L, R>) -> i8 { + self.ore_compare(nonce, &other.right) + } +} + diff --git a/formats/src/ciphertext/left.rs b/formats/src/ciphertext/left.rs new file mode 100644 index 0000000..ea256a5 --- /dev/null +++ b/formats/src/ciphertext/left.rs @@ -0,0 +1,91 @@ +use std::{marker::PhantomData, ops::{BitOr, BitAnd}, cmp::Ordering}; +use subtle_ng::{Choice, CtOption, ConditionallySelectable}; + +use crate::{data_with_header::{CtType, DataWithHeader}, header::Header, ParseError, LeftCipherTextBlock, OreBlockOrd, RightCipherTextBlock}; +use super::{CipherTextBlock, CipherText, LeftBlockEq}; + +pub struct LeftCiphertext<'a, B: LeftCipherTextBlock<'a>> { + pub(crate) data: DataWithHeader, + _phantom: PhantomData<&'a B>, +} + +impl<'a, B: LeftCipherTextBlock<'a>> LeftCiphertext<'a, B> { + pub fn new(num_blocks: usize) -> Self { + let hdr = Header::new(CtType::Left, num_blocks); + + Self { + data: DataWithHeader::new(hdr, num_blocks * ::Block::byte_size()), + _phantom: PhantomData + } + } + + pub fn add_block(&mut self, block: B) { + block.extend_into(&mut self.data); + } + + /// Compare all the blocks of self with all the blocks in the given iterator, up to the `n` + /// where `n` is the length of the shorter iterator. + /// The ordering mechanism is important here, too (i.e. Lexicographic or Numerical) + /// If its numerical then the shorter value is always less than the other. + pub fn compare_blocks(&'a self, nonce: &[u8], other: Box + 'a>) -> i8 + where + B: LeftBlockEq<'a, O> + OreBlockOrd<'a, O>, + O: RightCipherTextBlock<'a> + { + let mut ai = self.blocks(); + let mut bi = other; // TODO: Don't pass an iterator to this func, pass an impl CipherText + + // TODO: Perhaps the LeftBlock could define the whole comparison (rather than splitting Eq and Ord like this) + let mut result: CtOption = CtOption::new(0, Choice::from(0)); + + loop { + match (ai.next(), bi.next()) { + (None, None) => return result.unwrap_or(0), + (Some(_), None) => return result.unwrap_or(1), + (None, Some(_)) => return result.unwrap_or(-1), + (Some(x), Some(y)) => { + // Always do the ore compare (even though we don't need to) to keep timing constant. + // Then assign the comparison to result if result is None and the left values are not equal. + let comparison = CtOption::new(x.ore_compare(nonce, &y), Choice::from(1)); + result.conditional_assign(&comparison, result.is_none().bitand(!x.constant_eq(&y))); + } + } + } + } +} + +impl<'a, B: LeftCipherTextBlock<'a>> CipherText<'a> for LeftCiphertext<'a, B> { + type Block = B; + + fn len(&self) -> usize { + self.data.len() + } + + fn header(&self) -> Header { + self.data.header() + } + + fn blocks(&'a self) -> Box + 'a> { + // TODO: Should we assert that length is a multiple of the block size? + Box::new(self.data.body().chunks(Self::Block::byte_size()).map(|bytes| B::from(bytes))) + } +} + +impl<'a, B: LeftCipherTextBlock<'a>> TryFrom<&'a [u8]> for LeftCiphertext<'a, B> { + type Error = ParseError; + + fn try_from(data: &'a [u8]) -> Result { + let hdr = Header::from_slice(data); + if matches!(hdr.ct_type, CtType::Left) { + Ok(Self { data: data.into(), _phantom: PhantomData }) + } else { + Err(ParseError { }) + } + } +} + +impl<'a, B: LeftCipherTextBlock<'a>> AsRef<[u8]> for LeftCiphertext<'a, B> { + fn as_ref(&self) -> &[u8] { + self.data.as_ref() + } +} diff --git a/formats/src/ciphertext/mod.rs b/formats/src/ciphertext/mod.rs new file mode 100644 index 0000000..cdaa340 --- /dev/null +++ b/formats/src/ciphertext/mod.rs @@ -0,0 +1,43 @@ +use std::cmp::Ordering; + +use subtle_ng::{ConstantTimeEq, Choice}; +use crate::{header::Header, data_with_header::DataWithHeader}; +pub(crate) mod left; +pub(crate) mod right; +pub(crate) mod combined; + +// TODO: make the new and add_block functions a separate trait +pub trait CipherText<'a> { + type Block: CipherTextBlock<'a>; + + // TODO: Remove this + fn comparable(&self, to: &impl CipherText<'a>) -> bool { + self.header().comparable(&to.header()) + } + // TODO: Probs shouldn't expose the header + fn header(&self) -> Header; + + fn blocks(&'a self) -> Box + 'a>; + + fn len(&self) -> usize; +} + +pub trait CipherTextBlock<'a>: From<&'a [u8]> { // TODO: Zeroize + fn byte_size() -> usize; + fn extend_into(&self, out: &mut DataWithHeader); +} + +pub trait LeftCipherTextBlock<'a>: LeftBlockEq<'a> + CipherTextBlock<'a> {} + +pub trait RightCipherTextBlock<'a>: CipherTextBlock<'a> {} + +pub trait LeftBlockEq<'a, Other: ?Sized + CipherTextBlock<'a> = Self> { + //type Other: ?Sized + CipherTextBlock<'a>; + + // TODO: Maybe this is choice? Or a wrapper of choice at least + fn constant_eq(&self, other: &Other) -> Choice; +} + +pub trait OreBlockOrd<'a, Other: ?Sized + RightCipherTextBlock<'a>> { + fn ore_compare(&self, nonce: &[u8], right: &Other) -> i8; +} diff --git a/formats/src/ciphertext/right.rs b/formats/src/ciphertext/right.rs new file mode 100644 index 0000000..f137807 --- /dev/null +++ b/formats/src/ciphertext/right.rs @@ -0,0 +1,58 @@ +use std::marker::PhantomData; +use crate::{data_with_header::{DataWithHeader, CtType}, ParseError, header::Header}; +use super::{CipherTextBlock, CipherText, RightCipherTextBlock}; + +pub struct RightCiphertext<'a, B: RightCipherTextBlock<'a>> { + pub(crate) data: DataWithHeader, + _phantom: PhantomData<&'a B>, +} + +impl<'a, B: RightCipherTextBlock<'a>> RightCiphertext<'a, B> { + pub const NONCE_SIZE: usize = 16; + + pub fn new(num_blocks: usize, nonce: &[u8; 16]) -> Self { + let hdr = Header::new(CtType::Right, num_blocks); + let mut data = DataWithHeader::new(hdr, Self::NONCE_SIZE + (num_blocks * ::Block::byte_size())); + data.extend_from_slice(nonce); + Self { data, _phantom: PhantomData } + } + + pub fn add_block(&mut self, block: B) { + block.extend_into(&mut self.data); + } +} + +impl<'a, B: RightCipherTextBlock<'a>> CipherText<'a> for RightCiphertext<'a, B> { + type Block = B; + + fn len(&self) -> usize { + self.data.len() + } + + fn header(&self) -> Header { + self.data.header() + } + + fn blocks(&self) -> Box> { + todo!() + } +} + +impl<'a, B: RightCipherTextBlock<'a>> TryFrom<&'a [u8]> for RightCiphertext<'a, B> { + type Error = ParseError; + + fn try_from(data: &'a [u8]) -> Result { + let hdr = Header::from_slice(data); + if matches!(hdr.ct_type, CtType::Right) { + Ok(Self { data: data.into(), _phantom: PhantomData }) + } else { + Err(ParseError { }) + } + } +} + +impl<'a, B: RightCipherTextBlock<'a>> AsRef<[u8]> for RightCiphertext<'a, B> { + fn as_ref(&self) -> &[u8] { + self.data.as_ref() + } +} \ No newline at end of file diff --git a/formats/src/data_with_header.rs b/formats/src/data_with_header.rs new file mode 100644 index 0000000..f171c8f --- /dev/null +++ b/formats/src/data_with_header.rs @@ -0,0 +1,74 @@ +use crate::header::Header; + +pub struct DataWithHeader { + data: Vec +} + +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum CtType { + Left = 0, + Right = 1, + Combined = 2, +} + +impl From for CtType { + fn from(value: u8) -> Self { + match value { + 0 => Self::Left, + 1 => Self::Right, + 2 => Self::Combined, + _ => panic!("Unknown Ciphertext Type") + } + } +} + +impl DataWithHeader { + pub(crate) fn new(header: Header, body_len: usize) -> Self { + let mut data = Vec::with_capacity(Header::HEADER_LEN + body_len); + data.extend(header.to_vec()); + Self { data } + } + + pub(crate) fn len(&self) -> usize { + self.data.len() + } + + pub(crate) fn header(&self) -> Header { + Header::from_slice(&self.data) + } + + pub(crate) fn set_header(&mut self, hdr: &Header) { + self.data[0..Header::HEADER_LEN].copy_from_slice(&hdr.to_vec()); + } + + /// Returns a slice to the body of the ciphertext. + /// That is, everything after the header. + pub(crate) fn body(&self) -> &[u8] { + &self.data[Header::HEADER_LEN..] + } + + pub fn extend(&mut self, iter: I) + where + I: IntoIterator + { + self.data.extend(iter) + } + + pub fn extend_from_slice(&mut self, slice: &[u8]) { + self.data.extend_from_slice(slice); + } +} + +impl AsRef<[u8]> for DataWithHeader { + fn as_ref(&self) -> &[u8] { + self.data.as_ref() + } +} + +impl From<&[u8]> for DataWithHeader { + fn from(data: &[u8]) -> Self { + assert!(data.len() >= Header::HEADER_LEN); + // TODO: It would be nice if we could avoid this copy! + Self { data: data.to_vec() } + } +} \ No newline at end of file diff --git a/formats/src/header.rs b/formats/src/header.rs new file mode 100644 index 0000000..6198651 --- /dev/null +++ b/formats/src/header.rs @@ -0,0 +1,91 @@ +use crate::data_with_header::CtType; + +#[derive(PartialEq, Debug)] +pub struct Header { + pub version: u16, + pub scheme: u8, + pub ct_type: CtType, + pub num_blocks: u16 +} + +impl Header { + pub(super) const HEADER_LEN: usize = 6; + + pub(super) fn new(ct_type: CtType, num_blocks: usize) -> Self { + assert!(num_blocks < (u16::MAX as usize)); + + Self { + // Hardcode version and scheme for now + version: 0, + scheme: 0, + ct_type, + num_blocks: num_blocks as u16 + } + } + + /// Indicates if this ciphertext header is comparable to another. + /// This means the version and scheme must be the same. + /// Specific schemes may impose additional restrictions (such as matching lengths). + pub(super) fn comparable(&self, other: &Header) -> bool { + use CtType::*; + if self.version != other.version { return false } + if self.scheme != self.scheme { return false } + + match (self.ct_type, other.ct_type) { + (Combined, Combined) => true, + (Left, Right) => true, + _ => false + } + } + + pub(super) fn to_vec(&self) -> Vec { + let mut hdr: Vec = Vec::with_capacity(Self::HEADER_LEN); + hdr.extend(self.version.to_be_bytes()); + hdr.push(self.scheme); + hdr.push(*&self.ct_type as u8); + hdr.extend(self.num_blocks.to_be_bytes()); + hdr + } + + pub(super) fn from_slice(hdr: &[u8]) -> Self { // TODO: Handle error + assert!(hdr.len() >= Self::HEADER_LEN, "Header cannot be read from slice of less than {} bytes", Self::HEADER_LEN); + let mut iter = hdr.into_iter(); + let version: u16 = u16::from_be_bytes(iter.next_chunk::<2>().unwrap().map(|c| *c)); + let scheme: u8 = *iter.next().unwrap(); + let ct_type: CtType = (*iter.next().unwrap()).into(); + let num_blocks: u16 = u16::from_be_bytes(iter.next_chunk::<2>().unwrap().map(|c| *c)); + + Self { version, scheme, ct_type, num_blocks } + } +} + +#[cfg(test)] +mod tests { + use crate::data_with_header::CtType; + use super::Header; + + #[test] + fn test_new() { + let header = Header::new(CtType::Right, 12); + assert_eq!(header.version, 0); + assert_eq!(header.scheme, 0); + assert_eq!(header.ct_type, CtType::Right); + assert_eq!(header.num_blocks, 12); + } + + #[test] + fn test_roundtrip() { + let header = Header::new(CtType::Left, 8); + let bytes = header.to_vec(); + assert_eq!(header, Header::from_slice(&bytes)); + } + + #[test] + fn test_roundtrip_with_ignored_trailing_bytes() { + let header = Header::new(CtType::Left, 8); + let mut bytes: Vec = Vec::new(); + bytes.extend(header.to_vec()); + bytes.extend(vec![1, 2, 3, 4]); + assert_eq!(header, Header::from_slice(&bytes)); + } +} diff --git a/formats/src/lib.rs b/formats/src/lib.rs new file mode 100644 index 0000000..ba1e89d --- /dev/null +++ b/formats/src/lib.rs @@ -0,0 +1,21 @@ +#![feature(iter_next_chunk)] +mod data_with_header; +mod header; +mod ciphertext; + +pub use ciphertext::{ + CipherText, + CipherTextBlock, + LeftCipherTextBlock, + RightCipherTextBlock, + LeftBlockEq, + OreBlockOrd, + // TODO: Make the naming of these consistent + left::LeftCiphertext, + right::RightCiphertext, + combined::CombinedCiphertext +}; +pub use data_with_header::DataWithHeader; + +#[derive(Debug)] +pub struct ParseError {} diff --git a/ore-rs-5bit/Cargo.toml b/ore-rs-5bit/Cargo.toml new file mode 100644 index 0000000..8f03088 --- /dev/null +++ b/ore-rs-5bit/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ore-rs-5bit" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +primitives = { path = "../primitives" } +formats = { path = "../formats" } +zeroize = { workspace = true } +rand = { workspace = true } +rand_chacha = { workspace = true } +aes = { workspace = true } +subtle-ng = { workspace = true } +thiserror = { workspace = true } +quickcheck = { workspace = true } + +# TODO: Only use this in examples +hex-literal = "0.3.2" +hex = { workspace = true } diff --git a/ore-rs-5bit/src/left_block.rs b/ore-rs-5bit/src/left_block.rs new file mode 100644 index 0000000..841694b --- /dev/null +++ b/ore-rs-5bit/src/left_block.rs @@ -0,0 +1,117 @@ +use std::{cmp::Ordering, ops::BitAnd}; +use formats::{CipherTextBlock, DataWithHeader, LeftBlockEq, LeftCipherTextBlock, OreBlockOrd}; +use primitives::{hash::Aes128Z2Hash, Hash}; +use subtle_ng::ConstantTimeEq; +use crate::right_block::RightBlock; + +// TODO: We could make the array a reference +// That way we can encrypt it externally but just pass the prefix to an init function +#[derive(Debug)] +pub struct LeftBlock(pub(super) [u8; 16], pub(super) u8); + + +impl<'a> CipherTextBlock<'a> for LeftBlock { + fn byte_size() -> usize { + 17 + } + + fn extend_into(&self, out: &mut DataWithHeader) { + out.extend_from_slice(&self.0); + out.extend([self.1]); + } +} + + +impl<'a> LeftBlockEq<'a, LeftBlock> for LeftBlock { + fn constant_eq(&self, other: &Self) -> subtle_ng::Choice { + self.0.ct_eq(&other.0).bitand(self.1.ct_eq(&other.1)) + } +} + +impl<'a> OreBlockOrd<'a, RightBlock> for LeftBlock { + // FIXME: Nonce *must* be 16-bytes + fn ore_compare(&self, nonce: &[u8], right: &RightBlock) -> i8 { + // TODO: This would be cleaner if we defined a method on RightBlock + let hasher: Aes128Z2Hash = Hash::new(nonce.into()); + // TODO: Use conditional_select + //if ((right << self.1) as u8 & 1u8) ^ hasher.hash(&self.0) == 1 { + let mask = hasher.hash(&self.0); + if (right.get_bit(self.1) ^ mask) == 1 { + 1 + } else { + -1 + } + } +} + +// TODO: Derive macro? +impl<'a> LeftCipherTextBlock<'a> for LeftBlock {} + + +impl From<&[u8]> for LeftBlock { + fn from(value: &[u8]) -> Self { + assert!(value.len() == Self::byte_size()); + let mut buf: [u8; 16] = Default::default(); + buf.copy_from_slice(&value[0..16]); + LeftBlock(buf, value[16]) + } +} + +#[cfg(test)] +mod tests { + use primitives::{NewPrp, KnuthShuffleGenerator, PrpGenerator}; + use rand::{thread_rng, Fill}; + use super::*; + + #[test] + fn test_ore_compare_block() { + let mut rng = thread_rng(); + let mut prefix: [u8; 16] = Default::default(); + let mut nonce: [u8; 16] = Default::default(); + //prefix.try_fill(&mut rng).unwrap(); + nonce.try_fill(&mut rng).unwrap(); + let prp: NewPrp = KnuthShuffleGenerator::new(&prefix).generate(); + let mut right = RightBlock::init(10).shuffle(&prp); + + let mut ro_keys: [[u8; 16]; 32] = Default::default(); + + for (j, ro_key) in ro_keys.iter_mut().enumerate() { + ro_key.copy_from_slice(&prefix); + ro_key[15] = j as u8; + } + let hasher: Aes128Z2Hash = Hash::new(&nonce.into()); + //self.prf1.encrypt_all(&mut ro_keys); + let mask = hasher.hash_all_onto_u32(&ro_keys); + + println!("MASK: {mask:b}"); + println!("PERMUTE plaintext 10 becomes {}", prp.inverse_permute(10u8)); + println!("PERMUTE plaintext 5 becomes {}", prp.inverse_permute(5u8)); + println!("PERMUTE plaintext 24 becomes {}", prp.inverse_permute(24u8)); + + right ^= mask; + + assert_eq!( + LeftBlock( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, prp.inverse_permute(10u8)], + prp.inverse_permute(10u8) + ).ore_compare(&nonce, &right), + 1 + ); + + assert_eq!( + LeftBlock( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, prp.inverse_permute(5u8)], + prp.inverse_permute(5u8) + ).ore_compare(&nonce, &right), + -1 + ); + + assert_eq!( + LeftBlock( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, prp.inverse_permute(24u8)], + prp.inverse_permute(24u8) + ).ore_compare(&nonce, &right), + 1 + ); + } +} \ No newline at end of file diff --git a/ore-rs-5bit/src/lib.rs b/ore-rs-5bit/src/lib.rs new file mode 100644 index 0000000..ad27697 --- /dev/null +++ b/ore-rs-5bit/src/lib.rs @@ -0,0 +1,400 @@ +use std::{cell::RefCell, ops::{BitAnd, BitXor, BitXorAssign}, cmp::Ordering}; +use formats::{LeftCiphertext, CipherTextBlock, DataWithHeader, RightCiphertext, CombinedCiphertext, CipherText, LeftCipherTextBlock, RightCipherTextBlock, OreBlockOrd, LeftBlockEq}; +use left_block::LeftBlock; +use primitives::{prf::Aes128Prf, Prf, prp::bitwise::BitwisePrp, Prp, KnuthShuffleGenerator, PrpGenerator, NewPrp, hash::Aes128Z2Hash, Hash}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use right_block::RightBlock; +use subtle_ng::{ConstantTimeEq, Choice}; +use zeroize::ZeroizeOnDrop; +use aes::cipher::generic_array::GenericArray; +use crate::packing::prefixes; +pub mod packing; +mod right_block; +mod left_block; + +#[derive(Debug, ZeroizeOnDrop)] +pub struct Ore5Bit { + // TODO: Temp + k2: [u8; 16], + + prf1: Aes128Prf, + prf2: Aes128Prf, + #[zeroize(skip)] + rng: RefCell, +} + +// TODO: use a trait type +/// This type is deliberately opaque as to avoid potential side-channel leakage. +#[derive(Debug)] +pub struct OreError; + +pub type Ore5BitChaCha20 = Ore5Bit; +pub type Ore5BitLeft<'a> = LeftCiphertext<'a, LeftBlock>; +pub type Ore5BitRight<'a> = RightCiphertext<'a, RightBlock>; +pub type Ore5BitCombined<'a> = CombinedCiphertext<'a, LeftBlock, RightBlock>; + + + +// TODO: Make this use the ORE traits once we've cleaned these up +impl Ore5Bit { + // TODO: This should be an implementation of OreInit + pub fn init(k1: &[u8; 16], k2: &[u8; 16]) -> Result { + // TODO: k1 and k2 should be Key types and we should have a set of traits to abstract the + // behaviour ro parsing/loading etc + + let rng: R = SeedableRng::from_entropy(); + + return Ok(Self { + k2: k2.clone(), + prf1: Prf::new(GenericArray::from_slice(k1)), + prf2: Prf::new(GenericArray::from_slice(k2)), + rng: RefCell::new(rng), + }); + } + + /// Takes a slice of 5-bit values (represented by a slice of `u8` but the + /// most significant 3-bits of each value are ignored). + /// TODO: Create a wrapper type + /// TODO: This might be faster if we do it blocks of statically allocated chunks + pub fn encrypt_left(&self, input: &[u8]) -> Ore5BitLeft { + // We're limited to 16 input blocks for now because we're using AES as the PRF (1 block) + debug_assert!(input.len() <= 16); + let mut out = Ore5BitLeft::new(input.len()); + + // Here we'll model a PRF using a single block of AES + // This will be OK for up to 16-bytes of input (or 25 5-bit values) + // For larger inputs we can chain the values by XORing the last output + // with the next input (a little like CMAC). + let mut prefixes = prefixes(input); + + self.prf2.encrypt_all(&mut prefixes); + + // This deviates from the paper slightly. + // Instead of calling PRF1 with the plaintext prefix, we call it + // with the output of the PRF2 of the prefix. + // This avoids a copy and should have the same effect. + // TODO: Change this to use functional callbacks rather than for + let mut p_ns = vec![]; + + for (n, in_blk) in input.iter().enumerate() { + let prp: NewPrp = KnuthShuffleGenerator::new(&prefixes[n]).generate(); + let p_n = prp.permute(*in_blk); + p_ns.push(p_n); + + prefixes[n].iter_mut().for_each(|x| *x = 0); + prefixes[n][0..n].clone_from_slice(&input[0..n]); + prefixes[n][n] = p_n; + prefixes[n][15] = n as u8; + } + + self.prf1.encrypt_all(&mut prefixes); + + for (prf_block, permuted) in prefixes.iter().zip(p_ns.iter()) { + out.add_block(LeftBlock(*prf_block, *permuted)); + } + + out + } + + pub fn encrypt(&self, input: &[u8]) -> Ore5BitCombined { + let mut nonce: [u8; 16] = Default::default(); + self.rng.borrow_mut().try_fill(&mut nonce).unwrap(); + + // TODO: Can we pack the input bytes?? + debug_assert!(input.len() <= 16); + let mut out = Ore5BitCombined::new(input.len(), &nonce); + + // Here we'll model a PRF using a single block of AES + // This will be OK for up to 16-bytes of input (or 25 5-bit values) + // For larger inputs we can chain the values by XORing the last output + // with the next input (a little like CMAC). + let mut prefixes = prefixes(input); + let mut right_blocks: Vec = Vec::with_capacity(input.len()); + self.prf2.encrypt_all(&mut prefixes); + + // This deviates from the paper slightly. + // Instead of calling PRF1 with the plaintext prefix, we call it + // with the output of the PRF2 of the prefix. + // This avoids a copy and should have the same effect. + // We also use a mask to set the comparison bits in one constant time + // operation and then perform a bitwise shuffle using the PRP + // instead of performing comparisons on each value (which would not be constant time). + let mut p_ns: Vec = Vec::with_capacity(input.len()); + + for (n, in_blk) in input.iter().enumerate() { + let prp: NewPrp = KnuthShuffleGenerator::new(&prefixes[n]).generate(); + let p_n = prp.permute(*in_blk); + p_ns.push(p_n); + + prefixes[n].iter_mut().for_each(|x| *x = 0); + prefixes[n][0..n].clone_from_slice(&input[0..n]); + prefixes[n][n] = p_n; + prefixes[n][15] = n as u8; + + + // encrypt_left and encrypt functions are identical, except that we have this encrypt right block stuff in the middle + let mut right_blk = RightBlock::init(*in_blk).shuffle(&prp); + + // TODO: Use the same approach here as we do in the original implementation + let mut ro_keys: [[u8; 16]; 32] = Default::default(); + + for (j, ro_key) in ro_keys.iter_mut().enumerate() { + ro_key[0..n].copy_from_slice(&input[0..n]); + ro_key[n] = j as u8; + } + + self.prf1.encrypt_all(&mut ro_keys); + + // TODO: Hash all of these keys with the nonce + // set the bits and and Xor with the right_block + // Push bytes onto right output vec + let hasher: Aes128Z2Hash = Hash::new(&nonce.into()); + // TODO: Hash all onto could be generic (right block) + // A RightBlock is like an "indicator set" + let mask = hasher.hash_all_onto_u32(&ro_keys); + + right_blk ^= mask; + + right_blocks.push(right_blk); + + } + + self.prf1.encrypt_all(&mut prefixes); + + for ((left, p_n), right) in prefixes.into_iter().zip(p_ns.into_iter()).zip(right_blocks.into_iter()) { + out.add_block(LeftBlock(left, p_n), right); + } + + out + } + + // TODO: Do this as a PartialOrd impl (and handle versions) + // TODO: Handle different length slices. Compare the first n-bytes and if they're equal then the + // longer value will be more "more-than" + pub fn compare_slices(a: impl AsRef<[u8]>, b: impl AsRef<[u8]>) -> i8 { + let left: Ore5BitLeft = a.as_ref().try_into().unwrap(); + let combined: Ore5BitCombined = b.as_ref().try_into().unwrap(); + //assert!(left.comparable(&combined)); // TODO: Error + + // With most of the work now in the LeftCipherText and the block types, this + // could be a default implementation in the main trait + left.compare_blocks(combined.nonce(), combined.blocks()) + } + + // For the right encryption we could either use the approach that we do in the current version, + // or instead of doing a comparison of every number, we set all bits >= the input to 1 and then do + // a bitwise permutation. + // Current approach: + // - Permute + // - Traverse permutation + // - N comparisons (which may not be constant time!) + // - N bit sets (left shift, or) + // Proposed approach: + // - Set all bits to one (>= the plaintext) + // - Bitwise permute: N bit get, N bit set (we avoid the comparisons!) +} + +#[cfg(test)] +#[macro_use] +extern crate quickcheck; + +#[cfg(test)] +mod tests { + use quickcheck::{Arbitrary, QuickCheck}; + use super::*; + + type ORE = Ore5BitChaCha20; + + // TODO: Can we make these a macro so that we can reuse for every scheme? + + fn init_ore() -> Result { + let mut k1: [u8; 16] = Default::default(); + let mut k2: [u8; 16] = Default::default(); + + let mut rng = ChaCha20Rng::from_entropy(); + + rng.fill(&mut k1); + rng.fill(&mut k2); + + // TODO: This will work when have the trait setup correctly + //OreCipher::init(&k1, &k2).unwrap() + Ore5BitChaCha20::init(&k1, &k2) + } + + #[test] + fn test_single_block_eq() -> Result<(), OreError> { + let a = vec![10]; + let ore = init_ore()?; + let left = ore.encrypt_left(&a); + let combined = ore.encrypt(&a); + + assert_eq!(ORE::compare_slices(&left, &combined), 0); + Ok(()) + } + + #[test] + fn test_single_block_lt() -> Result<(), OreError> { + let a = vec![10]; + let b = vec![29]; + let ore = init_ore()?; + let left = ore.encrypt_left(&a); + let combined = ore.encrypt(&b); + + assert_eq!(ORE::compare_slices(&left, &combined), -1); + + Ok(()) + } + + #[test] + fn test_single_block_gt() -> Result<(), OreError> { + let a = vec![11]; + let b = vec![0]; + let ore = init_ore()?; + let left = ore.encrypt_left(&a); + let combined = ore.encrypt(&b); + + assert_eq!(ORE::compare_slices(&left, &combined), 1); + + Ok(()) + } + + #[test] + fn test_empty_lt() -> Result<(), OreError> { + let a = vec![]; + let b = vec![0]; + let ore = init_ore()?; + let left = ore.encrypt_left(&a); + let combined = ore.encrypt(&b); + + assert_eq!(ORE::compare_slices(&left, &combined), -1); + + Ok(()) + } + + #[test] + fn test_empty_gt() -> Result<(), OreError> { + let a = vec![0]; + let b = vec![]; + let ore = init_ore()?; + let left = ore.encrypt_left(&a); + let combined = ore.encrypt(&b); + + assert_eq!(ORE::compare_slices(&left, &combined), 1); + + Ok(()) + } + + #[test] + fn test_uneven_common_prefix_gt() -> Result<(), OreError> { + let a = vec![10, 15]; + let b = vec![10]; + let ore = init_ore()?; + let left = ore.encrypt_left(&a); + let combined = ore.encrypt(&b); + + assert_eq!(ORE::compare_slices(&left, &combined), 1); + + Ok(()) + } + + #[test] + fn test_uneven_gt() -> Result<(), OreError> { + let a = vec![10]; + let b = vec![7, 20]; + let ore = init_ore()?; + let left = ore.encrypt_left(&a); + let combined = ore.encrypt(&b); + + assert_eq!(ORE::compare_slices(&left, &combined), 1); + + Ok(()) + } + + #[test] + fn test_uneven_common_prefix_lt() -> Result<(), OreError> { + let a = vec![10]; + let b = vec![10, 15]; + let ore = init_ore()?; + let left = ore.encrypt_left(&a); + let combined = ore.encrypt(&b); + + assert_eq!(ORE::compare_slices(&left, &combined), -1); + + Ok(()) + } + + #[test] + fn test_uneven_lt() -> Result<(), OreError> { + let a = vec![10, 20]; + let b = vec![27]; + let ore = init_ore()?; + let left = ore.encrypt_left(&a); + let combined = ore.encrypt(&b); + + assert_eq!(ORE::compare_slices(&left, &combined), -1); + + Ok(()) + } + + #[derive(Debug, Copy, Clone, PartialEq)] + struct U5(u8); + + impl Arbitrary for U5 { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + loop { + let v: u8 = Arbitrary::arbitrary(g); + + if v <= 31 { + return Self(v) + } + + } + } + } + + impl PartialOrd for U5 { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } + } + + #[test] + fn test_quick() { + fn single_elem(a: U5, b: U5) -> bool { + let ore = init_ore().unwrap(); + let ax = [a.0]; + let bx = [b.0]; + let left = ore.encrypt_left(&ax); + let combined = ore.encrypt(&bx); + + match ORE::compare_slices(&left, &combined) { + -1 => a < b, + 0 => a == b, + 1 => a > b, + _ => panic!() + } + } + + QuickCheck::new().max_tests(1000).quickcheck(single_elem as fn(U5, U5) -> bool) + } + + /*#[test] + fn test_quick2() { + fn multiple_elems(a: Vec, b: Vec) -> bool { + let ax: Vec = a.into_iter().map(|U5(x)| x).collect(); + let bx: Vec = b.into_iter().map(|U5(x)| x).collect(); + let ore = init_ore().unwrap(); + let left = ore.encrypt_left(&ax); + let combined = ore.encrypt(&bx); + + match ORE::compare_slices(&left, &combined) { + Ordering::Less => ax < bx, + Ordering::Equal => ax == bx, + Ordering::Greater => ax > bx + } + } + + QuickCheck::new().max_tests(1).quickcheck(multiple_elems as fn(Vec, Vec) -> bool) + }*/ +} diff --git a/ore-rs-5bit/src/main.rs b/ore-rs-5bit/src/main.rs new file mode 100644 index 0000000..2ec8ab3 --- /dev/null +++ b/ore-rs-5bit/src/main.rs @@ -0,0 +1,33 @@ +use hex_literal::hex; +use ore_rs_5bit::Ore5BitChaCha20; +use formats::CipherText; + +// TODO: Revise the Main ORE traits +// TODO: Add PartialOrd implementations for CipherText variants + +fn main() { + let k1 = hex!("00010203 04050607 08090a0b 0c0d0e0f"); + let k2 = hex!("d0d007a5 3f9a6848 83bc1f21 0f6595a3"); + + let a = vec![7, 10, 2]; + let b = vec![7, 6, 1]; + let ore = Ore5BitChaCha20::init(&k1, &k2).unwrap(); + let left = ore.encrypt_left(&a); + let combined = ore.encrypt(&b); + println!("Left encrypt: [{} bytes] {}", left.len(), hex::encode(&left)); + println!("Combined : [{} bytes] {}", left.len(), hex::encode(&combined)); + + // TODO: + // Can we use a derive macro for types to implement LeftCipherTextBlock and RightCipherTextBlock + // Also, those names are very long + // * I think we only need the forward PRP now + // * Could the test failures be due to the changes I made to prefix generation? + // TODO: Tests and benchmarks + // TODO: create a plaintext trait for the ORE trait methods + // For the 5-bit scheme, create a U5/Packed variant + // TODO: Consider using Heapless https://docs.rs/heapless/0.7.16/heapless/ + // Input would need to be done in batches (let SIMD width help us decide). + // TODO: Try a SIMD PRP + dbg!(Ore5BitChaCha20::compare_slices(&left, &combined)); + //println!("{}, {}, {}", left.len(), right.len(), hex::encode(right)); +} \ No newline at end of file diff --git a/ore-rs-5bit/src/packing.rs b/ore-rs-5bit/src/packing.rs new file mode 100644 index 0000000..bd32fb7 --- /dev/null +++ b/ore-rs-5bit/src/packing.rs @@ -0,0 +1,13 @@ +use primitives::prf::PrfBlock; + + +pub fn prefixes(slice: &[u8]) -> Vec { + let mut prefixes: Vec = Vec::with_capacity(slice.len()); + for i in 0..slice.len() { + let mut fblock: PrfBlock = Default::default(); + fblock[0..i].copy_from_slice(&slice[0..i]); + prefixes.push(fblock); + } + + prefixes +} diff --git a/ore-rs-5bit/src/right_block.rs b/ore-rs-5bit/src/right_block.rs new file mode 100644 index 0000000..5ab6239 --- /dev/null +++ b/ore-rs-5bit/src/right_block.rs @@ -0,0 +1,101 @@ +use std::ops::BitXorAssign; + +use formats::{CipherTextBlock, DataWithHeader, RightCipherTextBlock}; +use primitives::{NewPrp, prp::bitwise::BitwisePrp}; + + +#[derive(Debug)] +pub struct RightBlock(pub(super) u32); + +impl<'a> RightCipherTextBlock<'a> for RightBlock {} + +impl RightBlock { + pub(crate) fn init(plaintext: u8) -> Self { + assert!(plaintext < 32, "Block cannot encode more than 32-bits"); + Self(0xFFFFFFFF << plaintext) + } + + pub(crate) fn shuffle(&self, prp: &NewPrp) -> Self { + Self(self.0.bitwise_inverse_shuffle(prp)) + } + + pub(crate) fn get_bit(&self, bit: u8) -> u8 { + ((self.0 >> bit) & 1).try_into().unwrap() + } + + // TODO: Instead of get_bit, we could define get_indicator which also takes a "blind" + // parameter (i.e. the hash function output) +} + +impl BitXorAssign for RightBlock { + fn bitxor_assign(&mut self, rhs: u32) { + self.0 ^= rhs; + } +} + +// TODO: Can we derive macro any of this, too?? +impl<'a> CipherTextBlock<'a> for RightBlock { + fn byte_size() -> usize { + 4 + } + + fn extend_into(&self, out: &mut DataWithHeader) { + out.extend(self.0.to_be_bytes()); + } +} + +impl From<&[u8]> for RightBlock { + fn from(value: &[u8]) -> Self { + assert!(value.len() == Self::byte_size()); + let mut buf: [u8; 4] = Default::default(); + buf.copy_from_slice(&value[0..4]); + RightBlock(u32::from_be_bytes(buf)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_init_0() { + let block = RightBlock::init(0); + assert!(matches!(block, RightBlock(0b11111111111111111111111111111111))); + } + + #[test] + fn test_init_28() { + let block = RightBlock::init(28); + assert!(matches!(block, RightBlock(0b11110000000000000000000000000000))); + } + + #[test] + fn test_init_31() { + let block = RightBlock::init(31); + assert!(matches!(block, RightBlock(0b10000000000000000000000000000000))); + } + + #[test] + #[should_panic(expected="Block cannot encode more than 32-bits")] + fn test_init_32() { + RightBlock::init(32); + } + + #[test] + fn get_bit() { + let block = RightBlock::init(28); + assert_eq!(block.get_bit(0), 0); + assert_eq!(block.get_bit(1), 0); + assert_eq!(block.get_bit(2), 0); + assert_eq!(block.get_bit(27), 0); + assert_eq!(block.get_bit(28), 1); + assert_eq!(block.get_bit(31), 1); + } + + #[test] + #[should_panic] + fn get_bit_out_of_range() { + let block = RightBlock::init(28); + block.get_bit(32); + } +} \ No newline at end of file diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml new file mode 100644 index 0000000..dd29688 --- /dev/null +++ b/primitives/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "primitives" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +thiserror = { workspace = true } +aes = { workspace = true } +zeroize = { workspace = true } +hex-literal = { workspace = true } +num-traits = "0.2.15" diff --git a/primitives/src/hash.rs b/primitives/src/hash.rs new file mode 100644 index 0000000..735a007 --- /dev/null +++ b/primitives/src/hash.rs @@ -0,0 +1,176 @@ +use std::slice; +use crate::{AesBlock, Hash, HashKey}; +use aes::cipher::KeyInit; +use aes::cipher::{generic_array::GenericArray, BlockEncrypt}; +use aes::Aes128; +use zeroize::ZeroizeOnDrop; + +pub type HashBlock = [u8; 16]; + +fn convert_slice<'a>(input: &'a [HashBlock]) -> &'a mut [AesBlock] { + let ptr = input.as_ptr() as *mut AesBlock; + unsafe { slice::from_raw_parts_mut(ptr, input.len()) } +} + +#[derive(ZeroizeOnDrop)] +pub struct Aes128Z2Hash { + cipher: Aes128, +} + +impl Aes128Z2Hash { + pub fn hash_all_onto_u32(&self, data: &[HashBlock]) -> u32 { + assert!(data.len() <= 32); + let mut out: u32 = 0; + let mut blocks = convert_slice(data); + self.cipher.encrypt_blocks(&mut blocks); + + for (i, block) in blocks.iter().enumerate() { + out |= ((block[0] & 1u8) as u32) << i; + } + + out + } +} + +impl Hash for Aes128Z2Hash { + fn new(key: &HashKey) -> Self { + let cipher = Aes128::new_from_slice(&key).unwrap(); + Self { cipher } + } + + fn hash(&self, data: &[u8]) -> u8 { + /* + * Slice size is not known at compile time so we assert here + * We could do this with compile checks but this would require an additional + * copy (and doesn't entirely avoid runtime checks anyway) + * See https://stackoverflow.com/questions/38168956/take-slice-of-certain-length-known-at-compile-time + */ + assert_eq!(data.len(), 16); + // Can we clone into GenericArray directly? Are we doing an extra copy? + let mut output = [0u8; 16]; + output.clone_from_slice(data); + let block = GenericArray::from_mut_slice(&mut output); + self.cipher.encrypt_block(block); + output[0] & 1u8 + } + + // TODO: this mutates - see how much a copy effects performance (clone_from_slice) + fn hash_all(&self, data: &mut [HashBlock]) -> Vec { + let mut blocks = convert_slice(data); + self.cipher.encrypt_blocks(&mut blocks); + + let mut vec = Vec::with_capacity(blocks.len()); + for &mut block in data { + // Output is Z2 (1-bit) + vec.push(block[0] & 1u8); + } + + vec + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + fn init_hash() -> Aes128Z2Hash { + let key: [u8; 16] = hex!("00010203 04050607 08090a0b 0c0d0e0f"); + let key_array = GenericArray::from_slice(&key); + Hash::new(key_array) + } + + #[test] + fn hash_test_1() { + let hash = init_hash(); + let input: [u8; 16] = hex!("00010203 04050607 08090a0b 0c0d0eaa"); + + assert_eq!(1u8, hash.hash(&input)); + } + + #[test] + fn hash_test_2() { + let hash = init_hash(); + let input: [u8; 16] = hex!("00010203 04050607 08090a0b 0c0d0e0f"); + + assert_eq!(0u8, hash.hash(&input)); + } + + #[test] + #[should_panic(expected = "assertion failed")] + fn hash_test_input_too_small() { + let hash = init_hash(); + let input: [u8; 8] = hex!("00010203 04050607"); + + assert_eq!(0u8, hash.hash(&input)); + } + + #[test] + #[should_panic(expected = "assertion failed")] + fn hash_test_input_too_large() { + let hash = init_hash(); + let input: [u8; 24] = hex!("00010203 04050607 ffffffff bbbbbbbb cccccccc abababab"); + + hash.hash(&input); + } + + #[test] + fn hash_all_onto_u32_one_elem() { + let hash = init_hash(); + + let mut input: [[u8; 16]; 1] = [ + hex!("00110211 04050607 08090a0b 0c0d0e0f"), + ]; + let res = hash.hash_all_onto_u32(&mut input); + assert_eq!( + res, + 0b1, + ); + } + + #[test] + fn hash_all_onto_u32_three_elems() { + let hash = init_hash(); + + let mut input: [[u8; 16]; 3] = [ + hex!("00110211 04050607 08090a0b 0c0d0e0f"), + hex!("00000000 04050607 08090a0b 0c0d0e0f"), + hex!("00110211 04050607 08090a0b 0c0d0e0f"), + ]; + let res = hash.hash_all_onto_u32(&mut input); + assert_eq!( + res, + 0b101, + ); + } + + #[test] + fn hash_all_onto_u32_16_elems() { + let hash = init_hash(); + + let mut input: [[u8; 16]; 16] = [ + hex!("00110211 04050607 08090a0b 0c0d0e0f"), + hex!("00000000 04050607 08090a0b 0c0d0e0f"), + hex!("00110211 04050607 08090a0b 0c0d0e0f"), + hex!("00000000 04050607 08090a0b 0c0d0e0f"), + hex!("00110211 04050607 08090a0b 0c0d0e0f"), + hex!("00000000 04050607 08090a0b 0c0d0e0f"), + hex!("00110211 04050607 08090a0b 0c0d0e0f"), + hex!("00000000 04050607 08090a0b 0c0d0e0f"), + hex!("00110211 04050607 08090a0b 0c0d0e0f"), + hex!("00000000 04050607 08090a0b 0c0d0e0f"), + hex!("00110211 04050607 08090a0b 0c0d0e0f"), + hex!("00000000 04050607 08090a0b 0c0d0e0f"), + hex!("00110211 04050607 08090a0b 0c0d0e0f"), + hex!("00000000 04050607 08090a0b 0c0d0e0f"), + hex!("00110211 04050607 08090a0b 0c0d0e0f"), + hex!("00110211 04050607 08090a0b 0c0d0e0f"), + ]; + let res = hash.hash_all_onto_u32(&mut input); + println!("{res:b}"); + assert_eq!( + res, + 0b1101_0101_0101_0101, + ); + } +} diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs new file mode 100644 index 0000000..6a09052 --- /dev/null +++ b/primitives/src/lib.rs @@ -0,0 +1,108 @@ +pub mod hash; +pub mod prf; +pub mod prp; + +use std::iter::Enumerate; +use std::slice::Iter; + +use aes::cipher::{consts::U16, generic_array::GenericArray}; +use aes::Block; +use hash::HashBlock; +use prf::PrfBlock; +use thiserror::Error; +use zeroize::Zeroize; + +use crate::prp::prng::Aes128Prng; +pub type AesBlock = Block; +pub type PrfKey = GenericArray; +pub type HashKey = GenericArray; +pub const NONCE_SIZE: usize = 16; + +pub trait Prf { + fn new(key: &PrfKey) -> Self; + fn encrypt_all(&self, data: &mut [PrfBlock]); +} + +pub trait Hash { + fn new(key: &HashKey) -> Self; + fn hash(&self, data: &[u8]) -> u8; + fn hash_all(&self, input: &mut [HashBlock]) -> Vec; +} + +#[derive(Debug, Error)] +#[error("PRP Error")] +pub struct PrpError; +pub type PrpResult = Result; + +// TODO: There should be a single "permutation type" +// and a generator trait to use different approaches of generating it +pub trait Prp: Sized { + fn new(key: &[u8]) -> PrpResult; + fn permute(&self, data: T) -> PrpResult; + fn invert(&self, data: T) -> PrpResult; + fn enumerate(&self) -> Enumerate>; +} + +pub struct NewPrp { + forward: [T; N], + inverse: [T; N] +} + +impl NewPrp { + pub fn forward(&self) -> Enumerate> { + self.forward.iter().enumerate() + } + + pub fn inverse(&self) -> Enumerate> { + self.inverse.iter().enumerate() + } + + // TODO: Can we make this able to be called only once? + pub fn permute(&self, input: impl Into) -> T { + self.forward[input.into()] + } + + pub fn inverse_permute(&self, input: impl Into) -> T { + self.inverse[input.into()] + } +} + +pub trait PrpGenerator { + fn generate(self) -> NewPrp; +} + +pub struct KnuthShuffleGenerator<'p> { + prng_seed: &'p [u8] +} + +impl<'p> KnuthShuffleGenerator<'p> { + pub fn new(prng_seed: &'p [u8]) -> Self { + Self { prng_seed } + } +} + +// TODO: We could avoid code repetition with macros +impl <'p> PrpGenerator for KnuthShuffleGenerator<'p> { + fn generate(self) -> NewPrp { + let mut rng = Aes128Prng::init(self.prng_seed); // TODO: Use Result type here, too + + let mut forward = [0u8; 32]; + let mut inverse = [0u8; 32]; + + // Initialize values + for i in 0..32 { + forward[i] = i as u8; + } + + (0..32).into_iter().rev().for_each(|i| { + let j = rng.gen_range(i as u8); + forward.swap(i, j as usize); + }); + + for (index, val) in forward.iter().enumerate() { + inverse[*val as usize] = index as u8; + } + + NewPrp { forward, inverse } + } +} \ No newline at end of file diff --git a/primitives/src/main.rs b/primitives/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/primitives/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/src/primitives/prf.rs b/primitives/src/prf.rs similarity index 53% rename from src/primitives/prf.rs rename to primitives/src/prf.rs index 2edf565..6467f6d 100644 --- a/src/primitives/prf.rs +++ b/primitives/src/prf.rs @@ -1,7 +1,15 @@ -use crate::primitives::{AesBlock, Prf, PrfKey}; +use crate::{AesBlock, Prf, PrfKey}; use aes::cipher::{BlockEncrypt, KeyInit}; use aes::Aes128; use zeroize::ZeroizeOnDrop; +use std::slice; + +pub type PrfBlock = [u8; 16]; + +fn convert_slice<'a>(input: &'a mut [PrfBlock]) -> &'a mut [AesBlock] { + let ptr = input.as_mut_ptr() as *mut AesBlock; + unsafe { slice::from_raw_parts_mut(ptr, input.len()) } +} #[derive(Debug, ZeroizeOnDrop)] pub struct Aes128Prf { @@ -14,20 +22,20 @@ pub struct Aes128Prf { */ impl Prf for Aes128Prf { fn new(key: &PrfKey) -> Self { - //let key_array = GenericArray::from_slice(key); let cipher = Aes128::new(key); Self { cipher } } - fn encrypt_all(&self, data: &mut [AesBlock]) { - self.cipher.encrypt_blocks(data); + fn encrypt_all(&self, data: &mut [PrfBlock]) { + let blocks = convert_slice(data); + self.cipher.encrypt_blocks(blocks); } } #[cfg(test)] mod tests { use super::*; - use aes::cipher::generic_array::{arr, GenericArray}; + use aes::cipher::generic_array::GenericArray; use hex_literal::hex; fn init_prf() -> Aes128Prf { @@ -38,21 +46,22 @@ mod tests { #[test] fn prf_test_single_block() { - let mut input = [arr![u8; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 170]]; + let mut input = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 170]]; + let prf = init_prf(); prf.encrypt_all(&mut input); assert_eq!( input, - [arr![u8; 183, 103, 151, 211, 249, 253, 170, 135, 117, 243, 131, 50, 27, 15, 170, 59]] + [[183, 103, 151, 211, 249, 253, 170, 135, 117, 243, 131, 50, 27, 15, 170, 59]] ); } #[test] fn prf_test_2_blocks() { let mut input = [ - arr![u8; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 170], - arr![u8; 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 170, 255, 221, 97, 170], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 170], + [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 170, 255, 221, 97, 170], ]; let prf = init_prf(); @@ -60,8 +69,8 @@ mod tests { assert_eq!( input, [ - arr![u8; 183, 103, 151, 211, 249, 253, 170, 135, 117, 243, 131, 50, 27, 15, 170, 59], - arr![u8; 100, 192, 41, 108, 208, 245, 146, 251, 188, 245, 156, 28, 33, 210, 70, 50] + [183, 103, 151, 211, 249, 253, 170, 135, 117, 243, 131, 50, 27, 15, 170, 59], + [100, 192, 41, 108, 208, 245, 146, 251, 188, 245, 156, 28, 33, 210, 70, 50] ] ); } diff --git a/src/primitives/prp.rs b/primitives/src/prp.rs similarity index 80% rename from src/primitives/prp.rs rename to primitives/src/prp.rs index ff41320..e889865 100644 --- a/src/primitives/prp.rs +++ b/primitives/src/prp.rs @@ -1,7 +1,10 @@ pub mod prng; -use crate::primitives::prp::prng::Aes128Prng; -use crate::primitives::{Prp, PrpError, PrpResult}; +pub mod bitwise; +use crate::prp::prng::Aes128Prng; +use crate::{Prp, PrpError, PrpResult}; use std::convert::TryFrom; +use std::iter::Enumerate; +use std::slice::Iter; use zeroize::{Zeroize, ZeroizeOnDrop}; #[derive(Zeroize)] @@ -20,25 +23,26 @@ impl Drop for KnuthShufflePRP { // Impl the ZeroizeOnDrop marker trait since we're zeroizing above impl ZeroizeOnDrop for KnuthShufflePRP {} -impl Prp for KnuthShufflePRP { +// TODO: This would make more sense if we defined PRP as a generator +impl Prp for KnuthShufflePRP { /* - * Initialize an 8-bit (256 element) PRP using a KnuthShuffle - * and a 64-bit random seed + * Initialize an 8-bit (N element) PRP using a KnuthShuffle */ fn new(key: &[u8]) -> PrpResult { + assert!(N <= 256); let mut rng = Aes128Prng::init(key); // TODO: Use Result type here, too let mut perm = Self { - permutation: [0u8; 256], - inverse: [0u8; 256], + permutation: [0u8; N], + inverse: [0u8; N], }; // Initialize values - for i in 0..=255 { + for i in 0..N { perm.permutation[i] = i as u8; } - (0..=255usize).into_iter().rev().for_each(|i| { + (0..N).into_iter().rev().for_each(|i| { let j = rng.gen_range(i as u8); perm.permutation.swap(i, j as usize); }); @@ -76,6 +80,10 @@ impl Prp for KnuthShufflePRP { None => Err(PrpError), } } + + fn enumerate(&self) -> Enumerate> { + self.permutation.iter().enumerate() + } } #[cfg(test)] diff --git a/primitives/src/prp/bitwise.rs b/primitives/src/prp/bitwise.rs new file mode 100644 index 0000000..f364e15 --- /dev/null +++ b/primitives/src/prp/bitwise.rs @@ -0,0 +1,89 @@ +use crate::{Prp, NewPrp}; + +pub trait BitwisePrp: Sized { + fn bitwise_shuffle(self, prp: &NewPrp) -> Self; + fn bitwise_inverse_shuffle(self, prp: &NewPrp) -> Self; +} + +impl BitwisePrp<32> for u32 { + fn bitwise_shuffle(self, prp: &NewPrp) -> Self { + let mut output: Self = 0; + + for (i, &p) in prp.forward() { + let bit = (self >> p) & 1; + output |= bit << i; + } + + output + } + + fn bitwise_inverse_shuffle(self, prp: &NewPrp) -> Self { + let mut output: Self = 0; + + for (i, &p) in prp.inverse() { + let bit = (self >> p) & 1; + output |= bit << i; + } + + output + } +} + +impl BitwisePrp<8> for u8 { + fn bitwise_shuffle(self, prp: &NewPrp) -> Self { + let mut output: Self = 0; + + for (i, &p) in prp.forward() { + let bit = (self >> p) & 1; + output |= bit << i; + } + + output + } + + fn bitwise_inverse_shuffle(self, prp: &NewPrp) -> Self { + let mut output: Self = 0; + + for (i, &p) in prp.inverse() { + let bit = (self >> p) & 1; + output |= bit << i; + } + + output + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::PrpGenerator; + + struct StaticPrpGenerator { + perm: [u8; 8] + } + + impl PrpGenerator for StaticPrpGenerator { + fn generate(self) -> crate::NewPrp { + let mut inverse: [u8; 8] = [0; 8]; + + for (index, val) in self.perm.iter().enumerate() { + inverse[*val as usize] = index as u8; + } + crate::NewPrp { + forward: self.perm, + inverse + } + } + } + + #[test] + fn test_forward() { + let gen = StaticPrpGenerator { + perm: [1, 3, 2, 7, 6, 4, 0, 5] + }; + let prp = PrpGenerator::generate(gen); + let input = 0b00110110u8; + assert_eq!(input, input.bitwise_shuffle(&prp).bitwise_inverse_shuffle(&prp)); + assert_eq!(0b10100101, input.bitwise_shuffle(&prp)); + } +} \ No newline at end of file diff --git a/src/primitives/prp/prng.rs b/primitives/src/prp/prng.rs similarity index 100% rename from src/primitives/prp/prng.rs rename to primitives/src/prp/prng.rs diff --git a/src/primitives.rs b/src/primitives.rs deleted file mode 100644 index d304618..0000000 --- a/src/primitives.rs +++ /dev/null @@ -1,33 +0,0 @@ -pub mod hash; -pub mod prf; -pub mod prp; - -use aes::cipher::{consts::U16, generic_array::GenericArray}; -use aes::Block; -use thiserror::Error; -pub type AesBlock = Block; -pub type PrfKey = GenericArray; -pub type HashKey = GenericArray; -pub const NONCE_SIZE: usize = 16; - -pub trait Prf { - fn new(key: &PrfKey) -> Self; - fn encrypt_all(&self, data: &mut [AesBlock]); -} - -pub trait Hash { - fn new(key: &HashKey) -> Self; - fn hash(&self, data: &[u8]) -> u8; - fn hash_all(&self, input: &mut [AesBlock]) -> Vec; -} - -#[derive(Debug, Error)] -#[error("PRP Error")] -pub struct PrpError; -pub type PrpResult = Result; - -pub trait Prp: Sized { - fn new(key: &[u8]) -> PrpResult; - fn permute(&self, data: T) -> PrpResult; - fn invert(&self, data: T) -> PrpResult; -} diff --git a/src/primitives/hash.rs b/src/primitives/hash.rs deleted file mode 100644 index afe1582..0000000 --- a/src/primitives/hash.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::primitives::{AesBlock, Hash, HashKey}; -use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit}; -use aes::Aes128; -use zeroize::ZeroizeOnDrop; - -#[derive(ZeroizeOnDrop)] -pub struct Aes128Z2Hash { - cipher: Aes128, -} - -impl Hash for Aes128Z2Hash { - fn new(key: &HashKey) -> Self { - let key_array = GenericArray::from_slice(key); - let cipher = Aes128::new(key_array); - Self { cipher } - } - - fn hash(&self, data: &[u8]) -> u8 { - /* - * Slice size is not known at compile time so we assert here - * We could do this with compile checks but this would require an additional - * copy (and doesn't entirely avoid runtime checks anyway) - * See https://stackoverflow.com/questions/38168956/take-slice-of-certain-length-known-at-compile-time - */ - assert_eq!(data.len(), 16); - // Can we clone into GenericArray directly? Are we doing an extra copy? - let mut output = [0u8; 16]; - output.clone_from_slice(data); - let block = GenericArray::from_mut_slice(&mut output); - self.cipher.encrypt_block(block); - output[0] & 1u8 - } - - // TODO: this mutates - see how much a copy effects performance (clone_from_slice) - fn hash_all(&self, data: &mut [AesBlock]) -> Vec { - self.cipher.encrypt_blocks(data); - - let mut vec = Vec::with_capacity(data.len()); - for &mut block in data { - // Output is Z2 (1-bit) - vec.push(block[0] & 1u8); - } - - vec - } -} - -#[cfg(test)] -mod tests { - use super::*; - use hex_literal::hex; - - fn init_hash() -> Aes128Z2Hash { - let key: [u8; 16] = hex!("00010203 04050607 08090a0b 0c0d0e0f"); - let key_array = GenericArray::from_slice(&key); - Hash::new(key_array) - } - - #[test] - fn hash_test_1() { - let hash = init_hash(); - let input: [u8; 16] = hex!("00010203 04050607 08090a0b 0c0d0eaa"); - - assert_eq!(1u8, hash.hash(&input)); - } - - #[test] - fn hash_test_2() { - let hash = init_hash(); - let input: [u8; 16] = hex!("00010203 04050607 08090a0b 0c0d0e0f"); - - assert_eq!(0u8, hash.hash(&input)); - } - - #[test] - #[should_panic(expected = "assertion failed")] - fn hash_test_input_too_small() { - let hash = init_hash(); - let input: [u8; 8] = hex!("00010203 04050607"); - - assert_eq!(0u8, hash.hash(&input)); - } - - #[test] - #[should_panic(expected = "assertion failed")] - fn hash_test_input_too_large() { - let hash = init_hash(); - let input: [u8; 24] = hex!("00010203 04050607 ffffffff bbbbbbbb cccccccc abababab"); - - hash.hash(&input); - } -}