Skip to main content

vcl_protocol/
crypto.rs

1//! # VCL Cryptographic Primitives
2//!
3//! Low-level cryptographic helpers used internally by [`VCLConnection`]:
4//!
5//! - [`KeyPair`] — Ed25519 signing/verifying key pair
6//! - [`encrypt_payload`] — XChaCha20-Poly1305 AEAD encryption
7//! - [`decrypt_payload`] — XChaCha20-Poly1305 AEAD decryption
8//! - [`hash_data`] — SHA-256 hashing
9//!
10//! [`VCLConnection`]: crate::connection::VCLConnection
11
12use ed25519_dalek::SigningKey;
13use rand::rngs::OsRng;
14use rand::RngCore;
15use chacha20poly1305::{XChaCha20Poly1305, KeyInit, AeadCore};
16use chacha20poly1305::aead::Aead;
17use chacha20poly1305::XNonce;
18use crate::error::VCLError;
19
20/// An Ed25519 key pair used for signing and verifying [`VCLPacket`] signatures.
21///
22/// [`VCLPacket`]: crate::packet::VCLPacket
23#[derive(Clone, Debug)]
24pub struct KeyPair {
25    /// 32-byte Ed25519 verifying (public) key.
26    pub public_key: Vec<u8>,
27    /// 32-byte Ed25519 signing (private) key.
28    pub private_key: Vec<u8>,
29}
30
31impl KeyPair {
32    /// Generate a new random Ed25519 key pair using a CSPRNG.
33    pub fn generate() -> Self {
34        let mut secret_bytes = [0u8; 32];
35        OsRng.fill_bytes(&mut secret_bytes);
36        let signing_key = SigningKey::from_bytes(&secret_bytes);
37        let verifying_key = signing_key.verifying_key();
38        KeyPair {
39            public_key: verifying_key.to_bytes().to_vec(),
40            private_key: signing_key.to_bytes().to_vec(),
41        }
42    }
43}
44
45/// Encrypt `data` with XChaCha20-Poly1305 using the given 32-byte `key`.
46///
47/// A random 24-byte nonce is generated internally.
48///
49/// # Returns
50/// `(ciphertext, nonce)` — both must be stored in the packet for decryption.
51///
52/// # Errors
53/// Returns [`VCLError::InvalidKey`] if `key` is not 32 bytes.
54/// Returns [`VCLError::CryptoError`] if encryption fails.
55pub fn encrypt_payload(data: &[u8], key: &[u8; 32]) -> Result<(Vec<u8>, [u8; 24]), VCLError> {
56    let cipher = XChaCha20Poly1305::new_from_slice(key)
57        .map_err(|e| VCLError::InvalidKey(format!("Invalid key length: {}", e)))?;
58
59    let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
60    let mut nonce_bytes = [0u8; 24];
61    nonce_bytes.copy_from_slice(nonce.as_slice());
62
63    let ciphertext = cipher
64        .encrypt(&nonce, data)
65        .map_err(|e| VCLError::CryptoError(format!("Encryption failed: {}", e)))?;
66
67    Ok((ciphertext, nonce_bytes))
68}
69
70/// Decrypt `ciphertext` with XChaCha20-Poly1305 using the given `key` and `nonce`.
71///
72/// # Errors
73/// Returns [`VCLError::InvalidKey`] if `key` is not 32 bytes.
74/// Returns [`VCLError::CryptoError`] if decryption or authentication fails
75/// (e.g. wrong key, tampered ciphertext).
76pub fn decrypt_payload(ciphertext: &[u8], key: &[u8; 32], nonce: &[u8; 24]) -> Result<Vec<u8>, VCLError> {
77    let cipher = XChaCha20Poly1305::new_from_slice(key)
78        .map_err(|e| VCLError::InvalidKey(format!("Invalid key length: {}", e)))?;
79
80    let nonce = XNonce::from_slice(nonce);
81
82    cipher
83        .decrypt(nonce, ciphertext)
84        .map_err(|e| VCLError::CryptoError(format!("Decryption failed: {}", e)))
85}
86
87/// Compute a SHA-256 hash of `data`. Returns a 32-byte `Vec<u8>`.
88#[allow(dead_code)]
89pub fn hash_data(data: &[u8]) -> Vec<u8> {
90    use sha2::{Sha256, Digest};
91    Sha256::new().chain_update(data).finalize().to_vec()
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_keypair_generate() {
100        let kp1 = KeyPair::generate();
101        let kp2 = KeyPair::generate();
102        assert_eq!(kp1.public_key.len(), 32);
103        assert_eq!(kp1.private_key.len(), 32);
104        assert_ne!(kp1.public_key, kp2.public_key);
105    }
106
107    #[test]
108    fn test_encrypt_decrypt() {
109        let key = [1u8; 32];
110        let data = b"Hello, VCL!";
111        let (ciphertext, nonce) = encrypt_payload(data, &key).unwrap();
112        let decrypted = decrypt_payload(&ciphertext, &key, &nonce).unwrap();
113        assert_eq!(data, decrypted.as_slice());
114    }
115
116    #[test]
117    fn test_decrypt_wrong_key_fails() {
118        let key1 = [1u8; 32];
119        let key2 = [2u8; 32];
120        let data = b"Secret message";
121        let (ciphertext, nonce) = encrypt_payload(data, &key1).unwrap();
122        let result = decrypt_payload(&ciphertext, &key2, &nonce);
123        assert!(result.is_err());
124    }
125
126    #[test]
127    fn test_hash_data() {
128        let h1 = hash_data(b"test");
129        let h2 = hash_data(b"test");
130        let h3 = hash_data(b"Test");
131        assert_eq!(h1, h2);
132        assert_ne!(h1, h3);
133        assert_eq!(h1.len(), 32);
134    }
135}