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}