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