rops 0.1.7

SOPS-like library in pure Rust
Documentation
use std::borrow::Cow;

use crate::*;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RopsValue {
    String(String),
    Boolean(bool),
    Integer(i64),
    Float(RopsFloat),
}

pub use rops_float::RopsFloat;
mod rops_float {

    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
    // Stored as String to enable hashing
    pub struct RopsFloat(String);

    impl From<f64> for RopsFloat {
        fn from(float: f64) -> Self {
            Self(float.to_string())
        }
    }

    impl From<RopsFloat> for f64 {
        fn from(rops_float: RopsFloat) -> Self {
            rops_float.0.parse().expect("inner string not a valid f64 string")
        }
    }

    impl RopsFloat {
        pub fn as_bytes(&self) -> &[u8] {
            self.0.as_bytes()
        }
    }
}

impl RopsValue {
    const BOOLEAN_TRUE_BYTES: &'static [u8] = b"True";
    const BOOLEAN_FALSE_BYTES: &'static [u8] = b"False";

    pub fn encrypt<C: Cipher>(
        &self,
        nonce: Nonce<C::NonceSize>,
        data_key: &DataKey,
        key_path: &KeyPath,
    ) -> Result<EncryptedRopsValue<C>, C::Error> {
        let mut in_place_buffer = self.as_bytes().to_vec();

        let authorization_tag = C::encrypt(&nonce, data_key, &mut in_place_buffer, key_path.as_ref())?;

        Ok(EncryptedRopsValue {
            data: in_place_buffer.into(),
            authorization_tag,
            nonce,
            value_variant: self.into(),
        })
    }

    pub fn as_bytes(&self) -> Cow<'_, [u8]> {
        match self {
            RopsValue::String(string) => Cow::Borrowed(string.as_bytes()),
            RopsValue::Boolean(boolean) => Cow::Borrowed(match boolean {
                true => Self::BOOLEAN_TRUE_BYTES,
                false => Self::BOOLEAN_FALSE_BYTES,
            }),
            RopsValue::Integer(integer) => Cow::Owned(integer.to_string().into_bytes()),
            RopsValue::Float(float_string) => Cow::Borrowed(float_string.as_bytes()),
        }
    }

    pub fn from_bytes(bytes: Vec<u8>, variant: RopsValueVariant) -> Result<Self, RopsValueFromBytesError> {
        Ok(match variant {
            RopsValueVariant::String => Self::String(std::str::from_utf8(&bytes)?.to_string()),
            RopsValueVariant::Boolean => Self::Boolean(match bytes.as_slice() {
                Self::BOOLEAN_TRUE_BYTES => true,
                Self::BOOLEAN_FALSE_BYTES => false,
                _ => return Err(RopsValueFromBytesError::Boolean(bytes)),
            }),
            RopsValueVariant::Integer => Self::Integer(std::str::from_utf8(&bytes)?.parse()?),
            RopsValueVariant::Float => Self::Float(std::str::from_utf8(&bytes)?.parse::<f64>()?.into()),
        })
    }
}

#[derive(Debug, thiserror::Error)]
pub enum RopsValueFromBytesError {
    #[error("unable to validate bytes as UTF-8: {0}")]
    String(#[from] std::str::Utf8Error),
    #[error("invalid byte representation for boolean values: {0:?}")]
    Boolean(Vec<u8>),
    #[error("integer parse error: {0}")]
    Integer(#[from] std::num::ParseIntError),
    #[error("float parse error: {0}")]
    Float(#[from] std::num::ParseFloatError),
}

impl From<&RopsValue> for RopsValueVariant {
    fn from(value: &RopsValue) -> Self {
        match value {
            RopsValue::String(_) => RopsValueVariant::String,
            RopsValue::Boolean(_) => RopsValueVariant::Boolean,
            RopsValue::Integer(_) => RopsValueVariant::Integer,
            RopsValue::Float(_) => RopsValueVariant::Float,
        }
    }
}

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

    impl MockTestUtil for RopsValue {
        fn mock() -> Self {
            Self::String("world!".to_string())
        }
    }
}

#[cfg(test)]
mod tests {
    #[cfg(feature = "aes-gcm")]
    mod aes_gcm {
        use crate::*;

        fn assert_encrypts_value(expected_encrypted_value_str: &str, key_path: &str, rops_value: RopsValue) {
            let key_path = KeyPath::from(key_path.to_string());
            let expected_encrypted_value = expected_encrypted_value_str.parse::<EncryptedRopsValue<AES256GCM>>().unwrap();
            assert_eq!(
                expected_encrypted_value,
                rops_value
                    .encrypt(expected_encrypted_value.nonce.clone(), &MockTestUtil::mock(), &key_path)
                    .unwrap()
            )
        }

        #[test]
        fn encrypts_string_value() {
            assert_encrypts_value(
                "ENC[AES256_GCM,data:3S1E9am/,iv:WUQoQTrRXw/tUgwpmSG69xWtd5dVMfe8qUly1VB8ucM=,tag:nQUDkuh0OR1cjR5hGC5jOw==,type:str]",
                "hello:",
                RopsValue::String("world!".to_string()),
            );
        }

        #[test]
        fn encrypts_boolean_true_value() {
            assert_encrypts_value(
                "ENC[AES256_GCM,data:0wTZfQ==,iv:BpeJcPsLzvRLyGOAyA/mM3nGhg3zIFEcpyfB5jJbul8=,tag:+OGu7RruuYSwMWZa1yWrqA==,type:bool]",
                "example_booleans:",
                RopsValue::Boolean(true),
            );
        }

        #[test]
        fn encrypts_boolean_false_value() {
            assert_encrypts_value(
                "ENC[AES256_GCM,data:4EgnUYs=,iv:g0r5WzzWt/Ln25wlEescMgrTg88JTJhlOdI0g/xVahk=,tag:zhv8xxJULpXIWdzm5+C0FA==,type:bool]",
                "example_booleans:",
                RopsValue::Boolean(false),
            );
        }

        #[test]
        fn encrypts_integer_value() {
            assert_encrypts_value(
                "ENC[AES256_GCM,data:lDJCrw==,iv:P8EXxNCPeYp5VBL0mCAxjQjGtvywbBFoQKWye2IK1Gc=,tag:56HP04AzkYfj+pmYIbijSA==,type:int]",
                "example_integer:",
                RopsValue::Integer(1234),
            );
        }
        #[test]
        fn encrypts_float_value() {
            assert_encrypts_value(
                "ENC[AES256_GCM,data:fglPlT+e9ACWrA==,iv:pefOFnMThS6qICGrLuai+rSBtrmliGWdqJrXzcl2qAo=,tag:tHVQiwreFZurqiroPCIXHw==,type:float]",
                "example_float:",
                RopsValue::Float(1234.56789.into()),
            );
        }
    }
}