Skip to main content

venice_e2ee_proxy/
keys.rs

1//! Startup proxy-instance key management.
2//!
3//! The proxy generates one secp256k1 keypair per process when configured to do
4//! so. E2EE code uses the private key to decrypt Venice response chunks and
5//! sends the uncompressed public key hex in Venice E2EE request headers.
6
7use std::{fmt, sync::Arc};
8
9use k256::{SecretKey, elliptic_curve::sec1::ToEncodedPoint};
10use rand_core::OsRng;
11use zeroize::ZeroizeOnDrop;
12
13use crate::config::KeysConfig;
14
15/// Per-process secp256k1 keypair used by this proxy instance.
16#[derive(Clone)]
17pub struct ProxyInstanceKey {
18    inner: Arc<ProxyInstanceKeyInner>,
19}
20
21/// Shared key material stored behind [`ProxyInstanceKey`] clones.
22struct ProxyInstanceKeyInner {
23    private_key: ProxyInstancePrivateKey,
24    public_key_hex: String,
25}
26
27/// Redacted, zeroizing wrapper around the proxy instance private key.
28struct ProxyInstancePrivateKey(SecretKey);
29
30impl ProxyInstancePrivateKey {
31    /// Wraps a secp256k1 private key for redacted debug output and zeroizing drop behavior.
32    fn new(private_key: SecretKey) -> Self {
33        Self(private_key)
34    }
35
36    /// Returns the matching uncompressed SEC1 public key as lowercase hex.
37    fn public_key_hex(&self) -> String {
38        let public_key = self.0.public_key();
39        hex::encode(public_key.to_encoded_point(false).as_bytes())
40    }
41
42    /// Returns the wrapped private key for local E2EE response decryption.
43    fn secret_key(&self) -> &SecretKey {
44        &self.0
45    }
46}
47
48impl ZeroizeOnDrop for ProxyInstancePrivateKey {}
49
50impl fmt::Debug for ProxyInstancePrivateKey {
51    /// Formats the private key wrapper without exposing key material.
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.write_str("[redacted]")
54    }
55}
56
57impl ProxyInstanceKey {
58    /// Generates a new secp256k1 keypair for this process.
59    pub fn generate() -> Self {
60        let private_key = SecretKey::random(&mut OsRng);
61        Self::from_private_key(private_key)
62    }
63
64    /// Generates a key when `keys.generate_proxy_instance_key_on_startup` is enabled.
65    pub fn generate_from_config(config: &KeysConfig) -> Option<Self> {
66        config
67            .generate_proxy_instance_key_on_startup
68            .then(Self::generate)
69    }
70
71    /// Builds proxy instance key state from an existing private key.
72    fn from_private_key(private_key: SecretKey) -> Self {
73        let private_key = ProxyInstancePrivateKey::new(private_key);
74        let public_key_hex = private_key.public_key_hex();
75
76        Self {
77            inner: Arc::new(ProxyInstanceKeyInner {
78                private_key,
79                public_key_hex,
80            }),
81        }
82    }
83
84    /// Uncompressed SEC1 public key encoded as lowercase hex.
85    ///
86    /// Venice expects 65 bytes (`04 || x || y`), represented as 130 hex chars.
87    pub fn public_key_hex(&self) -> &str {
88        &self.inner.public_key_hex
89    }
90
91    /// Returns the private key used to decrypt Venice E2EE response chunks.
92    #[allow(dead_code)]
93    pub(crate) fn private_key(&self) -> &SecretKey {
94        self.inner.private_key.secret_key()
95    }
96}
97
98impl fmt::Debug for ProxyInstanceKey {
99    /// Formats key metadata while redacting private key material.
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.debug_struct("ProxyInstanceKey")
102            .field("private_key", &"[redacted]")
103            .field("public_key_hex", &self.public_key_hex())
104            .finish()
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn generates_uncompressed_public_key_hex_in_venice_format() {
114        let key = ProxyInstanceKey::generate();
115
116        assert_eq!(key.public_key_hex().len(), 130);
117        assert!(key.public_key_hex().starts_with("04"));
118        assert!(key.public_key_hex().chars().all(|c| c.is_ascii_hexdigit()));
119        assert!(
120            key.public_key_hex()
121                .chars()
122                .all(|c| !c.is_ascii_uppercase())
123        );
124    }
125
126    #[test]
127    fn respects_startup_key_generation_config() {
128        let enabled = KeysConfig {
129            generate_proxy_instance_key_on_startup: true,
130        };
131        let disabled = KeysConfig {
132            generate_proxy_instance_key_on_startup: false,
133        };
134
135        assert!(ProxyInstanceKey::generate_from_config(&enabled).is_some());
136        assert!(ProxyInstanceKey::generate_from_config(&disabled).is_none());
137    }
138
139    #[test]
140    fn private_key_material_is_zeroized_on_drop() {
141        fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
142
143        assert_zeroize_on_drop::<SecretKey>();
144        assert_zeroize_on_drop::<ProxyInstancePrivateKey>();
145
146        let key = ProxyInstanceKey::generate();
147        let _private_key_ref: &SecretKey = key.private_key();
148    }
149
150    #[test]
151    fn debug_output_redacts_private_key_material() {
152        let key = ProxyInstanceKey::generate();
153        let debug = format!("{key:?}");
154
155        assert!(debug.contains("[redacted]"));
156        assert!(debug.contains(key.public_key_hex()));
157    }
158}