eidetica/auth/types/keys.rs
1//! Key management types for authentication
2//!
3//! This module defines types related to authentication keys, signatures,
4//! and key resolution.
5
6use serde::{Deserialize, Serialize};
7
8use super::permissions::{KeyStatus, Permission};
9use crate::auth::crypto::PublicKey;
10use crate::crdt::{CRDTError, Doc, doc::Value};
11use crate::entry::ID;
12
13/// Authentication key configuration stored in _settings.auth
14///
15/// Keys are indexed by pubkey in AuthSettings. The name field is optional
16/// metadata that can be used as a hint in signatures.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AuthKey {
19 /// Optional human-readable name for this key
20 /// Multiple keys can share the same name (aliases)
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub name: Option<String>,
23 /// Permission level for this key
24 permissions: Permission,
25 /// Current status of the key
26 status: KeyStatus,
27}
28
29impl AuthKey {
30 /// Create a new AuthKey with validation
31 ///
32 /// # Arguments
33 /// * `name` - Optional human-readable name for this key
34 /// * `permissions` - Permission level for this key
35 /// * `status` - Current status of the key
36 ///
37 /// # Examples
38 /// ```
39 /// use eidetica::auth::types::{AuthKey, Permission, KeyStatus};
40 ///
41 /// let key = AuthKey::new(
42 /// Some("alice_laptop"),
43 /// Permission::Write(10),
44 /// KeyStatus::Active
45 /// );
46 /// ```
47 pub fn new(name: Option<&str>, permissions: Permission, status: KeyStatus) -> Self {
48 Self {
49 name: name.map(|n| n.to_owned()),
50 permissions,
51 status,
52 }
53 }
54
55 /// Create a new active AuthKey (common case)
56 ///
57 /// # Arguments
58 /// * `name` - Optional human-readable name for this key
59 /// * `permissions` - Permission level for this key
60 ///
61 /// # Examples
62 /// ```
63 /// use eidetica::auth::types::{AuthKey, Permission};
64 ///
65 /// let key = AuthKey::active(
66 /// Some("alice_laptop"),
67 /// Permission::Admin(1)
68 /// );
69 /// ```
70 pub fn active(name: Option<&str>, permissions: Permission) -> Self {
71 Self::new(name, permissions, KeyStatus::Active)
72 }
73
74 /// Get the optional name
75 pub fn name(&self) -> Option<&str> {
76 self.name.as_deref()
77 }
78
79 /// Get the permissions
80 pub fn permissions(&self) -> &Permission {
81 &self.permissions
82 }
83
84 /// Get the status
85 pub fn status(&self) -> &KeyStatus {
86 &self.status
87 }
88
89 /// Whether this key currently grants access.
90 ///
91 /// The single definition of "usable key" at the settings layer — mirror of
92 /// [`ResolvedAuth::grants_access`]. Callers deciding access must gate on
93 /// this rather than re-checking `status` inline, so revocation is honoured
94 /// identically on every path.
95 pub fn is_active(&self) -> bool {
96 self.status == KeyStatus::Active
97 }
98
99 /// Set the status (e.g., for revocation)
100 pub fn set_status(&mut self, status: KeyStatus) {
101 self.status = status;
102 }
103
104 /// Set the permissions (e.g., for updates)
105 pub fn set_permissions(&mut self, permissions: Permission) {
106 self.permissions = permissions;
107 }
108
109 /// Set the name
110 pub fn set_name(&mut self, name: Option<&str>) {
111 self.name = name.map(|s| s.to_owned());
112 }
113}
114
115// ==================== Doc Conversions ====================
116
117impl From<AuthKey> for Value {
118 fn from(key: AuthKey) -> Value {
119 Value::Doc(Doc::from(key))
120 }
121}
122
123impl From<AuthKey> for Doc {
124 fn from(key: AuthKey) -> Doc {
125 // An AuthKey needs to be atomic, no partial merging.
126 let mut doc = Doc::atomic();
127 if let Some(name) = key.name {
128 doc.set("name", name);
129 }
130 doc.set("permissions", key.permissions);
131 let status_str = match key.status {
132 KeyStatus::Active => "Active",
133 KeyStatus::Revoked => "Revoked",
134 };
135 doc.set("status", status_str);
136 doc
137 }
138}
139
140impl TryFrom<&Doc> for AuthKey {
141 type Error = crate::Error;
142
143 fn try_from(doc: &Doc) -> crate::Result<Self> {
144 let name: Option<String> = doc.get_as::<&str>("name").map(String::from);
145
146 let perm_doc = match doc.get("permissions") {
147 Some(Value::Doc(d)) => d,
148 _ => {
149 return Err(CRDTError::ElementNotFound {
150 key: "permissions".to_string(),
151 }
152 .into());
153 }
154 };
155 let permissions = Permission::try_from(perm_doc)?;
156
157 let status_str =
158 doc.get_as::<&str>("status")
159 .ok_or_else(|| CRDTError::ElementNotFound {
160 key: "status".to_string(),
161 })?;
162 let status = match status_str {
163 "Active" => KeyStatus::Active,
164 "Revoked" => KeyStatus::Revoked,
165 other => {
166 return Err(CRDTError::DeserializationFailed {
167 reason: format!("unknown KeyStatus: {other}"),
168 }
169 .into());
170 }
171 };
172
173 Ok(AuthKey {
174 name,
175 permissions,
176 status,
177 })
178 }
179}
180
181/// Step in a delegation path
182///
183/// References a delegated tree by its root entry ID. The final signer hint is stored
184/// in the parent SigKey, not in the DelegationStep.
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
186pub struct DelegationStep {
187 /// Root entry ID of the delegated tree (used to look up the delegation in AuthSettings)
188 pub tree: ID,
189 /// Tips of the delegated tree at time of signing
190 pub tips: Vec<ID>,
191}
192
193fn is_false(v: &bool) -> bool {
194 !v
195}
196
197/// Key hint for resolving the signer
198///
199/// Contains explicit fields for each hint type. Exactly one hint field
200/// should be set. The hint is used to look up the actual public key
201/// in AuthSettings for signature verification.
202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
203pub struct KeyHint {
204 /// Public key hint (e.g. Ed25519 verifying key)
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub pubkey: Option<PublicKey>,
207 /// Name hint: "alice_laptop" - searches keys where name matches
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub name: Option<String>,
210 /// Whether this hint refers to the global permission
211 #[serde(default, skip_serializing_if = "is_false")]
212 pub is_global: bool,
213 // TODO: Fingerprint hint (future): "7F8A9B3C..." - matches hash of pubkey
214 // This is used in other systems and may be a better option than matching/revealing
215 // the full pubkey
216 // #[serde(skip_serializing_if = "Option::is_none")]
217 // pub fingerprint: Option<String>,
218}
219
220impl KeyHint {
221 /// Create a hint from a public key
222 pub fn from_pubkey(pubkey: &PublicKey) -> Self {
223 Self {
224 pubkey: Some(pubkey.clone()),
225 name: None,
226 is_global: false,
227 }
228 }
229
230 /// Create a hint from a name
231 pub fn from_name(name: impl Into<String>) -> Self {
232 Self {
233 pubkey: None,
234 name: Some(name.into()),
235 is_global: false,
236 }
237 }
238
239 /// Create a global permission hint with actual signer pubkey
240 pub fn global(actual_pubkey: &PublicKey) -> Self {
241 Self {
242 pubkey: Some(actual_pubkey.clone()),
243 name: None,
244 is_global: true,
245 }
246 }
247
248 /// Check if this is a global permission hint
249 pub fn is_global(&self) -> bool {
250 self.is_global
251 }
252
253 /// Check if any hint field is set
254 ///
255 /// Returns `true` if at least one of `pubkey`, `name`, or `is_global` is set.
256 ///
257 /// # Unsigned Entry Detection
258 ///
259 /// This method is primarily used to detect **unsigned entries** during validation.
260 /// An entry is considered unsigned when:
261 /// - The `SigKey` is `Direct` with an empty hint (`!hint.is_set()`)
262 /// - The signature field is `None`
263 ///
264 /// This allows databases to operate without authentication when no auth keys are
265 /// configured, supporting both authenticated and unauthenticated use cases.
266 ///
267 /// ```
268 /// # use eidetica::auth::types::KeyHint;
269 /// # use eidetica::auth::crypto::PrivateKey;
270 /// // Empty hint - represents an unsigned entry when combined with no signature
271 /// let empty = KeyHint::default();
272 /// assert!(!empty.is_set());
273 ///
274 /// // Hint with pubkey - this entry requires signature verification
275 /// let pubkey = PrivateKey::generate().public_key();
276 /// let with_pubkey = KeyHint::from_pubkey(&pubkey);
277 /// assert!(with_pubkey.is_set());
278 ///
279 /// // Hint with name only - also requires signature verification
280 /// let with_name = KeyHint::from_name("alice_laptop");
281 /// assert!(with_name.is_set());
282 /// ```
283 pub fn is_set(&self) -> bool {
284 self.pubkey.is_some() || self.name.is_some() || (self.is_global && self.pubkey.is_some())
285 }
286
287 /// Get the hint type as a string (for error messages)
288 pub fn hint_type(&self) -> &'static str {
289 if self.is_global && self.pubkey.is_some() {
290 "global"
291 } else if self.pubkey.is_some() {
292 "pubkey"
293 } else if self.name.is_some() {
294 "name"
295 } else {
296 "none"
297 }
298 }
299}
300
301/// Authentication key identifier for entry signing
302///
303/// Represents the path to resolve the signing key, either directly or through delegation.
304/// Uses explicit hint fields to point to the signer's public key.
305///
306/// # Serialization Format
307///
308/// Uses externally tagged serialization (serde default):
309/// - Direct: `{"Direct": {"pubkey": "ed25519:..."}}`
310/// - Delegation: `{"Delegation": {"path": [...], "hint": {...}}}`
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
312pub enum SigKey {
313 /// Direct reference to a key in the current tree's _settings.auth
314 Direct {
315 /// Key hint for resolving the signer
316 hint: KeyHint,
317 },
318 /// Delegation path through other trees
319 Delegation {
320 /// Path of delegation steps (tree references)
321 path: Vec<DelegationStep>,
322 /// Final signer hint (resolved in last delegated tree's auth)
323 hint: KeyHint,
324 },
325}
326
327impl Default for SigKey {
328 fn default() -> Self {
329 SigKey::Direct {
330 hint: KeyHint::default(),
331 }
332 }
333}
334
335impl SigKey {
336 /// Create a direct key reference from a pubkey
337 pub fn from_pubkey(pubkey: &PublicKey) -> Self {
338 SigKey::Direct {
339 hint: KeyHint::from_pubkey(pubkey),
340 }
341 }
342
343 /// Create a direct key reference from a name
344 pub fn from_name(name: impl Into<String>) -> Self {
345 SigKey::Direct {
346 hint: KeyHint::from_name(name),
347 }
348 }
349
350 /// Create a global permission key with actual signer pubkey
351 pub fn global(actual_pubkey: &PublicKey) -> Self {
352 SigKey::Direct {
353 hint: KeyHint::global(actual_pubkey),
354 }
355 }
356
357 /// Get the key hint (for both Direct and Delegation variants)
358 pub fn hint(&self) -> &KeyHint {
359 match self {
360 SigKey::Direct { hint } => hint,
361 SigKey::Delegation { hint, .. } => hint,
362 }
363 }
364
365 /// Get mutable reference to the key hint
366 pub fn hint_mut(&mut self) -> &mut KeyHint {
367 match self {
368 SigKey::Direct { hint } => hint,
369 SigKey::Delegation { hint, .. } => hint,
370 }
371 }
372
373 /// Check if this is a global permission key
374 pub fn is_global(&self) -> bool {
375 self.hint().is_global()
376 }
377
378 /// Human-readable identifier string for display and audit trails.
379 ///
380 /// Returns `"*"` for global, the pubkey string for pubkey-based,
381 /// the name for name-based, or `"unknown"` if no hint is set.
382 pub fn display_id(&self) -> String {
383 let hint = self.hint();
384 if hint.is_global() {
385 "*".to_string()
386 } else if let Some(pubkey) = &hint.pubkey {
387 pubkey.to_string()
388 } else if let Some(name) = &hint.name {
389 name.clone()
390 } else {
391 "unknown".to_string()
392 }
393 }
394
395 /// Check if this SigKey uses a specific pubkey hint
396 pub fn has_pubkey_hint(&self, pubkey: &PublicKey) -> bool {
397 self.hint().pubkey.as_ref() == Some(pubkey)
398 }
399
400 /// Check if this SigKey uses a specific name hint
401 pub fn has_name_hint(&self, name: &str) -> bool {
402 self.hint().name.as_deref() == Some(name)
403 }
404}
405
406/// Signature information embedded in an entry
407#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
408pub struct SigInfo {
409 /// Authentication signature - base64-encoded signature bytes
410 /// Optional to allow for entry creation before signing
411 #[serde(skip_serializing_if = "Option::is_none")]
412 pub sig: Option<String>,
413 /// Key lookup hint
414 pub key: SigKey,
415}
416
417impl SigInfo {
418 /// Create a new SigInfo with a pubkey hint
419 pub fn from_pubkey(pubkey: &PublicKey) -> Self {
420 Self {
421 sig: None,
422 key: SigKey::from_pubkey(pubkey),
423 }
424 }
425
426 /// Create a new SigInfo with a name hint
427 pub fn from_name(name: impl Into<String>) -> Self {
428 Self {
429 sig: None,
430 key: SigKey::from_name(name),
431 }
432 }
433
434 /// Create a new SigInfo for global permission
435 pub fn global(actual_pubkey: &PublicKey) -> Self {
436 Self {
437 sig: None,
438 key: SigKey::global(actual_pubkey),
439 }
440 }
441
442 /// Check if this is a global permission signature
443 pub fn is_global(&self) -> bool {
444 self.key.is_global()
445 }
446
447 /// Get the key hint
448 pub fn hint(&self) -> &KeyHint {
449 self.key.hint()
450 }
451
452 /// Create a new SigInfoBuilder for constructing SigInfo instances
453 pub fn builder() -> SigInfoBuilder {
454 SigInfoBuilder::new()
455 }
456
457 /// Check if this represents an unsigned/unauthenticated entry.
458 ///
459 /// An entry is unsigned when `SigInfo` is in its default state:
460 /// - Direct SigKey (not Delegation)
461 /// - Empty KeyHint (no pubkey, no name)
462 /// - No signature
463 pub fn is_unsigned(&self) -> bool {
464 matches!(self.key, SigKey::Direct { ref hint } if !hint.is_set()) && self.sig.is_none()
465 }
466
467 /// Check if this represents a malformed/inconsistent signature state.
468 ///
469 /// Returns `Some(reason)` if malformed, `None` if valid.
470 ///
471 /// Malformed states:
472 /// - Direct with hint but no signature (can't verify without signature)
473 /// - Direct with signature but no hint (can't verify without knowing which key)
474 /// - Delegation with no signature (delegation always requires signature)
475 pub fn malformed_reason(&self) -> Option<&'static str> {
476 match &self.key {
477 SigKey::Direct { hint } => {
478 if hint.is_set() && self.sig.is_none() {
479 Some("entry has key hint but no signature")
480 } else if !hint.is_set() && self.sig.is_some() {
481 Some("entry has signature but no key hint")
482 } else {
483 None
484 }
485 }
486 SigKey::Delegation { .. } => {
487 if self.sig.is_none() {
488 Some("delegation entry requires a signature")
489 } else {
490 None
491 }
492 }
493 }
494 }
495}
496
497/// Builder for constructing SigInfo instances
498///
499/// This builder provides a fluent interface for creating SigInfo objects.
500#[derive(Debug, Clone, Default)]
501pub struct SigInfoBuilder {
502 sig: Option<String>,
503 key: Option<SigKey>,
504}
505
506impl SigInfoBuilder {
507 /// Create a new empty SigInfoBuilder
508 pub fn new() -> Self {
509 Self::default()
510 }
511
512 /// Set the signature (base64-encoded signature bytes)
513 pub fn sig(mut self, sig: impl Into<String>) -> Self {
514 self.sig = Some(sig.into());
515 self
516 }
517
518 /// Set the authentication key reference
519 pub fn key(mut self, key: SigKey) -> Self {
520 self.key = Some(key);
521 self
522 }
523
524 /// Set a pubkey hint
525 pub fn pubkey_hint(mut self, pubkey: &PublicKey) -> Self {
526 self.key = Some(SigKey::from_pubkey(pubkey));
527 self
528 }
529
530 /// Set a name hint
531 pub fn name_hint(mut self, name: impl Into<String>) -> Self {
532 self.key = Some(SigKey::from_name(name));
533 self
534 }
535
536 /// Set a global permission hint with actual signer pubkey
537 pub fn global_hint(mut self, actual_pubkey: &PublicKey) -> Self {
538 self.key = Some(SigKey::global(actual_pubkey));
539 self
540 }
541
542 /// Build the final SigInfo instance
543 ///
544 /// # Panics
545 /// Panics if key is not set, as it's a required field.
546 pub fn build(self) -> SigInfo {
547 SigInfo {
548 sig: self.sig,
549 key: self.key.expect("key is required for SigInfo"),
550 }
551 }
552}
553
554/// Resolved authentication information after validation
555#[derive(Debug, Clone)]
556pub struct ResolvedAuth {
557 /// The actual public key used for signing
558 pub public_key: PublicKey,
559 /// Effective permission after clamping
560 pub effective_permission: Permission,
561 /// Current status of the key
562 pub key_status: KeyStatus,
563}
564
565impl ResolvedAuth {
566 /// Whether this resolved key may currently be used to authorize access.
567 ///
568 /// The single definition of "usable key" for every auth path — resolvers
569 /// return candidates regardless of status, so each consumer that turns
570 /// candidates into an access decision must gate on this, not re-check
571 /// `key_status` inline.
572 pub fn grants_access(&self) -> bool {
573 self.key_status == KeyStatus::Active
574 }
575}