cipherstash-client 0.12.5

The official CipherStash SDK
Documentation
use super::cts_config::CtsConfig;
use super::defaults::DEFAULT_VITUR_HOST;
use super::errors::ConfigError;
use super::vars::{
    CS_CLIENT_ACCESS_KEY, CS_CLIENT_ID, CS_CLIENT_KEY, CS_DECRYPTION_LOG, CS_WORKSPACE_ID,
    CS_ZEROKMS_HOST,
};
use crate::config::{
    console_config::ConsoleConfig, paths::resolve_config_dir, workspace::resolve_workspace,
};
use crate::credentials::service_credentials::ServiceCredentials;
use crate::zerokms::{ClientKey, ZeroKMS, ZeroKMSWithClientKey};
use std::path::PathBuf;
use std::str::FromStr;
use url::Url;
use uuid::Uuid;

#[derive(Default)]
pub struct ZeroKMSConfigBuilder {
    config_dir: Option<String>,
    base_url: Option<String>,
    workspace_id: Option<String>,
    decryption_log: Option<bool>,

    // auth
    access_key: Option<String>,
    console_config: Option<ConsoleConfig>,
    cts_config: Option<CtsConfig>,

    // client key/id
    client_id: Option<String>,
    client_key: Option<String>,
}

impl ZeroKMSConfigBuilder {
    pub fn build(&self) -> Result<ZeroKMSConfig, ConfigError> {
        let config_dir = match &self.config_dir {
            Some(s) => std::path::PathBuf::from(s),
            None => resolve_config_dir(None).map_err(|_| ConfigError::ValueNotSet("config_dir"))?,
        };
        std::fs::create_dir_all(&config_dir).map_err(|e| {
            ConfigError::Io(
                e.to_string(),
                config_dir.to_str().unwrap_or("Unknown").to_string(),
            )
        })?;

        let workspace_id = resolve_workspace(&config_dir, self.workspace_id.as_deref())
            .ok_or(ConfigError::ValueNotSet("workspace_id"))?;

        let workspace_dir = config_dir.join(&workspace_id);
        std::fs::create_dir_all(workspace_dir).map_err(|e| {
            ConfigError::Io(
                e.to_string(),
                config_dir.to_str().unwrap_or("Unknown").to_string(),
            )
        })?;

        let base_url = self
            .base_url
            .to_owned()
            .unwrap_or(DEFAULT_VITUR_HOST.to_string())
            .parse::<Url>()?;

        let auth_config = self.build_auth_config()?;

        let decryption_log = self.decryption_log.unwrap_or(false);

        Ok(ZeroKMSConfig {
            config_dir,
            base_url,
            workspace_id,
            auth_config,
            decryption_log,
            client_key: (),
        })
    }

    pub fn build_auth_config(&self) -> Result<ZeroKMSAuthConfig, ConfigError> {
        let console_config = match &self.console_config {
            Some(c) => c.to_owned(),
            None => ConsoleConfig::builder()
                .set_config_dir(self.config_dir.as_deref())
                .build()?,
        };

        let cts_config: CtsConfig = match &self.cts_config {
            Some(c) => c.to_owned(),
            None => CtsConfig::builder().build()?,
        };

        let auth_config = if let Some(access_key) = &self.access_key {
            ZeroKMSAuthConfig::AccessKey {
                access_key: access_key.to_string(),
                cts_config,
            }
        } else {
            ZeroKMSAuthConfig::UserAuth {
                cts_config,
                console_config,
            }
        };

        Ok(auth_config)
    }

    pub fn build_with_client_key(&self) -> Result<ZeroKMSConfigWithClientKey, ConfigError> {
        let base_config = self.build()?;

        let client_id = self
            .client_id
            .to_owned()
            .ok_or(ConfigError::ValueNotSet("Client ID"))?;

        let client_id = Uuid::from_str(&client_id).map_err(|_| {
            ConfigError::InvalidConfigError("Client ID was not a valid UUID".into())
        })?;

        let client_key = {
            let key_hex = self
                .client_key
                .to_owned()
                .ok_or(ConfigError::ValueNotSet("Client Key"))?;

            let key_bytes = hex::decode(key_hex)
                .map_err(|_| ConfigError::InvalidConfigError("Invalid Client Key".into()))?;

            ClientKey::from_bytes(client_id, &key_bytes)
                .map_err(|_| ConfigError::InvalidConfigError("Invalid Client Key".into()))?
        };

        Ok(ZeroKMSConfig::<ClientKey> {
            config_dir: base_config.config_dir,
            base_url: base_config.base_url,
            workspace_id: base_config.workspace_id,
            auth_config: base_config.auth_config,
            decryption_log: base_config.decryption_log,
            client_key,
        })
    }

    pub fn access_key(mut self, value: &str) -> Self {
        self.access_key = Some(value.to_string());
        self
    }

    pub fn base_url(mut self, value: &str) -> Self {
        self.base_url = Some(value.to_string());
        self
    }

    pub fn config_dir(mut self, value: &str) -> Self {
        self.config_dir = Some(value.to_owned());
        self
    }

    pub fn console_config(mut self, value: &ConsoleConfig) -> Self {
        self.console_config = Some(value.to_owned());
        self
    }

    pub fn cts_config(mut self, value: &CtsConfig) -> Self {
        self.cts_config = Some(value.to_owned());
        self
    }

    pub fn workspace_id(mut self, value: &str) -> Self {
        self.workspace_id = Some(value.to_owned());
        self
    }

    pub fn decryption_log(mut self, value: bool) -> Self {
        self.decryption_log = Some(value);
        self
    }

    pub fn client_id(mut self, value: &str) -> Self {
        self.client_id = Some(value.to_owned());
        self
    }

    pub fn client_key(mut self, value: &str) -> Self {
        self.client_key = Some(value.to_owned());
        self
    }

    /// Hydrate the configuration using environment variables
    pub fn with_env(mut self) -> Self {
        for name in CS_ZEROKMS_HOST {
            if let Ok(value) = std::env::var(name) {
                self.base_url = Some(value);
                break;
            }
        }

        if let Ok(value) = std::env::var(CS_CLIENT_ACCESS_KEY) {
            self.access_key = Some(value);
        }

        if let Ok(value) = std::env::var(CS_WORKSPACE_ID) {
            self.workspace_id = Some(value);
        }

        if let Ok(value) = std::env::var(CS_DECRYPTION_LOG) {
            self.decryption_log = Some(value.to_lowercase() == *"true");
        }

        if let Ok(client_id) = std::env::var(CS_CLIENT_ID) {
            self.client_id = Some(client_id);
        }

        if let Ok(client_key) = std::env::var(CS_CLIENT_KEY) {
            self.client_key = Some(client_key);
        }

        self
    }
}

#[derive(Clone)]
pub enum ZeroKMSAuthConfig {
    AccessKey {
        access_key: String,
        cts_config: CtsConfig,
    },
    UserAuth {
        console_config: ConsoleConfig,
        cts_config: CtsConfig,
    },
}

/// Configuration for the ZeroKMS client library.
///
/// The `ClientKeyState` generic is used to specify whether the config has a client key or not. If
/// the client doesn't have a `ClientKey` it can't be used to encrypt or decrypt data.
///
///
/// ```rust
/// # use cipherstash_client::ZeroKMSConfig;
/// let config = ZeroKMSConfig::builder()
///   .with_env()
///   .build()
///   .expect("failed to build config");
///
/// let client = config.create_client();
/// ```
#[derive(Clone)]
pub struct ZeroKMSConfig<ClientKeyState = ()> {
    config_dir: PathBuf,
    base_url: Url,
    workspace_id: String,
    auth_config: ZeroKMSAuthConfig,
    decryption_log: bool,
    client_key: ClientKeyState,
}

pub type ZeroKMSConfigWithClientKey = ZeroKMSConfig<ClientKey>;

impl ZeroKMSConfig {
    /// Create a new [`ZeroKMSConfigBuilder`] to build a [`ZeroKMS`] used to configure
    /// access to ZeroKMS.
    ///
    /// ```rust
    /// # use cipherstash_client::ZeroKMSConfig;
    /// let config = ZeroKMSConfig::builder()
    ///   .with_env()
    ///   .build()
    ///   .expect("failed to build config");
    ///
    /// let client = config.create_client();
    /// ```
    ///
    /// In situations where a [`ClientKey`] has been configured in the environment. You can use
    /// [`ZeroKMSConfigBuilder::build_with_client_key`].
    ///
    /// ```no_run
    /// # use cipherstash_client::ZeroKMSConfig;
    /// let config = ZeroKMSConfig::builder()
    ///   .with_env()
    ///   .build_with_client_key()
    ///   .expect("failed to build config");
    ///
    /// let client = config.create_client();
    /// ```
    pub fn builder() -> ZeroKMSConfigBuilder {
        ZeroKMSConfigBuilder::default()
    }

    /// Clone the current config and add in a [`ClientKey`].
    ///
    /// A [`ClientKey`] is required to create a [`ZeroKMS`] instance that can be used for
    /// encryption and decryption.
    pub fn clone_with_client_key(&self, client_key: ClientKey) -> ZeroKMSConfigWithClientKey {
        let ZeroKMSConfig {
            config_dir,
            base_url,
            workspace_id,
            auth_config,
            decryption_log,
            ..
        } = self.clone();

        ZeroKMSConfigWithClientKey {
            config_dir,
            base_url,
            workspace_id,
            auth_config,
            decryption_log,
            client_key,
        }
    }

    /// Create a new ZeroKMS client instance without a client key.
    ///
    /// In order to encrypt and decrypt data use [`ZeroKMS<ClientKey>`].
    pub fn create_client(&self) -> ZeroKMS<ServiceCredentials> {
        ZeroKMS::new(
            &self.base_url,
            self.credentials(),
            self.decryption_log_path().as_deref(),
        )
    }
}

impl ZeroKMSConfigWithClientKey {
    /// Return a copy of the [`ClientKey`] used for generating and retrieving data keys.
    pub fn client_key(&self) -> ClientKey {
        self.client_key.clone()
    }

    /// Create a new ZeroKMS client instance that can be used for encryption and decryption.
    pub fn create_client(&self) -> ZeroKMSWithClientKey<ServiceCredentials> {
        ZeroKMS::new_with_client_key(
            &self.base_url,
            self.credentials(),
            self.decryption_log_path().as_deref(),
            self.client_key(),
        )
    }
}

impl<T> ZeroKMSConfig<T> {
    /// The configuration directory for the current workspace.
    pub fn workspace_dir(&self) -> PathBuf {
        self.config_dir.join(&self.workspace_id)
    }

    /// The ID of the current workspace.
    pub fn workspace_id(&self) -> &str {
        &self.workspace_id
    }

    /// The path where the cached ZeroKMS access token is stored.
    pub fn token_path(&self) -> PathBuf {
        let token_file = match self.auth_config {
            ZeroKMSAuthConfig::AccessKey { .. } => "vitur-access-key-auth.json",
            ZeroKMSAuthConfig::UserAuth { .. } => "vitur-console-auth.json",
        };

        self.config_dir.join(&self.workspace_id).join(token_file)
    }

    /// The path to the decryption log file - if enabled.
    pub fn decryption_log_path(&self) -> Option<PathBuf> {
        self.decryption_log.then_some(
            self.config_dir
                .join(&self.workspace_id)
                .join("decryptions.log"),
        )
    }

    /// The base URL of the ZeroKMS API.
    pub fn base_url(&self) -> Url {
        self.base_url.to_owned()
    }

    pub(crate) fn auth_config(&self) -> &ZeroKMSAuthConfig {
        &self.auth_config
    }

    /// Return a [`crate::credentials::Credentials`] provider that can retrieve an access token to
    /// the ZeroKMS API.
    pub fn credentials(&self) -> ServiceCredentials {
        ServiceCredentials::build(self)
    }
}