newton-core 0.4.16

newton protocol core sdk
//! Encryption configuration for HPKE key management.
//!
//! Standalone encryption key, decoupled from the transaction signing key
//! (`task_generator_signer`). Used by both gateway and operators for
//! encrypting/decrypting privacy data, identity data, policy secrets, and confidential data.

use serde::{Deserialize, Serialize};

/// Configuration for the HPKE encryption key.
///
/// The private key is a raw 32-byte key (hex-encoded), NOT derived from
/// any ECDSA key. It's shared across gateway and all operators in Phase 1.
/// For privacy data (identity, confidential, ephemeral), Phase 2 replaces
/// this with per-operator FROST DKG key shares. For policy client secrets,
/// Phase 2 replaces this with KMS-attested enclave derivation via
/// `EnclaveConfig.kms_seed_ciphertext` — the host never sees the key.
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(default)]
pub struct EncryptionConfig {
    /// Hex-encoded 32-byte private key for HPKE encryption/decryption.
    /// Env override: `{PREFIX}__ENCRYPTION__PRIVATE_KEY` (e.g., `OPERATOR__ENCRYPTION__PRIVATE_KEY`)
    #[serde(default)]
    pub private_key: String,
}

impl std::fmt::Debug for EncryptionConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("EncryptionConfig")
            .field("private_key", &"[REDACTED]")
            .finish()
    }
}

impl EncryptionConfig {
    /// Returns the private key as raw bytes, or None if not configured.
    ///
    /// Logs a warning when a key is present but malformed (odd-length hex,
    /// invalid hex characters, wrong byte length) so misconfigured deployments
    /// are visible in Datadog instead of silently proceeding without HPKE.
    pub fn private_key_bytes(&self) -> Option<[u8; 32]> {
        let key_hex = self.private_key.strip_prefix("0x").unwrap_or(&self.private_key);
        if key_hex.is_empty() {
            return None;
        }
        let bytes = match hex::decode(key_hex) {
            Ok(b) => b,
            Err(e) => {
                tracing::warn!(
                    hex_len = key_hex.len(),
                    "encryption private key is not valid hex: {e}. \
                     Key will be treated as unconfigured — HPKE decryption unavailable."
                );
                return None;
            }
        };
        if bytes.len() != 32 {
            tracing::warn!(
                actual_len = bytes.len(),
                expected_len = 32,
                "encryption private key has wrong byte length. \
                 Key will be treated as unconfigured — HPKE decryption unavailable."
            );
            return None;
        }
        let mut arr = [0u8; 32];
        arr.copy_from_slice(&bytes);
        Some(arr)
    }

    /// Returns true if an encryption key is configured.
    pub fn is_configured(&self) -> bool {
        self.private_key_bytes().is_some()
    }
}

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

    #[test]
    fn empty_key_returns_none() {
        let config = EncryptionConfig::default();
        assert!(config.private_key_bytes().is_none());
        assert!(!config.is_configured());
    }

    #[test]
    fn valid_hex_key_parses() {
        let config = EncryptionConfig {
            private_key: "a".repeat(64),
        };
        assert!(config.private_key_bytes().is_some());
        assert!(config.is_configured());
    }

    #[test]
    fn hex_key_with_0x_prefix_parses() {
        let config = EncryptionConfig {
            private_key: format!("0x{}", "b".repeat(64)),
        };
        assert!(config.private_key_bytes().is_some());
    }

    #[test]
    fn wrong_length_returns_none() {
        let config = EncryptionConfig {
            private_key: "abcd".to_string(),
        };
        assert!(config.private_key_bytes().is_none());
    }

    #[test]
    fn invalid_hex_returns_none() {
        let config = EncryptionConfig {
            private_key: "xyz".repeat(22),
        };
        assert!(config.private_key_bytes().is_none());
    }
}