Skip to main content

rns_crypto/
identity.rs

1use alloc::vec::Vec;
2use core::fmt;
3
4use crate::ed25519::{Ed25519PrivateKey, Ed25519PublicKey};
5use crate::hkdf;
6use crate::sha256;
7use crate::token::{Token, TokenError};
8use crate::x25519::{X25519PrivateKey, X25519PublicKey};
9use crate::Rng;
10
11pub const KEYSIZE: usize = 512; // bits
12pub const DERIVED_KEY_LENGTH: usize = 64; // bytes
13pub const TRUNCATED_HASHLENGTH: usize = 128; // bits (16 bytes)
14
15#[derive(Debug)]
16pub enum CryptoError {
17    NoPrivateKey,
18    NoPublicKey,
19    TokenError(TokenError),
20    HkdfError(hkdf::HkdfError),
21    InvalidCiphertext,
22    InvalidPublicKey,
23}
24
25impl fmt::Display for CryptoError {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            CryptoError::NoPrivateKey => write!(f, "No private key"),
29            CryptoError::NoPublicKey => write!(f, "No public key"),
30            CryptoError::TokenError(e) => write!(f, "Token error: {}", e),
31            CryptoError::HkdfError(e) => write!(f, "HKDF error: {}", e),
32            CryptoError::InvalidCiphertext => write!(f, "Invalid ciphertext"),
33            CryptoError::InvalidPublicKey => write!(f, "Invalid public key"),
34        }
35    }
36}
37
38#[cfg(feature = "std")]
39impl std::error::Error for CryptoError {}
40
41pub struct Identity {
42    prv: Option<X25519PrivateKey>,
43    sig_prv: Option<Ed25519PrivateKey>,
44    pub_key: Option<X25519PublicKey>,
45    sig_pub: Option<Ed25519PublicKey>,
46    hash: [u8; 16],
47}
48
49impl Identity {
50    pub fn new(rng: &mut dyn Rng) -> Self {
51        let prv = X25519PrivateKey::generate(rng);
52        let sig_prv = Ed25519PrivateKey::generate(rng);
53
54        let pub_key = prv.public_key();
55        let sig_pub = sig_prv.public_key();
56
57        let mut pub_bytes = [0u8; 64];
58        pub_bytes[..32].copy_from_slice(&pub_key.public_bytes());
59        pub_bytes[32..].copy_from_slice(&sig_pub.public_bytes());
60
61        let hash = truncated_hash(&pub_bytes);
62
63        Identity {
64            prv: Some(prv),
65            sig_prv: Some(sig_prv),
66            pub_key: Some(pub_key),
67            sig_pub: Some(sig_pub),
68            hash,
69        }
70    }
71
72    /// Load a complete private identity from its fixed-width key material.
73    ///
74    /// Valid input is represented by the 64-byte type itself, so successful
75    /// construction returns `Self` directly instead of a fall-through status.
76    pub fn from_private_key(prv_bytes: &[u8; 64]) -> Self {
77        let x_prv_bytes: [u8; 32] = prv_bytes[..32].try_into().unwrap();
78        let ed_seed: [u8; 32] = prv_bytes[32..].try_into().unwrap();
79
80        let prv = X25519PrivateKey::from_bytes(&x_prv_bytes);
81        let sig_prv = Ed25519PrivateKey::from_bytes(&ed_seed);
82
83        let pub_key = prv.public_key();
84        let sig_pub = sig_prv.public_key();
85
86        let mut pub_bytes = [0u8; 64];
87        pub_bytes[..32].copy_from_slice(&pub_key.public_bytes());
88        pub_bytes[32..].copy_from_slice(&sig_pub.public_bytes());
89
90        let hash = truncated_hash(&pub_bytes);
91
92        Identity {
93            prv: Some(prv),
94            sig_prv: Some(sig_prv),
95            pub_key: Some(pub_key),
96            sig_pub: Some(sig_pub),
97            hash,
98        }
99    }
100
101    /// Load a public-only identity from fixed-width public key material.
102    ///
103    /// Any 32 bytes form a valid X25519 public key, but the Ed25519 half must
104    /// decode to a valid curve point; otherwise this returns
105    /// `CryptoError::InvalidPublicKey`.
106    pub fn from_public_key(pub_bytes: &[u8; 64]) -> Result<Self, CryptoError> {
107        let x_pub_bytes: [u8; 32] = pub_bytes[..32].try_into().unwrap();
108        let ed_pub_bytes: [u8; 32] = pub_bytes[32..].try_into().unwrap();
109
110        let pub_key = X25519PublicKey::from_bytes(&x_pub_bytes);
111        let sig_pub = Ed25519PublicKey::from_bytes(&ed_pub_bytes)
112            .map_err(|_| CryptoError::InvalidPublicKey)?;
113
114        let hash = truncated_hash(pub_bytes);
115
116        Ok(Identity {
117            prv: None,
118            sig_prv: None,
119            pub_key: Some(pub_key),
120            sig_pub: Some(sig_pub),
121            hash,
122        })
123    }
124
125    pub fn get_private_key(&self) -> Option<[u8; 64]> {
126        match (&self.prv, &self.sig_prv) {
127            (Some(prv), Some(sig_prv)) => {
128                let mut result = [0u8; 64];
129                result[..32].copy_from_slice(&prv.private_bytes());
130                result[32..].copy_from_slice(&sig_prv.private_bytes());
131                Some(result)
132            }
133            _ => None,
134        }
135    }
136
137    pub fn get_public_key(&self) -> Option<[u8; 64]> {
138        match (&self.pub_key, &self.sig_pub) {
139            (Some(pub_key), Some(sig_pub)) => {
140                let mut result = [0u8; 64];
141                result[..32].copy_from_slice(&pub_key.public_bytes());
142                result[32..].copy_from_slice(&sig_pub.public_bytes());
143                Some(result)
144            }
145            _ => None,
146        }
147    }
148
149    pub fn hash(&self) -> &[u8; 16] {
150        &self.hash
151    }
152
153    pub fn encrypt(&self, plaintext: &[u8], rng: &mut dyn Rng) -> Result<Vec<u8>, CryptoError> {
154        let pub_key = self.pub_key.as_ref().ok_or(CryptoError::NoPublicKey)?;
155        self.encrypt_to_public_key(plaintext, pub_key, rng)
156    }
157
158    pub fn encrypt_with_ratchet(
159        &self,
160        plaintext: &[u8],
161        ratchet: Option<&[u8; 32]>,
162        rng: &mut dyn Rng,
163    ) -> Result<Vec<u8>, CryptoError> {
164        match ratchet {
165            Some(ratchet_pub_bytes) => {
166                let ratchet_pub = X25519PublicKey::from_bytes(ratchet_pub_bytes);
167                self.encrypt_to_public_key(plaintext, &ratchet_pub, rng)
168            }
169            None => self.encrypt(plaintext, rng),
170        }
171    }
172
173    fn encrypt_to_public_key(
174        &self,
175        plaintext: &[u8],
176        target_public_key: &X25519PublicKey,
177        rng: &mut dyn Rng,
178    ) -> Result<Vec<u8>, CryptoError> {
179        let ephemeral = X25519PrivateKey::generate(rng);
180        let ephemeral_pub_bytes = ephemeral.public_key().public_bytes();
181        let shared_key = ephemeral.exchange(target_public_key);
182
183        let derived_key = hkdf::hkdf(DERIVED_KEY_LENGTH, &shared_key, Some(&self.hash), None)
184            .map_err(CryptoError::HkdfError)?;
185
186        let token = Token::new(&derived_key).map_err(CryptoError::TokenError)?;
187        let ciphertext = token.encrypt(plaintext, rng);
188
189        let mut result = Vec::with_capacity(32 + ciphertext.len());
190        result.extend_from_slice(&ephemeral_pub_bytes);
191        result.extend_from_slice(&ciphertext);
192        Ok(result)
193    }
194
195    /// Encrypt with a specific ephemeral key and IV for deterministic testing
196    pub fn encrypt_deterministic(
197        &self,
198        plaintext: &[u8],
199        ephemeral_prv: &[u8; 32],
200        iv: &[u8; 16],
201    ) -> Result<Vec<u8>, CryptoError> {
202        let pub_key = self.pub_key.as_ref().ok_or(CryptoError::NoPublicKey)?;
203
204        let ephemeral = X25519PrivateKey::from_bytes(ephemeral_prv);
205        let ephemeral_pub_bytes = ephemeral.public_key().public_bytes();
206        let shared_key = ephemeral.exchange(pub_key);
207
208        let derived_key = hkdf::hkdf(DERIVED_KEY_LENGTH, &shared_key, Some(&self.hash), None)
209            .map_err(CryptoError::HkdfError)?;
210
211        let token = Token::new(&derived_key).map_err(CryptoError::TokenError)?;
212        let ciphertext = token.encrypt_with_iv(plaintext, iv);
213
214        let mut result = Vec::with_capacity(32 + ciphertext.len());
215        result.extend_from_slice(&ephemeral_pub_bytes);
216        result.extend_from_slice(&ciphertext);
217        Ok(result)
218    }
219
220    pub fn decrypt(&self, ciphertext_token: &[u8]) -> Result<Vec<u8>, CryptoError> {
221        let prv = self.prv.as_ref().ok_or(CryptoError::NoPrivateKey)?;
222        self.decrypt_with_key(ciphertext_token, prv)
223    }
224
225    /// Try newest-first destination keys, optionally forbidding identity-key fallback.
226    /// The identity hash remains the HKDF salt, including for ratchet encryption.
227    pub fn decrypt_with_ratchets(
228        &self,
229        ciphertext: &[u8],
230        ratchets: &crate::ratchet::RatchetRing,
231        enforce: bool,
232    ) -> Result<crate::ratchet::Decrypted, CryptoError> {
233        // Token length is independent of the candidate key. Do not scan retained
234        // secrets when no candidate can possibly decrypt this input. Keep the
235        // existing enforced/fallback error paths below.
236        if ciphertext.len() > 32 + crate::token::TOKEN_OVERHEAD {
237            for key in ratchets.keys() {
238                let private = X25519PrivateKey::from_bytes(key);
239                if let Ok(plaintext) = self.decrypt_with_key(ciphertext, &private) {
240                    return Ok(crate::ratchet::Decrypted {
241                        plaintext,
242                        ratchet_id: Some(crate::ratchet::ratchet_id(
243                            &private.public_key().public_bytes(),
244                        )),
245                    });
246                }
247            }
248        }
249        if enforce {
250            return Err(CryptoError::InvalidCiphertext);
251        }
252        Ok(crate::ratchet::Decrypted {
253            plaintext: self.decrypt(ciphertext)?,
254            ratchet_id: None,
255        })
256    }
257
258    fn decrypt_with_key(
259        &self,
260        ciphertext_token: &[u8],
261        private: &X25519PrivateKey,
262    ) -> Result<Vec<u8>, CryptoError> {
263        if ciphertext_token.len() <= 32 {
264            return Err(CryptoError::InvalidCiphertext);
265        }
266        if ciphertext_token.len() <= 32 + crate::token::TOKEN_OVERHEAD {
267            return Err(CryptoError::TokenError(TokenError::InvalidToken));
268        }
269        let peer_pub_bytes: [u8; 32] = ciphertext_token[..32].try_into().unwrap();
270        let peer_pub = X25519PublicKey::from_bytes(&peer_pub_bytes);
271        let shared_key = zeroize::Zeroizing::new(private.exchange(&peer_pub));
272        let derived_key = zeroize::Zeroizing::new(
273            hkdf::hkdf(DERIVED_KEY_LENGTH, &*shared_key, Some(&self.hash), None)
274                .map_err(CryptoError::HkdfError)?,
275        );
276        let token = Token::new(&derived_key).map_err(CryptoError::TokenError)?;
277        token
278            .decrypt(&ciphertext_token[32..])
279            .map_err(CryptoError::TokenError)
280    }
281
282    pub fn sign(&self, message: &[u8]) -> Result<[u8; 64], CryptoError> {
283        let sig_prv = self.sig_prv.as_ref().ok_or(CryptoError::NoPrivateKey)?;
284        Ok(sig_prv.sign(message))
285    }
286
287    pub fn verify(&self, signature: &[u8; 64], message: &[u8]) -> bool {
288        match &self.sig_pub {
289            Some(sig_pub) => sig_pub.verify(signature, message),
290            None => false,
291        }
292    }
293}
294
295fn truncated_hash(data: &[u8]) -> [u8; 16] {
296    let full = sha256::sha256(data);
297    let mut result = [0u8; 16];
298    result.copy_from_slice(&full[..16]);
299    result
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use crate::FixedRng;
306
307    #[test]
308    fn from_public_key_with_invalid_signing_key_returns_error() {
309        let identity = Identity::from_private_key(&[0x37; 64]);
310        let mut public_key = identity.get_public_key().unwrap();
311        // y = 2 is not a valid compressed Ed25519 point encoding.
312        public_key[32..].copy_from_slice(&[0; 32]);
313        public_key[32] = 2;
314        assert!(matches!(
315            Identity::from_public_key(&public_key),
316            Err(CryptoError::InvalidPublicKey)
317        ));
318    }
319
320    #[test]
321    fn from_public_key_accepts_valid_public_key() {
322        let identity = Identity::from_private_key(&[0x37; 64]);
323        let public_key = identity.get_public_key().unwrap();
324        let public = Identity::from_public_key(&public_key).unwrap();
325        assert_eq!(public.get_public_key(), Some(public_key));
326        assert_eq!(public.hash(), identity.hash());
327    }
328
329    #[test]
330    fn short_ratchet_ciphertexts_preserve_error_and_fallback_policy() {
331        use crate::ratchet::RatchetRing;
332        let identity = Identity::from_private_key(&[0x37; 64]);
333        let public = Identity::from_public_key(&identity.get_public_key().unwrap()).unwrap();
334        for retained in [0, 1, 32] {
335            let ring = RatchetRing::from_keys(vec![[0x42; 32]; retained]).unwrap();
336            for length in [0, 1, 31, 32, 33, 48, 79, 80] {
337                let ciphertext = vec![0x63; length];
338                for owner in [&identity, &public] {
339                    assert!(matches!(
340                        owner.decrypt_with_ratchets(&ciphertext, &ring, true),
341                        Err(CryptoError::InvalidCiphertext)
342                    ));
343                    let expected = owner.decrypt(&ciphertext).unwrap_err();
344                    let actual = owner
345                        .decrypt_with_ratchets(&ciphertext, &ring, false)
346                        .err()
347                        .unwrap();
348                    assert_eq!(format!("{actual:?}"), format!("{expected:?}"));
349                }
350                assert!(matches!(
351                    public.decrypt(&ciphertext),
352                    Err(CryptoError::NoPrivateKey)
353                ));
354                if length <= 32 {
355                    assert!(matches!(
356                        identity.decrypt(&ciphertext),
357                        Err(CryptoError::InvalidCiphertext)
358                    ));
359                } else {
360                    assert!(matches!(
361                        identity.decrypt(&ciphertext),
362                        Err(CryptoError::TokenError(TokenError::InvalidToken))
363                    ));
364                }
365            }
366        }
367    }
368
369    #[test]
370    fn minimum_valid_ratchet_ciphertext_still_authenticates() {
371        use crate::ratchet::{ratchet_id, RatchetRing};
372        let identity = Identity::from_private_key(&[0x37; 64]);
373        let public_identity =
374            Identity::from_public_key(&identity.get_public_key().unwrap()).unwrap();
375        let ring = RatchetRing::from_keys(vec![[0x42; 32]]).unwrap();
376        let key = ring.current_public().unwrap();
377        let mut ciphertext = identity
378            .encrypt_with_ratchet(b"", Some(&key), &mut crate::FixedRng::new(&[0x63; 64]))
379            .unwrap();
380        assert_eq!(ciphertext.len(), 96);
381        let result = public_identity
382            .decrypt_with_ratchets(&ciphertext, &ring, true)
383            .unwrap();
384        assert!(result.plaintext.is_empty());
385        assert_eq!(result.ratchet_id, Some(ratchet_id(&key)));
386        *ciphertext.last_mut().unwrap() ^= 1;
387        assert!(public_identity
388            .decrypt_with_ratchets(&ciphertext, &ring, true)
389            .is_err());
390    }
391
392    #[test]
393    fn test_identity_key_roundtrip() {
394        let mut rng = FixedRng::new(&(0..64).collect::<Vec<u8>>());
395        let id = Identity::new(&mut rng);
396        let prv_bytes = id.get_private_key().unwrap();
397        let id2 = Identity::from_private_key(&prv_bytes);
398        assert_eq!(id.get_public_key().unwrap(), id2.get_public_key().unwrap());
399    }
400
401    #[test]
402    fn test_identity_hash() {
403        let mut rng = FixedRng::new(&(0..64).collect::<Vec<u8>>());
404        let id = Identity::new(&mut rng);
405        let pub_key = id.get_public_key().unwrap();
406        let expected_hash = truncated_hash(&pub_key);
407        assert_eq!(*id.hash(), expected_hash);
408    }
409
410    #[test]
411    fn test_identity_encrypt_decrypt_roundtrip() {
412        let mut rng = FixedRng::new(&(0..128).collect::<Vec<u8>>());
413        let id = Identity::new(&mut rng);
414        let plaintext = b"Hello, Reticulum! This is a test of the encrypt/decrypt pipeline.";
415        let mut rng2 = FixedRng::new(&(128..255).collect::<Vec<u8>>());
416        let ciphertext = id.encrypt(plaintext, &mut rng2).unwrap();
417        let decrypted = id.decrypt(&ciphertext).unwrap();
418        assert_eq!(decrypted, plaintext);
419    }
420
421    #[test]
422    fn test_identity_encrypt_with_ratchet_targets_ratchet_key() {
423        let mut rng = FixedRng::new(&(0..128).collect::<Vec<u8>>());
424        let remote_identity = Identity::new(&mut rng);
425        let ratchet_prv = X25519PrivateKey::from_bytes(&[0x42; 32]);
426        let ratchet_pub = ratchet_prv.public_key().public_bytes();
427        let plaintext = b"ratcheted";
428
429        let mut encrypt_rng = FixedRng::new(&(128..255).collect::<Vec<u8>>());
430        let ciphertext = remote_identity
431            .encrypt_with_ratchet(plaintext, Some(&ratchet_pub), &mut encrypt_rng)
432            .unwrap();
433
434        let peer_pub_bytes: [u8; 32] = ciphertext[..32].try_into().unwrap();
435        let peer_pub = X25519PublicKey::from_bytes(&peer_pub_bytes);
436        let shared_key = ratchet_prv.exchange(&peer_pub);
437        let derived_key = hkdf::hkdf(
438            DERIVED_KEY_LENGTH,
439            &shared_key,
440            Some(remote_identity.hash()),
441            None,
442        )
443        .unwrap();
444        let token = Token::new(&derived_key).unwrap();
445        let decrypted = token.decrypt(&ciphertext[32..]).unwrap();
446
447        assert_eq!(decrypted, plaintext);
448        assert!(remote_identity.decrypt(&ciphertext).is_err());
449    }
450
451    #[test]
452    fn test_identity_sign_verify() {
453        let mut rng = FixedRng::new(&(0..64).collect::<Vec<u8>>());
454        let id = Identity::new(&mut rng);
455        let msg = b"Sign this message";
456        let sig = id.sign(msg).unwrap();
457        assert!(id.verify(&sig, msg));
458        assert!(!id.verify(&sig, b"Wrong message"));
459    }
460}