coven/
keys.rs

1use ed25519_dalek::{Signer, Verifier};
2use rand::RngCore;
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use tracing::info;
6
7// Size constants matching libsodium conventions. Exported so callers (sync modules,
8// envelope.rs, etc.) can use them for array sizes and length checks.
9pub 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; // crypto_box PUBLICKEYBYTES + MACBYTES = 32 + 16
15
16#[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/// Credentials for the cloud home, stored as a single JSON keyring entry.
27#[derive(Clone, Debug, Serialize, Deserialize)]
28pub enum CloudHomeCredentials {
29    /// S3-compatible providers: access key + secret key.
30    S3 {
31        access_key: String,
32        secret_key: String,
33    },
34    /// Consumer cloud providers (Google Drive, Dropbox, OneDrive): OAuth token JSON.
35    OAuth { token_json: String },
36    /// iCloud: no credentials needed (macOS handles auth).
37    None,
38}
39
40/// Ed25519 keypair for signing changesets and membership changes.
41/// The same seed can derive an X25519 keypair for key wrapping.
42///
43/// This is a global identity (not per-library) so attestations accumulate
44/// under one pubkey across all libraries.
45#[derive(Clone)]
46pub struct UserKeypair {
47    pub signing_key: [u8; SIGN_SECRETKEYBYTES], // Ed25519 secret key (64 bytes: seed + public)
48    pub public_key: [u8; SIGN_PUBLICKEYBYTES],  // Ed25519 public key (32 bytes)
49}
50
51impl UserKeypair {
52    /// Generate a new random Ed25519 keypair. The unmanaged primitive behind
53    /// [`KeyService::get_or_create_user_keypair`]; also lets host code (and its
54    /// tests) mint an identity directly.
55    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    /// Sign a message, returning a 64-byte detached signature.
67    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    /// Derive the X25519 secret key from this Ed25519 signing key.
74    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    /// Derive the X25519 public key from this Ed25519 public key.
81    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
88/// Verify a detached Ed25519 signature against a public key.
89pub 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
101/// Encrypt a message to a recipient's X25519 public key using a sealed box.
102/// The sender is anonymous -- only the recipient can decrypt.
103///
104/// Reimplements crypto_box::PublicKey::seal() to avoid rand_core version
105/// mismatch (crypto_box uses rand_core 0.6, we use rand 0.9).
106pub 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    // Generate ephemeral X25519 keypair
116    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    // Nonce = Blake2b-192(ephemeral_pk || recipient_pk) -- matches libsodium sealed box spec
122    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    // Encrypt with XSalsa20-Poly1305
128    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    // Output: ephemeral_pk || ciphertext (matches libsodium format)
134    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
140/// Decrypt a sealed box using the recipient's X25519 keypair.
141pub 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
155/// Convert an Ed25519 public key to an X25519 public key.
156///
157/// This is used when we only have a remote user's Ed25519 public key (hex string)
158/// and need to encrypt something to them via sealed box. The `UserKeypair` methods
159/// handle the local case; this handles the remote case.
160pub 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
168/// Manages secret keys (Discogs API key, encryption key) with lazy reads.
169///
170/// In dev mode, reads from environment variables.
171/// In prod mode, reads from the OS keyring. Each library_id gets its own
172/// namespaced keyring entries so multiple libraries can have independent keys.
173///
174/// `new()` does no I/O -- keyring reads happen lazily in `get_*` methods,
175/// because the macOS protected keyring triggers a system password prompt.
176/// Keyring service name — the app's identity (e.g. "bae", "visible"), used as the
177/// first namespace component of every keyring entry. Set once at startup via
178/// [`set_keyring_service`]; defaults to "coven". Mirrors keyring_core's own
179/// process-global default store, which this design already relies on.
180static KEYRING_SERVICE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
181
182/// Set the keyring service name. Call once at startup, before any keyring access.
183pub fn set_keyring_service(name: impl Into<String>) {
184    let _ = KEYRING_SERVICE.set(name.into());
185}
186
187/// The configured keyring service name (see [`set_keyring_service`]). Public so
188/// host apps store their own credentials under the same service.
189pub fn keyring_service() -> &'static str {
190    KEYRING_SERVICE.get().map(String::as_str).unwrap_or("coven")
191}
192
193/// Read a keyring password by account name, distinguishing "not set"
194/// (`Ok(None)`) from a backend failure (`Err`). An empty stored value is
195/// treated as not set. Public so host apps read their own namespaced
196/// credentials with the same not-set/failure semantics.
197///
198/// Silently collapsing backend errors into `None` would mask the corrupt-
199/// local-identity case (a missing key looks identical to a broken keyring),
200/// which is the worst failure mode for security-sensitive surfaces.
201pub 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
211/// Read an env var, distinguishing "not set" (`Ok(None)`) from non-utf8
212/// content (`Err`). An empty value is treated as not set. Mirrors
213/// [`read_keyring`]'s semantics for dev-mode reads.
214pub 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    /// The library this key service is scoped to. Lets host apps namespace their
244    /// own keyring accounts the same way coven does.
245    pub fn library_id(&self) -> &str {
246        &self.library_id
247    }
248
249    /// Build a namespaced account name for keyring entries.
250    fn account(&self, base: &str) -> String {
251        format!("{}:{}", base, self.library_id)
252    }
253
254    /// Read from the dev-mode env var or the keyring depending on this service's
255    /// mode. The shared dispatch every getter needs; surfaces backend / non-utf8
256    /// failures as `Err` rather than collapsing them into `Ok(None)`.
257    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    /// Read the encryption master key. `Ok(None)` if not configured, `Err`
266    /// if the underlying read failed (keyring backend error or non-utf8 env).
267    ///
268    /// Dev mode: reads `BAE_ENCRYPTION_KEY` env var.
269    /// Prod mode: reads from OS keyring (may trigger a system prompt on first access).
270    pub fn get_encryption_key(&self) -> Result<Option<String>, KeyError> {
271        self.read("BAE_ENCRYPTION_KEY", &self.account("encryption_master_key"))
272    }
273
274    /// Get the encryption key, creating a new one if none exists.
275    /// Errors in dev mode (use environment variables instead).
276    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    /// Save the encryption master key to the OS keyring.
293    /// Errors in dev mode (use environment variables instead).
294    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    // -------------------------------------------------------------------------
306    // Cloud home credentials (library-scoped, single entry)
307    // -------------------------------------------------------------------------
308
309    /// Read cloud home credentials. Returns `Ok(None)` if not set,
310    /// `Err` if the stored value can't be parsed.
311    ///
312    /// Dev mode: reads `BAE_CLOUD_HOME_CREDENTIALS` env var (JSON).
313    /// Prod mode: reads from OS keyring.
314    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    /// Save cloud home credentials.
332    ///
333    /// Dev mode: sets the env var.
334    /// Prod mode: writes to OS keyring.
335    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    /// Delete cloud home credentials.
351    ///
352    /// Dev mode: removes the env var.
353    /// Prod mode: deletes from OS keyring. Silently ignores missing entries.
354    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    // -------------------------------------------------------------------------
372    // Global user keypair (Ed25519 identity, NOT library-scoped)
373    // -------------------------------------------------------------------------
374    //
375    // Only the 64-byte signing key is persisted. The public key (last 32 bytes
376    // of the Ed25519 keypair) is derived on load via `SigningKey::verifying_key`.
377    // Two-entry storage would make a torn-write or partial-restore look like a
378    // valid-but-mismatched keypair; this design makes that shape unrepresentable.
379
380    /// Dev-mode env var name, namespaced by library_id so parallel tests
381    /// don't stomp on each other's keypairs.
382    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    /// Load the user's Ed25519 keypair from the keyring. Returns an error if
389    /// no keypair exists (unlike `get_or_create_user_keypair` which creates one).
390    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    /// Load the user's Ed25519 keypair from the keyring, creating a new one if
396    /// none exists. This is a global identity shared across all libraries.
397    ///
398    /// Dev mode: reads env vars namespaced by library_id (hex).
399    /// Falls back to generating and storing in env vars so tests can round-trip.
400    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    /// Return just the user's Ed25519 public key. `Ok(None)` if not stored,
412    /// `Err` if the stored signing key is corrupt. Derives from the signing
413    /// key — there is no separate public-key entry.
414    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    /// Import an Ed25519 keypair from raw bytes (64 bytes: seed + public key).
419    /// Overwrites any existing keypair. Used during restore to preserve the
420    /// original device's membership identity.
421    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        // Validate it's a real keypair before storing, so a later load can't
430        // fail in a way the import path missed.
431        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    /// Persist the 64-byte signing key. Shared by generate-and-store and
440    /// import. The public key is not persisted — it's derived at load.
441    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    /// Internal: try to load the user keypair. Reads only the signing key and
453    /// derives the public key from it.
454    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        // Ed25519 secret key is 64 bytes, public key is 32 bytes
488        assert_eq!(kp.signing_key.len(), 64);
489        assert_eq!(kp.public_key.len(), 32);
490
491        // Keys should not be all zeros (astronomically unlikely)
492        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        // Should produce non-zero 32-byte keys
541        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], // shorter than SEALBYTES
601            &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        // No keypair yet
613        assert!(ks.get_user_public_key().unwrap().is_none());
614
615        // Generate and store
616        let kp = ks.get_or_create_user_keypair().unwrap();
617
618        // Should be retrievable now
619        let pk = ks.get_user_public_key().unwrap().unwrap();
620        assert_eq!(pk, kp.public_key);
621
622        // Calling again returns the same keypair (idempotent)
623        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        // Different library_id gets its own keypair (isolated in dev mode)
628        let ks2 = KeyService::new(true, "other-library".to_string());
629        assert!(ks2.get_user_public_key().unwrap().is_none());
630
631        // Stored keypair can sign and verify
632        let message = b"test message for signing";
633        let sig = kp.sign(message);
634        assert!(verify_signature(&sig, message, &kp.public_key));
635
636        // Reloaded keypair produces consistent verification
637        let kp3 = ks.get_or_create_user_keypair().unwrap();
638        assert!(verify_signature(&sig, message, &kp3.public_key));
639
640        // Import a different keypair and verify it replaces the current one
641        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        // Imported keypair can sign and verify
649        let sig2 = loaded.sign(b"import test");
650        assert!(verify_signature(&sig2, b"import test", &loaded.public_key));
651
652        // Import rejects wrong-length bytes
653        assert!(ks.import_user_keypair(&[0u8; 32]).is_err());
654
655        // Clean up
656        std::env::remove_var(ks.signing_key_env_var());
657    }
658
659    /// Corrupt hex in the stored signing key surfaces as `Err`, not `None`.
660    /// `get_user_public_key` derives the public key from the signing key, so
661    /// the decode error fires here too.
662    #[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    /// Hex that decodes but to the wrong length is also `Err`.
676    #[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        // 32 hex chars = 16 bytes; signing key needs 64 bytes.
680        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    /// Signing-key bytes that decode to the right length but aren't a valid
691    /// Ed25519 keypair (seed + verifying key mismatch) surface as `Err`. The
692    /// public key is derived from the signing key, so this is the only check
693    /// needed — there's no separate public-key entry to disagree with it.
694    #[test]
695    fn key_service_user_keypair_invalid_bytes_is_err() {
696        let ks = KeyService::new(true, "test-keypair-invalid-bytes".to_string());
697        // 128 hex chars = 64 bytes — right length, but the last 32 don't match
698        // the verifying key derived from the first 32, so from_keypair_bytes
699        // rejects it.
700        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    /// A non-utf8 env var surfaces as `Err` from `read_env`, not silently as
711    /// `None`. `VarError::NotUnicode` is broken state, not "not configured."
712    #[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        // 0xFF is invalid as the lead byte of any UTF-8 sequence.
718        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    /// Malformed JSON in the cloud-home credentials surfaces as `Err`, not
731    /// `Ok(None)`. Stored bytes that can't be parsed are corruption, not
732    /// "no credentials configured."
733    #[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}