alpine/crypto/
mod.rs

1use rand::rngs::OsRng;
2use thiserror::Error;
3use x25519_dalek::{PublicKey as X25519PublicKey, SharedSecret, StaticSecret as X25519Secret};
4
5use chacha20poly1305::aead::{AeadInPlace, KeyInit};
6use chacha20poly1305::{ChaCha20Poly1305, Key};
7use hkdf::Hkdf;
8use sha2::Sha256;
9
10pub mod identity;
11
12/// Algorithms supported for the initial key exchange.
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum KeyExchangeAlgorithm {
15    X25519,
16    EcdhP256,
17    None,
18}
19
20/// Derived session key material.
21#[derive(Debug, Clone)]
22pub struct SessionKeys {
23    pub shared_secret: Vec<u8>,
24    pub control_key: [u8; 32],
25    pub stream_key: [u8; 32],
26}
27
28/// Behavior required to complete the handshake key agreement.
29pub trait KeyExchange {
30    fn algorithm(&self) -> KeyExchangeAlgorithm;
31    fn public_key(&self) -> Vec<u8>;
32    fn derive_keys(&self, peer_public_key: &[u8], salt: &[u8]) -> Result<SessionKeys, CryptoError>;
33}
34
35/// Lightweight placeholder for X25519; replace with a real implementation later.
36pub struct X25519KeyExchange {
37    public_key: X25519PublicKey,
38    private_key: X25519Secret,
39}
40
41impl X25519KeyExchange {
42    pub fn new() -> Self {
43        let private_key = X25519Secret::random_from_rng(OsRng);
44        let public_key = X25519PublicKey::from(&private_key);
45        Self {
46            public_key,
47            private_key,
48        }
49    }
50}
51
52impl Default for X25519KeyExchange {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl KeyExchange for X25519KeyExchange {
59    fn algorithm(&self) -> KeyExchangeAlgorithm {
60        KeyExchangeAlgorithm::X25519
61    }
62
63    fn public_key(&self) -> Vec<u8> {
64        self.public_key.to_bytes().to_vec()
65    }
66
67    fn derive_keys(&self, peer_public_key: &[u8], salt: &[u8]) -> Result<SessionKeys, CryptoError> {
68        let peer_bytes: [u8; 32] = peer_public_key
69            .try_into()
70            .map_err(|_| CryptoError::InvalidPeerKey)?;
71        let peer_pk = X25519PublicKey::from(peer_bytes);
72        let shared_secret: SharedSecret = self.private_key.diffie_hellman(&peer_pk);
73        let shared_secret_bytes = shared_secret.as_bytes().to_vec();
74
75        let hkdf = Hkdf::<Sha256>::new(Some(salt), shared_secret.as_bytes());
76        let mut control_key = [0u8; 32];
77        let mut stream_key = [0u8; 32];
78        hkdf.expand(b"alpine-control", &mut control_key)
79            .map_err(|e| CryptoError::Hkdf(format!("{:?}", e)))?;
80        hkdf.expand(b"alpine-stream", &mut stream_key)
81            .map_err(|e| CryptoError::Hkdf(format!("{:?}", e)))?;
82
83        Ok(SessionKeys {
84            shared_secret: shared_secret_bytes,
85            control_key,
86            stream_key,
87        })
88    }
89}
90
91/// Interface that would wrap an external TLS channel when available.
92pub trait TlsWrapper {
93    fn wrap_stream(&self, plaintext: &[u8]) -> Vec<u8>;
94    fn unwrap_stream(&self, ciphertext: &[u8]) -> Vec<u8>;
95}
96
97/// Cryptographic helper errors.
98#[derive(Debug, Error)]
99pub enum CryptoError {
100    #[error("invalid peer public key")]
101    InvalidPeerKey,
102    #[error("hkdf expand error: {0}")]
103    Hkdf(String),
104    #[error("aead error: {0}")]
105    Aead(String),
106}
107
108/// Compute an authentication tag for a control payload using the derived control key.
109pub fn compute_mac(
110    keys: &SessionKeys,
111    seq: u64,
112    payload: &[u8],
113    aad: &[u8],
114) -> Result<Vec<u8>, CryptoError> {
115    let key = Key::from_slice(&keys.control_key);
116    let cipher = ChaCha20Poly1305::new(key);
117    let mut nonce = [0u8; 12];
118    nonce[..8].copy_from_slice(&seq.to_be_bytes());
119    let mut buffer = payload.to_vec();
120    let tag = cipher
121        .encrypt_in_place_detached(&nonce.into(), aad, &mut buffer)
122        .map_err(|e| CryptoError::Aead(e.to_string()))?;
123    Ok(tag.to_vec())
124}
125
126/// Validate an authentication tag for a control payload.
127pub fn verify_mac(keys: &SessionKeys, seq: u64, payload: &[u8], aad: &[u8], mac: &[u8]) -> bool {
128    const CHACHA_TAG_SIZE: usize = 16;
129    if mac.len() != CHACHA_TAG_SIZE {
130        return false;
131    }
132    match compute_mac(keys, seq, payload, aad) {
133        Ok(expected) => expected == mac,
134        Err(_) => false,
135    }
136}