huddle_core/crypto/
mod.rs1pub mod megolm;
2pub mod passphrase;
3pub mod sas;
4
5pub use megolm::RoomCrypto;
6
7use base64::engine::general_purpose::STANDARD as B64;
8use base64::Engine;
9use ed25519_dalek::{Signature, Verifier, VerifyingKey};
10
11use crate::error::{HuddleError, Result};
12use crate::identity::compute_fingerprint;
13use crate::network::protocol::{RoomMessage, SignedRoomMessage};
14
15pub fn verify_signed(env: &SignedRoomMessage) -> Result<(RoomMessage, String)> {
26 let pubkey_bytes = B64
27 .decode(&env.ed25519_pubkey_b64)
28 .map_err(|e| HuddleError::Session(format!("bad pubkey_b64: {e}")))?;
29 if pubkey_bytes.len() != 32 {
30 return Err(HuddleError::Session(format!(
31 "pubkey is {} bytes, expected 32",
32 pubkey_bytes.len()
33 )));
34 }
35 let mut pk_arr = [0u8; 32];
36 pk_arr.copy_from_slice(&pubkey_bytes);
37
38 let derived_fp = compute_fingerprint(&pk_arr);
39 if derived_fp != env.fingerprint {
40 return Err(HuddleError::Session(format!(
41 "fingerprint mismatch: envelope claims {}, key derives {}",
42 env.fingerprint, derived_fp
43 )));
44 }
45
46 let payload = B64
47 .decode(&env.payload_b64)
48 .map_err(|e| HuddleError::Session(format!("bad payload_b64: {e}")))?;
49 let sig_bytes = B64
50 .decode(&env.signature_b64)
51 .map_err(|e| HuddleError::Session(format!("bad signature_b64: {e}")))?;
52 if sig_bytes.len() != 64 {
53 return Err(HuddleError::Session(format!(
54 "signature is {} bytes, expected 64",
55 sig_bytes.len()
56 )));
57 }
58 let mut sig_arr = [0u8; 64];
59 sig_arr.copy_from_slice(&sig_bytes);
60 let signature = Signature::from_bytes(&sig_arr);
61
62 let verifying_key = VerifyingKey::from_bytes(&pk_arr)
63 .map_err(|e| HuddleError::Session(format!("bad verifying key: {e}")))?;
64 verifying_key
65 .verify(&payload, &signature)
66 .map_err(|e| HuddleError::Session(format!("signature verify failed: {e}")))?;
67
68 let msg: RoomMessage = serde_json::from_slice(&payload)
69 .map_err(|e| HuddleError::Session(format!("bad payload json: {e}")))?;
70 Ok((msg, derived_fp))
71}
72
73pub fn sign_message(
77 identity: &crate::identity::Identity,
78 msg: &RoomMessage,
79) -> Result<SignedRoomMessage> {
80 let payload = serde_json::to_vec(msg)
81 .map_err(|e| HuddleError::Session(format!("encode payload: {e}")))?;
82 let sig = identity.sign(&payload);
83 Ok(SignedRoomMessage {
84 fingerprint: identity.fingerprint().to_string(),
85 ed25519_pubkey_b64: B64.encode(identity.public_bytes()),
86 payload_b64: B64.encode(&payload),
87 signature_b64: B64.encode(sig),
88 })
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::identity::Identity;
95
96 fn sample_msg() -> RoomMessage {
97 RoomMessage::MemberLeave {
98 sender_fingerprint: "test-fp".into(),
99 }
100 }
101
102 #[test]
103 fn sign_verify_round_trip() {
104 let id = Identity::generate().unwrap();
105 let env = sign_message(&id, &sample_msg()).unwrap();
106 let (msg, fp) = verify_signed(&env).unwrap();
107 assert_eq!(fp, id.fingerprint());
108 assert!(matches!(msg, RoomMessage::MemberLeave { .. }));
109 }
110
111 #[test]
112 fn tampered_payload_fails() {
113 let id = Identity::generate().unwrap();
114 let mut env = sign_message(&id, &sample_msg()).unwrap();
115 let other = serde_json::to_vec(&RoomMessage::Typing {
117 sender_fingerprint: "evil-fp".into(),
118 })
119 .unwrap();
120 env.payload_b64 = B64.encode(&other);
121 assert!(verify_signed(&env).is_err());
122 }
123
124 #[test]
125 fn fingerprint_pubkey_mismatch_fails() {
126 let alice = Identity::generate().unwrap();
127 let bob = Identity::generate().unwrap();
128 let mut env = sign_message(&alice, &sample_msg()).unwrap();
129 env.fingerprint = bob.fingerprint().to_string();
132 assert!(verify_signed(&env).is_err());
133 }
134
135 #[test]
136 fn swapped_pubkey_fails_signature() {
137 let alice = Identity::generate().unwrap();
138 let bob = Identity::generate().unwrap();
139 let mut env = sign_message(&alice, &sample_msg()).unwrap();
140 env.ed25519_pubkey_b64 = B64.encode(bob.public_bytes());
144 env.fingerprint = bob.fingerprint().to_string();
145 assert!(verify_signed(&env).is_err());
146 }
147}