Skip to content

Commit

Permalink
Implement RopsMap::encrypt{,_with_saved_nonces}().
Browse files Browse the repository at this point in the history
  • Loading branch information
gibbz00 committed Dec 17, 2023
1 parent a2c1754 commit 164105f
Show file tree
Hide file tree
Showing 11 changed files with 233 additions and 121 deletions.
2 changes: 1 addition & 1 deletion crates/lib/src/cryptography/cipher/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use generic_array::ArrayLength;

use crate::*;

pub trait Cipher: Sized {
pub trait Cipher: Sized + Debug + PartialEq {
const NAME: &'static str;

type NonceSize: ArrayLength<u8> + Debug + PartialEq;
Expand Down
4 changes: 2 additions & 2 deletions crates/lib/src/rops_file/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::*;
#[serde(bound = "F: FileFormat")]
pub struct RopsFile<S: RopsFileState, F: FileFormat> {
#[serde(flatten)]
pub map: RopsFileMap<S, F>,
pub map: RopsFileFormatMap<S, F>,
#[serde(rename = "sops")]
pub metadata: RopsFileMetadata,
}
Expand All @@ -17,7 +17,7 @@ mod mock {

impl<S: RopsFileState, F: FileFormat> MockTestUtil for RopsFile<S, F>
where
RopsFileMap<S, F>: MockTestUtil,
RopsFileFormatMap<S, F>: MockTestUtil,
{
fn mock() -> Self {
Self {
Expand Down
43 changes: 43 additions & 0 deletions crates/lib/src/rops_file/format/map.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::marker::PhantomData;

use serde::{Deserialize, Serialize};

use crate::*;

#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RopsFileFormatMap<S: RopsFileState, F: FileFormat> {
#[serde(flatten)]
inner: F::Map,
#[serde(skip)]
state_marker: PhantomData<S>,
}

// IMPROVEMENT: Might be worth splitting distinguishing decrypted and
// encrypted map to tree errors by separating then into two enums.
#[derive(Debug, thiserror::Error)]
pub enum FormatToInternalMapError {
#[error("only string keys are supported, found: {0}")]
NonStringKey(String),
#[error("integer out of range, allowed values must fit inside an i64, found: {0}")]
IntegerOutOfRange(u64),
#[error("unable to parse encrypted value components: {0}")]
EncryptedRopsValue(#[from] EncryptedRopsValueFromStrError),
// TEMP: Deprecate once partial encryption feature arrives.
#[error("invalid valid for an encrypted file")]
InvalidValueForEncrypted(String),
}

impl<S: RopsFileState, F: FileFormat> RopsFileFormatMap<S, F> {
pub fn into_inner_map(self) -> F::Map {
self.inner
}

#[cfg(feature = "test-utils")]
pub fn from_inner_map(inner: F::Map) -> Self {
Self {
inner,
state_marker: PhantomData,
}
}
}
3 changes: 3 additions & 0 deletions crates/lib/src/rops_file/format/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
mod core;
pub use core::FileFormat;

mod map;
pub use map::{FormatToInternalMapError, RopsFileFormatMap};

#[cfg(feature = "yaml")]
mod yaml;
#[cfg(feature = "yaml")]
Expand Down
10 changes: 5 additions & 5 deletions crates/lib/src/rops_file/format/yaml/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ mod rops_file {

impl<S: RopsFileState> MockFileFormatUtil<YamlFileFormat> for RopsFile<S, YamlFileFormat>
where
RopsFileMap<S, YamlFileFormat>: MockFileFormatUtil<YamlFileFormat>,
RopsFileFormatMap<S, YamlFileFormat>: MockFileFormatUtil<YamlFileFormat>,
{
fn mock_format_display() -> String {
indoc::formatdoc! {"
{}
sops:
{}",
RopsFileMap::mock_format_display(),
RopsFileFormatMap::mock_format_display(),
textwrap::indent(&RopsFileMetadata::mock_format_display()," ")
}
}
Expand All @@ -20,7 +20,7 @@ mod rops_file {
mod map {
use crate::*;

impl MockFileFormatUtil<YamlFileFormat> for RopsFileMap<Decrypted, YamlFileFormat> {
impl MockFileFormatUtil<YamlFileFormat> for RopsFileFormatMap<Decrypted, YamlFileFormat> {
fn mock_format_display() -> String {
indoc::indoc! {"
hello: world!
Expand All @@ -40,7 +40,7 @@ mod map {
}

#[cfg(feature = "aes-gcm")]
impl MockFileFormatUtil<YamlFileFormat> for RopsFileMap<Encrypted<AES256GCM>, YamlFileFormat> {
impl MockFileFormatUtil<YamlFileFormat> for RopsFileFormatMap<Encrypted<AES256GCM>, YamlFileFormat> {
fn mock_format_display() -> String {
indoc::indoc! {"
hello: ENC[AES256_GCM,data:3S1E9am/,iv:WUQoQTrRXw/tUgwpmSG69xWtd5dVMfe8qUly1VB8ucM=,tag:nQUDkuh0OR1cjR5hGC5jOw==,type:str]
Expand All @@ -59,7 +59,7 @@ mod map {
}
}

impl<S: RopsFileState> MockTestUtil for RopsFileMap<S, YamlFileFormat>
impl<S: RopsFileState> MockTestUtil for RopsFileFormatMap<S, YamlFileFormat>
where
Self: MockFileFormatUtil<YamlFileFormat>,
{
Expand Down
2 changes: 1 addition & 1 deletion crates/lib/src/rops_file/format/yaml/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ impl FileFormat for YamlFileFormat {
}
}

mod map_to_tree;
mod to_internal_map;

#[cfg(feature = "test-utils")]
mod mock;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ use crate::*;
mod tree_traversal {
use super::*;

pub fn recursive_map_call<F, S: RopsFileState>(yaml_map: YamlMap, recursive_value_fn: F) -> Result<RopsTree<S>, MapToTreeError>
pub fn recursive_map_call<F, S: RopsFileState>(yaml_map: YamlMap, recursive_value_fn: F) -> Result<RopsMap<S>, FormatToInternalMapError>
where
F: Fn(YamlValue) -> Result<RopsTree<S>, MapToTreeError>,
F: Fn(YamlValue) -> Result<RopsTree<S>, FormatToInternalMapError>,
{
let mut inner_map = IndexMap::default();
let mut tree_map = IndexMap::default();

for (yaml_key, value_yaml) in yaml_map {
inner_map.insert(validate_key(yaml_key)?, recursive_value_fn(value_yaml)?);
tree_map.insert(validate_key(yaml_key)?, recursive_value_fn(value_yaml)?);
}

return Ok(RopsTree::Map(inner_map));
return Ok(tree_map.into());

fn validate_key(yaml_value: YamlValue) -> Result<String, MapToTreeError> {
fn validate_key(yaml_value: YamlValue) -> Result<String, FormatToInternalMapError> {
match yaml_value {
YamlValue::String(string) => Ok(string),
other => Err(MapToTreeError::NonStringKey(
other => Err(FormatToInternalMapError::NonStringKey(
serde_yaml::to_string(&other).expect("yaml value not serializable"),
)),
}
Expand All @@ -32,24 +32,24 @@ mod tree_traversal {
mod encrypted {
use super::*;

impl<C: Cipher> TryFrom<RopsFileMap<Encrypted<C>, YamlFileFormat>> for RopsTree<Encrypted<C>> {
type Error = MapToTreeError;
impl<C: Cipher> TryFrom<RopsFileFormatMap<Encrypted<C>, YamlFileFormat>> for RopsMap<Encrypted<C>> {
type Error = FormatToInternalMapError;

fn try_from(rops_file_map: RopsFileMap<Encrypted<C>, YamlFileFormat>) -> Result<Self, Self::Error> {
fn try_from(rops_file_map: RopsFileFormatMap<Encrypted<C>, YamlFileFormat>) -> Result<Self, Self::Error> {
return tree_traversal::recursive_map_call(rops_file_map.into_inner_map(), recursive_value_call);

fn recursive_value_call<Ci: Cipher>(yaml_value: YamlValue) -> Result<RopsTree<Encrypted<Ci>>, MapToTreeError> {
fn recursive_value_call<Ci: Cipher>(yaml_value: YamlValue) -> Result<RopsTree<Encrypted<Ci>>, FormatToInternalMapError> {
Ok(match yaml_value {
YamlValue::Tagged(tagged) => recursive_value_call(tagged.value)?,
YamlValue::Mapping(map) => tree_traversal::recursive_map_call(map, recursive_value_call)?,
YamlValue::Mapping(map) => RopsTree::Map(tree_traversal::recursive_map_call(map, recursive_value_call)?),
YamlValue::String(encrypted_string) => RopsTree::Leaf(encrypted_string.parse()?),
YamlValue::Sequence(sequence) => {
RopsTree::Sequence(sequence.into_iter().map(recursive_value_call).collect::<Result<Vec<_>, _>>()?)
}
YamlValue::Null => RopsTree::Null,
YamlValue::Bool(_) | YamlValue::Number(_) => {
// TEMP: handle as hard error until partial encryption support is added
return Err(MapToTreeError::InvalidValueForEncrypted(
return Err(FormatToInternalMapError::InvalidValueForEncrypted(
serde_yaml::to_string(&yaml_value).expect("unable to serialize yaml value"),
));
}
Expand All @@ -69,29 +69,31 @@ mod encrypted {
#[test]
fn transforms_encrypted_yaml_map() {
assert_eq!(
RopsTree::mock(),
RopsFileMap::<Encrypted<AES256GCM>, YamlFileFormat>::mock().try_into().unwrap()
RopsMap::mock(),
RopsFileFormatMap::<Encrypted<AES256GCM>, YamlFileFormat>::mock()
.try_into()
.unwrap()
)
}
}

#[test]
fn dissallows_non_string_keys() {
let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::<YamlMap>("true: xxx").unwrap());
let file_map = RopsFileFormatMap::from_inner_map(serde_yaml::from_str::<YamlMap>("true: xxx").unwrap());
assert!(matches!(
RopsTree::<Encrypted<StubCipher>>::try_from(file_map).unwrap_err(),
MapToTreeError::NonStringKey(_)
RopsMap::<Encrypted<StubCipher>>::try_from(file_map).unwrap_err(),
FormatToInternalMapError::NonStringKey(_)
))
}

/*
TEMP(NOTE): Not necassarily true once partial encryption arrives:
*/
fn assert_disallowed_value_helper(key_value_str: &str) {
let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::<YamlMap>(key_value_str).unwrap());
let file_map = RopsFileFormatMap::from_inner_map(serde_yaml::from_str::<YamlMap>(key_value_str).unwrap());
assert!(matches!(
RopsTree::<Encrypted<StubCipher>>::try_from(file_map).unwrap_err(),
MapToTreeError::InvalidValueForEncrypted(_)
RopsMap::<Encrypted<StubCipher>>::try_from(file_map).unwrap_err(),
FormatToInternalMapError::InvalidValueForEncrypted(_)
))
}

Expand All @@ -110,27 +112,27 @@ mod encrypted {
mod decrypted {
use super::*;

impl TryFrom<RopsFileMap<Decrypted, YamlFileFormat>> for RopsTree<Decrypted> {
type Error = MapToTreeError;
impl TryFrom<RopsFileFormatMap<Decrypted, YamlFileFormat>> for RopsMap<Decrypted> {
type Error = FormatToInternalMapError;

fn try_from(rops_file_map: RopsFileMap<Decrypted, YamlFileFormat>) -> Result<Self, Self::Error> {
fn try_from(rops_file_map: RopsFileFormatMap<Decrypted, YamlFileFormat>) -> Result<Self, Self::Error> {
return tree_traversal::recursive_map_call(rops_file_map.into_inner_map(), recursive_value_call);

fn recursive_value_call(yaml_value: YamlValue) -> Result<RopsTree<Decrypted>, MapToTreeError> {
fn recursive_value_call(yaml_value: YamlValue) -> Result<RopsTree<Decrypted>, FormatToInternalMapError> {
Ok(match yaml_value {
// SOPS simply throws away tags, so do we for now.
// It can, however, deserialize manually added tags to encrypted documents,
// so we could in theory keep the tags somewhere without breaking SOPS compatability.
YamlValue::Tagged(tagged) => recursive_value_call(tagged.value)?,
YamlValue::Mapping(map) => tree_traversal::recursive_map_call(map, recursive_value_call)?,
YamlValue::Mapping(map) => RopsTree::Map(tree_traversal::recursive_map_call(map, recursive_value_call)?),
YamlValue::Bool(boolean) => RopsTree::Leaf(RopsValue::Boolean(boolean)),
YamlValue::String(string) => RopsTree::Leaf(RopsValue::String(string)),
YamlValue::Number(number) => RopsTree::Leaf(match number.is_f64() {
true => RopsValue::Float(number.as_f64().expect("number not a f64").to_string()),
false => RopsValue::Integer(
number
.as_i64()
.ok_or_else(|| MapToTreeError::IntegerOutOfRange(number.as_u64().expect("number not an u64")))?,
.ok_or_else(|| FormatToInternalMapError::IntegerOutOfRange(number.as_u64().expect("number not an u64")))?,
),
}),
YamlValue::Sequence(sequence) => {
Expand All @@ -149,26 +151,27 @@ mod decrypted {
#[test]
fn transforms_decrypted_yaml_map() {
assert_eq!(
RopsTree::mock(),
RopsFileMap::<Decrypted, YamlFileFormat>::mock().try_into().unwrap()
RopsMap::mock(),
RopsFileFormatMap::<Decrypted, YamlFileFormat>::mock().try_into().unwrap()
)
}

#[test]
fn dissallows_non_string_keys() {
let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::<YamlMap>("123: 456").unwrap());
let file_map = RopsFileFormatMap::from_inner_map(serde_yaml::from_str::<YamlMap>("123: 456").unwrap());
assert!(matches!(
RopsTree::<Decrypted>::try_from(file_map).unwrap_err(),
MapToTreeError::NonStringKey(_)
RopsMap::<Decrypted>::try_from(file_map).unwrap_err(),
FormatToInternalMapError::NonStringKey(_)
))
}

#[test]
fn dissallows_out_of_range_integers() {
let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::<YamlMap>(&format!("invalid_integer: {}", u64::MAX)).unwrap());
let file_map =
RopsFileFormatMap::from_inner_map(serde_yaml::from_str::<YamlMap>(&format!("invalid_integer: {}", u64::MAX)).unwrap());
assert!(matches!(
RopsTree::<Decrypted>::try_from(file_map).unwrap_err(),
MapToTreeError::IntegerOutOfRange(_)
RopsMap::<Decrypted>::try_from(file_map).unwrap_err(),
FormatToInternalMapError::IntegerOutOfRange(_)
))
}
}
Expand Down
28 changes: 0 additions & 28 deletions crates/lib/src/rops_file/map.rs

This file was deleted.

5 changes: 1 addition & 4 deletions crates/lib/src/rops_file/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
mod core;
pub use core::RopsFile;

mod map;
pub use map::RopsFileMap;

mod key_path;
pub use key_path::KeyPath;

mod value;
pub use value::*;

mod tree;
pub use tree::{MapToTreeError, RopsTree, SavedRopsTreeNonces};
pub use tree::{RopsMap, RopsTree, SavedRopsTreeNonces};

mod metadata;
pub use metadata::*;
Expand Down
6 changes: 3 additions & 3 deletions crates/lib/src/rops_file/state.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::marker::PhantomData;
use std::{fmt::Debug, marker::PhantomData};

use crate::*;

pub trait RopsFileState {
type RopsTreeLeaf;
pub trait RopsFileState: Debug + PartialEq {
type RopsTreeLeaf: Debug + PartialEq;
}

#[derive(Debug, PartialEq)]
Expand Down
Loading

0 comments on commit 164105f

Please sign in to comment.