diff --git a/crates/rops/src/cryptography/data_key.rs b/crates/rops/src/cryptography/data_key.rs index b57942d..e51c72b 100644 --- a/crates/rops/src/cryptography/data_key.rs +++ b/crates/rops/src/cryptography/data_key.rs @@ -19,6 +19,10 @@ impl DataKey { DataKeySize::USIZE } + pub fn new() -> Self { + DataKey(RngKey::new()) + } + pub fn empty() -> Self { Self(RngKey::empty()) } diff --git a/crates/rops/src/integration/age.rs b/crates/rops/src/integration/age.rs index b5082fb..c1705ea 100644 --- a/crates/rops/src/integration/age.rs +++ b/crates/rops/src/integration/age.rs @@ -76,6 +76,10 @@ impl Integration for AgeIntegration { Ok(Some(decrypted_data_key_buffer)) } + + fn append_to_metadata(integration_metadata: &mut IntegrationMetadata, integration_metadata_unit: IntegrationMetadataUnit) { + integration_metadata.age.push(integration_metadata_unit) + } } mod error { @@ -94,6 +98,16 @@ mod error { } } +mod key_id { + use super::*; + + impl IntegrationKeyId for age::x25519::Recipient { + fn append_to_builder(self, rops_file_builder: &mut RopsFileBuilder) { + rops_file_builder.age_key_ids.push(self) + } + } +} + pub use config::AgeConfig; mod config { use serde::{Deserialize, Serialize}; @@ -112,6 +126,10 @@ mod config { impl IntegrationConfig for AgeConfig { const INCLUDE_DATA_KEY_CREATED_AT: bool = false; + fn new(key_id: ::KeyId) -> Self { + Self { key_id } + } + fn key_id(&self) -> &::KeyId { &self.key_id } diff --git a/crates/rops/src/integration/aws_kms/config.rs b/crates/rops/src/integration/aws_kms/config.rs index 46247cf..b5147cb 100644 --- a/crates/rops/src/integration/aws_kms/config.rs +++ b/crates/rops/src/integration/aws_kms/config.rs @@ -11,6 +11,10 @@ pub struct AwsKmsConfig { impl IntegrationConfig for AwsKmsConfig { const INCLUDE_DATA_KEY_CREATED_AT: bool = true; + fn new(key_id: ::KeyId) -> Self { + Self { key_id } + } + fn key_id(&self) -> &::KeyId { &self.key_id } diff --git a/crates/rops/src/integration/aws_kms/core.rs b/crates/rops/src/integration/aws_kms/core.rs index 81d92f5..8ded962 100644 --- a/crates/rops/src/integration/aws_kms/core.rs +++ b/crates/rops/src/integration/aws_kms/core.rs @@ -71,6 +71,10 @@ impl Integration for AwsKmsIntegration { .map(Some) .map_err(|error| IntegrationError::Decryption(error.into())) } + + fn append_to_metadata(integration_metadata: &mut IntegrationMetadata, integration_metadata_unit: IntegrationMetadataUnit) { + integration_metadata.kms.push(integration_metadata_unit) + } } fn tokio_blocking(future: impl Future) -> O { diff --git a/crates/rops/src/integration/aws_kms/key_id.rs b/crates/rops/src/integration/aws_kms/key_id.rs index 724b1d8..4ba4f2f 100644 --- a/crates/rops/src/integration/aws_kms/key_id.rs +++ b/crates/rops/src/integration/aws_kms/key_id.rs @@ -35,6 +35,12 @@ impl FromStr for AwsKeyId { } } +impl IntegrationKeyId for AwsKeyId { + fn append_to_builder(self, rops_file_builder: &mut RopsFileBuilder) { + rops_file_builder.aws_kms_key_ids.push(self) + } +} + #[cfg(feature = "test-utils")] mod mock { use super::*; diff --git a/crates/rops/src/integration/core.rs b/crates/rops/src/integration/core.rs index 99d2dc9..4bde020 100644 --- a/crates/rops/src/integration/core.rs +++ b/crates/rops/src/integration/core.rs @@ -6,7 +6,7 @@ const ROPS_APPLICATION_NAME: &str = "rops"; pub trait Integration: Sized { const NAME: &'static str; - type KeyId; + type KeyId: IntegrationKeyId; type PrivateKey; type Config: IntegrationConfig; @@ -66,11 +66,15 @@ pub trait Integration: Sized { fn encrypt_data_key(key_id: &Self::KeyId, data_key: &DataKey) -> IntegrationResult; fn decrypt_data_key(key_id: &Self::KeyId, encrypted_data_key: &str) -> IntegrationResult>; + + fn append_to_metadata(integration_metadata: &mut IntegrationMetadata, integration_metadata_unit: IntegrationMetadataUnit); } pub trait IntegrationConfig: Debug + PartialEq { const INCLUDE_DATA_KEY_CREATED_AT: bool; + fn new(key_id: I::KeyId) -> Self; + fn key_id(&self) -> &I::KeyId; } diff --git a/crates/rops/src/integration/key_id.rs b/crates/rops/src/integration/key_id.rs new file mode 100644 index 0000000..c772303 --- /dev/null +++ b/crates/rops/src/integration/key_id.rs @@ -0,0 +1,5 @@ +use crate::*; + +pub trait IntegrationKeyId { + fn append_to_builder(self, rops_file_builder: &mut RopsFileBuilder); +} diff --git a/crates/rops/src/integration/mod.rs b/crates/rops/src/integration/mod.rs index 7ea5955..87eeea9 100644 --- a/crates/rops/src/integration/mod.rs +++ b/crates/rops/src/integration/mod.rs @@ -1,6 +1,9 @@ mod core; pub use core::{Integration, IntegrationConfig}; +mod key_id; +pub use key_id::IntegrationKeyId; + mod error; pub use error::{IntegrationError, IntegrationResult}; diff --git a/crates/rops/src/integration/test_utils.rs b/crates/rops/src/integration/test_utils.rs index b080ef3..e6c0166 100644 --- a/crates/rops/src/integration/test_utils.rs +++ b/crates/rops/src/integration/test_utils.rs @@ -29,7 +29,7 @@ mod stub_integration { impl Integration for StubIntegration { const NAME: &'static str = "stub"; - type KeyId = (); + type KeyId = String; type PrivateKey = String; type Config = StubIntegrationConfig; @@ -48,6 +48,16 @@ mod stub_integration { fn decrypt_data_key(_key_id: &Self::KeyId, _encrypted_data_key: &str) -> IntegrationResult> { unimplemented!() } + + fn append_to_metadata(_integration_metadata: &mut IntegrationMetadata, _integration_metadata_unit: IntegrationMetadataUnit) { + unimplemented!() + } + } + + impl IntegrationKeyId for String { + fn append_to_builder(self, _rops_file_builder: &mut RopsFileBuilder) { + unimplemented!() + } } #[derive(Debug, PartialEq)] @@ -56,8 +66,12 @@ mod stub_integration { impl IntegrationConfig for StubIntegrationConfig { const INCLUDE_DATA_KEY_CREATED_AT: bool = false; + fn new(key_id: ::KeyId) -> Self { + Self(key_id) + } + fn key_id(&self) -> &::KeyId { - &() + &self.0 } } } diff --git a/crates/rops/src/rops_file/builder.rs b/crates/rops/src/rops_file/builder.rs new file mode 100644 index 0000000..76c8c79 --- /dev/null +++ b/crates/rops/src/rops_file/builder.rs @@ -0,0 +1,96 @@ +use crate::*; + +pub struct RopsFileBuilder { + plaintext_map: F::Map, + partial_encryption: Option, + mac_only_encrypted: Option, + #[cfg(feature = "age")] + pub(crate) age_key_ids: Vec<::KeyId>, + #[cfg(feature = "aws-kms")] + pub(crate) aws_kms_key_ids: Vec<::KeyId>, +} + +impl RopsFileBuilder { + pub fn new(plaintext_map: F::Map) -> Self { + Self { + plaintext_map, + partial_encryption: None, + mac_only_encrypted: None, + age_key_ids: Vec::new(), + aws_kms_key_ids: Vec::new(), + } + } + + pub fn with_partial_encryption(mut self, partial_encryption: PartialEncryptionConfig) -> Self { + self.partial_encryption = Some(partial_encryption); + self + } + + pub fn mac_only_encrypted(mut self) -> Self { + self.mac_only_encrypted = Some(true); + self + } + + pub fn add_integration_key(mut self, key_id: I::KeyId) -> Self { + key_id.append_to_builder(&mut self); + self + } + + pub fn encrypt(self) -> Result, F>, RopsFileEncryptError> { + #[rustfmt::skip] + let Self { plaintext_map, partial_encryption, mac_only_encrypted, age_key_ids, aws_kms_key_ids } = self; + + let data_key = DataKey::new(); + + let decrypted_map = plaintext_map.decrypted_to_internal()?; + + let mac = Mac::::compute( + MacOnlyEncryptedConfig::new(mac_only_encrypted, partial_encryption.as_ref()), + &decrypted_map, + ); + + let encrypted_map_result = decrypted_map.encrypt(&data_key, partial_encryption.as_ref()); + + let mut integration_metadata = IntegrationMetadata::default(); + #[cfg(feature = "age")] + integration_metadata.add_integration_keys::(age_key_ids, &data_key)?; + #[cfg(feature = "aws-kms")] + integration_metadata.add_integration_keys::(aws_kms_key_ids, &data_key)?; + + let encrypted_metadata_result = RopsFileMetadata { + intregation: integration_metadata, + last_modified: LastModifiedDateTime::now(), + mac, + partial_encryption, + mac_only_encrypted, + } + .encrypt(&data_key); + + RopsFile::from_parts_results(encrypted_map_result, encrypted_metadata_result) + } +} + +// Redundant to test combinations of file formats, integrations, ciphers and hashers if the +// respective trait implementations are well tested. +#[cfg(all(test, feature = "yaml", feature = "age", feature = "aes-gcm", feature = "sha2"))] +mod tests { + use super::*; + + #[test] + fn encrypts_with_builder() { + AgeIntegration::set_mock_private_key_env_var(); + + let builder_rops_file = + RopsFileBuilder::::new(RopsFileFormatMap::::mock().into_inner_map()) + .with_partial_encryption(MockTestUtil::mock()) + .mac_only_encrypted() + .add_integration_key::(AgeIntegration::mock_key_id()) + .encrypt::() + .unwrap() + .decrypt::() + .unwrap(); + + assert_eq!(RopsFileFormatMap::mock(), builder_rops_file.map); + assert_ne!(RopsFileMetadata::mock(), builder_rops_file.metadata); + } +} diff --git a/crates/rops/src/rops_file/core.rs b/crates/rops/src/rops_file/core.rs index b5e8421..9bf8410 100644 --- a/crates/rops/src/rops_file/core.rs +++ b/crates/rops/src/rops_file/core.rs @@ -18,30 +18,6 @@ where pub metadata: RopsFileMetadata, } -#[derive(Debug, Error)] -pub enum RopsFileEncryptError { - #[error("invalid decrypted map format: {0}")] - FormatToIntenrnalMap(#[from] FormatToInternalMapError), - #[error("unable to retrieve data key: {0}")] - DataKeyRetrieval(#[from] RopsFileMetadataDataKeyRetrievalError), - #[error("unable to encrypt map: {0}")] - MapEncryption(anyhow::Error), - #[error("unable to encrypt metadata: {0}")] - MetadataEncryption(anyhow::Error), -} - -#[derive(Debug, Error)] -pub enum RopsFileDecryptError { - #[error("invalid encrypted map format; {0}")] - FormatToIntenrnalMap(#[from] FormatToInternalMapError), - #[error("unable to decrypt map value: {0}")] - DecryptValue(#[from] DecryptRopsValueError), - #[error("unable to decrypt file metadata")] - Metadata(#[from] RopsFileMetadataDecryptError), - #[error("invalid MAC, computed {0}, stored {0}")] - MacMismatch(String, String), -} - impl RopsFile where <::Mac as FromStr>::Err: Display, @@ -88,7 +64,7 @@ impl RopsFile, F> { .to_internal()? .encrypt::(&data_key, self.metadata.partial_encryption.as_ref()); let encrypted_metadata = self.metadata.encrypt::(&data_key); - Self::file_from_parts_results(encrypted_map, encrypted_metadata) + RopsFile::from_parts_results(encrypted_map, encrypted_metadata) } pub fn encrypt_with_saved_parameters( @@ -104,16 +80,7 @@ impl RopsFile, F> { .encrypt_with_saved_nonces(&data_key, self.metadata.partial_encryption.as_ref(), &saved_map_nonces); let encrypted_metadata = self.metadata.encrypt_with_saved_mac_nonce::(&data_key, saved_mac_nonce); - Self::file_from_parts_results(encrypted_map, encrypted_metadata) - } - - fn file_from_parts_results( - encrypted_map_result: Result>, C::Error>, - encrypted_metadata_result: Result>, C::Error>, - ) -> Result, Fo>, RopsFileEncryptError> { - let encrypted_map = encrypted_map_result.map_err(|error| RopsFileEncryptError::MetadataEncryption(error.into()))?; - let encrypted_metadata = encrypted_metadata_result.map_err(|error| RopsFileEncryptError::MetadataEncryption(error.into()))?; - Ok(RopsFile::new(encrypted_map, encrypted_metadata)) + RopsFile::from_parts_results(encrypted_map, encrypted_metadata) } } @@ -154,7 +121,13 @@ impl RopsFile, F> { decrypted_map: &RopsMap, decrypted_metadata: &RopsFileMetadata>, ) -> Result<(), RopsFileDecryptError> { - let computed_mac = Mac::::compute(MacOnlyEncryptedConfig::new(decrypted_metadata), decrypted_map); + let computed_mac = Mac::::compute( + MacOnlyEncryptedConfig::new( + decrypted_metadata.mac_only_encrypted, + decrypted_metadata.partial_encryption.as_ref(), + ), + decrypted_map, + ); let stored_mac = &decrypted_metadata.mac; match &computed_mac != stored_mac { @@ -162,9 +135,18 @@ impl RopsFile, F> { false => Ok(()), } } + + pub(crate) fn from_parts_results( + encrypted_map_result: Result>, C::Error>, + encrypted_metadata_result: Result>, C::Error>, + ) -> Result { + let encrypted_map = encrypted_map_result.map_err(|error| RopsFileEncryptError::MetadataEncryption(error.into()))?; + let encrypted_metadata = encrypted_metadata_result.map_err(|error| RopsFileEncryptError::MetadataEncryption(error.into()))?; + Ok(RopsFile::new(encrypted_map, encrypted_metadata)) + } } -// Reduntant to test combinations of file formats, integrations, ciphers and hashers if the +// Redundant to test combinations of file formats, integrations, ciphers and hashers if the // respective trait implementations are well tested. #[cfg(all(test, feature = "yaml", feature = "age", feature = "aes-gcm", feature = "sha2"))] mod tests { diff --git a/crates/rops/src/rops_file/metadata/integration/core.rs b/crates/rops/src/rops_file/metadata/integration/core.rs index 72a1adf..7b4342d 100644 --- a/crates/rops/src/rops_file/metadata/integration/core.rs +++ b/crates/rops/src/rops_file/metadata/integration/core.rs @@ -15,6 +15,16 @@ pub struct IntegrationMetadata { } impl IntegrationMetadata { + pub fn add_integration_keys(&mut self, key_ids: Vec, data_key: &DataKey) -> IntegrationResult<()> { + key_ids + .into_iter() + .map(|key_id| I::Config::new(key_id)) + .map(|integration_config| IntegrationMetadataUnit::::new(integration_config, data_key)) + .try_for_each(|integation_metada_unit_result| { + integation_metada_unit_result.map(|integration_metadata| I::append_to_metadata(self, integration_metadata)) + }) + } + pub fn find_data_key(&self) -> IntegrationResult> { // In order of what is assumed to be quickest: diff --git a/crates/rops/src/rops_file/metadata/mac/mac_only_encrypted.rs b/crates/rops/src/rops_file/metadata/mac/mac_only_encrypted.rs index f2a38e6..58b6206 100644 --- a/crates/rops/src/rops_file/metadata/mac/mac_only_encrypted.rs +++ b/crates/rops/src/rops_file/metadata/mac/mac_only_encrypted.rs @@ -1,5 +1,3 @@ -use std::{fmt::Display, str::FromStr}; - use crate::*; #[derive(Clone, Copy)] @@ -9,13 +7,13 @@ pub struct MacOnlyEncryptedConfig<'a> { } impl MacOnlyEncryptedConfig<'_> { - pub fn new<'a, S: RopsMetadataState>(metadata: &'a RopsFileMetadata) -> MacOnlyEncryptedConfig<'a> - where - ::Err: Display, - { + pub fn new<'a>( + mac_only_encrypted: Option, + partial_encryption: Option<&'a PartialEncryptionConfig>, + ) -> MacOnlyEncryptedConfig<'a> { MacOnlyEncryptedConfig::<'a> { - mac_only_encrypted: metadata.mac_only_encrypted.unwrap_or_default(), - resolved_partial_encryption: metadata.partial_encryption.as_ref().into(), + mac_only_encrypted: mac_only_encrypted.unwrap_or_default(), + resolved_partial_encryption: partial_encryption.into(), } } } diff --git a/crates/rops/src/rops_file/mod.rs b/crates/rops/src/rops_file/mod.rs index 3939472..12a6582 100644 --- a/crates/rops/src/rops_file/mod.rs +++ b/crates/rops/src/rops_file/mod.rs @@ -1,5 +1,38 @@ mod core; -pub use core::{RopsFile, RopsFileDecryptError, RopsFileEncryptError, RopsFileFromStrError}; +pub use core::{RopsFile, RopsFileFromStrError}; + +pub use error::{RopsFileDecryptError, RopsFileEncryptError}; +mod error { + use thiserror::Error; + + use crate::*; + + #[derive(Debug, Error)] + pub enum RopsFileEncryptError { + #[error("invalid decrypted map format: {0}")] + FormatToIntenrnalMap(#[from] FormatToInternalMapError), + #[error("unable to retrieve data key: {0}")] + DataKeyRetrieval(#[from] RopsFileMetadataDataKeyRetrievalError), + #[error("unable to encrypt map: {0}")] + MapEncryption(anyhow::Error), + #[error("unable to encrypt metadata: {0}")] + MetadataEncryption(anyhow::Error), + #[error(transparent)] + Integration(#[from] IntegrationError), + } + + #[derive(Debug, Error)] + pub enum RopsFileDecryptError { + #[error("invalid encrypted map format; {0}")] + FormatToIntenrnalMap(#[from] FormatToInternalMapError), + #[error("unable to decrypt map value: {0}")] + DecryptValue(#[from] DecryptRopsValueError), + #[error("unable to decrypt file metadata")] + Metadata(#[from] RopsFileMetadataDecryptError), + #[error("invalid MAC, computed {0}, stored {0}")] + MacMismatch(String, String), + } +} mod state; pub use state::{DecryptedFile, EncryptedFile, RopsFileState}; @@ -10,6 +43,9 @@ pub use map::*; mod metadata; pub use metadata::*; +mod builder; +pub use builder::RopsFileBuilder; + mod format; pub use format::*;