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/// Authentication information embedded in an entry
407#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
408pub struct AuthInfo {
409 /// Authentication signature - base64-encoded signature bytes
410 /// Optional to allow for entry creation before signing
411 ///
412 /// Serialized as `sig` to keep the wire format, and therefore every existing
413 /// entry ID, unchanged.
414 #[serde(rename = "sig", skip_serializing_if = "Option::is_none")]
415 pub signature: Option<String>,
416 /// Key lookup hint
417 pub key: SigKey,
418}
419
420impl AuthInfo {
421 /// Create a new AuthInfo with a pubkey hint
422 pub fn from_pubkey(pubkey: &PublicKey) -> Self {
423 Self {
424 signature: None,
425 key: SigKey::from_pubkey(pubkey),
426 }
427 }
428
429 /// Create a new AuthInfo with a name hint
430 pub fn from_name(name: impl Into<String>) -> Self {
431 Self {
432 signature: None,
433 key: SigKey::from_name(name),
434 }
435 }
436
437 /// Create a new AuthInfo for global permission
438 pub fn global(actual_pubkey: &PublicKey) -> Self {
439 Self {
440 signature: None,
441 key: SigKey::global(actual_pubkey),
442 }
443 }
444
445 /// Check if this is a global permission signature
446 pub fn is_global(&self) -> bool {
447 self.key.is_global()
448 }
449
450 /// Get the key hint
451 pub fn hint(&self) -> &KeyHint {
452 self.key.hint()
453 }
454
455 /// Create a new AuthInfoBuilder for constructing AuthInfo instances
456 pub fn builder() -> AuthInfoBuilder {
457 AuthInfoBuilder::new()
458 }
459
460 /// Check if this represents an unsigned/unauthenticated entry.
461 ///
462 /// An entry is unsigned when `AuthInfo` is in its default state:
463 /// - Direct SigKey (not Delegation)
464 /// - Empty KeyHint (no pubkey, no name)
465 /// - No signature
466 pub fn is_unsigned(&self) -> bool {
467 matches!(self.key, SigKey::Direct { ref hint } if !hint.is_set())
468 && self.signature.is_none()
469 }
470
471 /// Check if this represents a malformed/inconsistent signature state.
472 ///
473 /// Returns `Some(reason)` if malformed, `None` if valid.
474 ///
475 /// Malformed states:
476 /// - Direct with hint but no signature (can't verify without signature)
477 /// - Direct with signature but no hint (can't verify without knowing which key)
478 /// - Delegation with no signature (delegation always requires signature)
479 pub fn malformed_reason(&self) -> Option<&'static str> {
480 match &self.key {
481 SigKey::Direct { hint } => {
482 if hint.is_set() && self.signature.is_none() {
483 Some("entry has key hint but no signature")
484 } else if !hint.is_set() && self.signature.is_some() {
485 Some("entry has signature but no key hint")
486 } else {
487 None
488 }
489 }
490 SigKey::Delegation { .. } => {
491 if self.signature.is_none() {
492 Some("delegation entry requires a signature")
493 } else {
494 None
495 }
496 }
497 }
498 }
499}
500
501/// Builder for constructing AuthInfo instances
502///
503/// This builder provides a fluent interface for creating AuthInfo objects.
504#[derive(Debug, Clone, Default)]
505pub struct AuthInfoBuilder {
506 signature: Option<String>,
507 key: Option<SigKey>,
508}
509
510impl AuthInfoBuilder {
511 /// Create a new empty AuthInfoBuilder
512 pub fn new() -> Self {
513 Self::default()
514 }
515
516 /// Set the signature (base64-encoded signature bytes)
517 pub fn signature(mut self, signature: impl Into<String>) -> Self {
518 self.signature = Some(signature.into());
519 self
520 }
521
522 /// Set the authentication key reference
523 pub fn key(mut self, key: SigKey) -> Self {
524 self.key = Some(key);
525 self
526 }
527
528 /// Set a pubkey hint
529 pub fn pubkey_hint(mut self, pubkey: &PublicKey) -> Self {
530 self.key = Some(SigKey::from_pubkey(pubkey));
531 self
532 }
533
534 /// Set a name hint
535 pub fn name_hint(mut self, name: impl Into<String>) -> Self {
536 self.key = Some(SigKey::from_name(name));
537 self
538 }
539
540 /// Set a global permission hint with actual signer pubkey
541 pub fn global_hint(mut self, actual_pubkey: &PublicKey) -> Self {
542 self.key = Some(SigKey::global(actual_pubkey));
543 self
544 }
545
546 /// Build the final AuthInfo instance
547 ///
548 /// # Panics
549 /// Panics if key is not set, as it's a required field.
550 pub fn build(self) -> AuthInfo {
551 AuthInfo {
552 signature: self.signature,
553 key: self.key.expect("key is required for AuthInfo"),
554 }
555 }
556}
557
558/// Resolved authentication information after validation
559#[derive(Debug, Clone)]
560pub struct ResolvedAuth {
561 /// The actual public key used for signing
562 pub public_key: PublicKey,
563 /// Effective permission after clamping
564 pub effective_permission: Permission,
565 /// Current status of the key
566 pub key_status: KeyStatus,
567}
568
569impl ResolvedAuth {
570 /// Whether this resolved key may currently be used to authorize access.
571 ///
572 /// The single definition of "usable key" for every auth path — resolvers
573 /// return candidates regardless of status, so each consumer that turns
574 /// candidates into an access decision must gate on this, not re-check
575 /// `key_status` inline.
576 pub fn grants_access(&self) -> bool {
577 self.key_status == KeyStatus::Active
578 }
579}