Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

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}