1use crate::{cipher, derivation, randomness, wire};
4use crate::{CircleKey, CryptoError, MaterialError, ObjectDigest, SecretBytes, StoreKey};
5use coven_foundation::id_source::MemberId;
6use ed25519_dalek::{Signer, VerifyingKey};
7use std::fmt;
8use x25519_dalek::{PublicKey, StaticSecret};
9use zeroize::Zeroizing;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
14pub struct SigningKey([u8; 32]);
15
16impl SigningKey {
17 #[expect(
19 clippy::map_err_ignore,
20 reason = "Ed25519's key error is opaque by design; InvalidSigningKey is all it says"
21 )]
22 pub fn from_bytes(bytes: [u8; 32]) -> Result<Self, CryptoError> {
23 let key = VerifyingKey::from_bytes(&bytes).map_err(|_| CryptoError::InvalidSigningKey)?;
24 if key.is_weak() {
25 return Err(CryptoError::InvalidSigningKey);
26 }
27 Ok(Self(bytes))
28 }
29
30 pub fn as_bytes(&self) -> &[u8; 32] {
32 &self.0
33 }
34
35 #[expect(
37 clippy::map_err_ignore,
38 reason = "Ed25519's verification error is opaque by design; Signature is all it says"
39 )]
40 pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), CryptoError> {
41 let key = VerifyingKey::from_bytes(&self.0)
42 .expect("SigningKey contains a validated Ed25519 public key");
43 key.verify_strict(message, &ed25519_dalek::Signature::from_bytes(&signature.0))
44 .map_err(|_| CryptoError::Signature)
45 }
46
47 pub fn verify_object(
51 &self,
52 path: &str,
53 digest: &ObjectDigest,
54 signature: &Signature,
55 ) -> Result<(), CryptoError> {
56 self.verify(&object_message(path, digest), signature)
57 }
58
59 pub fn verify_prefix(
63 &self,
64 path: &str,
65 prefix: &[u8],
66 signature: &Signature,
67 ) -> Result<(), CryptoError> {
68 self.verify(&prefix_message(path, prefix), signature)
69 }
70
71 pub fn verify_write_header(
76 &self,
77 path: &str,
78 prefix: &[u8],
79 header: &[u8],
80 roots: &[[u8; 32]],
81 signature: &Signature,
82 ) -> Result<(), CryptoError> {
83 self.verify(&header_message(path, prefix, header, roots), signature)
84 }
85}
86
87#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
90pub struct SealingKey([u8; 32]);
91
92impl SealingKey {
93 pub fn from_bytes(bytes: [u8; 32]) -> Self {
95 Self(bytes)
96 }
97 pub fn as_bytes(&self) -> &[u8; 32] {
99 &self.0
100 }
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)]
106pub struct MemberPublicKeys {
107 pub signing: SigningKey,
109 pub sealing: SealingKey,
111}
112
113pub const SIGNATURE_LEN: usize = 64;
115
116#[derive(Clone, Debug, Eq, PartialEq)]
118pub struct Signature([u8; SIGNATURE_LEN]);
119
120impl Signature {
121 pub fn from_bytes(bytes: [u8; SIGNATURE_LEN]) -> Self {
123 Self(bytes)
124 }
125 pub fn as_bytes(&self) -> &[u8; SIGNATURE_LEN] {
127 &self.0
128 }
129}
130
131#[derive(Clone)]
135pub struct MemberKeys {
136 signing: ed25519_dalek::SigningKey,
137 sealing: StaticSecret,
138}
139
140impl MemberKeys {
141 #[cfg(any(test, feature = "test-utils"))]
144 pub fn from_seed(seed: u8) -> Self {
145 Self {
146 signing: ed25519_dalek::SigningKey::from_bytes(&[seed; 32]),
147 sealing: StaticSecret::from([seed; 32]),
148 }
149 }
150
151 pub fn generate() -> Result<Self, CryptoError> {
153 let signing = randomness::secret_bytes()?;
154 let sealing = randomness::secret_bytes()?;
155 Ok(Self {
156 signing: ed25519_dalek::SigningKey::from_bytes(&signing),
157 sealing: StaticSecret::from(*sealing),
158 })
159 }
160
161 pub fn public_keys(&self) -> MemberPublicKeys {
163 MemberPublicKeys {
164 signing: SigningKey(self.signing.verifying_key().to_bytes()),
165 sealing: SealingKey(PublicKey::from(&self.sealing).to_bytes()),
166 }
167 }
168
169 pub fn sign(&self, bytes: &[u8]) -> Signature {
171 Signature(self.signing.sign(bytes).to_bytes())
172 }
173
174 pub fn sign_object(&self, path: &str, digest: &ObjectDigest) -> Signature {
186 self.sign(&object_message(path, digest))
187 }
188
189 pub fn sign_prefix(&self, path: &str, prefix: &[u8]) -> Signature {
193 self.sign(&prefix_message(path, prefix))
194 }
195
196 pub fn sign_write_header(
200 &self,
201 path: &str,
202 prefix: &[u8],
203 header: &[u8],
204 roots: &[[u8; 32]],
205 ) -> Signature {
206 self.sign(&header_message(path, prefix, header, roots))
207 }
208
209 pub fn to_secret_bytes(&self) -> SecretBytes {
211 let mut bytes = Zeroizing::new(Vec::with_capacity(MEMBER_KEYS_LENGTH));
212 bytes.extend_from_slice(MEMBER_KEYS_MAGIC);
213 bytes.extend_from_slice(self.signing.as_bytes());
214 bytes.extend_from_slice(self.sealing.as_bytes());
215 SecretBytes(bytes)
216 }
217
218 pub fn from_secret_bytes(mut bytes: &[u8]) -> Result<Self, MaterialError> {
220 wire::prefix(&mut bytes, MEMBER_KEYS_MAGIC)?;
221 let signing = Zeroizing::new(wire::array(&mut bytes)?);
222 let sealing = Zeroizing::new(wire::array(&mut bytes)?);
223 wire::end(bytes)?;
224 Ok(Self {
225 signing: ed25519_dalek::SigningKey::from_bytes(&signing),
226 sealing: StaticSecret::from(*sealing),
227 })
228 }
229
230 pub fn open_store_key(
235 &self,
236 path: &str,
237 prefix: &[u8],
238 sealed: &[u8],
239 ) -> Result<StoreKey, CryptoError> {
240 let plaintext =
241 self.open_box::<{ StoreKey::ENCODED_LEN }>(b"store", path, prefix, sealed)?;
242 Ok(StoreKey::from_encoded(&plaintext))
243 }
244
245 pub fn open_circle_key(
250 &self,
251 path: &str,
252 prefix: &[u8],
253 sealed: &[u8],
254 ) -> Result<CircleKey, CryptoError> {
255 let plaintext =
256 self.open_box::<{ CircleKey::ENCODED_LEN }>(b"circle", path, prefix, sealed)?;
257 Ok(CircleKey::from_encoded(&plaintext))
258 }
259
260 fn open_box<const N: usize>(
263 &self,
264 kind: &[u8],
265 path: &str,
266 prefix: &[u8],
267 sealed: &[u8],
268 ) -> Result<Zeroizing<[u8; N]>, CryptoError> {
269 let (sender, rest) = sealed.split_first_chunk::<32>().ok_or(CryptoError::Malformed)?;
270 let (nonce, rest) =
271 rest.split_first_chunk::<{ cipher::NONCE_LEN }>().ok_or(CryptoError::Malformed)?;
272 let (ciphertext, rest) = rest.split_first_chunk::<N>().ok_or(CryptoError::Malformed)?;
273 let (tag, rest) =
274 rest.split_first_chunk::<KEY_BOX_TAG_LEN>().ok_or(CryptoError::Malformed)?;
275 if !rest.is_empty() {
276 return Err(CryptoError::Malformed);
277 }
278 let recipient = self.public_keys().sealing;
279 let shared = self.sealing.diffie_hellman(&PublicKey::from(*sender));
280 if !shared.was_contributory() {
281 return Err(CryptoError::WeakSealingKey);
282 }
283 let context = box_context(kind, path, prefix, sender, recipient.as_bytes());
284 let key = derivation::derive(shared.as_bytes(), &context);
285 cipher::open_exact(&key, nonce, &context, ciphertext, tag)
286 }
287}
288
289impl fmt::Debug for MemberKeys {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 f.write_str("MemberKeys([REDACTED])")
292 }
293}
294
295#[derive(Clone)]
303pub struct MemberIdentity {
304 member: MemberId,
305 keys: MemberKeys,
306 retained: Vec<MemberKeys>,
307 candidate: Option<MemberKeys>,
308}
309
310impl MemberIdentity {
311 pub fn new(member: MemberId, keys: MemberKeys) -> Self {
313 Self { member, keys, retained: Vec::new(), candidate: None }
314 }
315
316 pub fn generate(member: MemberId) -> Result<Self, CryptoError> {
318 Ok(Self::new(member, MemberKeys::generate()?))
319 }
320
321 pub fn member(&self) -> MemberId {
323 self.member
324 }
325
326 pub fn keys(&self) -> &MemberKeys {
329 &self.keys
330 }
331
332 pub fn candidate(&self) -> Option<&MemberKeys> {
334 self.candidate.as_ref()
335 }
336
337 fn held(&self) -> impl Iterator<Item = &MemberKeys> {
339 std::iter::once(&self.keys).chain(&self.retained).chain(&self.candidate)
340 }
341
342 pub fn credential(&self, sealing: &SealingKey) -> Option<&MemberKeys> {
346 self.held().find(|keys| keys.public_keys().sealing == *sealing)
347 }
348
349 pub fn holds(&self, keys: &MemberPublicKeys) -> bool {
351 self.held().any(|held| held.public_keys() == *keys)
352 }
353
354 pub fn with_candidate(&self, candidate: MemberKeys) -> Self {
357 let mut next = self.clone();
358 next.retained.extend(next.candidate.replace(candidate));
359 next
360 }
361
362 pub fn activate(&self, current: &MemberPublicKeys) -> Option<Self> {
367 if self.keys.public_keys() == *current && self.candidate.is_none() {
368 return Some(self.clone());
369 }
370 let mut pairs: Vec<MemberKeys> = self.held().cloned().collect();
371 let index = pairs.iter().position(|keys| keys.public_keys() == *current)?;
372 let keys = pairs.remove(index);
373 let mut retained = Vec::new();
374 for pair in pairs {
375 if !retained.iter().any(|held: &MemberKeys| held.public_keys() == pair.public_keys()) {
376 retained.push(pair);
377 }
378 }
379 Some(Self { member: self.member, keys, retained, candidate: None })
380 }
381
382 pub fn retire_candidate(&self) -> Self {
385 let mut next = self.clone();
386 next.retained.extend(next.candidate.take());
387 next
388 }
389
390 pub fn without_candidate(&self) -> Self {
393 let mut next = self.clone();
394 next.candidate = None;
395 next
396 }
397
398 pub fn to_secret_bytes(&self) -> SecretBytes {
402 let pairs = 1 + self.retained.len() + usize::from(self.candidate.is_some());
404 let mut bytes = Zeroizing::new(Vec::with_capacity(
407 IDENTITY_MAGIC.len() + 16 + 8 + 1 + pairs * MEMBER_KEYS_LENGTH,
408 ));
409 bytes.extend_from_slice(IDENTITY_MAGIC);
410 bytes.extend_from_slice(self.member.0.as_bytes());
411 bytes.extend_from_slice(self.keys.to_secret_bytes().as_bytes());
412 bytes.extend_from_slice(&(self.retained.len() as u64).to_le_bytes());
413 for keys in &self.retained {
414 bytes.extend_from_slice(keys.to_secret_bytes().as_bytes());
415 }
416 match &self.candidate {
417 Some(keys) => {
418 bytes.push(1);
419 bytes.extend_from_slice(keys.to_secret_bytes().as_bytes());
420 }
421 None => bytes.push(0),
422 }
423 SecretBytes(bytes)
424 }
425
426 pub fn from_secret_bytes(mut bytes: &[u8]) -> Result<Self, MaterialError> {
428 fn pair(bytes: &mut &[u8]) -> Result<MemberKeys, MaterialError> {
429 MemberKeys::from_secret_bytes(wire::take(bytes, MEMBER_KEYS_LENGTH)?)
430 }
431 wire::prefix(&mut bytes, IDENTITY_MAGIC)?;
432 let member = MemberId(uuid::Uuid::from_bytes(wire::array(&mut bytes)?));
433 let keys = pair(&mut bytes)?;
434 let count = wire::number(&mut bytes)?;
435 if count > (bytes.len() / MEMBER_KEYS_LENGTH) as u64 {
436 return Err(MaterialError::Encoding);
437 }
438 let retained = (0..count).map(|_| pair(&mut bytes)).collect::<Result<Vec<_>, _>>()?;
439 let candidate = match wire::array(&mut bytes)? {
440 [0] => None,
441 [1] => Some(pair(&mut bytes)?),
442 _ => return Err(MaterialError::Encoding),
443 };
444 wire::end(bytes)?;
445 let identity = Self { member, keys, retained, candidate };
446 let mut public: Vec<_> = identity.held().map(MemberKeys::public_keys).collect();
448 public.sort();
449 public.dedup();
450 if public.len() != identity.held().count() {
451 return Err(MaterialError::Encoding);
452 }
453 Ok(identity)
454 }
455}
456
457const MEMBER_KEYS_MAGIC: &[u8] = b"CVMK\x01";
459const IDENTITY_MAGIC: &[u8] = b"CVMI\x01";
461const MEMBER_KEYS_LENGTH: usize = MEMBER_KEYS_MAGIC.len() + 2 * 32;
463
464impl fmt::Debug for MemberIdentity {
465 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466 f.debug_struct("MemberIdentity")
467 .field("member", &self.member)
468 .field("retained", &self.retained.len())
469 .field("candidate", &self.candidate.is_some())
470 .finish_non_exhaustive()
471 }
472}
473
474pub const KEY_BOX_HEAD_LEN: usize = 32 + cipher::NONCE_LEN;
477pub const KEY_BOX_TAG_LEN: usize = cipher::FILE_CHUNK_TAG_LEN;
479
480pub fn seal_store_key(
484 key: &StoreKey,
485 recipient: &SealingKey,
486 path: &str,
487 prefix: &[u8],
488) -> Result<Vec<u8>, CryptoError> {
489 let mut bytes = Zeroizing::new(Vec::with_capacity(StoreKey::ENCODED_LEN));
490 key.encode_into(&mut bytes);
491 seal_box(b"store", recipient, path, prefix, &bytes)
492}
493
494pub fn seal_circle_key(
498 key: &CircleKey,
499 recipient: &SealingKey,
500 path: &str,
501 prefix: &[u8],
502) -> Result<Vec<u8>, CryptoError> {
503 let mut bytes = Zeroizing::new(Vec::with_capacity(CircleKey::ENCODED_LEN));
504 key.encode_into(&mut bytes);
505 seal_box(b"circle", recipient, path, prefix, &bytes)
506}
507
508fn object_message(path: &str, digest: &ObjectDigest) -> Vec<u8> {
509 cipher::context(&[b"coven/object-signature/v1", cipher::storage_path(path), digest.as_bytes()])
510}
511
512fn prefix_message(path: &str, prefix: &[u8]) -> Vec<u8> {
513 cipher::context(&[b"coven/prefix-signature/v1", cipher::storage_path(path), prefix])
514}
515
516fn header_message(path: &str, prefix: &[u8], header: &[u8], roots: &[[u8; 32]]) -> Vec<u8> {
517 let roots: Vec<u8> = roots.iter().flatten().copied().collect();
518 cipher::context(&[
519 b"coven/write-header-signature/v1",
520 cipher::storage_path(path),
521 prefix,
522 header,
523 &roots,
524 ])
525}
526
527fn box_context(
528 kind: &[u8],
529 path: &str,
530 prefix: &[u8],
531 sender: &[u8; 32],
532 recipient: &[u8; 32],
533) -> Vec<u8> {
534 cipher::context(&[
535 derivation::SEALED_BOX,
536 kind,
537 cipher::storage_path(path),
538 prefix,
539 sender,
540 recipient,
541 ])
542}
543
544fn seal_box(
545 kind: &[u8],
546 recipient: &SealingKey,
547 path: &str,
548 prefix: &[u8],
549 plaintext: &[u8],
550) -> Result<Vec<u8>, CryptoError> {
551 let seed = randomness::secret_bytes()?;
552 let ephemeral = StaticSecret::from(*seed);
553 let sender = PublicKey::from(&ephemeral).to_bytes();
554 let shared = ephemeral.diffie_hellman(&PublicKey::from(recipient.0));
555 if !shared.was_contributory() {
556 return Err(CryptoError::WeakSealingKey);
557 }
558 let context = box_context(kind, path, prefix, &sender, &recipient.0);
559 let key = derivation::derive(shared.as_bytes(), &context);
560 let body = cipher::seal_random(&key, &context, plaintext)?;
561 let mut bytes = Vec::with_capacity(32 + body.len());
562 bytes.extend_from_slice(&sender);
563 bytes.extend_from_slice(&body);
564 Ok(bytes)
565}
566
567#[cfg(test)]
568#[path = "member_tests.rs"]
569mod tests;