1pub 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#[derive(Clone, Debug)]
25pub struct KeyPair {
26 pub pub_key: String,
28 pub priv_key: String,
30 pub epub_key: Option<String>,
32 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#[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#[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#[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#[derive(Clone, Debug)]
135pub struct Identity {
136 pub alias: String,
137 pub pub_key: String,
138 pub epub_key: Option<String>,
139}
140
141#[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 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 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#[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
215pub async fn generate_pair() -> Result<KeyPair, SeaError> {
217 pair::generate_pair().await
218}
219
220pub async fn sign(data: &JsonValue, pair: &KeyPair) -> Result<JsonValue, SeaError> {
222 sign::sign(data, pair).await
223}
224
225pub fn verify_sync(signed_data: &JsonValue, pub_key: &str) -> Result<JsonValue, SeaError> {
227 verify::verify_sync(signed_data, pub_key)
228}
229
230pub async fn verify(signed_data: &JsonValue, pub_key: &str) -> Result<JsonValue, SeaError> {
232 verify_sync(signed_data, pub_key)
233}
234
235pub 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
245pub use secret::secret_sync;
247pub use user::{accept_grant, verify_trust};
248
249pub async fn work(data: &[u8], salt: Option<&[u8]>, opts: WorkOptions) -> Result<String, SeaError> {
251 work::work(data, salt, opts).await
252}
253
254pub async fn secret(their_epub: &str, pair: &KeyPair) -> Result<String, SeaError> {
256 secret::secret(their_epub, pair).await
257}
258
259pub 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
268pub 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
277pub async fn encrypt_symmetric(data: &JsonValue, key: &[u8]) -> Result<JsonValue, SeaError> {
281 encrypt::encrypt_symmetric(data, key).await
282}
283
284pub async fn decrypt_symmetric(encrypted: &JsonValue, key: &[u8]) -> Result<JsonValue, SeaError> {
286 decrypt::decrypt_symmetric(encrypted, key).await
287}
288pub 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
299pub 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
307pub fn is_pubkey_certified(payload: &JsonValue, pubkey: &str) -> bool {
309 certify::is_certified(payload, pubkey)
310}
311
312pub 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 #[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 #[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 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 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 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_user
585 .trust(&bob_pair.pub_key, Some("test/data"), &mut node)
586 .await
587 .unwrap();
588
589 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 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 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 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_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 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 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 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 let bob = User::create("roundtripBob", "bobPass", &mut node)
730 .await
731 .unwrap();
732 let bob_pair = bob.pair();
733
734 alice
736 .trust(&bob_pair.pub_key, Some("api/credentials"), &mut node)
737 .await
738 .unwrap();
739
740 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 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 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 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}