harn_hostlib/secret_store/
mod.rs1use std::io;
23use std::sync::Arc;
24
25use harn_vm::VmValue;
26
27use crate::error::HostlibError;
28use crate::registry::{BuiltinRegistry, HostlibCapability, RegisteredBuiltin, SyncHandler};
29use crate::tools::args::{build_dict, dict_arg, require_string, str_value};
30
31mod file;
32#[cfg(any(target_os = "macos", target_os = "ios"))]
33mod keychain;
34#[cfg(target_os = "windows")]
35mod wincred;
36
37const GET_BUILTIN: &str = "hostlib_secret_store_get";
38const SET_BUILTIN: &str = "hostlib_secret_store_set";
39const DELETE_BUILTIN: &str = "hostlib_secret_store_delete";
40const LIST_BUILTIN: &str = "hostlib_secret_store_list";
41
42const BACKEND_ENV: &str = "HARN_SECRET_STORE_BACKEND";
45
46trait Backend {
48 fn name(&self) -> &'static str;
49 fn get(&self, account: &str, key: &str) -> io::Result<Option<String>>;
50 fn set(&self, account: &str, key: &str, value: &str) -> io::Result<()>;
51 fn delete(&self, account: &str, key: &str) -> io::Result<bool>;
52 fn list(&self, account: &str) -> io::Result<Vec<String>>;
53}
54
55#[derive(Default)]
59pub struct SecretStoreCapability;
60
61impl HostlibCapability for SecretStoreCapability {
62 fn module_name(&self) -> &'static str {
63 "secret_store"
64 }
65
66 fn register_builtins(&self, registry: &mut BuiltinRegistry) {
67 register(registry, GET_BUILTIN, "get", handle_get);
68 register(registry, SET_BUILTIN, "set", handle_set);
69 register(registry, DELETE_BUILTIN, "delete", handle_delete);
70 register(registry, LIST_BUILTIN, "list", handle_list);
71 }
72}
73
74fn register(
75 registry: &mut BuiltinRegistry,
76 name: &'static str,
77 method: &'static str,
78 runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
79) {
80 let handler: SyncHandler = Arc::new(runner);
81 registry.register(RegisteredBuiltin {
82 name,
83 module: "secret_store",
84 method,
85 handler,
86 });
87}
88
89fn handle_get(args: &[VmValue]) -> Result<VmValue, HostlibError> {
90 let dict = dict_arg(GET_BUILTIN, args)?;
91 let account = require_nonempty_string(GET_BUILTIN, &dict, "account")?;
92 let key = require_nonempty_string(GET_BUILTIN, &dict, "key")?;
93
94 let backend = select_backend();
95 let value = backend
96 .get(&account, &key)
97 .map_err(|err| backend_err(GET_BUILTIN, err))?;
98
99 Ok(build_dict([
100 ("account".to_string(), str_value(&account)),
101 ("key".to_string(), str_value(&key)),
102 (
103 "value".to_string(),
104 value.as_deref().map(str_value).unwrap_or(VmValue::Nil),
105 ),
106 ("backend".to_string(), str_value(backend.name())),
107 ]))
108}
109
110fn handle_set(args: &[VmValue]) -> Result<VmValue, HostlibError> {
111 let dict = dict_arg(SET_BUILTIN, args)?;
112 let account = require_nonempty_string(SET_BUILTIN, &dict, "account")?;
113 let key = require_nonempty_string(SET_BUILTIN, &dict, "key")?;
114 let value = require_string(SET_BUILTIN, &dict, "value")?;
115
116 let backend = select_backend();
117 backend
118 .set(&account, &key, &value)
119 .map_err(|err| backend_err(SET_BUILTIN, err))?;
120
121 Ok(build_dict([
122 ("account".to_string(), str_value(&account)),
123 ("key".to_string(), str_value(&key)),
124 ("backend".to_string(), str_value(backend.name())),
125 ]))
126}
127
128fn handle_delete(args: &[VmValue]) -> Result<VmValue, HostlibError> {
129 let dict = dict_arg(DELETE_BUILTIN, args)?;
130 let account = require_nonempty_string(DELETE_BUILTIN, &dict, "account")?;
131 let key = require_nonempty_string(DELETE_BUILTIN, &dict, "key")?;
132
133 let backend = select_backend();
134 let deleted = backend
135 .delete(&account, &key)
136 .map_err(|err| backend_err(DELETE_BUILTIN, err))?;
137
138 Ok(build_dict([
139 ("account".to_string(), str_value(&account)),
140 ("key".to_string(), str_value(&key)),
141 ("deleted".to_string(), VmValue::Bool(deleted)),
142 ("backend".to_string(), str_value(backend.name())),
143 ]))
144}
145
146fn handle_list(args: &[VmValue]) -> Result<VmValue, HostlibError> {
147 let dict = dict_arg(LIST_BUILTIN, args)?;
148 let account = require_nonempty_string(LIST_BUILTIN, &dict, "account")?;
149
150 let backend = select_backend();
151 let keys = backend
152 .list(&account)
153 .map_err(|err| backend_err(LIST_BUILTIN, err))?;
154
155 let items: Vec<VmValue> = keys.into_iter().map(|k| str_value(&k)).collect();
156 Ok(build_dict([
157 ("account".to_string(), str_value(&account)),
158 ("keys".to_string(), VmValue::List(Arc::new(items))),
159 ("backend".to_string(), str_value(backend.name())),
160 ]))
161}
162
163fn require_nonempty_string(
164 builtin: &'static str,
165 dict: &std::collections::BTreeMap<String, VmValue>,
166 key: &'static str,
167) -> Result<String, HostlibError> {
168 let value = require_string(builtin, dict, key)?;
169 if value.is_empty() {
170 Err(HostlibError::InvalidParameter {
171 builtin,
172 param: key,
173 message: "must be a non-empty string".to_string(),
174 })
175 } else {
176 Ok(value)
177 }
178}
179
180fn backend_err(builtin: &'static str, err: io::Error) -> HostlibError {
181 HostlibError::Backend {
182 builtin,
183 message: err.to_string(),
184 }
185}
186
187fn select_backend() -> Box<dyn Backend> {
188 if matches!(std::env::var(BACKEND_ENV).as_deref(), Ok("file")) {
189 return Box::new(file::FileStore::new());
190 }
191
192 #[cfg(any(target_os = "macos", target_os = "ios"))]
193 {
194 Box::new(keychain::KeychainStore::new())
195 }
196 #[cfg(target_os = "windows")]
197 {
198 Box::new(wincred::WinCredStore::new())
199 }
200 #[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))]
201 {
202 Box::new(file::FileStore::new())
203 }
204}