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};
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 registry.register_fn("secret_store", GET_BUILTIN, "get", handle_get);
68 registry.register_fn("secret_store", SET_BUILTIN, "set", handle_set);
69 registry.register_fn("secret_store", DELETE_BUILTIN, "delete", handle_delete);
70 registry.register_fn("secret_store", LIST_BUILTIN, "list", handle_list);
71 }
72}
73
74fn handle_get(args: &[VmValue]) -> Result<VmValue, HostlibError> {
75 let dict = dict_arg(GET_BUILTIN, args)?;
76 let account = require_nonempty_string(GET_BUILTIN, &dict, "account")?;
77 let key = require_nonempty_string(GET_BUILTIN, &dict, "key")?;
78
79 let backend = select_backend();
80 let value = backend
81 .get(&account, &key)
82 .map_err(|err| backend_err(GET_BUILTIN, err))?;
83
84 Ok(build_dict([
85 ("account".to_string(), str_value(&account)),
86 ("key".to_string(), str_value(&key)),
87 (
88 "value".to_string(),
89 value.as_deref().map(str_value).unwrap_or(VmValue::Nil),
90 ),
91 ("backend".to_string(), str_value(backend.name())),
92 ]))
93}
94
95fn handle_set(args: &[VmValue]) -> Result<VmValue, HostlibError> {
96 let dict = dict_arg(SET_BUILTIN, args)?;
97 let account = require_nonempty_string(SET_BUILTIN, &dict, "account")?;
98 let key = require_nonempty_string(SET_BUILTIN, &dict, "key")?;
99 let value = require_string(SET_BUILTIN, &dict, "value")?;
100
101 let backend = select_backend();
102 backend
103 .set(&account, &key, &value)
104 .map_err(|err| backend_err(SET_BUILTIN, err))?;
105
106 Ok(build_dict([
107 ("account".to_string(), str_value(&account)),
108 ("key".to_string(), str_value(&key)),
109 ("backend".to_string(), str_value(backend.name())),
110 ]))
111}
112
113fn handle_delete(args: &[VmValue]) -> Result<VmValue, HostlibError> {
114 let dict = dict_arg(DELETE_BUILTIN, args)?;
115 let account = require_nonempty_string(DELETE_BUILTIN, &dict, "account")?;
116 let key = require_nonempty_string(DELETE_BUILTIN, &dict, "key")?;
117
118 let backend = select_backend();
119 let deleted = backend
120 .delete(&account, &key)
121 .map_err(|err| backend_err(DELETE_BUILTIN, err))?;
122
123 Ok(build_dict([
124 ("account".to_string(), str_value(&account)),
125 ("key".to_string(), str_value(&key)),
126 ("deleted".to_string(), VmValue::Bool(deleted)),
127 ("backend".to_string(), str_value(backend.name())),
128 ]))
129}
130
131fn handle_list(args: &[VmValue]) -> Result<VmValue, HostlibError> {
132 let dict = dict_arg(LIST_BUILTIN, args)?;
133 let account = require_nonempty_string(LIST_BUILTIN, &dict, "account")?;
134
135 let backend = select_backend();
136 let keys = backend
137 .list(&account)
138 .map_err(|err| backend_err(LIST_BUILTIN, err))?;
139
140 let items: Vec<VmValue> = keys.into_iter().map(|k| str_value(&k)).collect();
141 Ok(build_dict([
142 ("account".to_string(), str_value(&account)),
143 ("keys".to_string(), VmValue::List(Arc::new(items))),
144 ("backend".to_string(), str_value(backend.name())),
145 ]))
146}
147
148fn require_nonempty_string(
149 builtin: &'static str,
150 dict: &harn_vm::value::DictMap,
151 key: &'static str,
152) -> Result<String, HostlibError> {
153 let value = require_string(builtin, dict, key)?;
154 if value.is_empty() {
155 Err(HostlibError::InvalidParameter {
156 builtin,
157 param: key,
158 message: "must be a non-empty string".to_string(),
159 })
160 } else {
161 Ok(value)
162 }
163}
164
165fn backend_err(builtin: &'static str, err: io::Error) -> HostlibError {
166 HostlibError::Backend {
167 builtin,
168 message: err.to_string(),
169 }
170}
171
172fn select_backend() -> Box<dyn Backend> {
173 if matches!(std::env::var(BACKEND_ENV).as_deref(), Ok("file")) {
174 return Box::new(file::FileStore::new());
175 }
176
177 #[cfg(any(target_os = "macos", target_os = "ios"))]
178 {
179 Box::new(keychain::KeychainStore::new())
180 }
181 #[cfg(target_os = "windows")]
182 {
183 Box::new(wincred::WinCredStore::new())
184 }
185 #[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))]
186 {
187 Box::new(file::FileStore::new())
188 }
189}