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

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