Skip to main content

rift_core/
e2ee.rs

1//! End-to-end encryption helpers.
2//!
3//! This module provides:
4//! - X25519 keypair generation for session encryption
5//! - Ed25519 signatures over ephemeral keys
6//! - HKDF-based shared key derivation
7
8use ed25519_dalek::{PublicKey as Ed25519PublicKey, Signature, Signer, Verifier};
9use hkdf::Hkdf;
10use rand::rngs::OsRng;
11use sha2::Sha256;
12use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
13
14use crate::Identity;
15
16pub struct E2eeKeypair {
17    /// X25519 private key.
18    pub secret: StaticSecret,
19    /// X25519 public key.
20    pub public: X25519PublicKey,
21}
22
23/// Generate an ephemeral X25519 keypair for E2EE.
24pub fn generate_e2ee_keypair() -> E2eeKeypair {
25    let secret = StaticSecret::new(OsRng);
26    let public = X25519PublicKey::from(&secret);
27    E2eeKeypair { secret, public }
28}
29
30/// Sign the ephemeral public key with the long-term Ed25519 identity.
31pub fn sign_e2ee_public(identity: &Identity, session_id: &[u8], public: &X25519PublicKey) -> Vec<u8> {
32    let msg = e2ee_message(session_id, public);
33    let signature: Signature = identity.keypair.sign(&msg);
34    signature.to_bytes().to_vec()
35}
36
37/// Verify a peer's signature over their ephemeral public key.
38pub fn verify_e2ee_public(
39    peer_public: &Ed25519PublicKey,
40    session_id: &[u8],
41    public: &X25519PublicKey,
42    signature: &[u8],
43) -> bool {
44    let Ok(signature) = Signature::from_bytes(signature) else {
45        return false;
46    };
47    let msg = e2ee_message(session_id, public);
48    peer_public.verify(&msg, &signature).is_ok()
49}
50
51/// Derive the shared session key from X25519 DH + HKDF.
52pub fn derive_e2ee_shared_key(
53    secret: &StaticSecret,
54    remote_public: &X25519PublicKey,
55    session_id: &[u8],
56) -> [u8; 32] {
57    let shared = secret.diffie_hellman(remote_public);
58    let hk = Hkdf::<Sha256>::new(Some(session_id), shared.as_bytes());
59    let mut out = [0u8; 32];
60    hk.expand(b"rift-e2ee-v1", &mut out)
61        .expect("hkdf expand");
62    out
63}
64
65/// Compose the message used for signature verification.
66fn e2ee_message(session_id: &[u8], public: &X25519PublicKey) -> Vec<u8> {
67    let mut msg = Vec::with_capacity(16 + session_id.len() + 32);
68    msg.extend_from_slice(b"rift-e2ee");
69    msg.extend_from_slice(session_id);
70    msg.extend_from_slice(public.as_bytes());
71    msg
72}
73
74/// Helper to convert raw bytes into an X25519 public key.
75pub fn public_key_from_bytes(bytes: [u8; 32]) -> X25519PublicKey {
76    X25519PublicKey::from(bytes)
77}
78
79/// Helper to convert raw bytes into an Ed25519 public key.
80pub fn ed25519_public_from_bytes(bytes: &[u8]) -> Option<Ed25519PublicKey> {
81    Ed25519PublicKey::from_bytes(bytes).ok()
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn e2ee_sign_verify_roundtrip() {
90        let identity = Identity::generate();
91        let session = [7u8; 32];
92        let kp = generate_e2ee_keypair();
93        let sig = sign_e2ee_public(&identity, &session, &kp.public);
94        let ok = verify_e2ee_public(&identity.keypair.public, &session, &kp.public, &sig);
95        assert!(ok);
96    }
97
98    #[test]
99    fn e2ee_shared_key_matches() {
100        let session = [9u8; 32];
101        let a = generate_e2ee_keypair();
102        let b = generate_e2ee_keypair();
103        let ka = derive_e2ee_shared_key(&a.secret, &b.public, &session);
104        let kb = derive_e2ee_shared_key(&b.secret, &a.public, &session);
105        assert_eq!(ka, kb);
106    }
107
108    #[test]
109    fn e2ee_bad_signature_rejected() {
110        let identity = Identity::generate();
111        let other = Identity::generate();
112        let session = [1u8; 32];
113        let kp = generate_e2ee_keypair();
114        let sig = sign_e2ee_public(&identity, &session, &kp.public);
115        let ok = verify_e2ee_public(&other.keypair.public, &session, &kp.public, &sig);
116        assert!(!ok);
117    }
118}