Skip to main content

common/crypto/
keys.rs

1use std::ops::Deref;
2
3use curve25519_dalek::edwards::CompressedEdwardsY;
4use iroh::{PublicKey as PPublicKey, SecretKey as SSecretKey};
5use serde::{Deserialize, Serialize};
6use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
7
8/// Size of Ed25519 private key in bytes
9pub const PRIVATE_KEY_SIZE: usize = 32;
10/// Size of Ed25519 public key in bytes
11pub const PUBLIC_KEY_SIZE: usize = 32;
12
13/// Errors that can occur during key operations
14#[derive(Debug, thiserror::Error)]
15pub enum KeyError {
16    #[error("key error: {0}")]
17    Default(#[from] anyhow::Error),
18}
19
20/// Public key for peer identity, key sharing, and update provenance
21///
22/// A thin wrapper around Iroh's `PublicKey`, representing the public part of an Ed25519 keypair.
23/// This key serves multiple purposes:
24/// - **Peer Identity**: Uniquely identifies a peer in the network (equivalent to Iroh's NodeId)
25/// - **Key Sharing**: Used in ECDH key exchange (after conversion to X25519)
26/// - **Access Control**: Listed in bucket shares to grant access
27///
28/// # Examples
29///
30/// ```ignore
31/// let secret_key = SecretKey::generate();
32/// let public_key = secret_key.public();
33///
34/// // Serialize to hex for storage/transmission
35/// let hex = public_key.to_hex();
36/// let recovered = PublicKey::from_hex(&hex)?;
37/// ```
38#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord, Copy)]
39pub struct PublicKey(PPublicKey);
40
41impl Deref for PublicKey {
42    type Target = PPublicKey;
43    fn deref(&self) -> &Self::Target {
44        &self.0
45    }
46}
47
48impl From<PPublicKey> for PublicKey {
49    fn from(key: PPublicKey) -> Self {
50        PublicKey(key)
51    }
52}
53
54impl From<PublicKey> for PPublicKey {
55    fn from(key: PublicKey) -> Self {
56        key.0
57    }
58}
59
60impl From<[u8; PUBLIC_KEY_SIZE]> for PublicKey {
61    fn from(bytes: [u8; PUBLIC_KEY_SIZE]) -> Self {
62        PublicKey(PPublicKey::from_bytes(&bytes).expect("valid public key"))
63    }
64}
65
66impl TryFrom<&[u8]> for PublicKey {
67    type Error = KeyError;
68    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
69        if bytes.len() != PUBLIC_KEY_SIZE {
70            return Err(anyhow::anyhow!(
71                "invalid public key size, expected {}, got {}",
72                PUBLIC_KEY_SIZE,
73                bytes.len()
74            )
75            .into());
76        }
77        let mut buff = [0; PUBLIC_KEY_SIZE];
78        buff.copy_from_slice(bytes);
79        Ok(buff.into())
80    }
81}
82
83impl PublicKey {
84    /// Parse a public key from a hexadecimal string
85    ///
86    /// Accepts both plain hex and "0x"-prefixed hex strings.
87    pub fn from_hex(hex: &str) -> Result<Self, KeyError> {
88        let hex = hex.strip_prefix("0x").unwrap_or(hex);
89        let mut buff = [0; PUBLIC_KEY_SIZE];
90        hex::decode_to_slice(hex, &mut buff)
91            .map_err(|_| anyhow::anyhow!("public key hex decode error"))?;
92        Ok(buff.into())
93    }
94
95    /// Convert public key to raw bytes
96    pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_SIZE] {
97        *self.0.as_bytes()
98    }
99
100    /// Convert public key to hexadecimal string
101    pub fn to_hex(&self) -> String {
102        hex::encode(self.to_bytes())
103    }
104
105    /// Convert Ed25519 public key to X25519 (Montgomery curve) for ECDH
106    ///
107    /// This conversion is necessary for the key sharing protocol, which uses
108    /// Elliptic Curve Diffie-Hellman (ECDH) to establish shared secrets.
109    /// Ed25519 uses the Edwards curve, while ECDH requires the Montgomery curve (X25519).
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if the Ed25519 point cannot be converted (invalid point).
114    #[allow(clippy::wrong_self_convention)]
115    pub(crate) fn to_x25519(&self) -> Result<X25519PublicKey, KeyError> {
116        let edwards_bytes = self.to_bytes();
117        let edwards_point = CompressedEdwardsY::from_slice(&edwards_bytes)
118            .map_err(|_| anyhow::anyhow!("public key invalid edwards point"))?
119            .decompress()
120            .ok_or_else(|| anyhow::anyhow!("public key failed to decompress edwards point"))?;
121
122        let montgomery_point = edwards_point.to_montgomery();
123        Ok(X25519PublicKey::from(montgomery_point.to_bytes()))
124    }
125
126    /// Verify an Ed25519 signature on a message.
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if:
131    /// - The public key bytes are invalid
132    /// - The signature verification fails
133    pub fn verify(
134        &self,
135        msg: &[u8],
136        signature: &ed25519_dalek::Signature,
137    ) -> Result<(), ed25519_dalek::SignatureError> {
138        let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&self.to_bytes())?;
139        verifying_key.verify_strict(msg, signature)
140    }
141}
142
143/// Secret key for peer identity and key sharing
144///
145/// A thin wrapper around Iroh's `SecretKey`, representing the private part of an Ed25519 keypair.
146/// This key should be kept secret and securely stored (e.g., in the local config file).
147///
148/// # Security Considerations
149///
150/// - Never share this key over the network
151/// - Store encrypted or in a secure location (e.g., `~/.config/jax/secret.pem`)
152/// - Generate a new key for each peer instance
153///
154/// # Examples
155///
156/// ```ignore
157/// // Generate a new keypair
158/// let secret_key = SecretKey::generate();
159/// let public_key = secret_key.public();
160///
161/// // Persist to PEM format
162/// let pem = secret_key.to_pem();
163/// std::fs::write("secret.pem", pem)?;
164///
165/// // Load from PEM
166/// let pem = std::fs::read_to_string("secret.pem")?;
167/// let recovered = SecretKey::from_pem(&pem)?;
168/// ```
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct SecretKey(pub SSecretKey);
171
172impl From<[u8; PRIVATE_KEY_SIZE]> for SecretKey {
173    fn from(secret: [u8; PRIVATE_KEY_SIZE]) -> Self {
174        Self(SSecretKey::from_bytes(&secret))
175    }
176}
177
178impl Deref for SecretKey {
179    type Target = SSecretKey;
180    fn deref(&self) -> &Self::Target {
181        &self.0
182    }
183}
184
185impl SecretKey {
186    /// Parse a secret key from a hexadecimal string
187    ///
188    /// Accepts both plain hex and "0x"-prefixed hex strings.
189    pub fn from_hex(hex: &str) -> Result<Self, KeyError> {
190        let hex = hex.strip_prefix("0x").unwrap_or(hex);
191        let mut buff = [0; PRIVATE_KEY_SIZE];
192        hex::decode_to_slice(hex, &mut buff)
193            .map_err(|_| anyhow::anyhow!("private key hex decode error"))?;
194        Ok(Self::from(buff))
195    }
196
197    /// Generate a new random secret key using a cryptographically secure RNG
198    pub fn generate() -> Self {
199        let mut bytes = [0u8; PRIVATE_KEY_SIZE];
200        getrandom::getrandom(&mut bytes).expect("failed to generate random bytes");
201        Self::from(bytes)
202    }
203
204    /// Derive the public key from this secret key
205    pub fn public(&self) -> PublicKey {
206        PublicKey(self.0.public())
207    }
208
209    /// Convert secret key to raw bytes
210    pub fn to_bytes(&self) -> [u8; PRIVATE_KEY_SIZE] {
211        self.0.to_bytes()
212    }
213
214    /// Convert secret key to hexadecimal string
215    pub fn to_hex(&self) -> String {
216        hex::encode(self.to_bytes())
217    }
218
219    /// Encode secret key in PEM format for secure storage
220    ///
221    /// Returns a PEM-encoded string with tag "PRIVATE KEY".
222    pub fn to_pem(&self) -> String {
223        let pem = pem::Pem::new("PRIVATE KEY", self.to_bytes());
224        pem::encode(&pem)
225    }
226
227    /// Parse a secret key from PEM format
228    ///
229    /// # Errors
230    ///
231    /// Returns an error if:
232    /// - The PEM string is malformed
233    /// - The PEM tag is not "PRIVATE KEY"
234    /// - The key size is incorrect
235    pub fn from_pem(pem_str: &str) -> Result<Self, KeyError> {
236        let pem = pem::parse(pem_str).map_err(|e| anyhow::anyhow!("failed to parse PEM: {}", e))?;
237
238        if pem.tag() != "PRIVATE KEY" {
239            return Err(anyhow::anyhow!("invalid PEM tag, expected PRIVATE KEY").into());
240        }
241
242        let contents = pem.contents();
243        if contents.len() != PRIVATE_KEY_SIZE {
244            return Err(anyhow::anyhow!(
245                "invalid private key size in PEM, expected {}, got {}",
246                PRIVATE_KEY_SIZE,
247                contents.len()
248            )
249            .into());
250        }
251
252        let mut bytes = [0u8; PRIVATE_KEY_SIZE];
253        bytes.copy_from_slice(contents);
254        Ok(Self::from(bytes))
255    }
256
257    /// Convert Ed25519 secret key to X25519 (Montgomery curve) for ECDH
258    ///
259    /// This conversion is used internally for the key sharing protocol.
260    /// The scalar bytes of the Ed25519 key are directly used as the X25519 private key.
261    pub(crate) fn to_x25519(&self) -> StaticSecret {
262        let signing_key = self.0.secret();
263        let scalar_bytes = signing_key.to_scalar_bytes();
264        StaticSecret::from(scalar_bytes)
265    }
266
267    /// Sign a message with this secret key using Ed25519.
268    ///
269    /// Returns a detached signature that can be verified with the corresponding public key.
270    pub fn sign(&self, msg: &[u8]) -> ed25519_dalek::Signature {
271        // iroh uses a different version of ed25519_dalek, so we need to convert
272        // the signature via bytes (both versions have the same 64-byte representation)
273        let sig = self.0.sign(msg);
274        ed25519_dalek::Signature::from_bytes(&sig.to_bytes())
275    }
276}
277
278#[cfg(test)]
279mod test {
280    use super::*;
281
282    #[test]
283    fn test_keypair_generation() {
284        let private_key = SecretKey::generate();
285        let public_key = private_key.public();
286
287        // Test round-trip conversion
288        let private_hex = private_key.to_hex();
289        let recovered_private = SecretKey::from_hex(&private_hex).unwrap();
290        assert_eq!(private_key.to_bytes(), recovered_private.to_bytes());
291
292        let public_hex = public_key.to_hex();
293        let recovered_public = PublicKey::from_hex(&public_hex).unwrap();
294        assert_eq!(public_key.to_bytes(), recovered_public.to_bytes());
295    }
296
297    #[test]
298    fn test_pem_serialization() {
299        let private_key = SecretKey::generate();
300
301        // Test round-trip PEM conversion
302        let pem = private_key.to_pem();
303        let recovered_private = SecretKey::from_pem(&pem).unwrap();
304        assert_eq!(private_key.to_bytes(), recovered_private.to_bytes());
305
306        // Verify the recovered key can produce the same public key
307        assert_eq!(
308            private_key.public().to_bytes(),
309            recovered_private.public().to_bytes()
310        );
311    }
312
313    #[test]
314    fn test_sign_and_verify() {
315        let secret_key = SecretKey::generate();
316        let public_key = secret_key.public();
317        let message = b"hello, world!";
318
319        // Sign the message
320        let signature = secret_key.sign(message);
321
322        // Verify the signature
323        assert!(public_key.verify(message, &signature).is_ok());
324
325        // Verify fails with wrong message
326        let wrong_message = b"hello, world?";
327        assert!(public_key.verify(wrong_message, &signature).is_err());
328
329        // Verify fails with wrong key
330        let other_key = SecretKey::generate().public();
331        assert!(other_key.verify(message, &signature).is_err());
332    }
333}