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
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 const SEED_A: [u8; 32] = [1u8; 32];
468 const SEED_B: [u8; 32] = [2u8; 32];
469
470 fn conv(seed_self: &[u8; 32], seed_peer: &[u8; 32]) -> [u8; 32] {
471 let our = x25519_scalar_from_ed25519_seed(seed_self);
472 let peer_pub = x25519_pub_from_ed25519_seed(seed_peer);
473 derive_conversation_key(&our, &peer_pub).unwrap()
474 }
475
476 fn hex_to_32(h: &str) -> [u8; 32] {
477 let v = hex::decode(h).expect("valid hex");
478 let mut a = [0u8; 32];
479 a.copy_from_slice(&v);
480 a
481 }
482
483 #[test]
484 fn round_trips_with_production_did_identity_form() {
485 let ck = conv(&SEED_A, &SEED_B);
489 let from = "did:wire:alice-1b1b58dd";
490 let to = "did:wire:bob-60346e7c";
491 let payload = seal(&ck, b"production-form message", from, to).unwrap();
492 assert_eq!(
493 open(&ck, &payload, from, to).unwrap(),
494 "production-form message"
495 );
496 assert_eq!(
500 open(&ck, &payload, "alice", to).unwrap_err(),
501 EncError::MacFail
502 );
503 }
504
505 #[test]
506 fn oversized_input_rejected_without_large_alloc() {
507 let ck = conv(&SEED_A, &SEED_B);
510 let bomb = "A".repeat(10_000_000);
511 assert_eq!(
512 open(&ck, &bomb, "a", "b").unwrap_err(),
513 EncError::BadPayloadLen
514 );
515 }
516
517 #[test]
518 fn truncated_payload_rejected() {
519 let ck = conv(&SEED_A, &SEED_B);
520 let payload = seal(&ck, b"hi", "a", "b").unwrap();
521 let raw = b64decode(&payload).unwrap();
522 let truncated = b64encode(&raw[..raw.len() - 40]); assert_eq!(
524 open(&ck, &truncated, "a", "b").unwrap_err(),
525 EncError::BadPayloadLen
526 );
527 }
528
529 #[test]
530 fn zero_shared_secret_is_rejected() {
531 let our = x25519_scalar_from_ed25519_seed(&SEED_A);
535 assert_eq!(
536 derive_conversation_key(&our, &[0u8; 32]).unwrap_err(),
537 EncError::ZeroSharedSecret
538 );
539 }
540
541 #[test]
542 fn decode_bomb_cap_boundary() {
543 let ck = conv(&SEED_A, &SEED_B);
546 let over = "A".repeat(MAX_RAW * 4 / 3 + 5);
547 assert_eq!(
548 open(&ck, &over, "a", "b").unwrap_err(),
549 EncError::BadPayloadLen
550 );
551 let big = vec![b'z'; 60000];
553 let payload = seal(&ck, &big, "a", "b").unwrap();
554 assert!(
555 payload.len() < MAX_RAW * 4 / 3 + 5,
556 "real payload is under the cap"
557 );
558 assert_eq!(open(&ck, &payload, "a", "b").unwrap().len(), 60000);
559 }
560
561 #[test]
562 fn calc_padded_len_conformance_nip44_vectors() {
563 let nip44: &[(usize, usize)] = &[
569 (1, 32),
570 (16, 32),
571 (32, 32),
572 (33, 64),
573 (37, 64),
574 (45, 64),
575 (49, 64),
576 (64, 64),
577 (65, 96),
578 (100, 128),
579 (111, 128),
580 (200, 224),
581 (250, 256),
582 (320, 320),
583 (383, 384),
584 (384, 384),
585 (400, 448),
586 (500, 512),
587 (512, 512),
588 (515, 640),
589 (700, 768),
590 (800, 896),
591 (900, 1024),
592 (1020, 1024),
593 (65536, 65536),
594 ];
595 for &(unpadded, padded) in nip44 {
596 assert_eq!(
597 calc_padded_len(unpadded),
598 padded,
599 "calc_padded_len({unpadded})"
600 );
601 }
602 }
603
604 #[test]
605 fn message_keys_conformance_nip44_vector() {
606 let conversation_key =
614 hex_to_32("a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54");
615 let nonce = hex_to_32("e1e6f880560d6d149ed83dcc7e5861ee62a5ee051f7fde9975fe5d25d2a02d72");
616 let (chacha_key, chacha_nonce, hmac_key) = message_keys(&conversation_key, &nonce);
617 assert_eq!(
618 hex::encode(chacha_key),
619 "f145f3bed47cb70dbeaac07f3a3fe683e822b3715edb7c4fe310829014ce7d76"
620 );
621 assert_eq!(hex::encode(chacha_nonce), "c4ad129bb01180c0933a160c");
622 assert_eq!(
623 hex::encode(hmac_key),
624 "027c1db445f05e2eee864a0975b0ddef5b7110583c8c192de3732571ca5838c4"
625 );
626 }
627
628 #[test]
629 fn conversation_key_is_symmetric() {
630 assert_eq!(conv(&SEED_A, &SEED_B), conv(&SEED_B, &SEED_A));
632 }
633
634 #[test]
635 fn derivation_is_deterministic() {
636 assert_eq!(
637 x25519_pub_from_ed25519_seed(&SEED_A),
638 x25519_pub_from_ed25519_seed(&SEED_A)
639 );
640 }
641
642 #[test]
643 fn golden_seed_to_pub_and_conv_key() {
644 let pub_a = x25519_pub_from_ed25519_seed(&SEED_A);
650 let pub_b = x25519_pub_from_ed25519_seed(&SEED_B);
651 assert_eq!(hex::encode(pub_a), GOLDEN_PUB_A);
652 assert_eq!(hex::encode(pub_b), GOLDEN_PUB_B);
653 assert_eq!(hex::encode(conv(&SEED_A, &SEED_B)), GOLDEN_CONV_AB);
654 }
655
656 #[test]
657 fn round_trip_across_lengths() {
658 let ck = conv(&SEED_A, &SEED_B);
659 for &len in &[1usize, 31, 32, 33, 256, 257, 1000, 65535] {
660 let pt = "x".repeat(len);
661 let payload = seal(&ck, pt.as_bytes(), "alice", "bob").unwrap();
662 let got = open(&ck, &payload, "alice", "bob").unwrap();
663 assert_eq!(got, pt, "round-trip failed at len {len}");
664 }
665 }
666
667 #[test]
668 fn direction_binding_rejects_reflection() {
669 let ck = conv(&SEED_A, &SEED_B);
672 let payload = seal(&ck, b"secret", "alice", "bob").unwrap();
673 assert_eq!(
674 open(&ck, &payload, "bob", "alice").unwrap_err(),
675 EncError::MacFail
676 );
677 }
678
679 #[test]
680 fn tamper_is_rejected_before_decrypt() {
681 let ck = conv(&SEED_A, &SEED_B);
682 let payload = seal(&ck, b"hello world", "alice", "bob").unwrap();
683 let raw = b64decode(&payload).unwrap();
684
685 let mut t = raw.clone();
687 t[40] ^= 0x01;
688 assert_eq!(
689 open(&ck, &b64encode(&t), "alice", "bob").unwrap_err(),
690 EncError::MacFail
691 );
692
693 let mut t = raw.clone();
695 t[1] ^= 0x01;
696 assert_eq!(
697 open(&ck, &b64encode(&t), "alice", "bob").unwrap_err(),
698 EncError::MacFail
699 );
700
701 let n = raw.len();
703 let mut t = raw.clone();
704 t[n - 1] ^= 0x01;
705 assert_eq!(
706 open(&ck, &b64encode(&t), "alice", "bob").unwrap_err(),
707 EncError::MacFail
708 );
709
710 let mut t = raw.clone();
712 t[0] = 0x01;
713 assert_eq!(
714 open(&ck, &b64encode(&t), "alice", "bob").unwrap_err(),
715 EncError::BadVersion
716 );
717 }
718
719 #[test]
720 fn plaintext_bounds_enforced() {
721 let ck = conv(&SEED_A, &SEED_B);
722 assert_eq!(
723 seal(&ck, b"", "a", "b").unwrap_err(),
724 EncError::BadLength(0)
725 );
726 let too_big = vec![0u8; 65536];
727 assert_eq!(
728 seal(&ck, &too_big, "a", "b").unwrap_err(),
729 EncError::BadLength(65536)
730 );
731 }
732
733 #[test]
734 fn wrong_conversation_key_fails() {
735 let ck = conv(&SEED_A, &SEED_B);
736 let payload = seal(&ck, b"secret", "alice", "bob").unwrap();
737 let other = x25519_scalar_from_ed25519_seed(&[9u8; 32]);
738 let wrong =
739 derive_conversation_key(&other, &x25519_pub_from_ed25519_seed(&SEED_B)).unwrap();
740 assert_eq!(
741 open(&wrong, &payload, "alice", "bob").unwrap_err(),
742 EncError::MacFail
743 );
744 }
745
746 #[test]
747 fn event_level_round_trip_and_verify_gate() {
748 use crate::signing::{generate_keypair, make_key_id, sign_message_v31};
752
753 let (a_seed, a_pk) = generate_keypair();
754 let (b_seed, b_pk) = generate_keypair();
755 let a_did = "did:wire:alice-1b1b58dd";
756 let b_did = "did:wire:bob-60346e7c";
757 let b_dh = x25519_pub_from_ed25519_seed(&b_seed);
758 let a_dh = x25519_pub_from_ed25519_seed(&a_seed);
759 let _ = &b_pk;
760
761 let trust_b = json!({"agents": {"alice": {
763 "public_keys": [{"key_id": make_key_id("alice", &a_pk), "key": b64encode(&a_pk), "active": true}],
764 "card": {"dh_pubkey": b64encode(&a_dh)},
765 }}});
766
767 let mut event = json!({
769 "from": a_did, "to": b_did, "type": "decision", "kind": 1000,
770 "body": "secret hello",
771 });
772 seal_event_body(&mut event, &b_dh, &a_seed).unwrap();
773 assert_eq!(event["enc"], json!(ENC_DISCRIMINATOR));
774 assert!(
775 event["body"]["ct"].is_string(),
776 "body replaced with ciphertext"
777 );
778 assert_ne!(event["body"], json!("secret hello"));
779 let signed = sign_message_v31(&event, &a_seed, &a_pk, "alice").unwrap();
780
781 assert_eq!(
783 open_event_body(&signed, &trust_b, &b_seed).unwrap(),
784 Some(json!("secret hello"))
785 );
786
787 let mut tampered = signed.clone();
790 tampered["body"]["ct"] = json!("AAAAAAAA");
791 assert!(open_event_body(&tampered, &trust_b, &b_seed).is_err());
792
793 let plain = json!({"from": a_did, "to": b_did, "body": "hi"});
795 assert_eq!(open_event_body(&plain, &trust_b, &b_seed).unwrap(), None);
796 }
797
798 #[test]
799 fn full_card_pin_seal_read_pipeline() {
800 use crate::agent_card::{build_agent_card, card_dh_pubkey, sign_agent_card};
806 use crate::signing::{generate_keypair, sign_message_v31};
807 use crate::trust::{add_agent_card_pin, empty_trust};
808
809 let (a_seed, a_pk) = generate_keypair();
810 let (b_seed, b_pk) = generate_keypair();
811 let a_card = sign_agent_card(&build_agent_card("alice", &a_pk, None, None, None), &a_seed);
812 let _b_card = sign_agent_card(&build_agent_card("bob", &b_pk, None, None, None), &b_seed);
813
814 assert_eq!(
816 card_dh_pubkey(&a_card).unwrap(),
817 b64encode(&x25519_pub_from_ed25519_seed(&a_seed))
818 );
819
820 let mut trust_b = empty_trust();
822 add_agent_card_pin(&mut trust_b, &a_card, Some("VERIFIED"));
823
824 let a_handle = a_card["handle"].as_str().unwrap().to_string();
826 let a_did = a_card["did"].as_str().unwrap().to_string();
827 let b_did = _b_card["did"].as_str().unwrap().to_string();
828 let b_dh = x25519_pub_from_ed25519_seed(&b_seed);
829 let mut event = json!({
830 "from": a_did, "to": b_did, "type": "decision", "kind": 1000,
831 "body": "pipeline secret",
832 });
833 seal_event_body(&mut event, &b_dh, &a_seed).unwrap();
834 let signed = sign_message_v31(&event, &a_seed, &a_pk, &a_handle).unwrap();
835
836 let viewed = decrypt_event_for_read(&signed, &trust_b, &b_seed);
838 assert_eq!(viewed["body"], json!("pipeline secret"));
839 assert_eq!(viewed["dec"], json!(true));
840 }
841
842 const GOLDEN_PUB_A: &str = "1b1b58dd50ea14b60da17b790cd02754d970c9bab864ebb3c0f3016fe51d3f57";
844 const GOLDEN_PUB_B: &str = "60346e7c911a5f6ba154129174cafe75b294ac3bbd5549632f48cec6266f8410";
845 const GOLDEN_CONV_AB: &str = "9ade86510fe31aa30c0a583c7282a2cce1447103f2cd70e165489ac5b09dbd2e";
846
847 #[test]
848 #[ignore = "run with --ignored --nocapture to (re)capture golden literals"]
849 fn print_golden() {
850 eprintln!(
851 "PUB_A={}",
852 hex::encode(x25519_pub_from_ed25519_seed(&SEED_A))
853 );
854 eprintln!(
855 "PUB_B={}",
856 hex::encode(x25519_pub_from_ed25519_seed(&SEED_B))
857 );
858 eprintln!("CONV_AB={}", hex::encode(conv(&SEED_A, &SEED_B)));
859 }
860}