#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::memories::frag::{Frag, FragStrategy};
use crate::{
locked_memory::LockedMemory,
memories::{buffer::Buffer, file_memory::FileMemory, ram_memory::RamMemory},
utils::*,
MemoryError::*,
*,
};
use core::{
fmt::{self, Debug, Formatter},
marker::PhantomData,
};
use crypto::hashes::{blake2b, Digest};
use zeroize::{Zeroize, Zeroizing};
use serde::{
de::{Deserialize, Deserializer, SeqAccess, Visitor},
ser::{Serialize, Serializer},
};
use std::{cell::RefCell, sync::Mutex};
#[allow(dead_code)]
static IMPOSSIBLE_CASE: &str = "NonContiguousMemory: this case should not happen if allocated properly";
static POISONED_LOCK: &str = "NonContiguousMemory potentially in an unsafe state";
pub const NC_DATA_SIZE: usize = 32;
pub const NC_CONFIGURATION: NCConfig = FullRam;
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum NCConfig {
FullFile,
FullRam,
RamAndFile,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
FragAllocation(FragStrategy),
}
use NCConfig::*;
#[derive(Clone)]
enum MemoryShard {
File(FileMemory),
Ram(RamMemory),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Frag(Frag<[u8; NC_DATA_SIZE]>),
}
use MemoryShard::*;
pub struct NonContiguousMemory {
shard1: Mutex<RefCell<MemoryShard>>,
shard2: Mutex<RefCell<MemoryShard>>,
config: NCConfig,
}
impl Clone for NonContiguousMemory {
fn clone(&self) -> Self {
let mut1 = self.shard1.lock().expect(POISONED_LOCK);
let mut2 = self.shard2.lock().expect(POISONED_LOCK);
NonContiguousMemory {
shard1: Mutex::new(mut1.clone()),
shard2: Mutex::new(mut2.clone()),
config: self.config.clone(),
}
}
}
impl LockedMemory for NonContiguousMemory {
fn update(self, payload: Buffer<u8>, size: usize) -> Result<Self, MemoryError> {
NonContiguousMemory::alloc(&payload.borrow(), size, self.config.clone())
}
fn unlock(&self) -> Result<Buffer<u8>, MemoryError> {
let (randomness, mut blinded_secret) = self.get_shards_data()?;
let hashed_randomness = &blake2b::Blake2b256::digest(randomness);
xor_mut(&mut blinded_secret, hashed_randomness, NC_DATA_SIZE);
self.refresh()?;
Ok(Buffer::alloc(&blinded_secret, NC_DATA_SIZE))
}
}
impl NonContiguousMemory {
pub fn alloc(secret: &[u8], size: usize, config: NCConfig) -> Result<Self, MemoryError> {
if size != NC_DATA_SIZE {
return Err(NCSizeNotAllowed);
};
let randomness = random_vec(NC_DATA_SIZE);
let mut blinded_secret = blake2b::Blake2b256::digest(&randomness);
xor_mut(&mut blinded_secret, secret, NC_DATA_SIZE);
let (shard1, shard2) = MemoryShard::new_shards(&randomness, &blinded_secret, &config)?;
let mem = NonContiguousMemory {
shard1: Mutex::new(RefCell::new(shard1)),
shard2: Mutex::new(RefCell::new(shard2)),
config,
};
Ok(mem)
}
pub fn refresh(&self) -> Result<(), MemoryError> {
let randomness_delta = random_vec(NC_DATA_SIZE);
let (mut randomness, mut blinded_secret) = self.get_shards_data()?;
let hashed_randomness = &blake2b::Blake2b256::digest(&randomness);
xor_mut(&mut randomness, &randomness_delta, NC_DATA_SIZE);
let hashed_delta = &blake2b::Blake2b256::digest(&randomness);
xor_mut(&mut blinded_secret, hashed_delta, NC_DATA_SIZE);
xor_mut(&mut blinded_secret, hashed_randomness, NC_DATA_SIZE);
let (shard1, shard2) = MemoryShard::new_shards(&randomness, &blinded_secret, &self.config)?;
let m1 = self.shard1.lock().expect(POISONED_LOCK);
let m2 = self.shard2.lock().expect(POISONED_LOCK);
m1.replace(shard1);
m2.replace(shard2);
Ok(())
}
#[allow(clippy::type_complexity)]
fn get_shards_data(&self) -> Result<(Zeroizing<Vec<u8>>, Zeroizing<Vec<u8>>), MemoryError> {
let m1 = self.shard1.lock().expect(POISONED_LOCK);
let m2 = self.shard2.lock().expect(POISONED_LOCK);
let shard1 = &*m1.borrow();
let shard2 = &*m2.borrow();
Ok((shard1.get()?, shard2.get()?))
}
#[cfg(test)]
pub fn get_ptr_addresses(&self) -> Result<(usize, usize), MemoryError> {
let muta = self.shard1.lock().expect(POISONED_LOCK);
let mutb = self.shard2.lock().expect(POISONED_LOCK);
let a = &*muta.borrow();
let b = &*mutb.borrow();
let (a_ptr, b_ptr) = match (a, b) {
(Ram(a), Ram(b)) => (a.get_ptr_address(), b.get_ptr_address()),
(Frag(a), Frag(b)) => (
a.get()? as *const [u8; NC_DATA_SIZE] as usize,
b.get()? as *const [u8; NC_DATA_SIZE] as usize,
),
_ => {
return Err(MemoryError::Allocation(
"Cannot get pointers. Unsupported MemoryShard configuration".to_owned(),
));
}
};
Ok((a_ptr, b_ptr))
}
}
impl MemoryShard {
fn new_shards(data1: &[u8], data2: &[u8], config: &NCConfig) -> Result<(Self, Self), MemoryError> {
match config {
RamAndFile => {
let ram = RamMemory::alloc(data1, NC_DATA_SIZE)?;
let fmem = FileMemory::alloc(data2, NC_DATA_SIZE)?;
Ok((Ram(ram), File(fmem)))
}
FullRam => {
let ram1 = RamMemory::alloc(data1, NC_DATA_SIZE)?;
let ram2 = RamMemory::alloc(data2, NC_DATA_SIZE)?;
Ok((Ram(ram1), Ram(ram2)))
}
FullFile => {
let fmem1 = FileMemory::alloc(data1, NC_DATA_SIZE)?;
let fmem2 = FileMemory::alloc(data2, NC_DATA_SIZE)?;
Ok((File(fmem1), File(fmem2)))
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
FragAllocation(strat) => {
let (frag1, frag2) = Frag::alloc_initialized(
*strat,
data1.try_into().map_err(|_| MemoryError::NCSizeNotAllowed)?,
data2.try_into().map_err(|_| MemoryError::NCSizeNotAllowed)?,
)?;
Ok((Frag(frag1), Frag(frag2)))
}
}
}
fn get(&self) -> Result<Zeroizing<Vec<u8>>, MemoryError> {
match self {
File(fm) => {
let buf = fm.unlock()?;
let v = buf.borrow().to_vec().into();
Ok(v)
}
Ram(ram) => {
let buf = ram.unlock()?;
let v = buf.borrow().to_vec().into();
Ok(v)
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Frag(frag) => {
if frag.is_live() {
Ok(frag.get()?.to_vec().into())
} else {
Err(IllegalZeroizedUsage)
}
}
}
}
}
impl Debug for NonContiguousMemory {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", DEBUG_MSG)
}
}
impl Zeroize for MemoryShard {
fn zeroize(&mut self) {
match self {
File(fm) => fm.zeroize(),
Ram(buf) => buf.zeroize(),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Frag(frag) => frag.zeroize(),
}
}
}
impl Zeroize for NonContiguousMemory {
fn zeroize(&mut self) {
let mut1 = self.shard1.lock().expect(POISONED_LOCK);
let mut2 = self.shard2.lock().expect(POISONED_LOCK);
mut1.borrow_mut().zeroize();
mut2.borrow_mut().zeroize();
self.config = FullRam;
}
}
impl ZeroizeOnDrop for NonContiguousMemory {}
impl Drop for NonContiguousMemory {
fn drop(&mut self) {
self.zeroize();
}
}
impl Serialize for NonContiguousMemory {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let buf = self
.unlock()
.expect("Failed to unlock NonContiguousMemory for serialization");
buf.serialize(serializer)
}
}
struct NonContiguousMemoryVisitor {
marker: PhantomData<fn() -> NonContiguousMemory>,
}
impl NonContiguousMemoryVisitor {
fn new() -> Self {
NonContiguousMemoryVisitor { marker: PhantomData }
}
}
impl<'de> Visitor<'de> for NonContiguousMemoryVisitor {
type Value = NonContiguousMemory;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
formatter.write_str("NonContiguousMemory not found")
}
fn visit_seq<E>(self, mut access: E) -> Result<Self::Value, E::Error>
where
E: SeqAccess<'de>,
{
let mut seq = Vec::<u8>::with_capacity(access.size_hint().unwrap_or(0));
while let Some(e) = access.next_element()? {
seq.push(e);
}
let seq = NonContiguousMemory::alloc(seq.as_slice(), seq.len(), NC_CONFIGURATION)
.expect("Failed to allocate NonContiguousMemory during deserialization");
Ok(seq)
}
}
impl<'de> Deserialize<'de> for NonContiguousMemory {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_seq(NonContiguousMemoryVisitor::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
static ERR: &str = "Error while testing non-contiguous memory ";
const NC_CONFIGS: [NCConfig; 6] = [
FullFile,
RamAndFile,
FullRam,
FragAllocation(FragStrategy::Map),
FragAllocation(FragStrategy::Direct),
FragAllocation(FragStrategy::Hybrid),
];
#[test]
fn test_ncm_refresh() {
let data = random_vec(NC_DATA_SIZE);
for config in NC_CONFIGS {
println!("config: {:?}", config);
let ncm = NonContiguousMemory::alloc(&data, NC_DATA_SIZE, config).expect(ERR);
test_refresh(ncm, &data);
}
}
fn test_refresh(ncm: NonContiguousMemory, original_data: &[u8]) {
let (data1_before_refresh, data2_before_refresh) = ncm.get_shards_data().expect(ERR);
assert!(ncm.refresh().is_ok());
let (data1_after_refresh, data2_after_refresh) = ncm.get_shards_data().expect(ERR);
let buf = ncm.unlock();
assert!(buf.is_ok());
let buf = buf.unwrap();
assert_eq!((*buf.borrow()), *original_data);
assert_ne!(data1_before_refresh, data1_after_refresh);
assert_ne!(data2_before_refresh, data2_after_refresh);
}
#[test]
fn test_ncm_boojum_security() {
let original_data = random_vec(NC_DATA_SIZE);
for config in NC_CONFIGS {
let ncm = NonContiguousMemory::alloc(&original_data, NC_DATA_SIZE, config).expect(ERR);
let (data1, data2) = ncm.get_shards_data().expect(ERR);
assert_ne!(data1, original_data);
assert_ne!(data2, original_data);
}
}
#[test]
fn test_ncm_zeroize() {
let data = random_vec(NC_DATA_SIZE);
for config in NC_CONFIGS {
let mut ncm = NonContiguousMemory::alloc(&data, NC_DATA_SIZE, config).expect(ERR);
ncm.zeroize();
assert!(ncm.get_shards_data().is_err());
}
}
#[test]
fn test_distance_between_shards() {
let configs = [
FullRam,
FragAllocation(FragStrategy::Map),
FragAllocation(FragStrategy::Direct),
FragAllocation(FragStrategy::Hybrid),
];
let data = random_vec(NC_DATA_SIZE);
for config in configs {
let ncm = NonContiguousMemory::alloc(&data, NC_DATA_SIZE, config).expect(ERR);
let ptrs = ncm.get_ptr_addresses().expect(ERR);
let (a, b) = ptrs;
let distance = a.abs_diff(b);
assert!(
distance >= crate::memories::frag::FRAG_MIN_DISTANCE,
"Pointer distance below threshold: 0x{:08X}",
distance
);
}
}
}