Skip to main content

openclaw_node/auth/
credentials.rs

1//! Encrypted credential storage bindings.
2
3use napi::bindgen_prelude::*;
4use napi_derive::napi;
5use std::path::PathBuf;
6use std::sync::Arc;
7use tokio::sync::RwLock;
8
9use openclaw_core::secrets::CredentialStore as RustCredentialStore;
10
11use super::api_key::NodeApiKey;
12use crate::error::OpenClawError;
13
14/// Encrypted credential storage.
15///
16/// Stores API keys and other credentials encrypted with AES-256-GCM.
17/// Credentials are stored on disk with restrictive permissions.
18///
19/// ```javascript
20/// // Create with 32-byte hex encryption key
21/// const store = new CredentialStore(
22///   '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
23///   '/path/to/credentials'
24/// );
25///
26/// // Store a credential
27/// const key = new NodeApiKey('sk-secret');
28/// await store.store('anthropic', key);
29///
30/// // Load it back
31/// const loaded = await store.load('anthropic');
32/// ```
33#[napi]
34pub struct CredentialStore {
35    inner: Arc<RwLock<RustCredentialStore>>,
36}
37
38#[napi]
39impl CredentialStore {
40    /// Create a new credential store.
41    ///
42    /// # Arguments
43    ///
44    /// * `encryption_key_hex` - 32-byte encryption key as hex string (64 characters)
45    /// * `store_path` - Directory to store encrypted credentials
46    ///
47    /// # Example
48    ///
49    /// ```javascript
50    /// // Generate a key: require('crypto').randomBytes(32).toString('hex')
51    /// const store = new CredentialStore(keyHex, '/path/to/creds');
52    /// ```
53    #[napi(constructor)]
54    pub fn new(encryption_key_hex: String, store_path: String) -> Result<Self> {
55        let key_bytes = hex::decode(&encryption_key_hex)
56            .map_err(|e| OpenClawError::new("INVALID_KEY", format!("Invalid hex key: {e}")))?;
57
58        if key_bytes.len() != 32 {
59            return Err(OpenClawError::new(
60                "INVALID_KEY",
61                format!(
62                    "Encryption key must be 32 bytes (64 hex chars), got {}",
63                    key_bytes.len()
64                ),
65            )
66            .into());
67        }
68
69        let mut key = [0u8; 32];
70        key.copy_from_slice(&key_bytes);
71
72        let store = RustCredentialStore::new(key, PathBuf::from(store_path));
73
74        Ok(Self {
75            inner: Arc::new(RwLock::new(store)),
76        })
77    }
78
79    /// Store an encrypted credential.
80    ///
81    /// The credential is encrypted with AES-256-GCM and written to disk
82    /// with restrictive permissions (0600 on Unix).
83    #[napi]
84    pub async fn store(&self, name: String, api_key: &NodeApiKey) -> Result<()> {
85        let store = self.inner.read().await;
86        store
87            .store(&name, &api_key.inner)
88            .map_err(|e| OpenClawError::from_credential_error(e).into())
89    }
90
91    /// Load and decrypt a credential.
92    ///
93    /// Returns the decrypted API key wrapped in `NodeApiKey` for safety.
94    #[napi]
95    pub async fn load(&self, name: String) -> Result<NodeApiKey> {
96        let store = self.inner.read().await;
97        let key = store
98            .load(&name)
99            .map_err(OpenClawError::from_credential_error)?;
100        Ok(NodeApiKey { inner: key })
101    }
102
103    /// Delete a stored credential.
104    #[napi]
105    pub async fn delete(&self, name: String) -> Result<()> {
106        let store = self.inner.read().await;
107        store
108            .delete(&name)
109            .map_err(|e| OpenClawError::from_credential_error(e).into())
110    }
111
112    /// List all stored credential names.
113    #[napi]
114    pub async fn list(&self) -> Result<Vec<String>> {
115        let store = self.inner.read().await;
116        store
117            .list()
118            .map_err(|e| OpenClawError::from_credential_error(e).into())
119    }
120
121    /// Check if a credential exists.
122    #[napi]
123    pub async fn exists(&self, name: String) -> Result<bool> {
124        let names = self.list().await?;
125        Ok(names.contains(&name))
126    }
127}