rops 0.1.7

SOPS-like library in pure Rust
Documentation
use std::io::{Read, Write};

use age::{
    armor::{ArmoredReader, ArmoredWriter, Format},
    Decryptor, Recipient,
};

use crate::*;

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

impl AgeIntegration {
    const APPROX_MAX_ARMORED_DATA_KEY_LENGTH: usize = 400;
}

impl Integration for AgeIntegration {
    const NAME: &'static str = "age";
    type KeyId = age::x25519::Recipient;
    type PrivateKey = age::x25519::Identity;
    type Config = AgeConfig;

    fn parse_key_id(key_id_str: &str) -> IntegrationResult<Self::KeyId> {
        key_id_str
            .parse()
            .map_err(|err: &str| IntegrationError::KeyIdParsing(anyhow::anyhow!(err)))
    }

    fn parse_private_key(private_key_str: impl AsRef<str>) -> IntegrationResult<Self::PrivateKey> {
        private_key_str
            .as_ref()
            .parse()
            .map_err(|err: &str| IntegrationError::PrivateKeyParsing(anyhow::anyhow!(err)))
    }

    fn encrypt_data_key(key_id: &Self::KeyId, data_key: &DataKey) -> IntegrationResult<String> {
        let unarmored_buffer = {
            // IMPROVEMENT: avoid vec box allocation
            let encryptor =
                age::Encryptor::with_recipients([key_id as &dyn Recipient].into_iter()).expect("provided recipients should be non-empty");

            let mut unarmored_encrypted_buffer = Vec::with_capacity(DataKey::byte_size());
            let mut encryption_writer = encryptor.wrap_output(&mut unarmored_encrypted_buffer)?;
            encryption_writer.write_all(data_key.as_ref())?;
            encryption_writer.finish()?;
            unarmored_encrypted_buffer
        };

        let mut armored_buffer = Vec::with_capacity(Self::APPROX_MAX_ARMORED_DATA_KEY_LENGTH);
        let mut armored_writer = ArmoredWriter::wrap_output(&mut armored_buffer, Format::AsciiArmor)?;
        armored_writer.write_all(&unarmored_buffer)?;
        armored_writer.finish()?;

        Ok(String::from_utf8(armored_buffer)?)
    }

    fn decrypt_data_key(key_id: &Self::KeyId, encrypted_data_key: &str) -> IntegrationResult<Option<DataKey>> {
        let private_keys = Self::retrieve_private_keys()?;

        let Some(matched_private_key) = private_keys.into_iter().find(|private_key| &private_key.to_public() == key_id) else {
            return Ok(None);
        };

        let mut unarmored_encrypted_buffer = Vec::with_capacity(Self::APPROX_MAX_ARMORED_DATA_KEY_LENGTH);

        ArmoredReader::new(encrypted_data_key.as_bytes()).read_to_end(&mut unarmored_encrypted_buffer)?;

        let decryptor = Decryptor::new(unarmored_encrypted_buffer.as_slice())?;

        let mut decrypted_data_key_buffer = DataKey::empty();
        let mut reader = decryptor.decrypt(std::iter::once(&matched_private_key as &dyn age::Identity))?;
        reader.read_exact(decrypted_data_key_buffer.as_mut())?;

        Ok(Some(decrypted_data_key_buffer))
    }

    fn select_metadata_units(integration_metadata: &mut IntegrationMetadata) -> &mut IntegrationMetadataUnits<Self> {
        &mut integration_metadata.age
    }
}

mod error {
    use super::*;

    impl From<age::EncryptError> for IntegrationError {
        fn from(encrypt_error: age::EncryptError) -> Self {
            Self::Encryption(encrypt_error.into())
        }
    }

    impl From<age::DecryptError> for IntegrationError {
        fn from(decrypt_error: age::DecryptError) -> Self {
            Self::Decryption(decrypt_error.into())
        }
    }
}

mod key_id {
    use super::*;

    impl AppendIntegrationKey<AgeIntegration> for age::x25519::Recipient {
        fn append_to_metadata_builder(self, integration_metadata_builder: &mut IntegrationMetadataBuilder) {
            integration_metadata_builder.age_key_ids.push(self)
        }
    }

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

        impl MockDisplayTestUtil for age::x25519::Recipient {
            fn mock_display() -> String {
                "age1se5ghfycr4n8kcwc3qwf234ymvmr2lex2a99wh8gpfx97glwt9hqch4569".to_string()
            }
        }

        impl MockTestUtil for age::x25519::Recipient {
            fn mock() -> Self {
                Self::mock_display().parse().unwrap()
            }
        }

        impl MockOtherTestUtil for age::x25519::Recipient {
            fn mock_other() -> Self {
                "age1qazf43xll4ramx3wcn7h2yl9scycxdhrwge8862vv6zj97pafdvq0d5mn6".parse().unwrap()
            }
        }
    }
}

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 key_id: <AgeIntegration as Integration>::KeyId,
    }

    impl IntegrationConfig<AgeIntegration> for AgeConfig {
        const INCLUDE_DATA_KEY_CREATED_AT: bool = false;

        fn new(key_id: <AgeIntegration as Integration>::KeyId) -> Self {
            Self { key_id }
        }

        fn key_id(&self) -> &<AgeIntegration as Integration>::KeyId {
            &self.key_id
        }
    }

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

        impl MockTestUtil for AgeConfig {
            fn mock() -> Self {
                Self {
                    key_id: MockTestUtil::mock(),
                }
            }
        }
    }
}

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

    impl IntegrationTestUtils for AgeIntegration {
        fn mock_private_key_str() -> impl AsRef<str> {
            "AGE-SECRET-KEY-1EQUCGFZH8UZKSZ0Z5N5T234YRNDT4U9H7QNYXWRRNJYDDVXE6FWSCPGNJ7"
        }

        fn mock_encrypted_data_key_str() -> &'static str {
            indoc::indoc! {"
                -----BEGIN AGE ENCRYPTED FILE-----
                YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKeE9VRHJpNmc4Z1NFeDd6
                L3cybjRHblYvaFUxbk9JZDZ4RFdENGpiNmhZCnZCRXRNSlRZeno0SDlJWXdhT0xl
                Y1BlMzcyYUdVWFJ6WEVMTlRRaDRGbFUKLS0tIGc0V3gzU043MzBUd01BVTVKTEwr
                azRyUldHUXo0cTV2YlZWa2pwcWFweGcKQdFW597WOM0bYfycoA2A0JxjKlrka+lc
                MLuTri7QMM+g8yXcjneEGxjobGIqnvARlzDwcnFMxBoZ5/KRjMipXA==
                -----END AGE ENCRYPTED FILE-----
            "}
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    generate_integration_test_suite!(AgeIntegration);
}