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;
11pub const SEALBYTES: usize = crypto_box::SEALBYTES;
12
13#[derive(Error, Debug)]
14pub enum KeyError {
15 #[error("Crypto error: {0}")]
16 Crypto(String),
17 #[error("Key persistence error: {0}")]
18 Persistence(String),
19 #[error(
20 "no keyring store is installed; the host must install the platform keyring store at startup (set_keyring_service) before any key operation"
21 )]
22 StoreNotInstalled,
23 #[error(
24 "no bundled keyring store exists for this target; the host must supply one via keyring_core::set_default_store before registering the keyring service"
25 )]
26 UnsupportedKeyringPlatform,
27 #[error(
28 "no keyring service is registered; the host must call set_keyring_service at startup before any key operation"
29 )]
30 ServiceNotRegistered,
31 #[error(
32 "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"
33 )]
34 NoDeviceIdentity,
35 #[error(
36 "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"
37 )]
38 IdentityMismatch {
39 existing_pubkey_hex: String,
40 imported_pubkey_hex: String,
41 },
42 #[error(
43 "no pending identity is held for join request {request_public_key_hex}; the request may have already completed, been abandoned, or never existed"
44 )]
45 NoPendingIdentity { request_public_key_hex: String },
46 #[error("invalid host secret name {name:?}: {reason}")]
47 InvalidSecretName { name: String, reason: String },
48 #[error("stale key rotation: the incoming keyring does not extend the live one")]
53 StaleKeyRotation,
54 #[error(
66 "the OS refused this keychain operation with errSecMissingEntitlement \
67 (OSStatus -34018): the process has no team-prefixed keychain-access-groups \
68 entitlement backed by an embedded provisioning profile; set DEVELOPMENT_TEAM \
69 so Xcode's automatic signing fetches and embeds one (a keychain-access-groups \
70 entitlement present WITHOUT a provisioning profile is a different failure: the \
71 process is killed by the kernel at launch, not this error) — a build with no \
72 team must omit the entitlement and will hit this same error on first key use"
73 )]
74 MissingKeychainEntitlement,
75}
76
77#[derive(Clone, Serialize, Deserialize)]
82pub enum CloudHomeCredentials {
83 S3 {
85 access_key: String,
86 secret_key: String,
87 },
88 OAuth { token_json: String },
90 None,
92}
93
94impl std::fmt::Debug for CloudHomeCredentials {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 match self {
97 CloudHomeCredentials::S3 {
98 access_key,
99 secret_key: _,
100 } => f
101 .debug_struct("S3")
102 .field("access_key", access_key)
103 .field("secret_key", &"<redacted>")
104 .finish(),
105 CloudHomeCredentials::OAuth { token_json: _ } => f
106 .debug_struct("OAuth")
107 .field("token_json", &"<redacted>")
108 .finish(),
109 CloudHomeCredentials::None => f.write_str("None"),
110 }
111 }
112}
113
114#[derive(Clone)]
122pub struct UserKeypair {
123 signing_key: SigningKey,
124}
125
126impl UserKeypair {
127 pub fn generate() -> Self {
131 let mut seed = [0u8; 32];
132 rand::rng().fill_bytes(&mut seed);
133 let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
134 Self { signing_key }
135 }
136
137 pub fn from_signing_key_bytes(
143 signing_key: &[u8; SIGN_SECRETKEYBYTES],
144 ) -> Result<Self, KeyError> {
145 let signing_key = ed25519_dalek::SigningKey::from_keypair_bytes(signing_key)
146 .map_err(|e| KeyError::Crypto(format!("Invalid signing key bytes: {e}")))?;
147 Ok(Self { signing_key })
148 }
149
150 pub fn public_key(&self) -> [u8; SIGN_PUBLICKEYBYTES] {
151 self.signing_key.verifying_key().to_bytes()
152 }
153
154 pub fn to_keypair_bytes(&self) -> [u8; SIGN_SECRETKEYBYTES] {
155 self.signing_key.to_keypair_bytes()
156 }
157
158 pub(crate) fn derive_signing_key(&self, domain: &[u8], context: &[u8]) -> Self {
159 use sha2::{Digest, Sha256};
160
161 let mut derivation = Sha256::new();
162 derivation.update(domain);
163 derivation.update(self.signing_key.to_bytes());
164 derivation.update(context);
165 let seed: [u8; 32] = derivation.finalize().into();
166 Self {
167 signing_key: SigningKey::from_bytes(&seed),
168 }
169 }
170
171 pub fn sign(&self, message: &[u8]) -> [u8; SIGN_BYTES] {
173 self.signing_key.sign(message).to_bytes()
174 }
175
176 pub fn to_x25519_secret_key(&self) -> [u8; CURVE25519_SECRETKEYBYTES] {
178 self.signing_key.to_scalar_bytes()
179 }
180
181 pub fn to_x25519_public_key(&self) -> [u8; CURVE25519_PUBLICKEYBYTES] {
183 self.signing_key.verifying_key().to_montgomery().to_bytes()
184 }
185}
186
187pub fn public_key_hex(keypair: &UserKeypair) -> String {
189 hex::encode(keypair.public_key())
190}
191
192pub fn sign_hex(keypair: &UserKeypair, message: &[u8]) -> (String, String) {
194 (public_key_hex(keypair), hex::encode(keypair.sign(message)))
195}
196
197pub fn verify_signature(
199 signature: &[u8; SIGN_BYTES],
200 message: &[u8],
201 public_key: &[u8; SIGN_PUBLICKEYBYTES],
202) -> bool {
203 let Ok(vk) = ed25519_dalek::VerifyingKey::from_bytes(public_key) else {
204 return false;
205 };
206 let sig = ed25519_dalek::Signature::from_bytes(signature);
207 vk.verify(message, &sig).is_ok()
208}
209
210pub fn verify_signature_hex(pk_hex: &str, sig_hex: &str, message: &[u8]) -> bool {
216 let Ok(pk_bytes) = hex::decode(pk_hex) else {
217 return false;
218 };
219 let Ok(sig_bytes) = hex::decode(sig_hex) else {
220 return false;
221 };
222 let Ok(pk): Result<[u8; SIGN_PUBLICKEYBYTES], _> = pk_bytes.try_into() else {
223 return false;
224 };
225 let Ok(sig): Result<[u8; SIGN_BYTES], _> = sig_bytes.try_into() else {
226 return false;
227 };
228 verify_signature(&sig, message, &pk)
229}
230
231pub fn seal_box_encrypt(
234 message: &[u8],
235 recipient_x25519_pk: &[u8; CURVE25519_PUBLICKEYBYTES],
236) -> Vec<u8> {
237 crypto_box::PublicKey::from(*recipient_x25519_pk)
238 .seal(&mut crypto_box::aead::OsRng, message)
239 .expect("sealed box encryption should not fail")
240}
241
242pub fn seal_box_decrypt(
245 ciphertext: &[u8],
246 recipient_x25519_sk: &[u8; CURVE25519_SECRETKEYBYTES],
247) -> Result<Vec<u8>, KeyError> {
248 crypto_box::SecretKey::from(*recipient_x25519_sk)
249 .unseal(ciphertext)
250 .map_err(|_| {
251 KeyError::Crypto("Sealed box decryption failed (wrong key or tampered)".to_string())
252 })
253}
254
255pub fn ed25519_to_x25519_public_key(
261 ed25519_pk: &[u8; SIGN_PUBLICKEYBYTES],
262) -> Result<[u8; CURVE25519_PUBLICKEYBYTES], KeyError> {
263 let vk = ed25519_dalek::VerifyingKey::from_bytes(ed25519_pk)
264 .map_err(|_| KeyError::Crypto("invalid Ed25519 public key point".to_string()))?;
265 if vk.is_weak() {
266 return Err(KeyError::Crypto(
267 "weak Ed25519 public key point cannot identify a recipient".to_string(),
268 ));
269 }
270 Ok(vk.to_montgomery().to_bytes())
271}
272
273pub fn x25519_shared_secret(
277 local_secret: [u8; CURVE25519_SECRETKEYBYTES],
278 peer_public: [u8; CURVE25519_PUBLICKEYBYTES],
279) -> Result<[u8; CURVE25519_PUBLICKEYBYTES], KeyError> {
280 if peer_public == [0; CURVE25519_PUBLICKEYBYTES] {
281 return Err(KeyError::Crypto(
282 "all-zero X25519 public key cannot identify a recipient".to_string(),
283 ));
284 }
285 let shared = x25519_dalek::x25519(local_secret, peer_public);
286 if shared == [0; CURVE25519_PUBLICKEYBYTES] {
287 return Err(KeyError::Crypto(
288 "all-zero X25519 shared secret cannot identify a recipient".to_string(),
289 ));
290 }
291 Ok(shared)
292}
293
294use crate::encryption::MasterKeyring;
295
296pub trait MasterKeyCustody: Send + Sync {
301 fn unlock(&self) -> Result<Option<MasterKeyring>, KeyError>;
306
307 fn persist(&self, keyring: &MasterKeyring) -> Result<(), KeyError>;
311
312 fn forget(&self) -> Result<(), KeyError>;
314}
315
316pub trait DeviceIdentityCustody: Send + Sync {
322 fn unlock(&self) -> Result<Option<UserKeypair>, KeyError>;
326
327 fn persist(&self, keypair: &UserKeypair) -> Result<(), KeyError>;
329
330 fn forget(&self) -> Result<(), KeyError>;
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn keypair_generation_produces_valid_keys() {
340 let kp = UserKeypair::generate();
341
342 assert_eq!(kp.to_keypair_bytes().len(), SIGN_SECRETKEYBYTES);
343 assert_eq!(kp.public_key().len(), SIGN_PUBLICKEYBYTES);
344
345 assert!(kp.to_keypair_bytes().iter().any(|&b| b != 0));
347 assert!(kp.public_key().iter().any(|&b| b != 0));
348 }
349
350 #[test]
351 fn two_keypairs_are_distinct() {
352 let kp1 = UserKeypair::generate();
353 let kp2 = UserKeypair::generate();
354 assert_ne!(kp1.public_key(), kp2.public_key());
355 }
356
357 #[test]
358 fn sign_and_verify_roundtrip() {
359 let kp = UserKeypair::generate();
360 let message = b"changeset payload";
361
362 let sig = kp.sign(message);
363 assert!(verify_signature(&sig, message, &kp.public_key()));
364 }
365
366 #[test]
367 fn keypair_bytes_roundtrip_preserves_signing_identity() {
368 let kp = UserKeypair::generate();
369 let keypair_bytes = kp.to_keypair_bytes();
370 let restored =
371 UserKeypair::from_signing_key_bytes(&keypair_bytes).expect("stored keypair bytes");
372 let message = b"persisted identity";
373
374 assert_eq!(restored.to_keypair_bytes(), keypair_bytes);
375 assert_eq!(restored.public_key(), kp.public_key());
376 assert!(verify_signature(
377 &restored.sign(message),
378 message,
379 &restored.public_key()
380 ));
381 }
382
383 #[test]
384 fn sign_hex_returns_public_key_and_valid_signature() {
385 let kp = UserKeypair::generate();
386 let message = b"changeset payload";
387
388 let (pk_hex, sig_hex) = sign_hex(&kp, message);
389
390 assert_eq!(pk_hex, public_key_hex(&kp));
391 assert!(verify_signature_hex(&pk_hex, &sig_hex, message));
392 }
393
394 #[test]
395 fn verify_rejects_wrong_message() {
396 let kp = UserKeypair::generate();
397 let sig = kp.sign(b"original");
398 assert!(!verify_signature(&sig, b"tampered", &kp.public_key()));
399 }
400
401 #[test]
402 fn verify_rejects_wrong_key() {
403 let kp1 = UserKeypair::generate();
404 let kp2 = UserKeypair::generate();
405 let sig = kp1.sign(b"message");
406 assert!(!verify_signature(&sig, b"message", &kp2.public_key()));
407 }
408
409 #[test]
410 fn sign_empty_message() {
411 let kp = UserKeypair::generate();
412 let sig = kp.sign(b"");
413 assert!(verify_signature(&sig, b"", &kp.public_key()));
414 }
415
416 #[test]
417 fn ed25519_to_x25519_conversion() {
418 let kp = UserKeypair::generate();
419 let x_sk = kp.to_x25519_secret_key();
420 let x_pk = kp.to_x25519_public_key();
421 let converted = ed25519_to_x25519_public_key(&kp.public_key()).unwrap();
422
423 assert_eq!(x_sk.len(), 32);
425 assert_eq!(x_pk.len(), 32);
426 assert!(x_sk.iter().any(|&b| b != 0));
427 assert!(x_pk.iter().any(|&b| b != 0));
428 assert_eq!(converted, x_pk);
429 }
430
431 #[test]
432 fn ed25519_to_x25519_rejects_off_curve_bytes() {
433 let mut bytes = [0u8; SIGN_PUBLICKEYBYTES];
434 bytes[0] = 2;
435
436 let error = ed25519_to_x25519_public_key(&bytes).expect_err("invalid point fails");
437
438 assert!(matches!(error, KeyError::Crypto(_)));
439 assert!(error
440 .to_string()
441 .contains("invalid Ed25519 public key point"));
442 }
443
444 #[test]
445 fn ed25519_to_x25519_rejects_the_identity_point() {
446 let mut identity = [0; SIGN_PUBLICKEYBYTES];
447 identity[0] = 1;
448
449 let error = ed25519_to_x25519_public_key(&identity)
450 .expect_err("a weak recipient point must not produce a shared key");
451
452 assert!(matches!(error, KeyError::Crypto(_)));
453 }
454
455 #[test]
456 fn x25519_shared_secret_rejects_the_all_zero_public_key() {
457 let local = UserKeypair::generate();
458
459 let error =
460 x25519_shared_secret(local.to_x25519_secret_key(), [0; CURVE25519_PUBLICKEYBYTES])
461 .expect_err("an all-zero public key must not produce recipient identity material");
462
463 assert!(matches!(error, KeyError::Crypto(_)));
464 }
465
466 #[test]
467 fn x25519_shared_secret_rejects_a_nonzero_low_order_public_key() {
468 let local = UserKeypair::generate();
469 let mut low_order = [0; CURVE25519_PUBLICKEYBYTES];
470 low_order[0] = 1;
471
472 let error = x25519_shared_secret(local.to_x25519_secret_key(), low_order)
473 .expect_err("a low-order public key must not produce recipient identity material");
474
475 assert!(matches!(error, KeyError::Crypto(_)));
476 }
477
478 #[test]
479 fn ed25519_to_x25519_is_deterministic() {
480 let kp = UserKeypair::generate();
481 let x_sk1 = kp.to_x25519_secret_key();
482 let x_sk2 = kp.to_x25519_secret_key();
483 assert_eq!(x_sk1, x_sk2);
484 }
485
486 #[test]
487 fn sealed_box_roundtrip() {
488 let kp = UserKeypair::generate();
489 let x_pk = kp.to_x25519_public_key();
490 let x_sk = kp.to_x25519_secret_key();
491
492 let plaintext = b"store encryption key material";
493 let ciphertext = seal_box_encrypt(plaintext, &x_pk);
494
495 assert_eq!(ciphertext.len(), plaintext.len() + SEALBYTES);
496
497 let decrypted = seal_box_decrypt(&ciphertext, &x_sk).unwrap();
498 assert_eq!(decrypted, plaintext);
499 }
500
501 #[test]
502 fn sealed_box_wrong_key_fails() {
503 let kp1 = UserKeypair::generate();
504 let kp2 = UserKeypair::generate();
505
506 let ciphertext = seal_box_encrypt(b"secret", &kp1.to_x25519_public_key());
507
508 let result = seal_box_decrypt(&ciphertext, &kp2.to_x25519_secret_key());
509 assert!(result.is_err());
510 }
511
512 #[test]
513 fn sealed_box_empty_message() {
514 let kp = UserKeypair::generate();
515 let x_pk = kp.to_x25519_public_key();
516 let x_sk = kp.to_x25519_secret_key();
517
518 let ciphertext = seal_box_encrypt(b"", &x_pk);
519 let decrypted = seal_box_decrypt(&ciphertext, &x_sk).unwrap();
520 assert!(decrypted.is_empty());
521 }
522
523 #[test]
524 fn sealed_box_too_short_ciphertext() {
525 let kp = UserKeypair::generate();
526 let result = seal_box_decrypt(&[0u8; 10], &kp.to_x25519_secret_key());
527 assert!(result.is_err());
528 }
529
530 #[test]
535 fn missing_keychain_entitlement_message_names_the_real_error_and_fix() {
536 let message = KeyError::MissingKeychainEntitlement.to_string();
537
538 assert!(message.contains("-34018"), "{message}");
539 assert!(message.contains("errSecMissingEntitlement"), "{message}");
540 assert!(message.contains("keychain-access-groups"), "{message}");
541 assert!(message.contains("provisioning profile"), "{message}");
542 assert!(message.contains("DEVELOPMENT_TEAM"), "{message}");
543 assert!(
544 !message.contains("must be signed"),
545 "a bare 'signed binary' is the wrong fix and must not be implied: {message}"
546 );
547 }
548
549 #[test]
550 fn credentials_debug_redacts_s3_secret_and_oauth_token() {
551 let s3 = CloudHomeCredentials::S3 {
552 access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
553 secret_key: "s3-secret-value-do-not-print".to_string(),
554 };
555 let debug = format!("{s3:?}");
556 assert!(debug.contains("<redacted>"), "{debug}");
557 assert!(debug.contains("AKIAIOSFODNN7EXAMPLE"), "{debug}");
558 assert!(
559 !debug.contains("s3-secret-value-do-not-print"),
560 "S3 secret key leaked: {debug}"
561 );
562
563 let oauth = CloudHomeCredentials::OAuth {
564 token_json: "{\"access_token\":\"oauth-token-do-not-print\"}".to_string(),
565 };
566 let debug = format!("{oauth:?}");
567 assert!(debug.contains("<redacted>"), "{debug}");
568 assert!(
569 !debug.contains("oauth-token-do-not-print"),
570 "OAuth token leaked: {debug}"
571 );
572 }
573}