1use chacha20::ChaCha20;
50use chacha20::cipher::{KeyIvInit, StreamCipher};
51use hkdf::Hkdf;
52use hmac::{Hmac, Mac};
53use rand::RngCore;
54use serde_json::{Value, json};
55use sha2::{Digest, Sha256, Sha512};
56use thiserror::Error;
57use x25519_dalek::{PublicKey, StaticSecret};
58use zeroize::Zeroizing;
59
60use crate::signing::{b64decode, b64encode};
61
62pub const ENC_DISCRIMINATOR: &str = "wire-x25519.v1";
64const HKDF_SALT: &[u8] = b"wire-x25519-v1";
67const VERSION: u8 = 0x02;
68const MAX_PLAINTEXT: usize = 65535;
69const MIN_RAW: usize = 1 + 32 + 34 + 32; const MAX_RAW: usize = 1 + 32 + (2 + 65536) + 32; type HmacSha256 = Hmac<Sha256>;
75
76#[derive(Debug, Error, PartialEq, Eq)]
77pub enum EncError {
78 #[error("x25519 produced an all-zero shared secret (low-order/contributory point)")]
79 ZeroSharedSecret,
80 #[error("plaintext length {0} out of range 1..=65535")]
81 BadLength(usize),
82 #[error("base64 decode failed")]
83 BadBase64,
84 #[error("payload length out of bounds")]
85 BadPayloadLen,
86 #[error("unsupported version")]
87 BadVersion,
88 #[error("mac verification failed")]
89 MacFail,
90 #[error("invalid padding")]
91 BadPadding,
92 #[error("plaintext is not valid utf-8")]
93 BadUtf8,
94}
95
96pub fn x25519_scalar_from_ed25519_seed(seed: &[u8; 32]) -> [u8; 32] {
103 let h = Sha512::digest(seed);
104 let mut s = [0u8; 32];
105 s.copy_from_slice(&h[0..32]);
106 s[0] &= 248;
107 s[31] &= 127;
108 s[31] |= 64;
109 s
110}
111
112pub fn x25519_pub_from_ed25519_seed(seed: &[u8; 32]) -> [u8; 32] {
115 let secret = StaticSecret::from(x25519_scalar_from_ed25519_seed(seed));
116 PublicKey::from(&secret).to_bytes()
117}
118
119pub fn derive_conversation_key(
123 our_scalar: &[u8; 32],
124 peer_pub: &[u8; 32],
125) -> Result<[u8; 32], EncError> {
126 let secret = StaticSecret::from(*our_scalar);
127 let peer = PublicKey::from(*peer_pub);
128 let shared = secret.diffie_hellman(&peer);
129 let shared_bytes = shared.to_bytes();
130 if shared_bytes == [0u8; 32] {
131 return Err(EncError::ZeroSharedSecret);
132 }
133 let (prk, _hk) = Hkdf::<Sha256>::extract(Some(HKDF_SALT), &shared_bytes);
134 let mut ck = [0u8; 32];
135 ck.copy_from_slice(&prk);
136 Ok(ck)
137}
138
139fn context_info(nonce: &[u8; 32], from: &str, to: &str) -> Vec<u8> {
148 debug_assert!(
152 from.len() <= u16::MAX as usize && to.len() <= u16::MAX as usize,
153 "identity too long for u16 length-prefix framing"
154 );
155 let mut v = Vec::with_capacity(32 + 2 + from.len() + 2 + to.len());
156 v.extend_from_slice(nonce);
157 v.extend_from_slice(&(from.len() as u16).to_be_bytes());
158 v.extend_from_slice(from.as_bytes());
159 v.extend_from_slice(&(to.len() as u16).to_be_bytes());
160 v.extend_from_slice(to.as_bytes());
161 v
162}
163
164fn message_keys(conversation_key: &[u8; 32], info: &[u8]) -> ([u8; 32], [u8; 12], [u8; 32]) {
166 let hk = Hkdf::<Sha256>::from_prk(conversation_key).expect("32-byte prk is valid");
167 let mut okm = [0u8; 76];
168 hk.expand(info, &mut okm).expect("76 < 255*32");
169 let mut chacha_key = [0u8; 32];
170 chacha_key.copy_from_slice(&okm[0..32]);
171 let mut chacha_nonce = [0u8; 12];
172 chacha_nonce.copy_from_slice(&okm[32..44]);
173 let mut hmac_key = [0u8; 32];
174 hmac_key.copy_from_slice(&okm[44..76]);
175 (chacha_key, chacha_nonce, hmac_key)
176}
177
178fn calc_padded_len(unpadded: usize) -> usize {
182 if unpadded <= 32 {
183 return 32;
184 }
185 let l = unpadded as u32;
186 let next_power = 1usize << (32 - (l - 1).leading_zeros());
188 let chunk = if next_power <= 256 {
189 32
190 } else {
191 next_power / 8
192 };
193 chunk * (((unpadded - 1) / chunk) + 1)
194}
195
196fn pad(pt: &[u8]) -> Result<Vec<u8>, EncError> {
198 let l = pt.len();
199 if !(1..=MAX_PLAINTEXT).contains(&l) {
200 return Err(EncError::BadLength(l));
201 }
202 let total = 2 + calc_padded_len(l);
203 let mut buf = Vec::with_capacity(total);
204 buf.extend_from_slice(&(l as u16).to_be_bytes());
205 buf.extend_from_slice(pt);
206 buf.resize(total, 0);
207 Ok(buf)
208}
209
210fn unpad(buf: &[u8]) -> Result<Vec<u8>, EncError> {
212 if buf.len() < 2 {
213 return Err(EncError::BadPadding);
214 }
215 let l = u16::from_be_bytes([buf[0], buf[1]]) as usize;
216 let end = 2usize.checked_add(l).ok_or(EncError::BadPadding)?;
217 if l == 0 || buf.len() < end {
218 return Err(EncError::BadPadding);
219 }
220 let out = &buf[2..end];
221 if out.len() != l || buf.len() != 2 + calc_padded_len(l) {
222 return Err(EncError::BadPadding);
223 }
224 Ok(out.to_vec())
225}
226
227pub(crate) fn seal(
235 conversation_key: &[u8; 32],
236 plaintext: &[u8],
237 from: &str,
238 to: &str,
239) -> Result<String, EncError> {
240 let mut nonce = [0u8; 32];
241 rand::thread_rng().fill_bytes(&mut nonce);
242 let (chacha_key, chacha_nonce, hmac_key) =
243 message_keys(conversation_key, &context_info(&nonce, from, to));
244
245 let mut ct = pad(plaintext)?;
246 let mut cipher = ChaCha20::new(
247 chacha20::Key::from_slice(&chacha_key),
248 chacha20::Nonce::from_slice(&chacha_nonce),
249 );
250 cipher.apply_keystream(&mut ct);
251
252 let mut mac = HmacSha256::new_from_slice(&hmac_key).expect("hmac accepts any key length");
253 mac.update(&nonce);
254 mac.update(&ct);
255 let tag = mac.finalize().into_bytes();
256
257 let mut payload = Vec::with_capacity(1 + 32 + ct.len() + 32);
258 payload.push(VERSION);
259 payload.extend_from_slice(&nonce);
260 payload.extend_from_slice(&ct);
261 payload.extend_from_slice(&tag);
262 Ok(b64encode(&payload))
263}
264
265pub(crate) fn open(
272 conversation_key: &[u8; 32],
273 payload_b64: &str,
274 from: &str,
275 to: &str,
276) -> Result<String, EncError> {
277 if payload_b64.as_bytes().first() == Some(&b'#') {
279 return Err(EncError::BadVersion);
280 }
281 if payload_b64.len() > MAX_RAW * 4 / 3 + 4 {
285 return Err(EncError::BadPayloadLen);
286 }
287 let raw = b64decode(payload_b64).map_err(|_| EncError::BadBase64)?;
288 if !(MIN_RAW..=MAX_RAW).contains(&raw.len()) {
289 return Err(EncError::BadPayloadLen);
290 }
291 if raw[0] != VERSION {
292 return Err(EncError::BadVersion);
293 }
294 let mut nonce = [0u8; 32];
295 nonce.copy_from_slice(&raw[1..33]);
296 let mac_start = raw.len() - 32;
297 let ct = &raw[33..mac_start];
298 let tag = &raw[mac_start..];
299
300 let (chacha_key, chacha_nonce, hmac_key) =
301 message_keys(conversation_key, &context_info(&nonce, from, to));
302
303 let mut mac = HmacSha256::new_from_slice(&hmac_key).expect("hmac accepts any key length");
304 mac.update(&nonce);
305 mac.update(ct);
306 mac.verify_slice(tag).map_err(|_| EncError::MacFail)?; let mut buf = ct.to_vec();
309 let mut cipher = ChaCha20::new(
310 chacha20::Key::from_slice(&chacha_key),
311 chacha20::Nonce::from_slice(&chacha_nonce),
312 );
313 cipher.apply_keystream(&mut buf);
314 let out = unpad(&buf)?;
315 String::from_utf8(out).map_err(|_| EncError::BadUtf8)
316}
317
318pub fn self_dh_pubkey_b64(seed: &[u8; 32]) -> String {
327 b64encode(&x25519_pub_from_ed25519_seed(seed))
328}
329
330fn decode_dh(b64: &str) -> Option<[u8; 32]> {
331 let v = b64decode(b64).ok()?;
332 let arr: [u8; 32] = v.try_into().ok()?;
333 Some(arr)
334}
335
336pub fn peer_dh_pubkey(trust: &Value, peer_did_or_handle: &str) -> Option<[u8; 32]> {
339 let handle = crate::agent_card::display_handle_from_did(peer_did_or_handle);
340 let b64 = trust
341 .get("agents")?
342 .get(handle)?
343 .get("card")?
344 .get("dh_pubkey")?
345 .as_str()?;
346 decode_dh(b64)
347}
348
349pub fn seal_event_body(
355 event: &mut Value,
356 peer_dh_pubkey: &[u8; 32],
357 our_seed: &[u8; 32],
358) -> anyhow::Result<()> {
359 let from = event
360 .get("from")
361 .and_then(Value::as_str)
362 .ok_or_else(|| anyhow::anyhow!("event missing `from`"))?
363 .to_string();
364 let to = event
365 .get("to")
366 .and_then(Value::as_str)
367 .ok_or_else(|| anyhow::anyhow!("encryption requires a `to` recipient on the event"))?
368 .to_string();
369 let our_scalar = Zeroizing::new(x25519_scalar_from_ed25519_seed(our_seed));
370 let ck = Zeroizing::new(
371 derive_conversation_key(&our_scalar, peer_dh_pubkey)
372 .map_err(|e| anyhow::anyhow!("derive conversation key: {e}"))?,
373 );
374 let pt = serde_json::to_vec(event.get("body").unwrap_or(&Value::Null))?;
375 let ct = seal(&ck, &pt, &from, &to).map_err(|e| anyhow::anyhow!("seal: {e}"))?;
376 let obj = event
377 .as_object_mut()
378 .ok_or_else(|| anyhow::anyhow!("event is not a JSON object"))?;
379 obj.insert("enc".into(), json!(ENC_DISCRIMINATOR));
380 obj.insert("body".into(), json!({ "ct": ct }));
381 Ok(())
382}
383
384pub fn open_event_body(
394 event: &Value,
395 trust: &Value,
396 our_seed: &[u8; 32],
397) -> anyhow::Result<Option<Value>> {
398 match event.get("enc").and_then(Value::as_str) {
399 Some(ENC_DISCRIMINATOR) => {}
400 Some(_) | None => return Ok(None),
401 }
402 crate::signing::verify_message_v31(event, trust)
404 .map_err(|e| anyhow::anyhow!("refusing to decrypt unverified event: {e}"))?;
405
406 let from = event
407 .get("from")
408 .and_then(Value::as_str)
409 .ok_or_else(|| anyhow::anyhow!("event missing `from`"))?;
410 let to = event
411 .get("to")
412 .and_then(Value::as_str)
413 .ok_or_else(|| anyhow::anyhow!("encrypted event missing `to`"))?;
414 let ct = event
415 .get("body")
416 .and_then(|b| b.get("ct"))
417 .and_then(Value::as_str)
418 .ok_or_else(|| anyhow::anyhow!("enc event body missing `ct`"))?;
419
420 let peer_dh = peer_dh_pubkey(trust, from)
421 .ok_or_else(|| anyhow::anyhow!("sender has no pinned dh_pubkey — cannot decrypt"))?;
422 let our_scalar = Zeroizing::new(x25519_scalar_from_ed25519_seed(our_seed));
423 let ck = Zeroizing::new(
424 derive_conversation_key(&our_scalar, &peer_dh)
425 .map_err(|e| anyhow::anyhow!("derive conversation key: {e}"))?,
426 );
427 let pt = open(&ck, ct, from, to).map_err(|_| anyhow::anyhow!("decryption failed"))?;
430 let body: Value = serde_json::from_str(&pt)
431 .map_err(|_| anyhow::anyhow!("decrypted body is not valid json"))?;
432 Ok(Some(body))
433}
434
435pub fn self_seed_for_read() -> Option<[u8; 32]> {
438 crate::config::read_private_key()
439 .ok()
440 .and_then(|v| v.get(..32).and_then(|s| <[u8; 32]>::try_from(s).ok()))
441}
442
443pub fn decrypt_event_for_read(event: &Value, trust: &Value, seed: &[u8; 32]) -> Value {
449 if event.get("enc").and_then(Value::as_str) == Some(ENC_DISCRIMINATOR)
450 && let Ok(Some(plain)) = open_event_body(event, trust, seed)
451 {
452 let mut e = event.clone();
453 if let Some(obj) = e.as_object_mut() {
454 obj.insert("body".into(), plain);
455 obj.insert("dec".into(), json!(true));
456 }
457 return e;
458 }
459 event.clone()
460}
461
462pub fn is_encrypted_unreadable(event: &Value) -> bool {
472 event.get("enc").and_then(Value::as_str).is_some()
473 && event.get("dec").and_then(Value::as_bool) != Some(true)
474}
475
476pub fn undecryptable_body_placeholder(event: &Value) -> String {
479 let enc = event.get("enc").and_then(Value::as_str).unwrap_or("?");
480 format!(
481 "<encrypted DM (enc={enc}) — this wire build could not decrypt it; run `wire upgrade`, or check that the peer pinned your current key>"
482 )
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 #[test]
490 fn is_encrypted_unreadable_flags_enc_without_dec() {
491 let ev = json!({"enc": ENC_DISCRIMINATOR, "body": {"ct": "AAAA"}});
493 assert!(is_encrypted_unreadable(&ev));
494 let ev_ok = json!({"enc": ENC_DISCRIMINATOR, "dec": true, "body": "hi"});
496 assert!(!is_encrypted_unreadable(&ev_ok));
497 let ev_plain = json!({"body": "hi"});
499 assert!(!is_encrypted_unreadable(&ev_plain));
500 let ev_future = json!({"enc": "future.v9", "body": {"ct": "BBBB"}});
502 assert!(is_encrypted_unreadable(&ev_future));
503 }
504
505 #[test]
506 fn undecryptable_placeholder_names_scheme_and_hides_ciphertext() {
507 let ev = json!({"enc": ENC_DISCRIMINATOR, "body": {"ct": "SECRETCIPHERTEXT"}});
508 let p = undecryptable_body_placeholder(&ev);
509 assert!(p.contains(ENC_DISCRIMINATOR), "names scheme: {p}");
510 assert!(p.contains("wire upgrade"), "points at fix: {p}");
511 assert!(!p.contains("SECRETCIPHERTEXT"), "never leaks ct: {p}");
512 }
513
514 const SEED_A: [u8; 32] = [1u8; 32];
516 const SEED_B: [u8; 32] = [2u8; 32];
517
518 fn conv(seed_self: &[u8; 32], seed_peer: &[u8; 32]) -> [u8; 32] {
519 let our = x25519_scalar_from_ed25519_seed(seed_self);
520 let peer_pub = x25519_pub_from_ed25519_seed(seed_peer);
521 derive_conversation_key(&our, &peer_pub).unwrap()
522 }
523
524 fn hex_to_32(h: &str) -> [u8; 32] {
525 let v = hex::decode(h).expect("valid hex");
526 let mut a = [0u8; 32];
527 a.copy_from_slice(&v);
528 a
529 }
530
531 #[test]
532 fn round_trips_with_production_did_identity_form() {
533 let ck = conv(&SEED_A, &SEED_B);
537 let from = "did:wire:alice-1b1b58dd";
538 let to = "did:wire:bob-60346e7c";
539 let payload = seal(&ck, b"production-form message", from, to).unwrap();
540 assert_eq!(
541 open(&ck, &payload, from, to).unwrap(),
542 "production-form message"
543 );
544 assert_eq!(
548 open(&ck, &payload, "alice", to).unwrap_err(),
549 EncError::MacFail
550 );
551 }
552
553 #[test]
554 fn oversized_input_rejected_without_large_alloc() {
555 let ck = conv(&SEED_A, &SEED_B);
558 let bomb = "A".repeat(10_000_000);
559 assert_eq!(
560 open(&ck, &bomb, "a", "b").unwrap_err(),
561 EncError::BadPayloadLen
562 );
563 }
564
565 #[test]
566 fn truncated_payload_rejected() {
567 let ck = conv(&SEED_A, &SEED_B);
568 let payload = seal(&ck, b"hi", "a", "b").unwrap();
569 let raw = b64decode(&payload).unwrap();
570 let truncated = b64encode(&raw[..raw.len() - 40]); assert_eq!(
572 open(&ck, &truncated, "a", "b").unwrap_err(),
573 EncError::BadPayloadLen
574 );
575 }
576
577 #[test]
578 fn zero_shared_secret_is_rejected() {
579 let our = x25519_scalar_from_ed25519_seed(&SEED_A);
583 assert_eq!(
584 derive_conversation_key(&our, &[0u8; 32]).unwrap_err(),
585 EncError::ZeroSharedSecret
586 );
587 }
588
589 #[test]
590 fn decode_bomb_cap_boundary() {
591 let ck = conv(&SEED_A, &SEED_B);
594 let over = "A".repeat(MAX_RAW * 4 / 3 + 5);
595 assert_eq!(
596 open(&ck, &over, "a", "b").unwrap_err(),
597 EncError::BadPayloadLen
598 );
599 let big = vec![b'z'; 60000];
601 let payload = seal(&ck, &big, "a", "b").unwrap();
602 assert!(
603 payload.len() < MAX_RAW * 4 / 3 + 5,
604 "real payload is under the cap"
605 );
606 assert_eq!(open(&ck, &payload, "a", "b").unwrap().len(), 60000);
607 }
608
609 #[test]
610 fn calc_padded_len_conformance_nip44_vectors() {
611 let nip44: &[(usize, usize)] = &[
617 (1, 32),
618 (16, 32),
619 (32, 32),
620 (33, 64),
621 (37, 64),
622 (45, 64),
623 (49, 64),
624 (64, 64),
625 (65, 96),
626 (100, 128),
627 (111, 128),
628 (200, 224),
629 (250, 256),
630 (320, 320),
631 (383, 384),
632 (384, 384),
633 (400, 448),
634 (500, 512),
635 (512, 512),
636 (515, 640),
637 (700, 768),
638 (800, 896),
639 (900, 1024),
640 (1020, 1024),
641 (65536, 65536),
642 ];
643 for &(unpadded, padded) in nip44 {
644 assert_eq!(
645 calc_padded_len(unpadded),
646 padded,
647 "calc_padded_len({unpadded})"
648 );
649 }
650 }
651
652 #[test]
653 fn message_keys_conformance_nip44_vector() {
654 let conversation_key =
662 hex_to_32("a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54");
663 let nonce = hex_to_32("e1e6f880560d6d149ed83dcc7e5861ee62a5ee051f7fde9975fe5d25d2a02d72");
664 let (chacha_key, chacha_nonce, hmac_key) = message_keys(&conversation_key, &nonce);
665 assert_eq!(
666 hex::encode(chacha_key),
667 "f145f3bed47cb70dbeaac07f3a3fe683e822b3715edb7c4fe310829014ce7d76"
668 );
669 assert_eq!(hex::encode(chacha_nonce), "c4ad129bb01180c0933a160c");
670 assert_eq!(
671 hex::encode(hmac_key),
672 "027c1db445f05e2eee864a0975b0ddef5b7110583c8c192de3732571ca5838c4"
673 );
674 }
675
676 #[test]
677 fn conversation_key_is_symmetric() {
678 assert_eq!(conv(&SEED_A, &SEED_B), conv(&SEED_B, &SEED_A));
680 }
681
682 #[test]
683 fn derivation_is_deterministic() {
684 assert_eq!(
685 x25519_pub_from_ed25519_seed(&SEED_A),
686 x25519_pub_from_ed25519_seed(&SEED_A)
687 );
688 }
689
690 #[test]
691 fn golden_seed_to_pub_and_conv_key() {
692 let pub_a = x25519_pub_from_ed25519_seed(&SEED_A);
698 let pub_b = x25519_pub_from_ed25519_seed(&SEED_B);
699 assert_eq!(hex::encode(pub_a), GOLDEN_PUB_A);
700 assert_eq!(hex::encode(pub_b), GOLDEN_PUB_B);
701 assert_eq!(hex::encode(conv(&SEED_A, &SEED_B)), GOLDEN_CONV_AB);
702 }
703
704 #[test]
705 fn round_trip_across_lengths() {
706 let ck = conv(&SEED_A, &SEED_B);
707 for &len in &[1usize, 31, 32, 33, 256, 257, 1000, 65535] {
708 let pt = "x".repeat(len);
709 let payload = seal(&ck, pt.as_bytes(), "alice", "bob").unwrap();
710 let got = open(&ck, &payload, "alice", "bob").unwrap();
711 assert_eq!(got, pt, "round-trip failed at len {len}");
712 }
713 }
714
715 #[test]
716 fn direction_binding_rejects_reflection() {
717 let ck = conv(&SEED_A, &SEED_B);
720 let payload = seal(&ck, b"secret", "alice", "bob").unwrap();
721 assert_eq!(
722 open(&ck, &payload, "bob", "alice").unwrap_err(),
723 EncError::MacFail
724 );
725 }
726
727 #[test]
728 fn tamper_is_rejected_before_decrypt() {
729 let ck = conv(&SEED_A, &SEED_B);
730 let payload = seal(&ck, b"hello world", "alice", "bob").unwrap();
731 let raw = b64decode(&payload).unwrap();
732
733 let mut t = raw.clone();
735 t[40] ^= 0x01;
736 assert_eq!(
737 open(&ck, &b64encode(&t), "alice", "bob").unwrap_err(),
738 EncError::MacFail
739 );
740
741 let mut t = raw.clone();
743 t[1] ^= 0x01;
744 assert_eq!(
745 open(&ck, &b64encode(&t), "alice", "bob").unwrap_err(),
746 EncError::MacFail
747 );
748
749 let n = raw.len();
751 let mut t = raw.clone();
752 t[n - 1] ^= 0x01;
753 assert_eq!(
754 open(&ck, &b64encode(&t), "alice", "bob").unwrap_err(),
755 EncError::MacFail
756 );
757
758 let mut t = raw.clone();
760 t[0] = 0x01;
761 assert_eq!(
762 open(&ck, &b64encode(&t), "alice", "bob").unwrap_err(),
763 EncError::BadVersion
764 );
765 }
766
767 #[test]
768 fn plaintext_bounds_enforced() {
769 let ck = conv(&SEED_A, &SEED_B);
770 assert_eq!(
771 seal(&ck, b"", "a", "b").unwrap_err(),
772 EncError::BadLength(0)
773 );
774 let too_big = vec![0u8; 65536];
775 assert_eq!(
776 seal(&ck, &too_big, "a", "b").unwrap_err(),
777 EncError::BadLength(65536)
778 );
779 }
780
781 #[test]
782 fn wrong_conversation_key_fails() {
783 let ck = conv(&SEED_A, &SEED_B);
784 let payload = seal(&ck, b"secret", "alice", "bob").unwrap();
785 let other = x25519_scalar_from_ed25519_seed(&[9u8; 32]);
786 let wrong =
787 derive_conversation_key(&other, &x25519_pub_from_ed25519_seed(&SEED_B)).unwrap();
788 assert_eq!(
789 open(&wrong, &payload, "alice", "bob").unwrap_err(),
790 EncError::MacFail
791 );
792 }
793
794 #[test]
795 fn event_level_round_trip_and_verify_gate() {
796 use crate::signing::{generate_keypair, make_key_id, sign_message_v31};
800
801 let (a_seed, a_pk) = generate_keypair();
802 let (b_seed, b_pk) = generate_keypair();
803 let a_did = "did:wire:alice-1b1b58dd";
804 let b_did = "did:wire:bob-60346e7c";
805 let b_dh = x25519_pub_from_ed25519_seed(&b_seed);
806 let a_dh = x25519_pub_from_ed25519_seed(&a_seed);
807 let _ = &b_pk;
808
809 let trust_b = json!({"agents": {"alice": {
811 "public_keys": [{"key_id": make_key_id("alice", &a_pk), "key": b64encode(&a_pk), "active": true}],
812 "card": {"dh_pubkey": b64encode(&a_dh)},
813 }}});
814
815 let mut event = json!({
817 "from": a_did, "to": b_did, "type": "decision", "kind": 1000,
818 "body": "secret hello",
819 });
820 seal_event_body(&mut event, &b_dh, &a_seed).unwrap();
821 assert_eq!(event["enc"], json!(ENC_DISCRIMINATOR));
822 assert!(
823 event["body"]["ct"].is_string(),
824 "body replaced with ciphertext"
825 );
826 assert_ne!(event["body"], json!("secret hello"));
827 let signed = sign_message_v31(&event, &a_seed, &a_pk, "alice").unwrap();
828
829 assert_eq!(
831 open_event_body(&signed, &trust_b, &b_seed).unwrap(),
832 Some(json!("secret hello"))
833 );
834
835 let mut tampered = signed.clone();
838 tampered["body"]["ct"] = json!("AAAAAAAA");
839 assert!(open_event_body(&tampered, &trust_b, &b_seed).is_err());
840
841 let plain = json!({"from": a_did, "to": b_did, "body": "hi"});
843 assert_eq!(open_event_body(&plain, &trust_b, &b_seed).unwrap(), None);
844 }
845
846 #[test]
847 fn full_card_pin_seal_read_pipeline() {
848 use crate::agent_card::{build_agent_card, card_dh_pubkey, sign_agent_card};
854 use crate::signing::{generate_keypair, sign_message_v31};
855 use crate::trust::{add_agent_card_pin, empty_trust};
856
857 let (a_seed, a_pk) = generate_keypair();
858 let (b_seed, b_pk) = generate_keypair();
859 let a_card = sign_agent_card(&build_agent_card("alice", &a_pk, None, None, None), &a_seed);
860 let _b_card = sign_agent_card(&build_agent_card("bob", &b_pk, None, None, None), &b_seed);
861
862 assert_eq!(
864 card_dh_pubkey(&a_card).unwrap(),
865 b64encode(&x25519_pub_from_ed25519_seed(&a_seed))
866 );
867
868 let mut trust_b = empty_trust();
870 add_agent_card_pin(&mut trust_b, &a_card, Some("VERIFIED")).unwrap();
871
872 let a_handle = a_card["handle"].as_str().unwrap().to_string();
874 let a_did = a_card["did"].as_str().unwrap().to_string();
875 let b_did = _b_card["did"].as_str().unwrap().to_string();
876 let b_dh = x25519_pub_from_ed25519_seed(&b_seed);
877 let mut event = json!({
878 "from": a_did, "to": b_did, "type": "decision", "kind": 1000,
879 "body": "pipeline secret",
880 });
881 seal_event_body(&mut event, &b_dh, &a_seed).unwrap();
882 let signed = sign_message_v31(&event, &a_seed, &a_pk, &a_handle).unwrap();
883
884 let viewed = decrypt_event_for_read(&signed, &trust_b, &b_seed);
886 assert_eq!(viewed["body"], json!("pipeline secret"));
887 assert_eq!(viewed["dec"], json!(true));
888 }
889
890 const GOLDEN_PUB_A: &str = "1b1b58dd50ea14b60da17b790cd02754d970c9bab864ebb3c0f3016fe51d3f57";
892 const GOLDEN_PUB_B: &str = "60346e7c911a5f6ba154129174cafe75b294ac3bbd5549632f48cec6266f8410";
893 const GOLDEN_CONV_AB: &str = "9ade86510fe31aa30c0a583c7282a2cce1447103f2cd70e165489ac5b09dbd2e";
894
895 #[test]
896 #[ignore = "run with --ignored --nocapture to (re)capture golden literals"]
897 fn print_golden() {
898 eprintln!(
899 "PUB_A={}",
900 hex::encode(x25519_pub_from_ed25519_seed(&SEED_A))
901 );
902 eprintln!(
903 "PUB_B={}",
904 hex::encode(x25519_pub_from_ed25519_seed(&SEED_B))
905 );
906 eprintln!("CONV_AB={}", hex::encode(conv(&SEED_A, &SEED_B)));
907 }
908}