1use ed25519_dalek::{Signer, SigningKey, Verifier};
2use rand::RngCore;
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6pub const SIGN_PUBLICKEYBYTES: usize = ed25519_dalek::PUBLIC_KEY_LENGTH;
7pub const SIGN_SECRETKEYBYTES: usize = ed25519_dalek::KEYPAIR_LENGTH;
8pub const SIGN_BYTES: usize = ed25519_dalek::SIGNATURE_LENGTH;
9pub const CURVE25519_PUBLICKEYBYTES: usize = crypto_box::KEY_SIZE;
10pub const CURVE25519_SECRETKEYBYTES: usize = crypto_box::KEY_SIZE;
11#[cfg(test)]
12pub(crate) const SEALBYTES: usize = crypto_box::SEALBYTES;
13
14#[derive(Error, Debug)]
15pub enum KeyError {
16 #[error("file error: {0}")]
17 File(#[from] coven_foundation::atomic_file::FileError),
18 #[error("keyring operation failed: {0}")]
19 Keyring(#[source] keyring_core::Error),
20 #[error("failed to start the keyring worker: {0}")]
21 KeyringWorkerStart(#[source] std::io::Error),
22 #[error("the keyring worker stopped while attempting to {operation}")]
23 KeyringWorkerStopped { operation: &'static str },
24 #[error("key custody {operation} failed: {source}")]
25 Custody {
26 operation: &'static str,
27 #[source]
28 source: Box<dyn std::error::Error + Send + Sync + 'static>,
29 },
30 #[error("{operation}: {source}")]
31 Json {
32 operation: &'static str,
33 #[source]
34 source: serde_json::Error,
35 },
36 #[error("{subject} is not valid hexadecimal: {source}")]
37 Hex {
38 subject: &'static str,
39 #[source]
40 source: hex::FromHexError,
41 },
42 #[error("{subject} has length {actual}, expected {expected}")]
43 InvalidLength {
44 subject: &'static str,
45 expected: usize,
46 actual: usize,
47 },
48 #[error("stored signing key is invalid: {0}")]
49 SigningKey(#[source] ed25519_dalek::SignatureError),
50 #[error("key encryption failed: {0}")]
51 Encryption(#[from] crate::encryption::EncryptionError),
52 #[error("decrypted key material is not UTF-8: {0}")]
53 Utf8(#[from] std::string::FromUtf8Error),
54 #[error("base64-encoded key material is invalid: {0}")]
55 Base64(#[from] base64::DecodeError),
56 #[error("passphrase key derivation {operation} failed: {source}")]
57 PassphraseKdf {
58 operation: &'static str,
59 #[source]
60 source: Box<dyn std::error::Error + Send + Sync + 'static>,
61 },
62 #[error("passphrase envelope is version {actual}, not this build's version {expected}; update to a build that understands it")]
63 UnsupportedPassphraseEnvelopeVersion { actual: u32, expected: u32 },
64 #[error("passphrase envelope names KDF {actual:?}, but this build only supports {expected:?}")]
65 UnsupportedPassphraseKdf {
66 actual: String,
67 expected: &'static str,
68 },
69 #[error("passphrase envelope's Argon2id {parameter} ({actual}) is below the required floor ({minimum})")]
70 WeakArgon2Parameter {
71 parameter: &'static str,
72 actual: u32,
73 minimum: u32,
74 },
75 #[error("envelope decryption failed: wrong passphrase or a corrupt file")]
76 PassphraseEnvelopeDecryption,
77 #[error("sealed box decryption failed (wrong key or tampered)")]
78 SealedBoxDecryption,
79 #[error("invalid Ed25519 public key point")]
80 InvalidEd25519PublicKey,
81 #[error("weak Ed25519 public key point cannot identify a recipient")]
82 WeakEd25519PublicKey,
83 #[error("all-zero X25519 public key cannot identify a recipient")]
84 AllZeroX25519PublicKey,
85 #[error("all-zero X25519 shared secret cannot identify a recipient")]
86 AllZeroX25519SharedSecret,
87 #[error("cannot rotate the key of a plaintext cloud home")]
88 PlaintextCloudKeyRotation,
89 #[error("live keyring changed without retaining an adopted rotation")]
90 UnretainedKeyRotation,
91 #[error("keyring service is already registered as {registered:?}; cannot re-register as {requested:?}")]
92 ServiceAlreadyRegistered {
93 registered: String,
94 requested: String,
95 },
96 #[error("keyring entry {account} is present but empty (corrupt)")]
97 EmptyKeyringEntry { account: String },
98 #[error("cannot {operation} cloud-home credentials after their setup was rolled back")]
99 CloudCredentialsRolledBack { operation: &'static str },
100 #[error("cloud-home credentials belong to a replaced provider connection")]
101 CloudCredentialsSuperseded,
102 #[error("cannot {operation} a master key after its setup was rolled back")]
103 MasterKeySetupRolledBack { operation: &'static str },
104 #[error("Apple keyring entry was not constructed by the protected-data store")]
105 UnexpectedAppleKeyringEntry,
106 #[cfg(any(test, feature = "test-utils"))]
107 #[error("test keyring entry was not constructed by the mock store")]
108 UnexpectedTestKeyringEntry,
109 #[error(
110 "no keyring store is installed; the host must install the platform keyring store at startup (set_keyring_service) before any key operation"
111 )]
112 StoreNotInstalled,
113 #[error(
114 "no bundled keyring store exists for this target; the host must supply one via keyring_core::set_default_store before registering the keyring service"
115 )]
116 UnsupportedKeyringPlatform,
117 #[error(
118 "no keyring service is registered; the host must call set_keyring_service at startup before any key operation"
119 )]
120 ServiceNotRegistered,
121 #[error(
122 "no identity is established for this store; create, join, or restore the store first — each establishes this store's identity as part of what it does"
123 )]
124 NoDeviceIdentity,
125 #[error(
126 "this store's identity is already established under a different key (existing {existing_pubkey_hex}, attempted import {imported_pubkey_hex}); importing a different identity would strand this store's membership entries"
127 )]
128 IdentityMismatch {
129 existing_pubkey_hex: String,
130 imported_pubkey_hex: String,
131 },
132 #[error(
133 "no pending identity is held for device pairing {pending_public_key_hex}; the pairing may have already completed, been abandoned, or never existed"
134 )]
135 NoPendingIdentity { pending_public_key_hex: String },
136 #[error("invalid host secret name {name:?}: {reason}")]
137 InvalidSecretName { name: String, reason: String },
138 #[error(
150 "the OS refused this keychain operation with errSecMissingEntitlement \
151 (OSStatus -34018): the process has no team-prefixed keychain-access-groups \
152 entitlement backed by an embedded provisioning profile; set DEVELOPMENT_TEAM \
153 so Xcode's automatic signing fetches and embeds one (a keychain-access-groups \
154 entitlement present WITHOUT a provisioning profile is a different failure: the \
155 process is killed by the kernel at launch, not this error) — a build with no \
156 team must omit the entitlement and will hit this same error on first key use"
157 )]
158 MissingKeychainEntitlement,
159 #[error(
166 "the OS refused this keychain operation with errSecInteractionNotAllowed \
167 (OSStatus -25308): the keychain is locked, the display is asleep, or this \
168 login session cannot show UI — the same operation succeeds once the session \
169 unlocks, so retry it then rather than treating the key as missing"
170 )]
171 KeychainTemporarilyUnavailable,
172}
173
174#[derive(Clone, Serialize, Deserialize)]
179pub enum CloudHomeCredentials {
180 S3 {
182 access_key: String,
183 secret_key: String,
184 },
185 OAuth { tokens: crate::keys::OAuthTokens },
187}
188
189impl std::fmt::Debug for CloudHomeCredentials {
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 match self {
192 CloudHomeCredentials::S3 {
193 access_key,
194 secret_key: _,
195 } => f
196 .debug_struct("S3")
197 .field("access_key", access_key)
198 .field("secret_key", &"<redacted>")
199 .finish(),
200 CloudHomeCredentials::OAuth { tokens: _ } => f
201 .debug_struct("OAuth")
202 .field("tokens", &"<redacted>")
203 .finish(),
204 }
205 }
206}
207
208#[derive(Clone)]
216pub struct UserKeypair {
217 signing_key: SigningKey,
218}
219
220pub trait DeviceSigningAuthority: Send + Sync {
222 fn public_key_hex(&self) -> String;
223 fn sign(&self, message: &[u8]) -> [u8; SIGN_BYTES];
224}
225
226pub trait IdentityKeyAuthority: Send + Sync {
228 fn public_key(&self) -> [u8; SIGN_PUBLICKEYBYTES];
229 fn sign(&self, message: &[u8]) -> [u8; SIGN_BYTES];
230 fn to_x25519_secret_key(&self) -> [u8; CURVE25519_SECRETKEYBYTES];
231}
232
233impl IdentityKeyAuthority for UserKeypair {
234 fn public_key(&self) -> [u8; SIGN_PUBLICKEYBYTES] {
235 self.public_key()
236 }
237
238 fn sign(&self, message: &[u8]) -> [u8; SIGN_BYTES] {
239 self.sign(message)
240 }
241
242 fn to_x25519_secret_key(&self) -> [u8; CURVE25519_SECRETKEYBYTES] {
243 self.to_x25519_secret_key()
244 }
245}
246
247impl DeviceSigningAuthority for UserKeypair {
248 fn public_key_hex(&self) -> String {
249 public_key_hex(self)
250 }
251
252 fn sign(&self, message: &[u8]) -> [u8; SIGN_BYTES] {
253 self.sign(message)
254 }
255}
256
257impl UserKeypair {
258 pub fn generate() -> Self {
262 let mut seed = [0u8; 32];
263 rand::rng().fill_bytes(&mut seed);
264 let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
265 Self { signing_key }
266 }
267
268 pub fn from_signing_key_bytes(
274 signing_key: &[u8; SIGN_SECRETKEYBYTES],
275 ) -> Result<Self, KeyError> {
276 let signing_key = ed25519_dalek::SigningKey::from_keypair_bytes(signing_key)
277 .map_err(KeyError::SigningKey)?;
278 Ok(Self { signing_key })
279 }
280
281 pub fn public_key(&self) -> [u8; SIGN_PUBLICKEYBYTES] {
282 self.signing_key.verifying_key().to_bytes()
283 }
284
285 pub fn to_keypair_bytes(&self) -> [u8; SIGN_SECRETKEYBYTES] {
286 self.signing_key.to_keypair_bytes()
287 }
288
289 pub fn derive_signing_key(&self, domain: &[u8], context: &[u8]) -> Self {
290 use sha2::{Digest, Sha256};
291
292 let mut derivation = Sha256::new();
293 derivation.update(domain);
294 derivation.update(self.signing_key.to_bytes());
295 derivation.update(context);
296 let seed: [u8; 32] = derivation.finalize().into();
297 Self {
298 signing_key: SigningKey::from_bytes(&seed),
299 }
300 }
301
302 pub fn sign(&self, message: &[u8]) -> [u8; SIGN_BYTES] {
304 self.signing_key.sign(message).to_bytes()
305 }
306
307 pub fn to_x25519_secret_key(&self) -> [u8; CURVE25519_SECRETKEYBYTES] {
309 self.signing_key.to_scalar_bytes()
310 }
311
312 pub fn to_x25519_public_key(&self) -> [u8; CURVE25519_PUBLICKEYBYTES] {
314 self.signing_key.verifying_key().to_montgomery().to_bytes()
315 }
316}
317
318pub fn public_key_hex<A: IdentityKeyAuthority + ?Sized>(keypair: &A) -> String {
320 hex::encode(keypair.public_key())
321}
322
323pub fn sign_hex<A: IdentityKeyAuthority + ?Sized>(keypair: &A, message: &[u8]) -> (String, String) {
325 (public_key_hex(keypair), hex::encode(keypair.sign(message)))
326}
327
328pub(crate) fn verify_signature(
330 signature: &[u8; SIGN_BYTES],
331 message: &[u8],
332 public_key: &[u8; SIGN_PUBLICKEYBYTES],
333) -> bool {
334 let Ok(vk) = ed25519_dalek::VerifyingKey::from_bytes(public_key) else {
335 return false;
336 };
337 let sig = ed25519_dalek::Signature::from_bytes(signature);
338 vk.verify(message, &sig).is_ok()
339}
340
341pub fn verify_signature_hex(pk_hex: &str, sig_hex: &str, message: &[u8]) -> bool {
348 let Ok(pk_bytes) = hex::decode(pk_hex) else {
349 return false;
350 };
351 let Ok(sig_bytes) = hex::decode(sig_hex) else {
352 return false;
353 };
354 let Ok(pk): Result<[u8; SIGN_PUBLICKEYBYTES], _> = pk_bytes.try_into() else {
355 return false;
356 };
357 let Ok(sig): Result<[u8; SIGN_BYTES], _> = sig_bytes.try_into() else {
358 return false;
359 };
360 verify_signature(&sig, message, &pk)
361}
362
363pub fn seal_box_encrypt(
366 message: &[u8],
367 recipient_x25519_pk: &[u8; CURVE25519_PUBLICKEYBYTES],
368) -> Vec<u8> {
369 crypto_box::PublicKey::from(*recipient_x25519_pk)
370 .seal(&mut crypto_box::aead::OsRng, message)
371 .expect("sealed box encryption should not fail")
372}
373
374pub fn seal_box_decrypt(
377 ciphertext: &[u8],
378 recipient_x25519_sk: &[u8; CURVE25519_SECRETKEYBYTES],
379) -> Result<Vec<u8>, KeyError> {
380 crypto_box::SecretKey::from(*recipient_x25519_sk)
381 .unseal(ciphertext)
382 .map_err(|_| KeyError::SealedBoxDecryption)
383}
384
385pub fn ed25519_to_x25519_public_key(
391 ed25519_pk: &[u8; SIGN_PUBLICKEYBYTES],
392) -> Result<[u8; CURVE25519_PUBLICKEYBYTES], KeyError> {
393 let vk = ed25519_dalek::VerifyingKey::from_bytes(ed25519_pk)
394 .map_err(|_| KeyError::InvalidEd25519PublicKey)?;
395 if vk.is_weak() {
396 return Err(KeyError::WeakEd25519PublicKey);
397 }
398 Ok(vk.to_montgomery().to_bytes())
399}
400
401pub fn ed25519_hex_to_x25519_public_key(
402 ed25519_pubkey_hex: &str,
403) -> Result<[u8; CURVE25519_PUBLICKEYBYTES], KeyError> {
404 let public_key = hex::decode(ed25519_pubkey_hex).map_err(|source| KeyError::Hex {
405 subject: "public key",
406 source,
407 })?;
408 let actual = public_key.len();
409 let public_key: [u8; SIGN_PUBLICKEYBYTES] =
410 public_key.try_into().map_err(|_| KeyError::InvalidLength {
411 subject: "public key",
412 expected: SIGN_PUBLICKEYBYTES,
413 actual,
414 })?;
415 ed25519_to_x25519_public_key(&public_key)
416}
417
418pub fn x25519_shared_secret(
422 local_secret: [u8; CURVE25519_SECRETKEYBYTES],
423 peer_public: [u8; CURVE25519_PUBLICKEYBYTES],
424) -> Result<[u8; CURVE25519_PUBLICKEYBYTES], KeyError> {
425 if peer_public == [0; CURVE25519_PUBLICKEYBYTES] {
426 return Err(KeyError::AllZeroX25519PublicKey);
427 }
428 let shared = x25519_dalek::x25519(local_secret, peer_public);
429 if shared == [0; CURVE25519_PUBLICKEYBYTES] {
430 return Err(KeyError::AllZeroX25519SharedSecret);
431 }
432 Ok(shared)
433}
434
435use crate::encryption::MasterKeyring;
436
437pub trait MasterKeyCustody: Send + Sync {
442 fn unlock(&self) -> Result<Option<MasterKeyring>, KeyError>;
447
448 fn persist(&self, keyring: &MasterKeyring) -> Result<(), KeyError>;
452
453 fn forget(&self) -> Result<(), KeyError>;
455}
456
457#[derive(Debug, Error)]
459pub enum RoutingEncryptionError {
460 #[error("custody error: {0}")]
464 Custody(#[from] KeyError),
465 #[error("a scoped write requires an established Store key")]
468 NotEstablished,
469}
470
471pub trait DeviceIdentityCustody: Send + Sync {
476 fn unlock(&self) -> Result<Option<UserKeypair>, KeyError>;
480
481 fn persist(&self, keypair: &UserKeypair) -> Result<(), KeyError>;
483
484 fn establish(&self, keypair: &UserKeypair) -> Result<(), KeyError> {
487 if let Some(existing) = self.unlock()? {
488 if existing.public_key() != keypair.public_key() {
489 return Err(KeyError::IdentityMismatch {
490 existing_pubkey_hex: public_key_hex(&existing),
491 imported_pubkey_hex: public_key_hex(keypair),
492 });
493 }
494 }
495 self.persist(keypair)?;
496 tracing::info!("Established this store's Ed25519 signing identity");
497 Ok(())
498 }
499
500 fn forget(&self) -> Result<(), KeyError>;
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
509 fn keypair_generation_produces_valid_keys() {
510 let kp = UserKeypair::generate();
511
512 assert_eq!(kp.to_keypair_bytes().len(), SIGN_SECRETKEYBYTES);
513 assert_eq!(kp.public_key().len(), SIGN_PUBLICKEYBYTES);
514
515 assert!(kp.to_keypair_bytes().iter().any(|&b| b != 0));
517 assert!(kp.public_key().iter().any(|&b| b != 0));
518 }
519
520 #[test]
521 fn two_keypairs_are_distinct() {
522 let kp1 = UserKeypair::generate();
523 let kp2 = UserKeypair::generate();
524 assert_ne!(kp1.public_key(), kp2.public_key());
525 }
526
527 #[test]
528 fn sign_and_verify_roundtrip() {
529 let kp = UserKeypair::generate();
530 let message = b"changeset payload";
531
532 let sig = kp.sign(message);
533 assert!(verify_signature(&sig, message, &kp.public_key()));
534 }
535
536 #[test]
537 fn keypair_bytes_roundtrip_preserves_signing_identity() {
538 let kp = UserKeypair::generate();
539 let keypair_bytes = kp.to_keypair_bytes();
540 let restored =
541 UserKeypair::from_signing_key_bytes(&keypair_bytes).expect("stored keypair bytes");
542 let message = b"persisted identity";
543
544 assert_eq!(restored.to_keypair_bytes(), keypair_bytes);
545 assert_eq!(restored.public_key(), kp.public_key());
546 assert!(verify_signature(
547 &restored.sign(message),
548 message,
549 &restored.public_key()
550 ));
551 }
552
553 #[test]
554 fn sign_hex_returns_public_key_and_valid_signature() {
555 let kp = UserKeypair::generate();
556 let message = b"changeset payload";
557
558 let (pk_hex, sig_hex) = sign_hex(&kp, message);
559
560 assert_eq!(pk_hex, public_key_hex(&kp));
561 assert!(verify_signature_hex(&pk_hex, &sig_hex, message));
562 }
563
564 #[test]
565 fn verify_rejects_wrong_message() {
566 let kp = UserKeypair::generate();
567 let sig = kp.sign(b"original");
568 assert!(!verify_signature(&sig, b"tampered", &kp.public_key()));
569 }
570
571 #[test]
572 fn verify_rejects_wrong_key() {
573 let kp1 = UserKeypair::generate();
574 let kp2 = UserKeypair::generate();
575 let sig = kp1.sign(b"message");
576 assert!(!verify_signature(&sig, b"message", &kp2.public_key()));
577 }
578
579 #[test]
580 fn sign_empty_message() {
581 let kp = UserKeypair::generate();
582 let sig = kp.sign(b"");
583 assert!(verify_signature(&sig, b"", &kp.public_key()));
584 }
585
586 #[test]
587 fn ed25519_to_x25519_conversion() {
588 let kp = UserKeypair::generate();
589 let x_sk = kp.to_x25519_secret_key();
590 let x_pk = kp.to_x25519_public_key();
591 let converted = ed25519_to_x25519_public_key(&kp.public_key()).unwrap();
592
593 assert_eq!(x_sk.len(), 32);
595 assert_eq!(x_pk.len(), 32);
596 assert!(x_sk.iter().any(|&b| b != 0));
597 assert!(x_pk.iter().any(|&b| b != 0));
598 assert_eq!(converted, x_pk);
599 }
600
601 #[test]
602 fn ed25519_to_x25519_rejects_off_curve_bytes() {
603 let mut bytes = [0u8; SIGN_PUBLICKEYBYTES];
604 bytes[0] = 2;
605
606 let error = ed25519_to_x25519_public_key(&bytes).expect_err("invalid point fails");
607
608 assert!(matches!(error, KeyError::InvalidEd25519PublicKey));
609 assert!(error
610 .to_string()
611 .contains("invalid Ed25519 public key point"));
612 }
613
614 #[test]
615 fn ed25519_to_x25519_rejects_the_identity_point() {
616 let mut identity = [0; SIGN_PUBLICKEYBYTES];
617 identity[0] = 1;
618
619 let error = ed25519_to_x25519_public_key(&identity)
620 .expect_err("a weak recipient point must not produce a shared key");
621
622 assert!(matches!(error, KeyError::WeakEd25519PublicKey));
623 }
624
625 #[test]
626 fn x25519_shared_secret_rejects_the_all_zero_public_key() {
627 let local = UserKeypair::generate();
628
629 let error =
630 x25519_shared_secret(local.to_x25519_secret_key(), [0; CURVE25519_PUBLICKEYBYTES])
631 .expect_err("an all-zero public key must not produce recipient identity material");
632
633 assert!(matches!(error, KeyError::AllZeroX25519PublicKey));
634 }
635
636 #[test]
637 fn x25519_shared_secret_rejects_a_nonzero_low_order_public_key() {
638 let local = UserKeypair::generate();
639 let mut low_order = [0; CURVE25519_PUBLICKEYBYTES];
640 low_order[0] = 1;
641
642 let error = x25519_shared_secret(local.to_x25519_secret_key(), low_order)
643 .expect_err("a low-order public key must not produce recipient identity material");
644
645 assert!(matches!(error, KeyError::AllZeroX25519SharedSecret));
646 }
647
648 #[test]
649 fn ed25519_to_x25519_is_deterministic() {
650 let kp = UserKeypair::generate();
651 let x_sk1 = kp.to_x25519_secret_key();
652 let x_sk2 = kp.to_x25519_secret_key();
653 assert_eq!(x_sk1, x_sk2);
654 }
655
656 #[test]
657 fn sealed_box_roundtrip() {
658 let kp = UserKeypair::generate();
659 let x_pk = kp.to_x25519_public_key();
660 let x_sk = kp.to_x25519_secret_key();
661
662 let plaintext = b"store encryption key material";
663 let ciphertext = seal_box_encrypt(plaintext, &x_pk);
664
665 assert_eq!(ciphertext.len(), plaintext.len() + SEALBYTES);
666
667 let decrypted = seal_box_decrypt(&ciphertext, &x_sk).unwrap();
668 assert_eq!(decrypted, plaintext);
669 }
670
671 #[test]
672 fn sealed_box_wrong_key_fails() {
673 let kp1 = UserKeypair::generate();
674 let kp2 = UserKeypair::generate();
675
676 let ciphertext = seal_box_encrypt(b"secret", &kp1.to_x25519_public_key());
677
678 let result = seal_box_decrypt(&ciphertext, &kp2.to_x25519_secret_key());
679 assert!(result.is_err());
680 }
681
682 #[test]
683 fn sealed_box_empty_message() {
684 let kp = UserKeypair::generate();
685 let x_pk = kp.to_x25519_public_key();
686 let x_sk = kp.to_x25519_secret_key();
687
688 let ciphertext = seal_box_encrypt(b"", &x_pk);
689 let decrypted = seal_box_decrypt(&ciphertext, &x_sk).unwrap();
690 assert!(decrypted.is_empty());
691 }
692
693 #[test]
694 fn sealed_box_too_short_ciphertext() {
695 let kp = UserKeypair::generate();
696 let result = seal_box_decrypt(&[0u8; 10], &kp.to_x25519_secret_key());
697 assert!(result.is_err());
698 }
699
700 #[test]
705 fn missing_keychain_entitlement_message_names_the_real_error_and_fix() {
706 let message = KeyError::MissingKeychainEntitlement.to_string();
707
708 assert!(message.contains("-34018"), "{message}");
709 assert!(message.contains("errSecMissingEntitlement"), "{message}");
710 assert!(message.contains("keychain-access-groups"), "{message}");
711 assert!(message.contains("provisioning profile"), "{message}");
712 assert!(message.contains("DEVELOPMENT_TEAM"), "{message}");
713 assert!(
714 !message.contains("must be signed"),
715 "a bare 'signed binary' is the wrong fix and must not be implied: {message}"
716 );
717 }
718
719 #[test]
720 fn credentials_debug_redacts_s3_secret_and_oauth_token() {
721 let s3 = CloudHomeCredentials::S3 {
722 access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
723 secret_key: "s3-secret-value-do-not-print".to_string(),
724 };
725 let debug = format!("{s3:?}");
726 assert!(debug.contains("<redacted>"), "{debug}");
727 assert!(debug.contains("AKIAIOSFODNN7EXAMPLE"), "{debug}");
728 assert!(
729 !debug.contains("s3-secret-value-do-not-print"),
730 "S3 secret key leaked: {debug}"
731 );
732
733 let oauth = CloudHomeCredentials::OAuth {
734 tokens: crate::keys::OAuthTokens {
735 access_token: "oauth-token-do-not-print".to_string(),
736 refresh_token: None,
737 expires_at: None,
738 },
739 };
740 let debug = format!("{oauth:?}");
741 assert!(debug.contains("<redacted>"), "{debug}");
742 assert!(
743 !debug.contains("oauth-token-do-not-print"),
744 "OAuth token leaked: {debug}"
745 );
746 }
747}