nomad_protocol/crypto/
keys.rs

1//! X25519 key management
2//!
3//! Provides secure key generation and handling for the NOMAD protocol.
4
5use crate::core::{PRIVATE_KEY_SIZE, PUBLIC_KEY_SIZE, SESSION_ID_SIZE};
6use rand::{rngs::OsRng, RngCore};
7use x25519_dalek::{PublicKey, StaticSecret};
8use zeroize::Zeroize;
9
10/// A static X25519 keypair for long-term identity.
11///
12/// The private key is zeroized on drop for security.
13#[derive(Clone)]
14pub struct StaticKeypair {
15    /// Private key (32 bytes) - zeroized on drop
16    private: [u8; PRIVATE_KEY_SIZE],
17    /// Public key (32 bytes)
18    public: [u8; PUBLIC_KEY_SIZE],
19}
20
21impl StaticKeypair {
22    /// Generate a new random keypair.
23    pub fn generate() -> Self {
24        // Use snow's keypair generation for proper X25519 keys
25        let builder = snow::Builder::new("Noise_IK_25519_ChaChaPoly_BLAKE2s".parse().unwrap());
26        let keypair = builder.generate_keypair().unwrap();
27
28        let mut private_key = [0u8; PRIVATE_KEY_SIZE];
29        let mut public_key = [0u8; PUBLIC_KEY_SIZE];
30        private_key.copy_from_slice(&keypair.private);
31        public_key.copy_from_slice(&keypair.public);
32
33        Self {
34            private: private_key,
35            public: public_key,
36        }
37    }
38
39    /// Create a keypair from existing key material.
40    ///
41    /// # Safety
42    /// The caller must ensure the private key is valid X25519 key material.
43    pub fn from_bytes(private: [u8; PRIVATE_KEY_SIZE], public: [u8; PUBLIC_KEY_SIZE]) -> Self {
44        Self { private, public }
45    }
46
47    /// Get the public key.
48    pub fn public_key(&self) -> &[u8; PUBLIC_KEY_SIZE] {
49        &self.public
50    }
51
52    /// Get the private key.
53    ///
54    /// # Security
55    /// Handle with care - this exposes sensitive key material.
56    pub fn private_key(&self) -> &[u8; PRIVATE_KEY_SIZE] {
57        &self.private
58    }
59
60    /// Compute the static DH shared secret with a remote public key.
61    ///
62    /// This computes DH(our_static, their_static) which is used for
63    /// deriving the rekey authentication key for PCS.
64    ///
65    /// # Arguments
66    /// * `remote_public` - The remote party's static public key
67    ///
68    /// # Returns
69    /// The 32-byte shared secret
70    pub fn compute_static_dh(&self, remote_public: &[u8; PUBLIC_KEY_SIZE]) -> [u8; 32] {
71        let secret = StaticSecret::from(self.private);
72        let public = PublicKey::from(*remote_public);
73        let shared = secret.diffie_hellman(&public);
74        *shared.as_bytes()
75    }
76}
77
78impl Drop for StaticKeypair {
79    fn drop(&mut self) {
80        self.private.zeroize();
81    }
82}
83
84/// Session ID - 48-bit random identifier for session demultiplexing.
85#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
86pub struct SessionId(pub [u8; SESSION_ID_SIZE]);
87
88impl SessionId {
89    /// Generate a new random session ID.
90    pub fn generate() -> Self {
91        let mut id = [0u8; SESSION_ID_SIZE];
92        OsRng.fill_bytes(&mut id);
93        Self(id)
94    }
95
96    /// Create from raw bytes.
97    pub fn from_bytes(bytes: [u8; SESSION_ID_SIZE]) -> Self {
98        Self(bytes)
99    }
100
101    /// Get the raw bytes.
102    pub fn as_bytes(&self) -> &[u8; SESSION_ID_SIZE] {
103        &self.0
104    }
105}
106
107impl AsRef<[u8]> for SessionId {
108    fn as_ref(&self) -> &[u8] {
109        &self.0
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_keypair_generation() {
119        let kp1 = StaticKeypair::generate();
120        let kp2 = StaticKeypair::generate();
121
122        // Keys should be different
123        assert_ne!(kp1.public_key(), kp2.public_key());
124        assert_ne!(kp1.private_key(), kp2.private_key());
125
126        // Keys should be correct size
127        assert_eq!(kp1.public_key().len(), PUBLIC_KEY_SIZE);
128        assert_eq!(kp1.private_key().len(), PRIVATE_KEY_SIZE);
129    }
130
131    #[test]
132    fn test_session_id_generation() {
133        let id1 = SessionId::generate();
134        let id2 = SessionId::generate();
135
136        // IDs should be different (with overwhelming probability)
137        assert_ne!(id1, id2);
138        assert_eq!(id1.as_bytes().len(), SESSION_ID_SIZE);
139    }
140
141    #[test]
142    fn test_session_id_from_bytes() {
143        let bytes = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
144        let id = SessionId::from_bytes(bytes);
145        assert_eq!(id.as_bytes(), &bytes);
146    }
147}