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