diff --git a/crates/lib/src/cryptography/cipher/mod.rs b/crates/lib/src/cryptography/cipher/mod.rs index 724eb1e..69785db 100644 --- a/crates/lib/src/cryptography/cipher/mod.rs +++ b/crates/lib/src/cryptography/cipher/mod.rs @@ -5,3 +5,8 @@ pub use core::AeadCipher; mod aes256_gcm; #[cfg(feature = "aes-gcm")] pub use aes256_gcm::AES256GCM; + +#[cfg(feature = "test-utils")] +mod stub_cipher; +#[cfg(feature = "test-utils")] +pub use stub_cipher::StubCipher; diff --git a/crates/lib/src/cryptography/cipher/stub_cipher.rs b/crates/lib/src/cryptography/cipher/stub_cipher.rs new file mode 100644 index 0000000..c6d54aa --- /dev/null +++ b/crates/lib/src/cryptography/cipher/stub_cipher.rs @@ -0,0 +1,23 @@ +use generic_array::typenum::U32; + +use crate::*; + +#[derive(Debug, PartialEq)] +pub struct StubCipher; + +impl AeadCipher for StubCipher { + const NAME: &'static str = "STUB"; + + type NonceSize = U32; + type AuthorizationTagSize = U32; + type EncryptError = (); + + fn encrypt( + _nonce: &Nonce, + _data_key: &DataKey, + _in_place_buffer: &mut [u8], + _associated_data: &[u8], + ) -> Result, Self::EncryptError> { + unimplemented!() + } +} diff --git a/crates/lib/src/rops_file/decrypt.rs b/crates/lib/src/rops_file/decrypt.rs new file mode 100644 index 0000000..26c8315 --- /dev/null +++ b/crates/lib/src/rops_file/decrypt.rs @@ -0,0 +1,17 @@ +use crate::*; + +#[derive(Debug, thiserror::Error)] +pub enum RopsFileDecryptError { + #[error("invalid map: {0}")] + MapToTree(#[from] MapToTreeError), +} + +impl RopsFile, F> { + pub fn decrypt(self) -> Result<(), RopsFileDecryptError> + where + RopsTree>: TryFrom, F>, Error = MapToTreeError>, + { + let _tree = RopsTree::>::try_from(self.map)?; + todo!() + } +} diff --git a/crates/lib/src/rops_file/format/yaml/decrypted_map_to_tree.rs b/crates/lib/src/rops_file/format/yaml/decrypted_map_to_tree.rs deleted file mode 100644 index a1b727b..0000000 --- a/crates/lib/src/rops_file/format/yaml/decrypted_map_to_tree.rs +++ /dev/null @@ -1,86 +0,0 @@ -use indexmap::IndexMap; -use serde_yaml::{Mapping as YamlMap, Value as YamlValue}; - -use crate::*; - -impl TryFrom> for RopsTree { - type Error = DecryptedMapToTreeError; - - fn try_from(rops_file_map: RopsFileMap) -> Result { - return recursive_map_call(rops_file_map.into_inner_map()); - - fn recursive_map_call(yaml_map: YamlMap) -> Result, DecryptedMapToTreeError> { - let mut inner_map = IndexMap::default(); - - for (yaml_key, value_yaml) in yaml_map { - inner_map.insert(validate_key(yaml_key)?, recursive_value_call(value_yaml)?); - } - - return Ok(RopsTree::Map(inner_map)); - - fn validate_key(yaml_value: YamlValue) -> Result { - match yaml_value { - YamlValue::String(string) => Ok(string), - other => Err(DecryptedMapToTreeError::NonStringKey( - serde_yaml::to_string(&other).expect("yaml value not serializable"), - )), - } - } - } - - fn recursive_value_call(yaml_value: YamlValue) -> Result, DecryptedMapToTreeError> { - 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) => recursive_map_call(map)?, - 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")), - false => RopsValue::Integer( - number - .as_i64() - .ok_or_else(|| DecryptedMapToTreeError::IntegerOutOfRange(number.as_u64().expect("number not an u64")))?, - ), - }), - YamlValue::Sequence(sequence) => { - RopsTree::Sequence(sequence.into_iter().map(recursive_value_call).collect::, _>>()?) - } - YamlValue::Null => RopsTree::Null, - }) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn transforms_decrypted_yaml_map() { - assert_eq!( - RopsTree::mock(), - RopsFileMap::::mock().try_into().unwrap() - ) - } - - #[test] - fn dissallows_non_string_keys() { - let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::("123: 456").unwrap()); - assert!(matches!( - RopsTree::try_from(file_map).unwrap_err(), - DecryptedMapToTreeError::NonStringKey(_) - )) - } - - #[test] - fn dissallows_out_of_range_integers() { - let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::(&format!("invalid_integer: {}", u64::MAX)).unwrap()); - assert!(matches!( - RopsTree::try_from(file_map).unwrap_err(), - DecryptedMapToTreeError::IntegerOutOfRange(_) - )) - } -} diff --git a/crates/lib/src/rops_file/format/yaml/map_to_tree.rs b/crates/lib/src/rops_file/format/yaml/map_to_tree.rs new file mode 100644 index 0000000..7467d6e --- /dev/null +++ b/crates/lib/src/rops_file/format/yaml/map_to_tree.rs @@ -0,0 +1,175 @@ +use indexmap::IndexMap; +use serde_yaml::{Mapping as YamlMap, Value as YamlValue}; + +use crate::*; + +mod tree_traversal { + use super::*; + + pub fn recursive_map_call(yaml_map: YamlMap, recursive_value_fn: F) -> Result, MapToTreeError> + where + F: Fn(YamlValue) -> Result, MapToTreeError>, + { + let mut inner_map = IndexMap::default(); + + for (yaml_key, value_yaml) in yaml_map { + inner_map.insert(validate_key(yaml_key)?, recursive_value_fn(value_yaml)?); + } + + return Ok(RopsTree::Map(inner_map)); + + fn validate_key(yaml_value: YamlValue) -> Result { + match yaml_value { + YamlValue::String(string) => Ok(string), + other => Err(MapToTreeError::NonStringKey( + serde_yaml::to_string(&other).expect("yaml value not serializable"), + )), + } + } + } +} + +mod encrypted { + use super::*; + + impl TryFrom, YamlFileFormat>> for RopsTree> { + type Error = MapToTreeError; + + fn try_from(rops_file_map: RopsFileMap, YamlFileFormat>) -> Result { + return tree_traversal::recursive_map_call(rops_file_map.into_inner_map(), recursive_value_call); + + fn recursive_value_call(yaml_value: YamlValue) -> Result>, MapToTreeError> { + 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::String(encrypted_string) => RopsTree::Leaf(encrypted_string.parse()?), + YamlValue::Sequence(sequence) => { + RopsTree::Sequence(sequence.into_iter().map(recursive_value_call).collect::, _>>()?) + } + YamlValue::Null => RopsTree::Null, + YamlValue::Bool(_) | YamlValue::Number(_) => { + // TEMP: handle as hard error until partial encryption support is added + return Err(MapToTreeError::InvalidValueForEncrypted( + serde_yaml::to_string(&yaml_value).expect("unable to serialize yaml value"), + )); + } + }) + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[cfg(feature = "aes-gcm")] + mod aes_gcm { + use super::*; + + #[test] + fn transforms_encrypted_yaml_map() { + assert_eq!( + RopsTree::mock(), + RopsFileMap::, YamlFileFormat>::mock().try_into().unwrap() + ) + } + } + + #[test] + fn dissallows_non_string_keys() { + let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::("true: xxx").unwrap()); + assert!(matches!( + RopsTree::>::try_from(file_map).unwrap_err(), + MapToTreeError::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::(key_value_str).unwrap()); + assert!(matches!( + RopsTree::>::try_from(file_map).unwrap_err(), + MapToTreeError::InvalidValueForEncrypted(_) + )) + } + + #[test] + fn dissallows_boolean_values() { + assert_disallowed_value_helper("disallowed_boolean: true") + } + + #[test] + fn dissallows_integer_values() { + assert_disallowed_value_helper("disallowed_integer: 1") + } + } +} + +mod decrypted { + use super::*; + + impl TryFrom> for RopsTree { + type Error = MapToTreeError; + + fn try_from(rops_file_map: RopsFileMap) -> Result { + return tree_traversal::recursive_map_call(rops_file_map.into_inner_map(), recursive_value_call); + + fn recursive_value_call(yaml_value: YamlValue) -> Result, MapToTreeError> { + 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::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")), + false => RopsValue::Integer( + number + .as_i64() + .ok_or_else(|| MapToTreeError::IntegerOutOfRange(number.as_u64().expect("number not an u64")))?, + ), + }), + YamlValue::Sequence(sequence) => { + RopsTree::Sequence(sequence.into_iter().map(recursive_value_call).collect::, _>>()?) + } + YamlValue::Null => RopsTree::Null, + }) + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn transforms_decrypted_yaml_map() { + assert_eq!( + RopsTree::mock(), + RopsFileMap::::mock().try_into().unwrap() + ) + } + + #[test] + fn dissallows_non_string_keys() { + let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::("123: 456").unwrap()); + assert!(matches!( + RopsTree::::try_from(file_map).unwrap_err(), + MapToTreeError::NonStringKey(_) + )) + } + + #[test] + fn dissallows_out_of_range_integers() { + let file_map = RopsFileMap::from_inner_map(serde_yaml::from_str::(&format!("invalid_integer: {}", u64::MAX)).unwrap()); + assert!(matches!( + RopsTree::::try_from(file_map).unwrap_err(), + MapToTreeError::IntegerOutOfRange(_) + )) + } + } +} diff --git a/crates/lib/src/rops_file/format/yaml/mod.rs b/crates/lib/src/rops_file/format/yaml/mod.rs index 2487be5..8859019 100644 --- a/crates/lib/src/rops_file/format/yaml/mod.rs +++ b/crates/lib/src/rops_file/format/yaml/mod.rs @@ -17,9 +17,7 @@ impl FileFormat for YamlFileFormat { } } -mod encrypted_map_to_tree {} - -mod decrypted_map_to_tree; +mod map_to_tree; #[cfg(feature = "test-utils")] mod mock; diff --git a/crates/lib/src/rops_file/mod.rs b/crates/lib/src/rops_file/mod.rs index 75b8f64..4c52d48 100644 --- a/crates/lib/src/rops_file/mod.rs +++ b/crates/lib/src/rops_file/mod.rs @@ -1,6 +1,8 @@ mod core; pub use core::RopsFile; +mod decrypt; + mod map; pub use map::RopsFileMap; @@ -11,7 +13,7 @@ mod value; pub use value::*; mod tree; -pub use tree::{DecryptedMapToTreeError, EncryptedMapToTreeError, RopsTree}; +pub use tree::{MapToTreeError, RopsTree}; mod metadata; pub use metadata::*; diff --git a/crates/lib/src/rops_file/tree.rs b/crates/lib/src/rops_file/tree.rs index db911aa..ac46b1b 100644 --- a/crates/lib/src/rops_file/tree.rs +++ b/crates/lib/src/rops_file/tree.rs @@ -8,17 +8,21 @@ pub enum RopsTree { Leaf(S::RopsTreeLeaf), } +// 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 DecryptedMapToTreeError { +pub enum MapToTreeError { #[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] EncryptedRopsValueError), + // TEMP: Deprecate once partial encryption feature arrives. + #[error("invalid valid for an encrypted file")] + InvalidValueForEncrypted(String), } -#[derive(Debug, thiserror::Error)] -pub enum EncryptedMapToTreeError {} - #[cfg(feature = "test-utils")] mod mock { use indexmap::indexmap; @@ -51,4 +55,36 @@ mod mock { }) } } + + #[cfg(feature = "aes-gcm")] + impl MockTestUtil for RopsTree> { + fn mock() -> Self { + return Self::Map(indexmap! { + "hello".to_string() => leaf("ENC[AES256_GCM,data:3S1E9am/,iv:WUQoQTrRXw/tUgwpmSG69xWtd5dVMfe8qUly1VB8ucM=,tag:nQUDkuh0OR1cjR5hGC5jOw==,type:str]"), + "nested_map".to_string() => RopsTree::Map(indexmap! { + "null_key".to_string() => RopsTree::Null, + "array".to_string() => RopsTree::Sequence(vec![ + leaf("ENC[AES256_GCM,data:ANbeNrGp,iv:PRWGCPdOttPr5dlzT9te7WWCZ90J7+CvfY1vp60aADM=,tag:PvSLx4pLT5zRKOU0df8Xlg==,type:str]"), + RopsTree::Map(indexmap! { + "nested_map_in_array".to_string() => RopsTree::Map(indexmap!{ + "integer".to_string() => leaf("ENC[AES256_GCM,data:qTW5qw==,iv:ugMxvR8YPwDgn2MbBpDX0lpCqzJY3GerhbA5jEKUbwE=,tag:d8utfA76C4XPzJyDfgE4Pw==,type:int]") + }), + }), + RopsTree::Map(indexmap!{ + "float".to_string() => leaf("ENC[AES256_GCM,data:/MTg0fCennyN8g==,iv:+/8+Ljm+cls7BbDYZnlg6NVFkrkw4GkEfWU2aGW57qE=,tag:26uMp2JmVAckySIaL2BLCg==,type:float]") + }), + ]), + } + ), + "booleans".to_string() => RopsTree::Sequence(vec![ + leaf("ENC[AES256_GCM,data:bCdz2A==,iv:8kD+h1jClyVHBj9o2WZuAkjk+uD6A2lgNpcGljpQEhk=,tag:u3/fktl5HfFrVLERVvLRGw==,type:bool]"), + leaf("ENC[AES256_GCM,data:SgBh7wY=,iv:0s9Q9pQWbsZm2yHsmFalCzX0IqNb6ZqeY6QQYCWc+qU=,tag:OZb76BWCKbDLbcil4c8fYA==,type:bool]") + ]) + }); + + fn leaf(encrpyted_value_str: &str) -> RopsTree> { + encrpyted_value_str.parse().map(RopsTree::Leaf).unwrap() + } + } + } }