rops 0.1.7

SOPS-like library in pure Rust
Documentation
use std::{
    fmt::{Display, Formatter},
    str::FromStr,
};

use crate::*;

#[impl_tools::autoimpl(Debug, PartialEq)]
pub struct EncryptedRopsValue<C: Cipher> {
    pub data: EncryptedData,
    pub authorization_tag: AuthorizationTag<C>,
    pub nonce: Nonce<C::NonceSize>,
    pub value_variant: RopsValueVariant,
}

#[derive(Debug, thiserror::Error)]
pub enum DecryptRopsValueError {
    #[error("encountered cipher error: {0}")]
    Cipher(anyhow::Error),
    #[error("unable convert value from decrypted bytes: {0}")]
    FromBytes(#[from] RopsValueFromBytesError),
}

#[derive(Debug, thiserror::Error)]
pub enum EncryptedRopsValueFromStrError {
    #[error("missing {0}")]
    Missing(&'static str),
    #[error("invalid cipher: {0}, expected: {1}")]
    InvalidCipher(String, &'static str),
    #[error("unable to parse value type: {0}")]
    ValueVariantFromStr(String),
    #[error(transparent)]
    Base64Decode(#[from] Base64DecodeError),
}

impl<C: Cipher> EncryptedRopsValue<C> {
    pub fn decrypt(self, data_key: &DataKey, key_path: &KeyPath) -> Result<RopsValue, DecryptRopsValueError> {
        let mut in_place_buffer = self.data;

        C::decrypt(
            &self.nonce,
            data_key,
            in_place_buffer.as_mut(),
            key_path.as_ref(),
            &self.authorization_tag,
        )
        .map_err(|error| DecryptRopsValueError::Cipher(error.into()))?;

        RopsValue::from_bytes(in_place_buffer.into(), self.value_variant).map_err(Into::into)
    }
}

impl<C: Cipher> Display for EncryptedRopsValue<C> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "ENC[{},data:{},iv:{},tag:{},type:{}]",
            C::NAME,
            self.data.encode_base64(),
            self.nonce.encode_base64(),
            self.authorization_tag.as_ref().encode_base64(),
            self.value_variant.as_ref(),
        )
    }
}

impl<C: Cipher> FromStr for EncryptedRopsValue<C> {
    type Err = EncryptedRopsValueFromStrError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        use EncryptedRopsValueFromStrError::*;

        let mut encrypted_value_components = input
            .strip_prefix("ENC")
            .ok_or(Missing("ENC prefix"))?
            .strip_prefix('[')
            .ok_or(Missing("opening ('[') bracket"))?
            .strip_suffix(']')
            .ok_or(Missing("closing (']') bracket"))?
            .split(',');

        let cipher_variant_str = encrypted_value_components.next().ok_or(Missing("cipher"))?;

        if cipher_variant_str != C::NAME {
            return Err(InvalidCipher(cipher_variant_str.to_string(), C::NAME));
        }

        let data = encrypted_value_components
            .next()
            .and_then(|next_component| next_component.strip_prefix("data:"))
            .ok_or(Missing("'data' key-value pair"))
            .and_then(|base64_str| base64_str.parse().map_err(Into::into))?;

        let nonce = encrypted_value_components
            .next()
            .and_then(|next_component| next_component.strip_prefix("iv:"))
            .ok_or(Missing("'iv' (initialization vector) key-value pair"))
            .and_then(|base64_str| base64_str.parse().map_err(Into::into))?;

        let authorization_tag = encrypted_value_components
            .next()
            .and_then(|next_component| next_component.strip_prefix("tag:"))
            .ok_or(Missing("'tag' (authorization tag) key-value pair"))
            .and_then(|base64_str| base64_str.parse().map_err(Into::into))?;

        let value_variant = encrypted_value_components
            .next()
            .and_then(|value_type_component| value_type_component.strip_prefix("type:"))
            .ok_or(Missing("'type' (value type) key-value pair"))
            .and_then(|variant_str| variant_str.parse().map_err(|_| ValueVariantFromStr(variant_str.to_string())))?;

        Ok(Self {
            data,
            authorization_tag,
            nonce,
            value_variant,
        })
    }
}

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

    impl<C: Cipher> MockTestUtil for EncryptedRopsValue<C>
    where
        AuthorizationTag<C>: MockTestUtil,
        Nonce<C::NonceSize>: MockTestUtil,
    {
        fn mock() -> Self {
            Self {
                data: MockTestUtil::mock(),
                authorization_tag: MockTestUtil::mock(),
                nonce: MockTestUtil::mock(),
                value_variant: RopsValueVariant::String,
            }
        }
    }

    impl<C: Cipher> MockDisplayTestUtil for EncryptedRopsValue<C>
    where
        AuthorizationTag<C>: MockDisplayTestUtil,
    {
        fn mock_display() -> String {
            format!(
                "ENC[{},data:{},iv:{},tag:{},type:str]",
                C::NAME,
                EncryptedData::mock_display(),
                Nonce::mock_display(),
                AuthorizationTag::mock_display()
            )
        }
    }
}

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

        #[test]
        fn disallows_cipher_name_mismatch() {
            let parse_error = EncryptedRopsValue::<AES256GCM>::mock_display()
                .replace(AES256GCM::NAME, "gibberish")
                .parse::<EncryptedRopsValue<AES256GCM>>()
                .unwrap_err();
            assert!(matches!(parse_error, EncryptedRopsValueFromStrError::InvalidCipher(_, _)))
        }

        #[test]
        fn displays_value() {
            DisplayTestUtils::assert_display::<EncryptedRopsValue<AES256GCM>>()
        }

        #[test]
        fn parses_value() {
            FromStrTestUtils::assert_parse::<EncryptedRopsValue<AES256GCM>>()
        }

        #[test]
        fn decrypts_value() {
            assert_eq!(
                RopsValue::mock(),
                EncryptedRopsValue::<AES256GCM>::mock()
                    .decrypt(&MockTestUtil::mock(), &MockTestUtil::mock())
                    .unwrap()
            )
        }
    }
}