Skip to content

Commit

Permalink
Initial SOPS parity integration testing:
Browse files Browse the repository at this point in the history
* Fixes `LastModified` bytes as additional data for MAC encryption.
* Generalizes integration metadata.
  • Loading branch information
gibbz00 committed Dec 25, 2023
1 parent 83b4d25 commit 9465157
Show file tree
Hide file tree
Showing 24 changed files with 334 additions and 146 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [ "crates/*" ]

[workspace.dependencies]
age = "0.9"
anyhow = "1"
aes-gcm = { version = "0.10", features = ["std"] }
base64 = "0.21"
chrono = { version = "0.4", features = ["serde"] }
Expand Down
1 change: 1 addition & 0 deletions crates/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ sha2 = ["dep:sha2"]
test-utils = ["dep:pretty_assertions"]

[dependencies]
anyhow.workspace = true
base64.workspace = true
chrono.workspace = true
derive_more.workspace = true
Expand Down
4 changes: 4 additions & 0 deletions crates/lib/src/cryptography/data_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ impl DataKey {
pub fn empty() -> Self {
Self(RngKey::empty())
}

pub fn new() -> Self {
Self(RngKey::new())
}
}

#[cfg(feature = "test-utils")]
Expand Down
37 changes: 37 additions & 0 deletions crates/lib/src/cryptography/integration/age.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use age::{

use crate::*;

#[derive(Debug, PartialEq)]
pub struct AgeIntegration;

impl AgeIntegration {
Expand All @@ -17,6 +18,7 @@ impl Integration for AgeIntegration {
const NAME: &'static str = "age";
type PublicKey = age::x25519::Recipient;
type PrivateKey = age::x25519::Identity;
type Config = AgeConfig;

fn parse_public_key(public_key_str: &str) -> IntegrationResult<Self::PublicKey> {
public_key_str
Expand Down Expand Up @@ -70,6 +72,41 @@ impl Integration for AgeIntegration {
}
}

pub use config::AgeConfig;
mod config {
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};

use crate::*;

#[serde_as]
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct AgeConfig {
#[serde_as(as = "DisplayFromStr")]
#[serde(rename = "recipient")]
pub public_key: <AgeIntegration as Integration>::PublicKey,
}

impl<'a> From<&'a AgeConfig> for &'a <AgeIntegration as Integration>::PublicKey {
fn from(config: &'a AgeConfig) -> Self {
&config.public_key
}
}

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

impl MockTestUtil for AgeConfig {
fn mock() -> Self {
Self {
public_key: AgeIntegration::mock_public_key(),
}
}
}
}
}

#[cfg(feature = "test-utils")]
mod mock {
use super::*;
Expand Down
9 changes: 6 additions & 3 deletions crates/lib/src/cryptography/integration/core.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use std::{env::VarError, fmt::Display};
use std::{
env::VarError,
fmt::{Debug, Display},
};

use crate::*;

pub trait Integration {
const NAME: &'static str;
type PublicKey: Display;
type PrivateKey;
type Config: Debug + PartialEq;

fn private_key_env_var_name() -> String {
format!("ROPS_{}", Self::NAME.to_uppercase())
Expand Down Expand Up @@ -52,10 +56,9 @@ mod stub_integration {

impl Integration for StubIntegration {
const NAME: &'static str = "stub";

type PublicKey = String;

type PrivateKey = ();
type Config = ();

fn parse_public_key(_public_key_str: &str) -> IntegrationResult<Self::PublicKey> {
unimplemented!()
Expand Down
4 changes: 2 additions & 2 deletions crates/lib/src/cryptography/integration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ pub use error::{IntegrationError, IntegrationResult};
#[cfg(feature = "age")]
mod age;
#[cfg(feature = "age")]
pub use age::AgeIntegration;
pub use age::{AgeConfig, AgeIntegration};

#[cfg(feature = "test-utils")]
mod test_utils;
#[cfg(feature = "test-utils")]
pub use test_utils::{IntegrationTestUtils, IntegrationsHelper};
pub use test_utils::{IntegrationTestUtils, IntegrationsTestUtils};
4 changes: 2 additions & 2 deletions crates/lib/src/cryptography/integration/test_utils.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::*;

pub struct IntegrationsHelper;
pub struct IntegrationsTestUtils;

impl IntegrationsHelper {
impl IntegrationsTestUtils {
pub fn set_private_keys() {
#[cfg(feature = "age")]
AgeIntegration::set_mock_private_key_env_var();
Expand Down
2 changes: 1 addition & 1 deletion crates/lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod rops_file;
pub use rops_file::*;

mod base64utils;
pub use base64utils::*;
pub(crate) use base64utils::*;

#[cfg(feature = "test-utils")]
mod test_utils;
Expand Down
33 changes: 30 additions & 3 deletions crates/lib/src/rops_file/core.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{fmt::Display, str::FromStr};

use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::*;

Expand All @@ -17,7 +18,7 @@ where
pub metadata: RopsFileMetadata<S::MetadataState>,
}

#[derive(Debug, thiserror::Error)]
#[derive(Debug, Error)]
pub enum RopsFileEncryptError {
#[error("invalid decrypted map format: {0}")]
FormatToIntenrnalMap(#[from] FormatToInternalMapError),
Expand All @@ -29,13 +30,13 @@ pub enum RopsFileEncryptError {
MetadataEncryption(String),
}

#[derive(Debug, thiserror::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: {0}")]
#[error("unable to decrypt file metadata")]
Metadata(#[from] RopsFileMetadataDecryptError),
#[error("invalid MAC, computed {0}, stored {0}")]
MacMismatch(String, String),
Expand All @@ -50,6 +51,32 @@ where
}
}

impl<S: RopsFileState, F: FileFormat> Display for RopsFile<S, F>
where
<<S::MetadataState as RopsMetadataState>::Mac as FromStr>::Err: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", F::serialize_to_string(self).expect("failed to serialize rops map"))
}
}

#[derive(Debug, Error)]
pub enum RopsFileFromStrError {
#[error("deserialize error")]
Deserialize(anyhow::Error),
}

impl<S: RopsFileState, F: FileFormat> FromStr for RopsFile<S, F>
where
<<S::MetadataState as RopsMetadataState>::Mac as FromStr>::Err: Display,
{
type Err = RopsFileFromStrError;

fn from_str(str: &str) -> Result<Self, Self::Err> {
F::deserialize_from_str(str).map_err(|error| RopsFileFromStrError::Deserialize(error.into()))
}
}

impl<H: Hasher, F: FileFormat> RopsFile<DecryptedFile<H>, F> {
pub fn encrypt<C: Cipher, Fo: FileFormat>(self) -> Result<RopsFile<EncryptedFile<C, H>, Fo>, RopsFileEncryptError>
where
Expand Down
4 changes: 3 additions & 1 deletion crates/lib/src/rops_file/format/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ use std::fmt::Debug;

use serde::{de::DeserializeOwned, Serialize};

pub trait FileFormat {
pub trait FileFormat: Sized {
type Map: Serialize + DeserializeOwned + PartialEq + Debug;

type SerializeError: std::error::Error + Send + Sync + 'static;
type DeserializeError: std::error::Error + Send + Sync + 'static;

fn serialize_to_string<T: Serialize>(t: &T) -> Result<String, Self::SerializeError>;

fn deserialize_from_str<T: DeserializeOwned>(str: &str) -> Result<T, Self::DeserializeError>;
}
8 changes: 7 additions & 1 deletion crates/lib/src/rops_file/format/map.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::marker::PhantomData;
use std::{fmt::Display, marker::PhantomData};

use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -51,6 +51,12 @@ where
}
}

impl<S: RopsMapState, F: FileFormat> Display for RopsFileFormatMap<S, F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", F::serialize_to_string(self).expect("file format map not serializable"))
}
}

#[cfg(feature = "test-utils")]
mod mock {
use super::*;
Expand Down
59 changes: 39 additions & 20 deletions crates/lib/src/rops_file/format/yaml/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@ mod metadata {

use crate::*;

impl<I: IntegrationTestUtils> MockFileFormatUtil<YamlFileFormat> for IntegrationMetadataUnit<I>
where
I::Config: MockFileFormatUtil<YamlFileFormat>,
for<'a> &'a I::PublicKey: From<&'a I::Config>,
{
fn mock_format_display() -> String {
indoc::formatdoc! {"
{}
enc: |
{}",
I::Config::mock_format_display(), textwrap::indent(I::mock_encrypted_data_key_str(), " ")
}
}
}

impl<S: RopsMetadataState> MockFileFormatUtil<YamlFileFormat> for RopsFileMetadata<S>
where
S::Mac: MockDisplayTestUtil,
Expand All @@ -89,23 +104,33 @@ mod metadata {

#[cfg(feature = "age")]
{
let age_metadata_yaml_string = RopsFileAgeMetadata::mock_format_display();
let (first_line, remaining_lines) = age_metadata_yaml_string
.split_once('\n')
.expect("no newline delimeter in yaml age metadata");
metadata_string.push_str(&indoc::formatdoc! {"
age:
- {}
{}",
first_line,
textwrap::indent(remaining_lines, " ")
});
metadata_string.push_str(&display_integration_metadata_unit::<AgeIntegration>());
}

metadata_string.push_str(&format!("lastmodified: {}\n", LastModifiedDateTime::mock_display()));
metadata_string.push_str(&format!("mac: {}\n", S::Mac::mock_display()));

metadata_string
return metadata_string;

fn display_integration_metadata_unit<I: IntegrationTestUtils>() -> String
where
IntegrationMetadataUnit<I>: MockFileFormatUtil<YamlFileFormat>,
for<'a> &'a I::PublicKey: From<&'a I::Config>,
{
let integration_metadata = IntegrationMetadataUnit::<I>::mock_format_display();
let (first_metadata_line, remaning_metata_lines) = integration_metadata
.split_once('\n')
.expect("no newline delimeter in integration metadata");

indoc::formatdoc! {"
{}:
- {}
{}",
I::NAME,
first_metadata_line,
textwrap::indent(remaning_metata_lines, " ")
}
}
}
}
}
Expand All @@ -114,15 +139,9 @@ mod metadata {
mod age {
use crate::*;

impl MockFileFormatUtil<YamlFileFormat> for RopsFileAgeMetadata {
impl MockFileFormatUtil<YamlFileFormat> for AgeConfig {
fn mock_format_display() -> String {
indoc::formatdoc! {"
recipient: {}
enc: |
{}",
AgeIntegration::mock_public_key_str(),
textwrap::indent(AgeIntegration::mock_encrypted_data_key_str()," ")
}
format!("recipient: {}", AgeIntegration::mock_public_key_str(),)
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions crates/lib/src/rops_file/format/yaml/mod.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
use serde::{de::DeserializeOwned, Serialize};

use crate::*;

#[derive(Debug, PartialEq)]
pub struct YamlFileFormat;

impl FileFormat for YamlFileFormat {
type Map = serde_yaml::Mapping;

type SerializeError = serde_yaml::Error;
type DeserializeError = serde_yaml::Error;

fn serialize_to_string<T: serde::Serialize>(t: &T) -> Result<String, Self::SerializeError> {
fn serialize_to_string<T: Serialize>(t: &T) -> Result<String, Self::SerializeError> {
serde_yaml::to_string(t)
}

fn deserialize_from_str<T: serde::de::DeserializeOwned>(str: &str) -> Result<T, Self::DeserializeError> {
fn deserialize_from_str<T: DeserializeOwned>(str: &str) -> Result<T, Self::DeserializeError> {
serde_yaml::from_str(str)
}
}
Expand Down
Loading

0 comments on commit 9465157

Please sign in to comment.