1use std::fmt;
4use std::str::FromStr;
5
6use hkdf::Hkdf;
7use hmac::{Hmac, KeyInit, Mac};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use sha2::Sha256;
10
11use super::membership::MembershipGrantId;
12use super::store_commit::ObjectHash;
13use coven_keys::encryption::EncryptionService;
14
15pub use super::circle_control::*;
16pub use super::circle_roster::*;
17
18const CIRCLE_ID_ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
19const CIRCLE_ID_LENGTH: usize = 26;
20const ROW_ROUTING_KEY_DOMAIN: &[u8] = b"coven.row-routing.v1";
21const ROW_ROUTING_ID_DOMAIN: &[u8] = b"coven.row-routing-id.v1\0";
22const CIRCLE_ID_FOUNDER_DOMAIN: &str = "coven.circle-id-founder.v1";
23const CIRCLE_EPOCH_ID_GENERATION_DOMAIN: &[u8] = b"coven.circle-epoch-id-generation.v1\0";
24const ACCESS_LEAF_ID_GENERATION_DOMAIN: &[u8] = b"coven.circle-access-leaf-id-generation.v1\0";
25
26#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct CircleId([u8; 16]);
29
30impl CircleId {
31 pub fn founder(
32 store_root_hash: ObjectHash,
33 author_pubkey: &str,
34 owner_grant: &MembershipGrantId,
35 ) -> Self {
36 #[derive(Serialize)]
37 struct Founder<'a> {
38 domain: &'static str,
39 store_root_hash: ObjectHash,
40 author_pubkey: &'a str,
41 owner_grant: &'a MembershipGrantId,
42 }
43 let digest = ObjectHash::digest(
44 &serde_json::to_vec(&Founder {
45 domain: CIRCLE_ID_FOUNDER_DOMAIN,
46 store_root_hash,
47 author_pubkey,
48 owner_grant,
49 })
50 .expect("Circle ID founder serialization cannot fail"),
51 );
52 let mut bytes = [0_u8; 16];
53 bytes.copy_from_slice(&digest.as_bytes()[..16]);
54 Self(bytes)
55 }
56
57 pub fn from_bytes(bytes: [u8; 16]) -> Self {
58 Self(bytes)
59 }
60
61 pub fn as_bytes(&self) -> &[u8; 16] {
62 &self.0
63 }
64}
65
66impl fmt::Debug for CircleId {
67 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68 fmt::Display::fmt(self, formatter)
69 }
70}
71
72impl fmt::Display for CircleId {
73 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74 formatter.write_str(&encode_base32(&self.0))
75 }
76}
77
78impl FromStr for CircleId {
79 type Err = CircleIdError;
80
81 fn from_str(value: &str) -> Result<Self, Self::Err> {
82 let bytes = decode_base32(value)?;
83 let id = Self(bytes);
84 if id.to_string() != value || value == "local" {
85 return Err(CircleIdError(value.to_string()));
86 }
87 Ok(id)
88 }
89}
90
91impl Serialize for CircleId {
92 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
93 where
94 S: Serializer,
95 {
96 serializer.serialize_str(&self.to_string())
97 }
98}
99
100impl<'de> Deserialize<'de> for CircleId {
101 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
102 where
103 D: Deserializer<'de>,
104 {
105 String::deserialize(deserializer)?
106 .parse()
107 .map_err(serde::de::Error::custom)
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
112#[error("circle id must be canonical 128-bit lowercase base32: {0:?}")]
113pub struct CircleIdError(String);
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "snake_case")]
117pub enum CircleRole {
118 Owner,
119 Member,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum CircleInfo {
129 Active {
130 id: CircleId,
131 name: String,
132 role: CircleRole,
133 rotation_required: bool,
138 },
139 Conflicted {
140 id: CircleId,
141 branches: Vec<CircleControlCoord>,
145 },
146 Deleted { id: CircleId },
149}
150
151impl CircleInfo {
152 pub fn id(&self) -> CircleId {
153 match self {
154 Self::Active { id, .. } | Self::Conflicted { id, .. } | Self::Deleted { id } => *id,
155 }
156 }
157
158 pub fn name(&self) -> Option<&str> {
161 match self {
162 Self::Active { name, .. } => Some(name),
163 Self::Conflicted { .. } | Self::Deleted { .. } => None,
164 }
165 }
166
167 pub fn rotation_required(&self) -> bool {
171 matches!(
172 self,
173 Self::Active {
174 rotation_required: true,
175 ..
176 }
177 )
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum CircleState {
185 Active,
188 Inactive,
191 Closing,
194 RotationRequired { removed_members: Vec<String> },
198 ControlConflict { branches: Vec<CircleControlCoord> },
201 Deleted,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct Circle {
212 pub id: CircleId,
213 pub name: Option<String>,
214 pub role: Option<CircleRole>,
215 pub state: CircleState,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum CircleCloseSettlement {
223 Responded,
224 Excluded,
225 Pending,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct CircleCloseParticipant {
231 pub device_id: crate::store_commit::StoreDeviceId,
232 pub settlement: CircleCloseSettlement,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct CircleCloseStatus {
239 pub circle_id: CircleId,
240 pub close_id: CircleEpochCloseId,
241 pub participants: Vec<CircleCloseParticipant>,
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
248pub enum CirclePublicationBlocked {
249 RotationRequired {
250 circle_id: CircleId,
251 removed_members: Vec<String>,
252 },
253}
254
255impl std::fmt::Display for CirclePublicationBlocked {
256 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 match self {
258 Self::RotationRequired {
259 circle_id,
260 removed_members,
261 } => write!(
262 formatter,
263 "Circle {circle_id} requires rotation: its roster names removed Store members {removed_members:?}"
264 ),
265 }
266 }
267}
268
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct CircleMemberInfo {
271 pub pubkey: String,
272 pub role: CircleRole,
273 pub is_self: bool,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
277#[serde(transparent)]
278pub struct CircleOperationId(crate::write::WriteId);
279
280impl CircleOperationId {
281 pub fn from_write_id(write_id: crate::write::WriteId) -> Self {
282 Self(write_id)
283 }
284
285 #[cfg(any(test, feature = "test-utils"))]
288 pub fn placeholder(seed: &str) -> Self {
289 Self::from_write_id(crate::write::WriteId::from_generated(seed.to_string()))
290 }
291
292 pub fn as_str(&self) -> &str {
293 self.0.as_str()
294 }
295
296 pub fn finalization_write_id(&self) -> crate::write::WriteId {
297 crate::write::WriteId::from_generated(
298 crate::store_commit::ObjectHash::digest(
299 &[
300 b"coven.circle-epoch-close-finalization-write.v1\0".as_slice(),
301 self.as_str().as_bytes(),
302 ]
303 .concat(),
304 )
305 .to_string(),
306 )
307 }
308
309 pub fn cancellation_write_id(&self) -> crate::write::WriteId {
310 crate::write::WriteId::from_generated(
311 crate::store_commit::ObjectHash::digest(
312 &[
313 b"coven.circle-epoch-close-cancellation-write.v1\0".as_slice(),
314 self.as_str().as_bytes(),
315 ]
316 .concat(),
317 )
318 .to_string(),
319 )
320 }
321}
322
323impl fmt::Display for CircleOperationId {
324 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
325 formatter.write_str(self.as_str())
326 }
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
334#[serde(transparent)]
335pub struct CircleEpochCloseId(ObjectHash);
336
337impl CircleEpochCloseId {
338 pub fn from_operation_id(operation_id: &CircleOperationId) -> Self {
339 Self(ObjectHash::digest(
340 &[
341 b"coven.circle-epoch-close-id.v1\0".as_slice(),
342 operation_id.as_str().as_bytes(),
343 ]
344 .concat(),
345 ))
346 }
347}
348
349impl fmt::Display for CircleEpochCloseId {
350 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
351 fmt::Display::fmt(&self.0, formatter)
352 }
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356pub enum CircleOperationKind {
357 Create,
358 Rename,
359 AddMember,
360 RemoveMember,
361 ResolveControl,
362 Delete,
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
368#[serde(rename_all = "snake_case", deny_unknown_fields)]
369pub enum CircleOperationBlock {
370 AuthorityLost {
372 grant_id: crate::membership::MembershipGrantId,
373 },
374 PositionLost {
380 winner_commit: crate::store_commit::ObjectHash,
381 },
382}
383
384impl std::fmt::Display for CircleOperationBlock {
385 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 match self {
387 Self::AuthorityLost { grant_id } => write!(
388 formatter,
389 "author grant {grant_id} no longer has current Store write authority"
390 ),
391 Self::PositionLost { winner_commit } => write!(
392 formatter,
393 "Store commit {winner_commit} took this device's stream position \
394 before the operation published"
395 ),
396 }
397 }
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401#[serde(rename_all = "snake_case", deny_unknown_fields)]
402pub enum CircleOperationState {
403 Pending,
404 WaitingForCloseResponses,
405 Finalizing,
406 Blocked {
407 block: CircleOperationBlock,
408 },
409 Discarding,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct CircleOperationInfo {
417 pub operation_id: CircleOperationId,
418 pub circle_id: CircleId,
419 pub kind: CircleOperationKind,
420 pub state: CircleOperationState,
421}
422
423#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
425pub enum Audience {
426 Store,
427 Circle(CircleId),
428 Local,
429}
430
431impl Audience {
432 pub fn from_column(value: Option<&str>) -> Result<Self, CircleIdError> {
433 match value {
434 None => Ok(Self::Store),
435 Some("local") => Ok(Self::Local),
436 Some(circle) => circle.parse().map(Self::Circle),
437 }
438 }
439
440 pub fn column_value(&self) -> Option<String> {
441 match self {
442 Self::Store => None,
443 Self::Circle(circle) => Some(circle.to_string()),
444 Self::Local => Some("local".to_string()),
445 }
446 }
447}
448
449macro_rules! generated_hex_id {
450 ($name:ident, $domain:ident) => {
451 #[derive(
452 Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
453 )]
454 #[serde(transparent)]
455 pub struct $name([u8; 16]);
456
457 impl $name {
458 pub fn generate(ids: &dyn coven_foundation::id_provider::IdProvider) -> Self {
459 Self(generated_id_bytes(ids, $domain))
460 }
461 }
462
463 impl fmt::Display for $name {
464 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
465 formatter.write_str(&hex::encode(self.0))
466 }
467 }
468 };
469}
470
471generated_hex_id!(CircleEpochId, CIRCLE_EPOCH_ID_GENERATION_DOMAIN);
472generated_hex_id!(AccessLeafId, ACCESS_LEAF_ID_GENERATION_DOMAIN);
473
474pub(crate) fn generated_id_digest(
475 ids: &dyn coven_foundation::id_provider::IdProvider,
476 domain: &[u8],
477) -> ObjectHash {
478 let id = ids.new_id();
479 let mut material = Vec::with_capacity(domain.len() + id.len());
480 material.extend_from_slice(domain);
481 material.extend_from_slice(id.as_bytes());
482 ObjectHash::digest(&material)
483}
484
485fn generated_id_bytes(
486 ids: &dyn coven_foundation::id_provider::IdProvider,
487 domain: &[u8],
488) -> [u8; 16] {
489 generated_id_digest(ids, domain).as_bytes()[..16]
490 .try_into()
491 .expect("SHA-256 digest prefix has fixed length")
492}
493
494#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
497pub struct RowRoutingId([u8; 32]);
498
499impl fmt::Debug for RowRoutingId {
500 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
501 fmt::Display::fmt(self, formatter)
502 }
503}
504
505impl fmt::Display for RowRoutingId {
506 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
507 formatter.write_str(&hex::encode(self.0))
508 }
509}
510
511impl FromStr for RowRoutingId {
512 type Err = RowRoutingIdError;
513
514 fn from_str(value: &str) -> Result<Self, Self::Err> {
515 if value.len() != 64
516 || value
517 .bytes()
518 .any(|byte| !byte.is_ascii_digit() && !(b'a'..=b'f').contains(&byte))
519 {
520 return Err(RowRoutingIdError(value.to_string()));
521 }
522 let bytes: [u8; 32] = hex::decode(value)
523 .map_err(|_| RowRoutingIdError(value.to_string()))?
524 .try_into()
525 .map_err(|_| RowRoutingIdError(value.to_string()))?;
526 Ok(Self(bytes))
527 }
528}
529
530impl Serialize for RowRoutingId {
531 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
532 where
533 S: Serializer,
534 {
535 serializer.serialize_str(&self.to_string())
536 }
537}
538
539impl<'de> Deserialize<'de> for RowRoutingId {
540 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
541 where
542 D: Deserializer<'de>,
543 {
544 String::deserialize(deserializer)?
545 .parse()
546 .map_err(serde::de::Error::custom)
547 }
548}
549
550#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
551#[error("row routing id must be exactly 64 lowercase hexadecimal characters: {0:?}")]
552pub struct RowRoutingIdError(String);
553
554#[derive(Clone)]
555pub struct RowRoutingKey([u8; 32]);
556
557#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
558pub enum RowRoutingKeyError {
559 #[error("Store keyring has no generation-1 key")]
560 MissingGenerationOne,
561 #[error("Store keyring has more than one generation-1 key")]
562 AmbiguousGenerationOne,
563}
564
565pub fn derive_row_routing_key(
566 encryption: &EncryptionService,
567 store_root_hash: ObjectHash,
568) -> Result<RowRoutingKey, RowRoutingKeyError> {
569 let mut generation_one = encryption
570 .keyring_entries()
571 .into_iter()
572 .filter_map(|(generation, key)| (generation == 1).then_some(key));
573 let key = generation_one
574 .next()
575 .ok_or(RowRoutingKeyError::MissingGenerationOne)?;
576 if generation_one.next().is_some() {
577 return Err(RowRoutingKeyError::AmbiguousGenerationOne);
578 }
579 let hkdf = Hkdf::<Sha256>::new(Some(ROW_ROUTING_KEY_DOMAIN), &key);
580 let mut derived = [0u8; 32];
581 hkdf.expand(store_root_hash.as_bytes(), &mut derived)
582 .expect("32 bytes is a valid HKDF output length");
583 Ok(RowRoutingKey(derived))
584}
585
586pub fn row_routing_id(key: &RowRoutingKey, table: &str, row_id: &str) -> RowRoutingId {
587 let mut mac = Hmac::<Sha256>::new_from_slice(&key.0).expect("HMAC accepts a 32-byte key");
588 mac.update(ROW_ROUTING_ID_DOMAIN);
589 mac.update(&(table.len() as u64).to_be_bytes());
590 mac.update(table.as_bytes());
591 mac.update(&(row_id.len() as u64).to_be_bytes());
592 mac.update(row_id.as_bytes());
593 RowRoutingId(mac.finalize().into_bytes().into())
594}
595
596fn encode_base32(bytes: &[u8; 16]) -> String {
597 let mut output = String::with_capacity(CIRCLE_ID_LENGTH);
598 let mut buffer = 0u32;
599 let mut bits = 0u8;
600 for byte in bytes {
601 buffer = (buffer << 8) | u32::from(*byte);
602 bits += 8;
603 while bits >= 5 {
604 bits -= 5;
605 output.push(CIRCLE_ID_ALPHABET[((buffer >> bits) & 0x1f) as usize] as char);
606 buffer &= (1u32 << bits).wrapping_sub(1);
607 }
608 }
609 if bits != 0 {
610 output.push(CIRCLE_ID_ALPHABET[((buffer << (5 - bits)) & 0x1f) as usize] as char);
611 }
612 output
613}
614
615fn decode_base32(value: &str) -> Result<[u8; 16], CircleIdError> {
616 if value.len() != CIRCLE_ID_LENGTH {
617 return Err(CircleIdError(value.to_string()));
618 }
619 let mut output = Vec::with_capacity(16);
620 let mut buffer = 0u32;
621 let mut bits = 0u8;
622 for byte in value.bytes() {
623 let digit = CIRCLE_ID_ALPHABET
624 .iter()
625 .position(|candidate| *candidate == byte)
626 .ok_or_else(|| CircleIdError(value.to_string()))? as u32;
627 buffer = (buffer << 5) | digit;
628 bits += 5;
629 while bits >= 8 {
630 bits -= 8;
631 output.push(((buffer >> bits) & 0xff) as u8);
632 buffer &= (1u32 << bits).wrapping_sub(1);
633 }
634 }
635 if output.len() != 16 || buffer != 0 {
636 return Err(CircleIdError(value.to_string()));
637 }
638 output
639 .try_into()
640 .map_err(|_| CircleIdError(value.to_string()))
641}
642
643#[cfg(test)]
644#[path = "circle_tests.rs"]
645mod tests;