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

eidetica/account/
key_manager.rs

1//! Session-owned unlocked credentials and public installation-local key metadata.
2
3use std::collections::HashMap;
4use zeroize::{Zeroize, ZeroizeOnDrop};
5
6use super::{AccountError, AccountKey, KeyStorage, UnlockedCredentials};
7use crate::{
8    Result,
9    auth::crypto::{PrivateKey, PublicKey},
10};
11
12/// Holds exactly the caller-unlocked keys for a session, never reference-derived
13/// credentials. Private keys and the guarded wrapping key are dropped on clear.
14pub struct AccountKeyManager {
15    credentials: UnlockedCredentials,
16    key_metadata: HashMap<PublicKey, AccountKey>,
17}
18
19impl AccountKeyManager {
20    pub(crate) fn new(credentials: UnlockedCredentials, metadata: Vec<AccountKey>) -> Result<Self> {
21        let mut manager = Self {
22            credentials,
23            key_metadata: HashMap::new(),
24        };
25        for key in metadata {
26            manager.add_key(key)?;
27        }
28        // Missing metadata is corruption, not permission to hide a bundled key.
29        if manager.key_metadata.len() != manager.credentials.key_ids().len() {
30            return Err(AccountError::InvalidLocalState.into());
31        }
32        Ok(manager)
33    }
34
35    pub fn get_signing_key(&self, key_id: &PublicKey) -> Option<&PrivateKey> {
36        self.credentials.get_signer(key_id)
37    }
38
39    /// Update public metadata only for an actual held signer.
40    pub fn add_key(&mut self, metadata: AccountKey) -> Result<()> {
41        if self.get_signing_key(&metadata.key_id).is_none() {
42            return Err(AccountError::KeyNotFound {
43                key_id: metadata.key_id.to_string(),
44            }
45            .into());
46        }
47        self.key_metadata.insert(metadata.key_id.clone(), metadata);
48        Ok(())
49    }
50
51    pub(crate) fn import_key(&mut self, key: PrivateKey, metadata: AccountKey) -> Result<()> {
52        self.credentials.add_key(key)?;
53        self.add_key(metadata)
54    }
55
56    pub(crate) fn unlock_storage(&self, storage: &KeyStorage) -> Result<UnlockedCredentials> {
57        self.credentials.unlock_storage(storage)
58    }
59
60    pub fn clear(&mut self) {
61        self.zeroize();
62    }
63
64    /// Public key IDs sorted by creation time, then key ID for ties.
65    pub fn list_key_ids(&self) -> Vec<PublicKey> {
66        let mut keys: Vec<_> = self.key_metadata.values().collect();
67        keys.sort_by_key(|key| (key.created_at, key.key_id.to_string()));
68        keys.into_iter().map(|key| key.key_id.clone()).collect()
69    }
70
71    pub fn get_key_metadata(&self, key_id: &PublicKey) -> Option<&AccountKey> {
72        self.key_metadata.get(key_id)
73    }
74
75    pub fn get_default_key_id(&self) -> Option<PublicKey> {
76        self.list_key_ids()
77            .into_iter()
78            .find(|id| self.key_metadata[id].is_default)
79            .or_else(|| self.list_key_ids().into_iter().next())
80    }
81}
82
83impl Zeroize for AccountKeyManager {
84    fn zeroize(&mut self) {
85        self.credentials.clear();
86        self.key_metadata.clear();
87    }
88}
89impl Drop for AccountKeyManager {
90    fn drop(&mut self) {
91        self.zeroize();
92    }
93}
94impl ZeroizeOnDrop for AccountKeyManager {}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::{ID, account::CredentialBundle, auth::crypto::generate_keypair};
100
101    fn metadata(key_id: PublicKey, created_at: i64, is_default: bool) -> AccountKey {
102        AccountKey {
103            key_id,
104            created_at,
105            is_default,
106            display_name: None,
107            last_used: None,
108            database_sigkeys: HashMap::new(),
109        }
110    }
111
112    #[test]
113    fn key_manager_holds_exact_bundle_keys_and_metadata() {
114        let (first, a) = generate_keypair();
115        let (second, b) = generate_keypair();
116        let account = ID::from_bytes("account");
117        let bundle = CredentialBundle::seal(&account, &[first, second], "password").unwrap();
118        let mut manager = AccountKeyManager::new(
119            bundle.unlock("password").unwrap(),
120            vec![metadata(a.clone(), 2, true), metadata(b.clone(), 1, false)],
121        )
122        .unwrap();
123        assert_eq!(manager.list_key_ids(), [b.clone(), a.clone()]);
124        assert_eq!(manager.get_default_key_id(), Some(a.clone()));
125        let message = b"actual-key";
126        b.verify(message, &manager.get_signing_key(&b).unwrap().sign(message))
127            .unwrap();
128        assert!(
129            manager
130                .add_key(metadata(generate_keypair().1, 3, false))
131                .is_err()
132        );
133        let mut updated = manager.get_key_metadata(&b).unwrap().clone();
134        updated.display_name = Some("new label".to_owned());
135        manager.add_key(updated).unwrap();
136        assert_eq!(
137            manager
138                .get_key_metadata(&b)
139                .unwrap()
140                .display_name
141                .as_deref(),
142            Some("new label")
143        );
144        let storage = manager.credentials.storage().unwrap();
145        assert_eq!(
146            manager.unlock_storage(&storage).unwrap().key_ids(),
147            manager.credentials.key_ids()
148        );
149        manager.clear();
150        assert!(manager.get_signing_key(&a).is_none());
151        assert!(manager.list_key_ids().is_empty());
152        assert!(manager.get_default_key_id().is_none());
153    }
154
155    #[test]
156    fn key_manager_rejects_incomplete_or_foreign_metadata() {
157        let (key, id) = generate_keypair();
158        let bundle =
159            CredentialBundle::seal(&ID::from_bytes("account"), &[key], "password").unwrap();
160        assert!(AccountKeyManager::new(bundle.unlock("password").unwrap(), vec![]).is_err());
161        assert!(
162            AccountKeyManager::new(
163                bundle.unlock("password").unwrap(),
164                vec![metadata(generate_keypair().1, 0, true)]
165            )
166            .is_err()
167        );
168        let mut manager = AccountKeyManager::new(
169            bundle.unlock("password").unwrap(),
170            vec![metadata(id.clone(), 0, false)],
171        )
172        .unwrap();
173        assert_eq!(manager.get_default_key_id(), Some(id));
174        let rewrapped = bundle
175            .unlock("password")
176            .unwrap()
177            .rewrap("new-password")
178            .unwrap();
179        assert!(
180            manager
181                .unlock_storage(&KeyStorage::Encrypted { bundle: rewrapped })
182                .is_err()
183        );
184        manager.clear();
185    }
186}