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 {
477 fn unlock(&self) -> Result<Option<UserKeypair>, KeyError>;
481
482 fn persist(&self, keypair: &UserKeypair) -> Result<(), KeyError>;
484
485 fn establish(&self, keypair: &UserKeypair) -> Result<(), KeyError> {
488 if let Some(existing) = self.unlock()? {
489 if existing.public_key() != keypair.public_key() {
490 return Err(KeyError::IdentityMismatch {
491 existing_pubkey_hex: public_key_hex(&existing),
492 imported_pubkey_hex: public_key_hex(keypair),
493 });
494 }
495 }
496 self.persist(keypair)?;
497 tracing::info!("Established this store's Ed25519 signing identity");
498 Ok(())
499 }
500
501 fn forget(&self) -> Result<(), KeyError>;
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508
509 #[test]
510 fn keypair_generation_produces_valid_keys() {
511 let kp = UserKeypair::generate();
512
513 assert_eq!(kp.to_keypair_bytes().len(), SIGN_SECRETKEYBYTES);
514 assert_eq!(kp.public_key().len(), SIGN_PUBLICKEYBYTES);
515
516 assert!(kp.to_keypair_bytes().iter().any(|&b| b != 0));
518 assert!(kp.public_key().iter().any(|&b| b != 0));
519 }
520
521 #[test]
522 fn two_keypairs_are_distinct() {
523 let kp1 = UserKeypair::generate();
524 let kp2 = UserKeypair::generate();
525 assert_ne!(kp1.public_key(), kp2.public_key());
526 }
527
528 #[test]
529 fn sign_and_verify_roundtrip() {
530 let kp = UserKeypair::generate();
531 let message = b"changeset payload";
532
533 let sig = kp.sign(message);
534 assert!(verify_signature(&sig, message, &kp.public_key()));
535 }
536
537 #[test]
538 fn keypair_bytes_roundtrip_preserves_signing_identity() {
539 let kp = UserKeypair::generate();
540 let keypair_bytes = kp.to_keypair_bytes();
541 let restored =
542 UserKeypair::from_signing_key_bytes(&keypair_bytes).expect("stored keypair bytes");
543 let message = b"persisted identity";
544
545 assert_eq!(restored.to_keypair_bytes(), keypair_bytes);
546 assert_eq!(restored.public_key(), kp.public_key());
547 assert!(verify_signature(
548 &restored.sign(message),
549 message,
550 &restored.public_key()
551 ));
552 }
553
554 #[test]
555 fn sign_hex_returns_public_key_and_valid_signature() {
556 let kp = UserKeypair::generate();
557 let message = b"changeset payload";
558
559 let (pk_hex, sig_hex) = sign_hex(&kp, message);
560
561 assert_eq!(pk_hex, public_key_hex(&kp));
562 assert!(verify_signature_hex(&pk_hex, &sig_hex, message));
563 }
564
565 #[test]
566 fn verify_rejects_wrong_message() {
567 let kp = UserKeypair::generate();
568 let sig = kp.sign(b"original");
569 assert!(!verify_signature(&sig, b"tampered", &kp.public_key()));
570 }
571
572 #[test]
573 fn verify_rejects_wrong_key() {
574 let kp1 = UserKeypair::generate();
575 let kp2 = UserKeypair::generate();
576 let sig = kp1.sign(b"message");
577 assert!(!verify_signature(&sig, b"message", &kp2.public_key()));
578 }
579
580 #[test]
581 fn sign_empty_message() {
582 let kp = UserKeypair::generate();
583 let sig = kp.sign(b"");
584 assert!(verify_signature(&sig, b"", &kp.public_key()));
585 }
586
587 #[test]
588 fn ed25519_to_x25519_conversion() {
589 let kp = UserKeypair::generate();
590 let x_sk = kp.to_x25519_secret_key();
591 let x_pk = kp.to_x25519_public_key();
592 let converted = ed25519_to_x25519_public_key(&kp.public_key()).unwrap();
593
594 assert_eq!(x_sk.len(), 32);
596 assert_eq!(x_pk.len(), 32);
597 assert!(x_sk.iter().any(|&b| b != 0));
598 assert!(x_pk.iter().any(|&b| b != 0));
599 assert_eq!(converted, x_pk);
600 }
601
602 #[test]
603 fn ed25519_to_x25519_rejects_off_curve_bytes() {
604 let mut bytes = [0u8; SIGN_PUBLICKEYBYTES];
605 bytes[0] = 2;
606
607 let error = ed25519_to_x25519_public_key(&bytes).expect_err("invalid point fails");
608
609 assert!(matches!(error, KeyError::InvalidEd25519PublicKey));
610 assert!(error
611 .to_string()
612 .contains("invalid Ed25519 public key point"));
613 }
614
615 #[test]
616 fn ed25519_to_x25519_rejects_the_identity_point() {
617 let mut identity = [0; SIGN_PUBLICKEYBYTES];
618 identity[0] = 1;
619
620 let error = ed25519_to_x25519_public_key(&identity)
621 .expect_err("a weak recipient point must not produce a shared key");
622
623 assert!(matches!(error, KeyError::WeakEd25519PublicKey));
624 }
625
626 #[test]
627 fn x25519_shared_secret_rejects_the_all_zero_public_key() {
628 let local = UserKeypair::generate();
629
630 let error =
631 x25519_shared_secret(local.to_x25519_secret_key(), [0; CURVE25519_PUBLICKEYBYTES])
632 .expect_err("an all-zero public key must not produce recipient identity material");
633
634 assert!(matches!(error, KeyError::AllZeroX25519PublicKey));
635 }
636
637 #[test]
638 fn x25519_shared_secret_rejects_a_nonzero_low_order_public_key() {
639 let local = UserKeypair::generate();
640 let mut low_order = [0; CURVE25519_PUBLICKEYBYTES];
641 low_order[0] = 1;
642
643 let error = x25519_shared_secret(local.to_x25519_secret_key(), low_order)
644 .expect_err("a low-order public key must not produce recipient identity material");
645
646 assert!(matches!(error, KeyError::AllZeroX25519SharedSecret));
647 }
648
649 #[test]
650 fn ed25519_to_x25519_is_deterministic() {
651 let kp = UserKeypair::generate();
652 let x_sk1 = kp.to_x25519_secret_key();
653 let x_sk2 = kp.to_x25519_secret_key();
654 assert_eq!(x_sk1, x_sk2);
655 }
656
657 #[test]
658 fn sealed_box_roundtrip() {
659 let kp = UserKeypair::generate();
660 let x_pk = kp.to_x25519_public_key();
661 let x_sk = kp.to_x25519_secret_key();
662
663 let plaintext = b"store encryption key material";
664 let ciphertext = seal_box_encrypt(plaintext, &x_pk);
665
666 assert_eq!(ciphertext.len(), plaintext.len() + SEALBYTES);
667
668 let decrypted = seal_box_decrypt(&ciphertext, &x_sk).unwrap();
669 assert_eq!(decrypted, plaintext);
670 }
671
672 #[test]
673 fn sealed_box_wrong_key_fails() {
674 let kp1 = UserKeypair::generate();
675 let kp2 = UserKeypair::generate();
676
677 let ciphertext = seal_box_encrypt(b"secret", &kp1.to_x25519_public_key());
678
679 let result = seal_box_decrypt(&ciphertext, &kp2.to_x25519_secret_key());
680 assert!(result.is_err());
681 }
682
683 #[test]
684 fn sealed_box_empty_message() {
685 let kp = UserKeypair::generate();
686 let x_pk = kp.to_x25519_public_key();
687 let x_sk = kp.to_x25519_secret_key();
688
689 let ciphertext = seal_box_encrypt(b"", &x_pk);
690 let decrypted = seal_box_decrypt(&ciphertext, &x_sk).unwrap();
691 assert!(decrypted.is_empty());
692 }
693
694 #[test]
695 fn sealed_box_too_short_ciphertext() {
696 let kp = UserKeypair::generate();
697 let result = seal_box_decrypt(&[0u8; 10], &kp.to_x25519_secret_key());
698 assert!(result.is_err());
699 }
700
701 #[test]
706 fn missing_keychain_entitlement_message_names_the_real_error_and_fix() {
707 let message = KeyError::MissingKeychainEntitlement.to_string();
708
709 assert!(message.contains("-34018"), "{message}");
710 assert!(message.contains("errSecMissingEntitlement"), "{message}");
711 assert!(message.contains("keychain-access-groups"), "{message}");
712 assert!(message.contains("provisioning profile"), "{message}");
713 assert!(message.contains("DEVELOPMENT_TEAM"), "{message}");
714 assert!(
715 !message.contains("must be signed"),
716 "a bare 'signed binary' is the wrong fix and must not be implied: {message}"
717 );
718 }
719
720 #[test]
721 fn credentials_debug_redacts_s3_secret_and_oauth_token() {
722 let s3 = CloudHomeCredentials::S3 {
723 access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
724 secret_key: "s3-secret-value-do-not-print".to_string(),
725 };
726 let debug = format!("{s3:?}");
727 assert!(debug.contains("<redacted>"), "{debug}");
728 assert!(debug.contains("AKIAIOSFODNN7EXAMPLE"), "{debug}");
729 assert!(
730 !debug.contains("s3-secret-value-do-not-print"),
731 "S3 secret key leaked: {debug}"
732 );
733
734 let oauth = CloudHomeCredentials::OAuth {
735 tokens: crate::keys::OAuthTokens {
736 access_token: "oauth-token-do-not-print".to_string(),
737 refresh_token: None,
738 expires_at: None,
739 },
740 };
741 let debug = format!("{oauth:?}");
742 assert!(debug.contains("<redacted>"), "{debug}");
743 assert!(
744 !debug.contains("oauth-token-do-not-print"),
745 "OAuth token leaked: {debug}"
746 );
747 }
748}