diff --git a/Cargo.toml b/Cargo.toml index 85c0199..1e4265a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ rand = "0.8" serde = { version = "1", features = ["derive"] } serde_with = "3" serde_yaml = "0.9" +sha2 = "0.10.8" strum = { version = "0.25", features = ["derive"] } textwrap = "0.16" thiserror = "1" diff --git a/README.md b/README.md index a15695a..557c6d1 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,8 @@ hashicorp_kv | `X` | `X` (Con: will skip initialization vector reuse for unchanged value.) +- `--ignore-mac` flag. Why? + - [Integrated formatting configuration](https://github.com/getsops/sops#32json-and-json_binary-indentation) - [Integrated secrets publishing](https://github.com/getsops/sops#219using-the-publish-command) diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 0f99034..00b3bb7 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -11,6 +11,8 @@ age-test-utils = ["age", "test-utils", "dep:indoc", "dep:textwrap"] yaml = ["dep:serde_yaml"] # Ciphers aes-gcm = ["dep:aes-gcm"] +# Hashers +sha2 = ["dep:sha2"] # Test utils test-utils = ["dep:pretty_assertions"] @@ -39,5 +41,8 @@ serde_yaml = { workspace = true, optional = true } # AES_GCM aes-gcm = { workspace = true, optional = true } +# SHA2 +sha2 = { workspace = true, optional = true } + # TEST_UTILS pretty_assertions = { workspace = true, optional = true } diff --git a/crates/lib/src/cryptography/hasher/mod.rs b/crates/lib/src/cryptography/hasher/mod.rs new file mode 100644 index 0000000..a8d3b02 --- /dev/null +++ b/crates/lib/src/cryptography/hasher/mod.rs @@ -0,0 +1,42 @@ +pub use core::Hasher; +mod core { + use generic_array::{ArrayLength, GenericArray}; + + pub trait Hasher { + type OutputSize: ArrayLength; + + fn new() -> Self; + + fn update(&mut self, input: impl AsRef<[u8]>); + + fn finalize(self) -> GenericArray; + } +} + +#[cfg(feature = "sha2")] +pub use sha512::SHA512; +#[cfg(feature = "sha2")] +mod sha512 { + use sha2::{digest::OutputSizeUser, Digest, Sha512}; + + use crate::*; + + #[derive(Debug)] + pub struct SHA512(Sha512); + + impl Hasher for SHA512 { + type OutputSize = ::OutputSize; + + fn new() -> Self { + Self(Sha512::new()) + } + + fn update(&mut self, input: impl AsRef<[u8]>) { + self.0.update(input) + } + + fn finalize(self) -> generic_array::GenericArray { + self.0.finalize() + } + } +} diff --git a/crates/lib/src/cryptography/mod.rs b/crates/lib/src/cryptography/mod.rs index 822f6a2..7d16778 100644 --- a/crates/lib/src/cryptography/mod.rs +++ b/crates/lib/src/cryptography/mod.rs @@ -16,5 +16,8 @@ pub use encrypted_data::EncryptedData; mod cipher; pub use cipher::*; +mod hasher; +pub use hasher::*; + mod integration; pub use integration::*; diff --git a/crates/lib/src/rops_file/mac.rs b/crates/lib/src/rops_file/mac.rs new file mode 100644 index 0000000..579cb36 --- /dev/null +++ b/crates/lib/src/rops_file/mac.rs @@ -0,0 +1,125 @@ +use std::ops::Add; + +use generic_array::{typenum::Sum, ArrayLength, GenericArray}; + +use crate::*; + +const MAC_ENCRYPTED_ONLY_INIT_BYTES: [u8; 32] = [ + 0x8a, 0x3f, 0xd2, 0xad, 0x54, 0xce, 0x66, 0x52, 0x7b, 0x10, 0x34, 0xf3, 0xd1, 0x47, 0xbe, 0xb, 0xb, 0x97, 0x5b, 0x3b, 0xf4, 0x4f, 0x72, + 0xc6, 0xfd, 0xad, 0xec, 0x81, 0x76, 0xf2, 0x7d, 0x69, +]; + +#[derive(Debug)] +pub struct Mac(GenericArray>) +where + H::OutputSize: Add, + Sum: ArrayLength; + +impl Mac +where + H::OutputSize: Add, + Sum: ArrayLength, +{ + pub fn compute(from_encrypted_values_only: bool, decrypted_map: &RopsMap) -> Self { + let mut hasher = H::new(); + if from_encrypted_values_only { + hasher.update(MAC_ENCRYPTED_ONLY_INIT_BYTES); + } + + traverse_map(&mut hasher, from_encrypted_values_only, decrypted_map); + + // IMPROVEMENT: would be nice if the heap allocation could be avoided. + return Mac(GenericArray::from_iter(format!("{:X}", hasher.finalize()).into_bytes())); + + fn traverse_map(hasher: &mut Ha, hash_encrypted_values_only: bool, map: &RopsMap) { + traverse_map_recursive(hasher, hash_encrypted_values_only, map); + + fn traverse_map_recursive(hasher: &mut H, hash_encrypted_values_only: bool, map: &RopsMap) { + for (_, tree) in map.iter() { + traverse_tree_recursive(hasher, hash_encrypted_values_only, tree) + } + } + + fn traverse_tree_recursive(hasher: &mut H, hash_encrypted_values_only: bool, tree: &RopsTree) { + match tree { + RopsTree::Sequence(sequence) => sequence + .iter() + .for_each(|sub_tree| traverse_tree_recursive(hasher, hash_encrypted_values_only, sub_tree)), + RopsTree::Map(map) => traverse_map_recursive(hasher, hash_encrypted_values_only, map), + RopsTree::Null => (), + RopsTree::Leaf(value) => { + // TODO: use hash_encrypted_only once partial encryption is added + hasher.update(value.as_bytes()) + } + } + } + } + } + + pub fn encrypt( + self, + data_key: &DataKey, + last_modified_date_time: &LastModifiedDateTime, + ) -> Result, C::Error> { + let mut in_place_buffer = self.0; + let nonce = Nonce::new(); + let authorization_tag = C::encrypt( + &nonce, + data_key, + in_place_buffer.as_mut_slice(), + last_modified_date_time.as_ref().to_rfc3339().as_bytes(), + )?; + + Ok(EncryptedRopsValue { + data: in_place_buffer.to_vec().into(), + authorization_tag, + nonce, + value_variant: RopsValueVariant::String, + }) + } +} + +// WORKAROUND: derive proc macro struggles with trait bounds +impl PartialEq for Mac +where + H::OutputSize: Add, + Sum: ArrayLength, +{ + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +#[cfg(feature = "test-utils")] +mod mock { + use super::*; + + #[cfg(feature = "sha2")] + mod sha2 { + use super::*; + + impl MockTestUtil for Mac + where + H::OutputSize: Add, + Sum: ArrayLength, + { + fn mock() -> Self { + Self(GenericArray::from_slice(b"A0FBBFF515AC1EF88827C911653675DE4155901880355C59BA4FE4043395A0DE5EA77762EB3CAC54CC6F2B37EDDD916127A32566E810B0A5DADFA2F60B061331").to_owned()) + } + } + } +} + +#[cfg(test)] +mod tests { + #[cfg(feature = "sha2")] + mod sha2 { + + use crate::*; + + #[test] + fn computes_mac() { + assert_eq!(Mac::mock(), Mac::::compute(false, &RopsMap::mock())) + } + } +} diff --git a/crates/lib/src/rops_file/metadata/last_modified.rs b/crates/lib/src/rops_file/metadata/last_modified.rs index 40c1bb0..f56ad32 100644 --- a/crates/lib/src/rops_file/metadata/last_modified.rs +++ b/crates/lib/src/rops_file/metadata/last_modified.rs @@ -1,9 +1,10 @@ use std::fmt::Display; use chrono::{DateTime, Utc}; +use derive_more::AsRef; use serde::{Deserialize, Serialize}; -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize, AsRef)] pub struct LastModifiedDateTime(DateTime); impl LastModifiedDateTime { diff --git a/crates/lib/src/rops_file/mod.rs b/crates/lib/src/rops_file/mod.rs index 3557289..e5a4d20 100644 --- a/crates/lib/src/rops_file/mod.rs +++ b/crates/lib/src/rops_file/mod.rs @@ -18,3 +18,6 @@ pub use state::{Decrypted, Encrypted, RopsFileState}; mod format; pub use format::*; + +mod mac; +pub use mac::Mac;