Skip to content

Commit

Permalink
Implement metadata encryption.
Browse files Browse the repository at this point in the history
  • Loading branch information
gibbz00 committed Dec 22, 2023
1 parent bcf7755 commit fd90240
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 40 deletions.
146 changes: 136 additions & 10 deletions crates/lib/src/rops_file/mac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ const MAC_ENCRYPTED_ONLY_INIT_BYTES: [u8; 32] = [
// size. The buffer contains in other words UTF-8 byte codes the hex string encoded from the
// computed MAC, and not the computed MAC itself.
//
// NOTE: Non-constant equality checking is currently protected against timing attacks
// since the MAC must be decrypted before being compared.
//
// TEMP(HACK): Inner buffer should ideally be a GenericArray<u8, Sum<H::OutputSize, H::OutputSize>>.
// But because where clauses aren't inferred in any function signature containing Mac<H>, a Vec is
// used instead. https://github.com/rust-lang/rust/issues/20671
#[impl_tools::autoimpl(Debug, PartialEq)]
//
// NOTE: Non-constant equality checking is currently protected against timing attacks
// by requiring that the MAC must be decrypted before being compared.
#[impl_tools::autoimpl(Debug, Clone, PartialEq)]
pub struct Mac<H: Hasher>(Vec<u8>, PhantomData<H>);

impl<H: Hasher> FromStr for Mac<H> {
Expand Down Expand Up @@ -82,8 +82,32 @@ impl<H: Hasher> Mac<H> {
data_key: &DataKey,
last_modified_date_time: &LastModifiedDateTime,
) -> Result<EncryptedMac<C, H>, C::Error> {
self.encrypt_impl(data_key, last_modified_date_time, None)
}

pub fn encrypt_with_saved_nonce<C: Cipher>(
self,
data_key: &DataKey,
last_modified_date_time: &LastModifiedDateTime,
saved_mac_nonce: SavedMacNonce<C, H>,
) -> Result<EncryptedMac<C, H>, C::Error> {
self.encrypt_impl(data_key, last_modified_date_time, Some(saved_mac_nonce))
}

fn encrypt_impl<C: Cipher>(
self,
data_key: &DataKey,
last_modified_date_time: &LastModifiedDateTime,
optional_saved_mac_nonce: Option<SavedMacNonce<C, H>>,
) -> Result<EncryptedMac<C, H>, C::Error> {
let nonce = match optional_saved_mac_nonce {
// IMPROVEMENT: throw error if saved nonce didn't match?
Some(saved_mac_nonce) => saved_mac_nonce.get_or_create(&self),
None => Nonce::new(),
};

let mut in_place_buffer = self.0;
let nonce = Nonce::new();

let authorization_tag = C::encrypt(
&nonce,
data_key,
Expand Down Expand Up @@ -120,6 +144,25 @@ impl<C: Cipher, H: Hasher> FromStr for EncryptedMac<C, H> {

impl<C: Cipher, H: Hasher> EncryptedMac<C, H> {
pub fn decrypt(self, data_key: &DataKey, last_modified_date_time: &LastModifiedDateTime) -> Result<Mac<H>, C::Error> {
self.decrypt_impl(data_key, last_modified_date_time).map(|(mac, _)| mac)
}

#[allow(clippy::type_complexity)]
pub fn decrypt_and_save_nonce(
self,
data_key: &DataKey,
last_modified_date_time: &LastModifiedDateTime,
) -> Result<(Mac<H>, SavedMacNonce<C, H>), C::Error> {
self.decrypt_impl(data_key, last_modified_date_time)
.map(|(mac, nonce)| (mac.clone(), SavedMacNonce::new(mac, nonce)))
}

#[allow(clippy::type_complexity)]
fn decrypt_impl(
self,
data_key: &DataKey,
last_modified_date_time: &LastModifiedDateTime,
) -> Result<(Mac<H>, Nonce<C::NonceSize>), C::Error> {
let mut in_place_buffer = Vec::from(self.0.data);
C::decrypt(
&self.0.nonce,
Expand All @@ -129,7 +172,44 @@ impl<C: Cipher, H: Hasher> EncryptedMac<C, H> {
&self.0.authorization_tag,
)?;

Ok(Mac(in_place_buffer, PhantomData))
let mac = Mac(in_place_buffer, PhantomData);
Ok((mac.clone(), self.0.nonce))
}
}

pub use saved_mac_nonce::SavedMacNonce;
mod saved_mac_nonce {
use crate::*;

#[impl_tools::autoimpl(Debug, PartialEq)]
pub struct SavedMacNonce<C: Cipher, H: Hasher>(Mac<H>, Nonce<C::NonceSize>);

impl<C: Cipher, H: Hasher> SavedMacNonce<C, H> {
pub fn new(mac: Mac<H>, nonce: Nonce<C::NonceSize>) -> Self {
Self(mac, nonce)
}

pub fn get_or_create(self, mac: &Mac<H>) -> Nonce<C::NonceSize> {
match &self.0 == mac {
true => self.1,
false => Nonce::new(),
}
}
}

#[cfg(feature = "test-utils")]
mod mock {
use super::*;

impl<C: Cipher, H: Hasher> MockTestUtil for SavedMacNonce<C, H>
where
Mac<H>: MockTestUtil,
EncryptedMac<C, H>: MockTestUtil,
{
fn mock() -> Self {
Self::new(Mac::mock(), EncryptedMac::<C, H>::mock().0.nonce)
}
}
}
}

Expand Down Expand Up @@ -187,6 +267,27 @@ mod tests {
mod aes_gcm {
use super::*;

#[test]
fn encrypts_mac() {
let data_key = DataKey::mock();
let last_modified = LastModifiedDateTime::mock();

let encrypted = Mac::<SHA512>::mock().encrypt::<AES256GCM>(&data_key, &last_modified).unwrap();
let decrypted = encrypted.decrypt(&data_key, &last_modified).unwrap();

assert_eq!(Mac::mock(), decrypted)
}

#[test]
fn encrypts_with_saved_nonce() {
assert_eq!(
EncryptedMac::<AES256GCM, SHA512>::mock(),
Mac::mock()
.encrypt_with_saved_nonce(&DataKey::mock(), &LastModifiedDateTime::mock(), SavedMacNonce::mock())
.unwrap()
);
}

#[test]
fn decrypts_mac() {
assert_eq!(
Expand All @@ -198,14 +299,39 @@ mod tests {
}

#[test]
fn encrypts_mac() {
fn decrypts_and_saves_nonce() {
let (decrypted_mac, saved_mac_nonce) = EncryptedMac::<AES256GCM, SHA512>::mock()
.decrypt_and_save_nonce(&DataKey::mock(), &LastModifiedDateTime::mock())
.unwrap();

assert_eq!(Mac::mock(), decrypted_mac);
assert_eq!(SavedMacNonce::mock(), saved_mac_nonce);
}

#[test]
fn protects_against_nonce_reuse() {
let mac = Mac::mock();
let data_key = DataKey::mock();
let last_modified = LastModifiedDateTime::mock();

let encrypted = Mac::<SHA512>::mock().encrypt::<AES256GCM>(&data_key, &last_modified).unwrap();
let decrypted = encrypted.decrypt(&data_key, &last_modified).unwrap();
let encrypted_mock_mac = mac
.clone()
.encrypt_with_saved_nonce::<AES256GCM>(&data_key, &last_modified, SavedMacNonce::mock())
.unwrap();

assert_eq!(Mac::mock(), decrypted)
let other_saved_nonce = SavedMacNonce::<AES256GCM, SHA512>::new(
Mac::compute(
false,
&RopsMap(indexmap::indexmap! {
"mumbo".to_string() => RopsTree::Leaf(RopsValue::String("jumbo".to_string()))
}),
),
Nonce::mock(),
);

let encrypted_with_other_mac = mac.encrypt_with_saved_nonce(&data_key, &last_modified, other_saved_nonce).unwrap();

assert_ne!(encrypted_mock_mac, encrypted_with_other_mac)
}
}
}
Expand Down
7 changes: 0 additions & 7 deletions crates/lib/src/rops_file/map/core.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::{borrow::Cow, collections::HashMap};

use derive_more::{Deref, DerefMut, From, Into};

use crate::*;
Expand All @@ -14,8 +12,3 @@ pub enum RopsTree<S: RopsMapState> {
Null,
Leaf(S::RopsTreeLeaf),
}

// WORKAROUND: Non-cow tuple key doesn't allow saved_nounces.get((&key, &value))
#[derive(Debug, PartialEq, Deref, DerefMut)]
#[allow(clippy::complexity)]
pub struct SavedRopsMapNonces<C: Cipher>(pub(crate) HashMap<(Cow<'static, KeyPath>, Cow<'static, RopsValue>), Nonce<C::NonceSize>>);
6 changes: 2 additions & 4 deletions crates/lib/src/rops_file/map/decrypt.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::{borrow::Cow, collections::HashMap};

use indexmap::IndexMap;

use crate::*;
Expand All @@ -13,7 +11,7 @@ impl<C: Cipher> RopsMap<EncryptedMap<C>> {
self,
data_key: &DataKey,
) -> Result<(RopsMap<DecryptedMap>, SavedRopsMapNonces<C>), DecryptRopsValueError> {
let mut saved_nonces = SavedRopsMapNonces(HashMap::new());
let mut saved_nonces = SavedRopsMapNonces::default();
Self::decrypt_impl(self, data_key, &KeyPath::default(), &mut Some(&mut saved_nonces)).map(|tree| (tree, saved_nonces))
}

Expand Down Expand Up @@ -65,7 +63,7 @@ impl<C: Cipher> RopsMap<EncryptedMap<C>> {
Some(saved_nonces) => {
let nonce = encrypted_value.nonce.clone();
let decrypted_value = encrypted_value.decrypt(data_key, key_path)?;
saved_nonces.insert((Cow::Owned(key_path.clone()), Cow::Owned(decrypted_value.clone())), nonce);
saved_nonces.insert((key_path.clone(), decrypted_value.clone()), nonce);
decrypted_value
}
None => encrypted_value.decrypt(data_key, key_path)?,
Expand Down
4 changes: 1 addition & 3 deletions crates/lib/src/rops_file/map/encrypt.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::borrow::Cow;

use indexmap::IndexMap;

use crate::*;
Expand Down Expand Up @@ -59,7 +57,7 @@ impl RopsMap<DecryptedMap> {
RopsTree::Null => RopsTree::Null,
RopsTree::Leaf(value) => {
let nonce = optional_saved_nonces
.map(|saved_nonces| saved_nonces.get(&(Cow::Borrowed(key_path), Cow::Borrowed(&value))).cloned())
.map(|saved_nonces| saved_nonces.get((key_path, &value)).cloned())
.flatten()
.unwrap_or_else(Nonce::new);
RopsTree::Leaf(value.encrypt(nonce, data_key, key_path)?)
Expand Down
6 changes: 2 additions & 4 deletions crates/lib/src/rops_file/map/mock.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::{borrow::Cow, collections::HashMap};

use indexmap::indexmap;

use crate::*;
Expand Down Expand Up @@ -36,7 +34,7 @@ where
RopsMap<EncryptedMap<C>>: MockTestUtil,
{
fn mock() -> Self {
let mut saved_nonces = SavedRopsMapNonces(HashMap::new());
let mut saved_nonces = SavedRopsMapNonces::default();
recurive_build(
RopsTree::Map(RopsMap::mock()),
&mut saved_nonces,
Expand All @@ -63,7 +61,7 @@ where
RopsTree::Leaf(encrypted_value) => {
let nonce = encrypted_value.nonce.clone();
let decrypted = encrypted_value.decrypt(data_key, key_path).unwrap();
saved_nonces.insert((Cow::Owned(key_path.clone()), Cow::Owned(decrypted)), nonce);
saved_nonces.insert((key_path.clone(), decrypted), nonce);
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion crates/lib/src/rops_file/map/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
mod core;
pub use core::{RopsMap, RopsTree, SavedRopsMapNonces};
pub use core::{RopsMap, RopsTree};

mod state;
pub use state::{DecryptedMap, EncryptedMap, RopsMapState};

mod saved_nonces;
pub use saved_nonces::SavedRopsMapNonces;

mod decrypt;

mod encrypt;
Expand Down
20 changes: 20 additions & 0 deletions crates/lib/src/rops_file/map/saved_nonces.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use std::{borrow::Cow, collections::HashMap};

use crate::*;

/// Nonce store that is supposed to only be used when both keypaths and values match.
#[derive(Debug, PartialEq)]
#[impl_tools::autoimpl(Default)]
#[allow(clippy::complexity)]
// WORKAROUND: Non-cow tuple key doesn't allow saved_nounces.get((&key, &value))
pub struct SavedRopsMapNonces<C: Cipher>(HashMap<(Cow<'static, KeyPath>, Cow<'static, RopsValue>), Nonce<C::NonceSize>>);

impl<C: Cipher> SavedRopsMapNonces<C> {
pub fn insert(&mut self, key: (KeyPath, RopsValue), value: Nonce<C::NonceSize>) {
self.0.insert((Cow::Owned(key.0), Cow::Owned(key.1)), value);
}

pub fn get<'a>(&'a self, key: (&'a KeyPath, &'a RopsValue)) -> Option<&'a Nonce<C::NonceSize>> {
self.0.get(&(Cow::Borrowed(key.0), Cow::Borrowed(key.1)))
}
}
Loading

0 comments on commit fd90240

Please sign in to comment.