Skip to main content

beam/sea/
mod.rs

1//! SEA (Security, Encryption, Authorization) module
2//! Based on Gun.js sea/ directory
3//! Provides encryption, authentication, and authorization capabilities
4
5pub mod certify;
6pub mod decrypt;
7pub mod encrypt;
8pub mod pair;
9pub mod secret;
10pub mod session;
11pub mod sign;
12pub mod user;
13pub mod verify;
14pub mod work;
15
16use crate::types::Value as BeamValue;
17use async_trait::async_trait;
18use serde_json::Value as JsonValue;
19use std::fmt;
20use std::sync::{Arc, RwLock};
21use zeroize::Zeroize;
22
23/// Key pair for signing and encryption
24#[derive(Clone, Debug)]
25pub struct KeyPair {
26    /// Public key for signing (ECDSA, P-256) in x.y base64 format
27    pub pub_key: String,
28    /// Private key for signing (ECDSA, P-256) base64 encoded scalar
29    pub priv_key: String,
30    /// Public key for encryption (ECDH, P-256) in x.y base64 format
31    pub epub_key: Option<String>,
32    /// Private key for encryption (ECDH, P-256) base64 encoded scalar
33    pub epriv_key: Option<String>,
34}
35
36impl Zeroize for KeyPair {
37    fn zeroize(&mut self) {
38        self.priv_key.zeroize();
39        self.pub_key.zeroize();
40        if let Some(ref mut e) = self.epub_key {
41            e.zeroize();
42        }
43        self.epub_key = None;
44        if let Some(ref mut e) = self.epriv_key {
45            e.zeroize();
46        }
47        self.epriv_key = None;
48    }
49}
50
51/// Options for SEA.work()
52#[derive(Clone, Debug)]
53pub struct WorkOptions {
54    pub name: Option<String>,
55    pub iterations: Option<u32>,
56    pub salt: Option<Vec<u8>>,
57    pub hash: Option<String>,
58    pub length: Option<usize>,
59    pub encode: Option<String>,
60}
61
62impl Default for WorkOptions {
63    fn default() -> Self {
64        Self {
65            name: Some("PBKDF2".to_string()),
66            iterations: Some(100_000),
67            salt: None,
68            hash: Some("SHA-256".to_string()),
69            length: Some(512),
70            encode: Some("base64".to_string()),
71        }
72    }
73}
74
75/// SEA module error types
76#[derive(Debug)]
77pub enum SeaError {
78    Crypto(String),
79    InvalidKey,
80    VerificationFailed,
81    Encryption(String),
82    Decryption(String),
83    UserExists,
84    AuthFailed,
85    NotAuthenticated,
86    SessionStorage(String),
87}
88
89impl fmt::Display for SeaError {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            SeaError::Crypto(s) => write!(f, "crypto error: {}", s),
93            SeaError::InvalidKey => write!(f, "invalid key format"),
94            SeaError::VerificationFailed => write!(f, "signature verification failed"),
95            SeaError::Encryption(s) => write!(f, "encryption error: {}", s),
96            SeaError::Decryption(s) => write!(f, "decryption error: {}", s),
97            SeaError::UserExists => write!(f, "user already exists"),
98            SeaError::AuthFailed => write!(f, "wrong user or password"),
99            SeaError::NotAuthenticated => write!(f, "not authenticated"),
100            SeaError::SessionStorage(s) => write!(f, "session storage error: {}", s),
101        }
102    }
103}
104
105impl std::error::Error for SeaError {}
106
107/// Session state behind `Arc<RwLock>` for shared invalidation across clones
108#[derive(Clone, Debug)]
109pub struct SessionState {
110    pub pair: KeyPair,
111    pub alias: Option<String>,
112    pub is_authenticated: bool,
113}
114
115impl Zeroize for SessionState {
116    fn zeroize(&mut self) {
117        self.pair.zeroize();
118        if let Some(ref mut a) = self.alias {
119            a.zeroize();
120        }
121        self.alias = None;
122        self.is_authenticated = false;
123    }
124}
125
126impl Drop for SessionState {
127    fn drop(&mut self) {
128        self.pair.zeroize();
129    }
130}
131
132/// Identity metadata for an authenticated user.
133/// Mirrors Gun.js `user.is` semantics.
134#[derive(Clone, Debug)]
135pub struct Identity {
136    pub alias: String,
137    pub pub_key: String,
138    pub epub_key: Option<String>,
139}
140
141/// Authenticated user with shared session state
142/// Clones share the same underlying session — leave() invalidates all holders.
143#[derive(Clone)]
144pub struct User {
145    pub(crate) inner: Arc<RwLock<SessionState>>,
146}
147
148impl fmt::Debug for User {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        let inner = self.inner.read().map_err(|_| fmt::Error)?;
151        f.debug_struct("User")
152            .field("alias", &inner.alias)
153            .field("is_authenticated", &inner.is_authenticated)
154            .field("pub_key", &inner.pair.pub_key)
155            .finish_non_exhaustive()
156    }
157}
158
159impl User {
160    pub fn from_state(state: SessionState) -> Self {
161        Self {
162            inner: Arc::new(RwLock::new(state)),
163        }
164    }
165
166    pub fn pub_key(&self) -> String {
167        self.inner.read().unwrap().pair.pub_key.clone()
168    }
169
170    pub fn pair(&self) -> KeyPair {
171        self.inner.read().unwrap().pair.clone()
172    }
173
174    pub fn alias(&self) -> Option<String> {
175        self.inner.read().unwrap().alias.clone()
176    }
177
178    pub fn is_authenticated(&self) -> bool {
179        self.inner.read().unwrap().is_authenticated
180    }
181
182    /// Return the user's identity if authenticated.
183    /// Mirrors Gun.js `user.is` — returns alias, pub, epub or None.
184    pub fn is(&self) -> Option<Identity> {
185        let inner = self.inner.read().ok()?;
186        if !inner.is_authenticated {
187            return None;
188        }
189        Some(Identity {
190            alias: inner.alias.clone()?,
191            pub_key: inner.pair.pub_key.clone(),
192            epub_key: inner.pair.epub_key.clone(),
193        })
194    }
195
196    /// Clear key pair from memory and mark unauthenticated (all clones invalidated)
197    pub fn leave(&self) {
198        if let Ok(mut inner) = self.inner.write() {
199            inner.pair.zeroize();
200            inner.alias.zeroize();
201            inner.alias = None;
202            inner.is_authenticated = false;
203        }
204    }
205}
206
207/// Session storage trait for recall() persistence — async by default
208#[async_trait]
209pub trait SessionStorage: Send + Sync {
210    async fn save(&self, alias: &str, pair: &KeyPair) -> Result<(), SeaError>;
211    async fn load(&self, alias: &str) -> Result<Option<KeyPair>, SeaError>;
212    async fn clear(&self, alias: &str) -> Result<(), SeaError>;
213}
214
215/// Generate a new key pair for signing and encryption
216pub async fn generate_pair() -> Result<KeyPair, SeaError> {
217    pair::generate_pair().await
218}
219
220/// Sign data with a key pair
221pub async fn sign(data: &JsonValue, pair: &KeyPair) -> Result<JsonValue, SeaError> {
222    sign::sign(data, pair).await
223}
224
225/// Verify a signature synchronously (for use from message.rs)
226pub fn verify_sync(signed_data: &JsonValue, pub_key: &str) -> Result<JsonValue, SeaError> {
227    verify::verify_sync(signed_data, pub_key)
228}
229
230/// Verify a signature (async wrapper for backwards compat)
231pub async fn verify(signed_data: &JsonValue, pub_key: &str) -> Result<JsonValue, SeaError> {
232    verify_sync(signed_data, pub_key)
233}
234
235/// Verify a signature asynchronously (non-blocking wrapper via spawn_blocking)
236/// Preferred for new code that must not block the async executor.
237pub async fn verify_async(signed_data: &JsonValue, pub_key: &str) -> Result<JsonValue, SeaError> {
238    let data = signed_data.clone();
239    let key = pub_key.to_string();
240    tokio::task::spawn_blocking(move || verify_sync(&data, &key))
241        .await
242        .map_err(|e| SeaError::Crypto(format!("task join error: {}", e)))?
243}
244
245/// Re-export synchronous secret derivation for use inside spawn_blocking closures
246pub use secret::secret_sync;
247pub use user::{accept_grant, verify_trust};
248
249/// Compute proof-of-work or content hash
250pub async fn work(data: &[u8], salt: Option<&[u8]>, opts: WorkOptions) -> Result<String, SeaError> {
251    work::work(data, salt, opts).await
252}
253
254/// Derive shared secret from ECDH key exchange
255pub async fn secret(their_epub: &str, pair: &KeyPair) -> Result<String, SeaError> {
256    secret::secret(their_epub, pair).await
257}
258
259/// Encrypt data using AES-GCM
260pub async fn encrypt(
261    data: &JsonValue,
262    pair: &KeyPair,
263    their_epub: Option<&str>,
264) -> Result<JsonValue, SeaError> {
265    encrypt::encrypt(data, pair, their_epub).await
266}
267
268/// Decrypt data using AES-GCM
269pub async fn decrypt(
270    encrypted: &JsonValue,
271    pair: &KeyPair,
272    their_epub: Option<&str>,
273) -> Result<JsonValue, SeaError> {
274    decrypt::decrypt(encrypted, pair, their_epub).await
275}
276
277// ─── Symmetric cipher re-exports (no ECDH/PBKDF2) ───
278
279/// Encrypt data using a raw 32-byte AES-256 key.
280pub async fn encrypt_symmetric(data: &JsonValue, key: &[u8]) -> Result<JsonValue, SeaError> {
281    encrypt::encrypt_symmetric(data, key).await
282}
283
284/// Decrypt data using a raw 32-byte AES-256 key.
285pub async fn decrypt_symmetric(encrypted: &JsonValue, key: &[u8]) -> Result<JsonValue, SeaError> {
286    decrypt::decrypt_symmetric(encrypted, key).await
287}
288// ─── Capability certificate re-exports ───
289
290/// Build and sign a capability certificate authorizing certificants.
291pub async fn certify(
292    certificants: &[String],
293    policies: Option<&JsonValue>,
294    authority: &KeyPair,
295) -> Result<JsonValue, SeaError> {
296    certify::certify(authority, certificants, policies).await
297}
298
299/// Verify a signed certificate against authority pubkey (sync).
300pub fn verify_certificate(
301    signed_cert: &JsonValue,
302    authority_pubkey: &str,
303) -> Result<JsonValue, SeaError> {
304    certify::verify_certificate(signed_cert, authority_pubkey)
305}
306
307/// Check if a pubkey appears in certificate's certificants list.
308pub fn is_pubkey_certified(payload: &JsonValue, pubkey: &str) -> bool {
309    certify::is_certified(payload, pubkey)
310}
311
312/// Sign JSON data and wrap as a BEAM Value::Text for user-space puts.
313/// The returned value is a JSON-serialized {"m": message, "s": signature} string.
314/// Call this before db.put(value) when writing authenticated user data.
315pub async fn sign_value(data: &JsonValue, pair: &KeyPair) -> Result<BeamValue, SeaError> {
316    let signed = sign(data, pair).await?;
317    let text = serde_json::to_string(&signed)
318        .map_err(|e| SeaError::Crypto(format!("serialize signed: {}", e)))?;
319    Ok(BeamValue::Text(text))
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325    use crate::sea::session::InMemorySessionStorage;
326    use base64::prelude::*;
327
328    use serde_json::json;
329
330    #[tokio::test]
331    async fn test_generate_pair() {
332        let pair = generate_pair().await.unwrap();
333        assert!(!pair.pub_key.is_empty());
334        assert!(!pair.priv_key.is_empty());
335        assert!(pair.epub_key.is_some());
336        assert!(pair.epriv_key.is_some());
337        let parts: Vec<&str> = pair.pub_key.split('.').collect();
338        assert_eq!(parts.len(), 2);
339    }
340
341    #[tokio::test]
342    async fn test_sign_verify_roundtrip() {
343        let pair = generate_pair().await.unwrap();
344        let data = json!({"hello": "world"});
345        let signed = sign(&data, &pair).await.unwrap();
346        let verified = verify(&signed, &pair.pub_key).await.unwrap();
347        assert_eq!(verified, data);
348    }
349
350    #[tokio::test]
351    async fn test_verify_wrong_key_fails() {
352        let pair = generate_pair().await.unwrap();
353        let wrong = generate_pair().await.unwrap();
354        let data = json!({"test": "data"});
355        let signed = sign(&data, &pair).await.unwrap();
356        assert!(verify(&signed, &wrong.pub_key).await.is_err());
357    }
358
359    #[tokio::test]
360    async fn test_work_pbkdf2_deterministic() {
361        let salt = b"test";
362        let data = b"pass";
363        let opts = WorkOptions::default();
364        let r1 = work(data, Some(salt), opts.clone()).await.unwrap();
365        let r2 = work(data, Some(salt), opts.clone()).await.unwrap();
366        assert_eq!(r1, r2);
367    }
368
369    #[tokio::test]
370    async fn test_work_sha256() {
371        let result = work(
372            b"hello",
373            None,
374            WorkOptions {
375                name: Some("SHA-256".to_string()),
376                ..Default::default()
377            },
378        )
379        .await
380        .unwrap();
381        assert!(!result.is_empty());
382    }
383
384    #[tokio::test]
385    async fn test_secret_shared_equality() {
386        let alice = generate_pair().await.unwrap();
387        let bob = generate_pair().await.unwrap();
388        let ab = secret(bob.epub_key.as_ref().unwrap(), &alice)
389            .await
390            .unwrap();
391        let ba = secret(alice.epub_key.as_ref().unwrap(), &bob)
392            .await
393            .unwrap();
394        assert_eq!(ab, ba);
395    }
396
397    #[tokio::test]
398    async fn test_encrypt_decrypt_roundtrip() {
399        let pair = generate_pair().await.unwrap();
400        let data = json!({"secret": "msg"});
401        let enc = encrypt(&data, &pair, None).await.unwrap();
402        let dec = decrypt(&enc, &pair, None).await.unwrap();
403        assert_eq!(dec, data);
404    }
405
406    #[tokio::test]
407    async fn test_user_create_and_auth() {
408        let mut node = crate::Node::new();
409        let user = User::create("testuser", "testpass", &mut node)
410            .await
411            .unwrap();
412        assert!(user.is_authenticated());
413        assert_eq!(user.alias(), Some("testuser".to_string()));
414        let auth = User::auth("testuser", "testpass", &mut node).await.unwrap();
415        assert_eq!(auth.pub_key(), user.pub_key());
416    }
417
418    #[tokio::test]
419    async fn test_user_create_duplicate() {
420        let mut node = crate::Node::new();
421        let _ = User::create("dupuser", "duppass", &mut node).await.unwrap();
422        assert!(User::create("dupuser", "duppass", &mut node).await.is_ok());
423    }
424
425    #[tokio::test]
426    async fn test_user_leave_zeroizes() {
427        let mut node = crate::Node::new();
428        let user = User::create("leaveuser", "leavepass", &mut node)
429            .await
430            .unwrap();
431        assert!(!user.pair().priv_key.is_empty());
432        user.leave();
433        assert!(!user.is_authenticated());
434        assert!(user.pair().priv_key.is_empty());
435        assert!(user.pair().pub_key.is_empty());
436        assert!(user.pair().epriv_key.is_none());
437        assert!(user.pair().epub_key.is_none());
438    }
439
440    #[tokio::test]
441    async fn test_user_builder_create() {
442        let mut node = crate::Node::new();
443        let user = node.user().create("b", "p").await.unwrap();
444        assert_eq!(user.alias(), Some("b".to_string()));
445        assert!(user.is_authenticated());
446    }
447
448    #[tokio::test]
449    async fn test_user_builder_auth() {
450        let mut node = crate::Node::new();
451        let user = node.user().create("a", "p").await.unwrap();
452        let auth = node.user().auth("a", "p").await.unwrap();
453        assert_eq!(auth.pub_key(), user.pub_key());
454    }
455
456    // === Session Tests (extracted InMemorySessionStorage) ===
457
458    #[tokio::test]
459    async fn test_session_memory_save_load_recall() {
460        let mut node = crate::Node::new();
461        let storage = InMemorySessionStorage::new();
462        let user = User::create("sessuser", "sesspass", &mut node)
463            .await
464            .unwrap();
465        user.save_to(&storage).await.unwrap();
466        let recalled = User::recall("sessuser", &storage).await.unwrap();
467        assert!(recalled.is_authenticated());
468        assert_eq!(recalled.pub_key(), user.pub_key());
469    }
470
471    #[tokio::test]
472    async fn test_session_recall_missing() {
473        let storage = InMemorySessionStorage::new();
474        assert!(matches!(
475            User::recall("nosuch", &storage).await,
476            Err(SeaError::AuthFailed)
477        ));
478    }
479
480    #[tokio::test]
481    async fn test_session_leave_invalidates_clones() {
482        let mut node = crate::Node::new();
483        let user = User::create("cloneuser", "clonepass", &mut node)
484            .await
485            .unwrap();
486        let clone = user.clone();
487        user.leave();
488        assert!(!user.is_authenticated());
489        assert!(!clone.is_authenticated());
490        assert!(user.pair().priv_key.is_empty());
491        assert!(clone.pair().priv_key.is_empty());
492    }
493
494    #[tokio::test]
495    async fn test_session_caller_side_remember() {
496        let mut node = crate::Node::new();
497        let storage = InMemorySessionStorage::new();
498        let _ = User::create("remember_user", "remember_pass", &mut node)
499            .await
500            .unwrap();
501        let user = node
502            .user()
503            .auth("remember_user", "remember_pass")
504            .await
505            .unwrap();
506        user.save_to(&storage).await.unwrap();
507        assert!(User::recall("remember_user", &storage).await.is_ok());
508    }
509
510    #[tokio::test]
511    async fn test_verify_async_roundtrip() {
512        let pair = generate_pair().await.unwrap();
513        let data = json!({"hello": "world"});
514        let signed = sign(&data, &pair).await.unwrap();
515        let verified = verify_async(&signed, &pair.pub_key).await.unwrap();
516        assert_eq!(verified, data);
517    }
518
519    // ─── SEA.certify tests ───
520
521    #[tokio::test]
522    async fn test_certify_and_verify() {
523        let authority = generate_pair().await.unwrap();
524        let alice = generate_pair().await.unwrap();
525        let bob = generate_pair().await.unwrap();
526
527        let certificants = vec![alice.pub_key.clone(), bob.pub_key.clone()];
528        let policies = Some(json!({"e": 9999999999999.0_f64, "r": ".*", "w": "skills/"}));
529        let signed = certify(&certificants, policies.as_ref(), &authority)
530            .await
531            .unwrap();
532
533        // Verify with correct authority
534        let payload = verify_certificate(&signed, &authority.pub_key).unwrap();
535        assert!(is_pubkey_certified(&payload, &alice.pub_key));
536        assert!(is_pubkey_certified(&payload, &bob.pub_key));
537        assert!(!is_pubkey_certified(&payload, "someRandomKey"));
538        assert_eq!(payload["r"].as_str(), Some(".*"));
539        assert_eq!(payload["w"].as_str(), Some("skills/"));
540    }
541
542    #[tokio::test]
543    async fn test_certify_expired_fails() {
544        let authority = generate_pair().await.unwrap();
545        let alice = generate_pair().await.unwrap();
546
547        // Expiry in the past (1970)
548        let policies = Some(json!({"e": 1000.0_f64}));
549        let signed = certify(
550            std::slice::from_ref(&alice.pub_key),
551            policies.as_ref(),
552            &authority,
553        )
554        .await
555        .unwrap();
556
557        assert!(verify_certificate(&signed, &authority.pub_key).is_err());
558    }
559
560    #[tokio::test]
561    async fn test_certify_wrong_authority_fails() {
562        let authority = generate_pair().await.unwrap();
563        let wrong = generate_pair().await.unwrap();
564        let alice = generate_pair().await.unwrap();
565
566        let signed = certify(&[alice.pub_key], None, &authority).await.unwrap();
567        assert!(verify_certificate(&signed, &wrong.pub_key).is_err());
568    }
569
570    #[tokio::test]
571    async fn test_trust_grant_accept_roundtrip() {
572        let mut node = crate::Node::new();
573
574        // Alice and Bob each create accounts
575        let alice_user = User::create("alice_int", "secretA", &mut node)
576            .await
577            .unwrap();
578        let bob_user = User::create("bob_int", "secretB", &mut node).await.unwrap();
579
580        let alice_pair = alice_user.pair();
581        let bob_pair = bob_user.pair();
582
583        // Alice trusts Bob to write at path "test/data"
584        alice_user
585            .trust(&bob_pair.pub_key, Some("test/data"), &mut node)
586            .await
587            .unwrap();
588
589        // Alice grants Bob access to secret at "test/data"
590        alice_user
591            .grant(
592                &bob_pair.pub_key,
593                bob_pair.epub_key.as_ref().unwrap(),
594                "test/data",
595                &mut node,
596            )
597            .await
598            .unwrap();
599
600        // Verify trust from Bob's perspective
601        let trusted = verify_trust(
602            &alice_pair.pub_key,
603            &bob_pair.pub_key,
604            Some("test/data"),
605            &mut node,
606        )
607        .await
608        .unwrap();
609        assert!(trusted, "Bob should be trusted by Alice for test/data");
610
611        // Bob accepts the grant and recovers the secret
612        let secret = accept_grant(
613            "test/data",
614            &alice_pair.pub_key,
615            alice_pair.epub_key.as_ref().unwrap(),
616            &bob_pair,
617            &mut node,
618        )
619        .await
620        .unwrap();
621
622        // Secret should be a non-empty base64 string
623        assert!(!secret.is_empty(), "secret should be recovered");
624    }
625
626    #[tokio::test]
627    async fn test_two_copy_grant_owner_can_recover() {
628        let mut node = crate::Node::new();
629
630        let alice_user = User::create("alice2", "passA", &mut node).await.unwrap();
631        let bob_user = User::create("bob2", "passB", &mut node).await.unwrap();
632
633        let alice_pair = alice_user.pair();
634        let bob_pair = bob_user.pair();
635
636        // Alice grants Bob
637        alice_user
638            .grant(
639                &bob_pair.pub_key,
640                bob_pair.epub_key.as_ref().unwrap(),
641                "docs/shared",
642                &mut node,
643            )
644            .await
645            .unwrap();
646
647        // Alice (as owner) reads her own backup copy at ~{pub}/grant/{path}/{my_pub}
648        let mut owner_grant = node
649            .get(&format!("~{}", alice_pair.pub_key))
650            .get("grant")
651            .get("docs__shared")
652            .get(&alice_pair.pub_key);
653
654        let owner_text = owner_grant.once(None).await.and_then(|v| match v {
655            BeamValue::Text(t) => Some(t),
656            _ => None,
657        });
658
659        assert!(owner_text.is_some(), "owner backup copy should exist");
660
661        // Verify it's a signed payload {m,s}
662        let parsed: JsonValue = serde_json::from_str(&owner_text.unwrap()).unwrap();
663        assert!(
664            parsed.get("m").is_some(),
665            "backup should be signed payload with m"
666        );
667        assert!(
668            parsed.get("s").is_some(),
669            "backup should be signed payload with s"
670        );
671    }
672
673    #[tokio::test]
674    async fn test_user_secret_roundtrip() {
675        let mut node = crate::Node::new();
676        let user = User::create("secretAlice", "hunter42", &mut node)
677            .await
678            .unwrap();
679        let pair = user.pair();
680
681        let payload = json!({"token": "abracadabra", "exp": 1234567890});
682        user.secret(&payload, "wallet/key", &mut node)
683            .await
684            .unwrap();
685
686        let path_key = "wallet__key";
687        let mut secret_node = node
688            .get(&format!("~{}", pair.pub_key))
689            .get("secret")
690            .get(path_key);
691
692        let stored = secret_node
693            .once(None)
694            .await
695            .and_then(|v| match v {
696                BeamValue::Text(t) => Some(t),
697                _ => None,
698            })
699            .expect("secret should be stored");
700
701        let outer: JsonValue = serde_json::from_str(&stored).unwrap();
702        let msg = outer["m"].as_str().expect("m should be string");
703        let enc: JsonValue = serde_json::from_str(msg).unwrap();
704
705        let epub = pair.epub_key.as_ref().unwrap();
706        let dh = secret(epub, &pair).await.unwrap();
707        let dh_bytes = BASE64_URL_SAFE_NO_PAD.decode(&dh).unwrap();
708
709        let decrypted = decrypt_symmetric(&enc, &dh_bytes).await.unwrap();
710        assert_eq!(decrypted, payload);
711    }
712
713    #[tokio::test]
714    async fn test_secret_grant_accept_full_roundtrip() {
715        let mut node = crate::Node::new();
716
717        // 1. Alice creates user and stores a self-encrypted secret
718        let alice = User::create("roundtripAlice", "alicePass", &mut node)
719            .await
720            .unwrap();
721        let alice_pair = alice.pair();
722        let secret_data = json!({"api_key": "sk-live-4242", "tier": "pro"});
723        alice
724            .secret(&secret_data, "api/credentials", &mut node)
725            .await
726            .unwrap();
727
728        // 2. Bob creates user
729        let bob = User::create("roundtripBob", "bobPass", &mut node)
730            .await
731            .unwrap();
732        let bob_pair = bob.pair();
733
734        // 3. Alice trusts Bob for the same path
735        alice
736            .trust(&bob_pair.pub_key, Some("api/credentials"), &mut node)
737            .await
738            .unwrap();
739
740        // 4. Alice grants Bob a random shared secret (grant generates 16 bytes internally)
741        let bob_epub = bob_pair.epub_key.as_ref().expect("bob has epub");
742        alice
743            .grant(&bob_pair.pub_key, bob_epub, "api/credentials", &mut node)
744            .await
745            .unwrap();
746
747        // 5. Bob accepts the grant and recovers a shared secret
748        let alice_epub = alice_pair.epub_key.as_ref().expect("alice has epub");
749        let recovered = accept_grant(
750            "api/credentials",
751            &alice_pair.pub_key,
752            alice_epub,
753            &bob_pair,
754            &mut node,
755        )
756        .await
757        .unwrap();
758        assert!(
759            !recovered.is_empty(),
760            "Bob should recover a non-empty secret"
761        );
762        assert_eq!(
763            recovered.len(),
764            22,
765            "grant generates 16 bytes => 22 chars base64 no-pad"
766        );
767
768        // 6. Alice's self-encrypted data is still intact and independent
769        let mut alice_secret = node
770            .get(&format!("~{}", alice_pair.pub_key))
771            .get("secret")
772            .get("api__credentials");
773
774        let self_enc = alice_secret.once(None).await.and_then(|v| match v {
775            BeamValue::Text(t) => Some(t),
776            _ => None,
777        });
778        assert!(
779            self_enc.is_some(),
780            "Alice's self-encrypted secret should still exist"
781        );
782
783        // Verify Bob's recovered secret !== Alice's self-encrypted data (different things)
784        let outer: JsonValue = serde_json::from_str(&self_enc.unwrap()).unwrap();
785        let msg = outer["m"].as_str().expect("m should be string");
786        let enc: JsonValue = serde_json::from_str(msg).unwrap();
787
788        let epub = alice_pair.epub_key.as_ref().unwrap();
789        let dh = secret(epub, &alice_pair).await.unwrap();
790        let dh_bytes = BASE64_URL_SAFE_NO_PAD.decode(&dh).unwrap();
791
792        let decrypted = decrypt_symmetric(&enc, &dh_bytes).await.unwrap();
793        assert_eq!(
794            decrypted, secret_data,
795            "Alice's self-encrypted copy should match original"
796        );
797    }
798}