1use ed25519_dalek::{Signer, Verifier};
2use rand::RngCore;
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use tracing::info;
6
7pub const SIGN_PUBLICKEYBYTES: usize = 32;
10pub const SIGN_SECRETKEYBYTES: usize = 64;
11pub const SIGN_BYTES: usize = 64;
12pub const CURVE25519_PUBLICKEYBYTES: usize = 32;
13pub const CURVE25519_SECRETKEYBYTES: usize = 32;
14pub const SEALBYTES: usize = 48; #[derive(Error, Debug)]
17pub enum KeyError {
18 #[error("Keyring error: {0}")]
19 Keyring(#[from] keyring_core::Error),
20 #[error("Cannot modify keys in dev mode (use environment variables)")]
21 DevMode,
22 #[error("Crypto error: {0}")]
23 Crypto(String),
24}
25
26#[derive(Clone, Debug, Serialize, Deserialize)]
28pub enum CloudHomeCredentials {
29 S3 {
31 access_key: String,
32 secret_key: String,
33 },
34 OAuth { token_json: String },
36 None,
38}
39
40#[derive(Clone)]
46pub struct UserKeypair {
47 pub signing_key: [u8; SIGN_SECRETKEYBYTES], pub public_key: [u8; SIGN_PUBLICKEYBYTES], }
50
51impl UserKeypair {
52 pub fn generate() -> Self {
56 let mut seed = [0u8; 32];
57 rand::rng().fill_bytes(&mut seed);
58 let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
59 let public_key = signing_key.verifying_key();
60 Self {
61 signing_key: signing_key.to_keypair_bytes(),
62 public_key: public_key.to_bytes(),
63 }
64 }
65
66 pub fn sign(&self, message: &[u8]) -> [u8; SIGN_BYTES] {
68 let sk = ed25519_dalek::SigningKey::from_keypair_bytes(&self.signing_key)
69 .expect("valid keypair bytes");
70 sk.sign(message).to_bytes()
71 }
72
73 pub fn to_x25519_secret_key(&self) -> [u8; CURVE25519_SECRETKEYBYTES] {
75 let sk = ed25519_dalek::SigningKey::from_keypair_bytes(&self.signing_key)
76 .expect("valid keypair bytes");
77 sk.to_scalar_bytes()
78 }
79
80 pub fn to_x25519_public_key(&self) -> [u8; CURVE25519_PUBLICKEYBYTES] {
82 let vk = ed25519_dalek::VerifyingKey::from_bytes(&self.public_key)
83 .expect("valid public key bytes");
84 vk.to_montgomery().to_bytes()
85 }
86}
87
88pub fn verify_signature(
90 signature: &[u8; SIGN_BYTES],
91 message: &[u8],
92 public_key: &[u8; SIGN_PUBLICKEYBYTES],
93) -> bool {
94 let Ok(vk) = ed25519_dalek::VerifyingKey::from_bytes(public_key) else {
95 return false;
96 };
97 let sig = ed25519_dalek::Signature::from_bytes(signature);
98 vk.verify(message, &sig).is_ok()
99}
100
101pub fn seal_box_encrypt(
107 message: &[u8],
108 recipient_x25519_pk: &[u8; CURVE25519_PUBLICKEYBYTES],
109) -> Vec<u8> {
110 use blake2::{digest::typenum::U24, Blake2b, Digest};
111 use crypto_box::aead::Aead;
112
113 let recipient_pk = crypto_box::PublicKey::from(*recipient_x25519_pk);
114
115 let mut ephemeral_bytes = [0u8; 32];
117 rand::rng().fill_bytes(&mut ephemeral_bytes);
118 let ephemeral_sk = crypto_box::SecretKey::from(ephemeral_bytes);
119 let ephemeral_pk = ephemeral_sk.public_key();
120
121 let mut hasher = Blake2b::<U24>::new();
123 hasher.update(ephemeral_pk.as_bytes());
124 hasher.update(recipient_pk.as_bytes());
125 let nonce = hasher.finalize();
126
127 let salsa_box = crypto_box::SalsaBox::new(&recipient_pk, &ephemeral_sk);
129 let encrypted = salsa_box
130 .encrypt(&nonce, message)
131 .expect("sealed box encryption should not fail");
132
133 let mut out = Vec::with_capacity(32 + encrypted.len());
135 out.extend_from_slice(ephemeral_pk.as_bytes());
136 out.extend_from_slice(&encrypted);
137 out
138}
139
140pub fn seal_box_decrypt(
142 ciphertext: &[u8],
143 _recipient_x25519_pk: &[u8; CURVE25519_PUBLICKEYBYTES],
144 recipient_x25519_sk: &[u8; CURVE25519_SECRETKEYBYTES],
145) -> Result<Vec<u8>, KeyError> {
146 if ciphertext.len() < SEALBYTES {
147 return Err(KeyError::Crypto("Ciphertext too short".to_string()));
148 }
149 let sk = crypto_box::SecretKey::from(*recipient_x25519_sk);
150 sk.unseal(ciphertext).map_err(|_| {
151 KeyError::Crypto("Sealed box decryption failed (wrong key or tampered)".to_string())
152 })
153}
154
155pub fn ed25519_to_x25519_public_key(
161 ed25519_pk: &[u8; SIGN_PUBLICKEYBYTES],
162) -> [u8; CURVE25519_PUBLICKEYBYTES] {
163 let vk = ed25519_dalek::VerifyingKey::from_bytes(ed25519_pk)
164 .expect("valid Ed25519 public key bytes");
165 vk.to_montgomery().to_bytes()
166}
167
168static KEYRING_SERVICE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
181
182pub fn set_keyring_service(name: impl Into<String>) {
184 let _ = KEYRING_SERVICE.set(name.into());
185}
186
187pub fn keyring_service() -> &'static str {
190 KEYRING_SERVICE.get().map(String::as_str).unwrap_or("coven")
191}
192
193pub fn read_keyring(account: &str) -> Result<Option<String>, KeyError> {
202 let entry = keyring_core::Entry::new(keyring_service(), account)?;
203 match entry.get_password() {
204 Ok(p) if p.is_empty() => Ok(None),
205 Ok(p) => Ok(Some(p)),
206 Err(keyring_core::Error::NoEntry) => Ok(None),
207 Err(e) => Err(KeyError::Keyring(e)),
208 }
209}
210
211pub fn read_env(var: &str) -> Result<Option<String>, KeyError> {
215 match std::env::var(var) {
216 Ok(v) if v.is_empty() => Ok(None),
217 Ok(v) => Ok(Some(v)),
218 Err(std::env::VarError::NotPresent) => Ok(None),
219 Err(e @ std::env::VarError::NotUnicode(_)) => {
220 Err(KeyError::Crypto(format!("env var {var}: {e}")))
221 }
222 }
223}
224
225#[derive(Clone)]
226pub struct KeyService {
227 dev_mode: bool,
228 library_id: String,
229}
230
231impl KeyService {
232 pub fn new(dev_mode: bool, library_id: String) -> Self {
233 Self {
234 dev_mode,
235 library_id,
236 }
237 }
238
239 pub fn is_dev_mode(&self) -> bool {
240 self.dev_mode
241 }
242
243 pub fn library_id(&self) -> &str {
246 &self.library_id
247 }
248
249 fn account(&self, base: &str) -> String {
251 format!("{}:{}", base, self.library_id)
252 }
253
254 fn read(&self, env_var: &str, account: &str) -> Result<Option<String>, KeyError> {
258 if self.dev_mode {
259 read_env(env_var)
260 } else {
261 read_keyring(account)
262 }
263 }
264
265 pub fn get_encryption_key(&self) -> Result<Option<String>, KeyError> {
271 self.read("BAE_ENCRYPTION_KEY", &self.account("encryption_master_key"))
272 }
273
274 pub fn get_or_create_encryption_key(&self) -> Result<String, KeyError> {
277 if self.dev_mode {
278 return self.get_encryption_key()?.ok_or(KeyError::DevMode);
279 }
280
281 if let Some(key) = self.get_encryption_key()? {
282 return Ok(key);
283 }
284
285 let key_hex = hex::encode(crate::encryption::generate_random_key());
286 keyring_core::Entry::new(keyring_service(), &self.account("encryption_master_key"))?
287 .set_password(&key_hex)?;
288 info!("Generated and saved new encryption key to keyring");
289 Ok(key_hex)
290 }
291
292 pub fn set_encryption_key(&self, value: &str) -> Result<(), KeyError> {
295 if self.dev_mode {
296 return Err(KeyError::DevMode);
297 }
298
299 keyring_core::Entry::new(keyring_service(), &self.account("encryption_master_key"))?
300 .set_password(value)?;
301 info!("Encryption key saved to keyring");
302 Ok(())
303 }
304
305 pub fn get_cloud_home_credentials(&self) -> Result<Option<CloudHomeCredentials>, KeyError> {
315 let json = self.read(
316 "BAE_CLOUD_HOME_CREDENTIALS",
317 &self.account("cloud_home_credentials"),
318 )?;
319
320 match json {
321 None => Ok(None),
322 Some(j) => {
323 let creds = serde_json::from_str(&j).map_err(|e| {
324 KeyError::Crypto(format!("malformed cloud home credentials JSON: {e}"))
325 })?;
326 Ok(Some(creds))
327 }
328 }
329 }
330
331 pub fn set_cloud_home_credentials(&self, creds: &CloudHomeCredentials) -> Result<(), KeyError> {
336 let json = serde_json::to_string(creds)
337 .map_err(|e| KeyError::Crypto(format!("serialize credentials: {e}")))?;
338
339 if self.dev_mode {
340 std::env::set_var("BAE_CLOUD_HOME_CREDENTIALS", &json);
341 return Ok(());
342 }
343
344 let account = self.account("cloud_home_credentials");
345 keyring_core::Entry::new(keyring_service(), &account)?.set_password(&json)?;
346 info!("Cloud home credentials saved to keyring");
347 Ok(())
348 }
349
350 pub fn delete_cloud_home_credentials(&self) -> Result<(), KeyError> {
355 if self.dev_mode {
356 std::env::remove_var("BAE_CLOUD_HOME_CREDENTIALS");
357 return Ok(());
358 }
359
360 let account = self.account("cloud_home_credentials");
361 match keyring_core::Entry::new(keyring_service(), &account)?.delete_credential() {
362 Ok(()) => {
363 info!("Cloud home credentials deleted from keyring");
364 Ok(())
365 }
366 Err(keyring_core::Error::NoEntry) => Ok(()),
367 Err(e) => Err(KeyError::Keyring(e)),
368 }
369 }
370
371 fn signing_key_env_var(&self) -> String {
383 format!("BAE_USER_SIGNING_KEY_{}", self.library_id)
384 }
385
386 const SIGNING_KEY_KEYRING_ACCOUNT: &'static str = "bae_user_signing_key";
387
388 pub fn get_user_keypair(&self) -> Result<UserKeypair, KeyError> {
391 self.get_user_keypair_inner()?
392 .ok_or_else(|| KeyError::Crypto("No user keypair found in keyring".to_string()))
393 }
394
395 pub fn get_or_create_user_keypair(&self) -> Result<UserKeypair, KeyError> {
401 if let Some(kp) = self.get_user_keypair_inner()? {
402 return Ok(kp);
403 }
404
405 let kp = UserKeypair::generate();
406 self.write_signing_key(&kp.signing_key)?;
407 info!("Generated and saved new user Ed25519 keypair");
408 Ok(kp)
409 }
410
411 pub fn get_user_public_key(&self) -> Result<Option<[u8; SIGN_PUBLICKEYBYTES]>, KeyError> {
415 Ok(self.get_user_keypair_inner()?.map(|kp| kp.public_key))
416 }
417
418 pub fn import_user_keypair(&self, signing_key_bytes: &[u8]) -> Result<(), KeyError> {
422 let signing_key: [u8; SIGN_SECRETKEYBYTES] =
423 signing_key_bytes.try_into().map_err(|_| {
424 KeyError::Crypto(format!(
425 "Signing key must be {SIGN_SECRETKEYBYTES} bytes, got {}",
426 signing_key_bytes.len()
427 ))
428 })?;
429 ed25519_dalek::SigningKey::from_keypair_bytes(&signing_key)
432 .map_err(|e| KeyError::Crypto(format!("Invalid keypair bytes: {e}")))?;
433
434 self.write_signing_key(&signing_key)?;
435 info!("Imported user Ed25519 keypair");
436 Ok(())
437 }
438
439 fn write_signing_key(&self, signing_key: &[u8; SIGN_SECRETKEYBYTES]) -> Result<(), KeyError> {
442 let sk_hex = hex::encode(signing_key);
443 if self.dev_mode {
444 std::env::set_var(self.signing_key_env_var(), &sk_hex);
445 } else {
446 keyring_core::Entry::new(keyring_service(), Self::SIGNING_KEY_KEYRING_ACCOUNT)?
447 .set_password(&sk_hex)?;
448 }
449 Ok(())
450 }
451
452 fn get_user_keypair_inner(&self) -> Result<Option<UserKeypair>, KeyError> {
455 let sk_hex = self.read(
456 &self.signing_key_env_var(),
457 Self::SIGNING_KEY_KEYRING_ACCOUNT,
458 )?;
459 let Some(sk_hex) = sk_hex else {
460 return Ok(None);
461 };
462
463 let signing_key: [u8; SIGN_SECRETKEYBYTES] = hex::decode(&sk_hex)
464 .map_err(|e| KeyError::Crypto(format!("Invalid signing key hex: {e}")))?
465 .try_into()
466 .map_err(|_| KeyError::Crypto("Signing key wrong length".to_string()))?;
467
468 let sk = ed25519_dalek::SigningKey::from_keypair_bytes(&signing_key)
469 .map_err(|e| KeyError::Crypto(format!("Invalid signing key bytes: {e}")))?;
470 let public_key = sk.verifying_key().to_bytes();
471
472 Ok(Some(UserKeypair {
473 signing_key,
474 public_key,
475 }))
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn keypair_generation_produces_valid_keys() {
485 let kp = UserKeypair::generate();
486
487 assert_eq!(kp.signing_key.len(), 64);
489 assert_eq!(kp.public_key.len(), 32);
490
491 assert!(kp.signing_key.iter().any(|&b| b != 0));
493 assert!(kp.public_key.iter().any(|&b| b != 0));
494 }
495
496 #[test]
497 fn two_keypairs_are_distinct() {
498 let kp1 = UserKeypair::generate();
499 let kp2 = UserKeypair::generate();
500 assert_ne!(kp1.public_key, kp2.public_key);
501 }
502
503 #[test]
504 fn sign_and_verify_roundtrip() {
505 let kp = UserKeypair::generate();
506 let message = b"changeset payload";
507
508 let sig = kp.sign(message);
509 assert!(verify_signature(&sig, message, &kp.public_key));
510 }
511
512 #[test]
513 fn verify_rejects_wrong_message() {
514 let kp = UserKeypair::generate();
515 let sig = kp.sign(b"original");
516 assert!(!verify_signature(&sig, b"tampered", &kp.public_key));
517 }
518
519 #[test]
520 fn verify_rejects_wrong_key() {
521 let kp1 = UserKeypair::generate();
522 let kp2 = UserKeypair::generate();
523 let sig = kp1.sign(b"message");
524 assert!(!verify_signature(&sig, b"message", &kp2.public_key));
525 }
526
527 #[test]
528 fn sign_empty_message() {
529 let kp = UserKeypair::generate();
530 let sig = kp.sign(b"");
531 assert!(verify_signature(&sig, b"", &kp.public_key));
532 }
533
534 #[test]
535 fn ed25519_to_x25519_conversion() {
536 let kp = UserKeypair::generate();
537 let x_sk = kp.to_x25519_secret_key();
538 let x_pk = kp.to_x25519_public_key();
539
540 assert_eq!(x_sk.len(), 32);
542 assert_eq!(x_pk.len(), 32);
543 assert!(x_sk.iter().any(|&b| b != 0));
544 assert!(x_pk.iter().any(|&b| b != 0));
545 }
546
547 #[test]
548 fn ed25519_to_x25519_is_deterministic() {
549 let kp = UserKeypair::generate();
550 let x_sk1 = kp.to_x25519_secret_key();
551 let x_sk2 = kp.to_x25519_secret_key();
552 assert_eq!(x_sk1, x_sk2);
553 }
554
555 #[test]
556 fn sealed_box_roundtrip() {
557 let kp = UserKeypair::generate();
558 let x_pk = kp.to_x25519_public_key();
559 let x_sk = kp.to_x25519_secret_key();
560
561 let plaintext = b"library encryption key material";
562 let ciphertext = seal_box_encrypt(plaintext, &x_pk);
563
564 assert_eq!(ciphertext.len(), plaintext.len() + SEALBYTES);
565
566 let decrypted = seal_box_decrypt(&ciphertext, &x_pk, &x_sk).unwrap();
567 assert_eq!(decrypted, plaintext);
568 }
569
570 #[test]
571 fn sealed_box_wrong_key_fails() {
572 let kp1 = UserKeypair::generate();
573 let kp2 = UserKeypair::generate();
574
575 let ciphertext = seal_box_encrypt(b"secret", &kp1.to_x25519_public_key());
576
577 let result = seal_box_decrypt(
578 &ciphertext,
579 &kp2.to_x25519_public_key(),
580 &kp2.to_x25519_secret_key(),
581 );
582 assert!(result.is_err());
583 }
584
585 #[test]
586 fn sealed_box_empty_message() {
587 let kp = UserKeypair::generate();
588 let x_pk = kp.to_x25519_public_key();
589 let x_sk = kp.to_x25519_secret_key();
590
591 let ciphertext = seal_box_encrypt(b"", &x_pk);
592 let decrypted = seal_box_decrypt(&ciphertext, &x_pk, &x_sk).unwrap();
593 assert!(decrypted.is_empty());
594 }
595
596 #[test]
597 fn sealed_box_too_short_ciphertext() {
598 let kp = UserKeypair::generate();
599 let result = seal_box_decrypt(
600 &[0u8; 10], &kp.to_x25519_public_key(),
602 &kp.to_x25519_secret_key(),
603 );
604 assert!(result.is_err());
605 }
606
607 #[test]
608 fn key_service_user_keypair() {
609 let ks = KeyService::new(true, "test-keypair".to_string());
610 std::env::remove_var(ks.signing_key_env_var());
611
612 assert!(ks.get_user_public_key().unwrap().is_none());
614
615 let kp = ks.get_or_create_user_keypair().unwrap();
617
618 let pk = ks.get_user_public_key().unwrap().unwrap();
620 assert_eq!(pk, kp.public_key);
621
622 let kp2 = ks.get_or_create_user_keypair().unwrap();
624 assert_eq!(kp2.public_key, kp.public_key);
625 assert_eq!(kp2.signing_key, kp.signing_key);
626
627 let ks2 = KeyService::new(true, "other-library".to_string());
629 assert!(ks2.get_user_public_key().unwrap().is_none());
630
631 let message = b"test message for signing";
633 let sig = kp.sign(message);
634 assert!(verify_signature(&sig, message, &kp.public_key));
635
636 let kp3 = ks.get_or_create_user_keypair().unwrap();
638 assert!(verify_signature(&sig, message, &kp3.public_key));
639
640 let new_kp = UserKeypair::generate();
642 ks.import_user_keypair(&new_kp.signing_key).unwrap();
643
644 let loaded = ks.get_user_keypair().unwrap();
645 assert_eq!(loaded.public_key, new_kp.public_key);
646 assert_eq!(loaded.signing_key, new_kp.signing_key);
647
648 let sig2 = loaded.sign(b"import test");
650 assert!(verify_signature(&sig2, b"import test", &loaded.public_key));
651
652 assert!(ks.import_user_keypair(&[0u8; 32]).is_err());
654
655 std::env::remove_var(ks.signing_key_env_var());
657 }
658
659 #[test]
663 fn key_service_user_public_key_corrupt_hex_is_err() {
664 let ks = KeyService::new(true, "test-pubkey-corrupt-hex".to_string());
665 std::env::set_var(ks.signing_key_env_var(), "not-hex-zzz");
666
667 assert!(
668 ks.get_user_public_key().is_err(),
669 "corrupt signing-key hex should be an Err"
670 );
671
672 std::env::remove_var(ks.signing_key_env_var());
673 }
674
675 #[test]
677 fn key_service_user_public_key_wrong_length_is_err() {
678 let ks = KeyService::new(true, "test-pubkey-wrong-length".to_string());
679 std::env::set_var(ks.signing_key_env_var(), "0".repeat(32));
681
682 assert!(
683 ks.get_user_public_key().is_err(),
684 "wrong-length signing key should be an Err"
685 );
686
687 std::env::remove_var(ks.signing_key_env_var());
688 }
689
690 #[test]
695 fn key_service_user_keypair_invalid_bytes_is_err() {
696 let ks = KeyService::new(true, "test-keypair-invalid-bytes".to_string());
697 std::env::set_var(ks.signing_key_env_var(), "0".repeat(128));
701
702 assert!(
703 ks.get_user_keypair().is_err(),
704 "signing-key bytes that aren't a valid Ed25519 keypair should be an Err"
705 );
706
707 std::env::remove_var(ks.signing_key_env_var());
708 }
709
710 #[test]
713 #[cfg(unix)]
714 fn read_env_non_utf8_is_err() {
715 use std::os::unix::ffi::OsStrExt;
716 let var = "COVEN_TEST_NOT_UTF8";
717 let bytes = [0xFFu8];
719 std::env::set_var(var, std::ffi::OsStr::from_bytes(&bytes));
720
721 let result = read_env(var);
722 assert!(
723 result.is_err(),
724 "non-utf8 env content should be an Err, got {result:?}"
725 );
726
727 std::env::remove_var(var);
728 }
729
730 #[test]
734 fn cloud_home_credentials_malformed_json_is_err() {
735 let ks = KeyService::new(true, "test-cloud-home-malformed".to_string());
736 std::env::set_var("BAE_CLOUD_HOME_CREDENTIALS", "{not valid json");
737
738 let result = ks.get_cloud_home_credentials();
739 assert!(
740 result.is_err(),
741 "malformed credentials JSON should be an Err, got {result:?}"
742 );
743
744 std::env::remove_var("BAE_CLOUD_HOME_CREDENTIALS");
745 }
746}