1#[derive(Clone, serde::Deserialize, serde::Serialize)]
2pub struct AllowedOnePasswordRef {
3 pub id: String,
4 pub reference: String,
5}
6
7#[derive(Clone, Copy, serde::Deserialize, serde::Serialize)]
8#[serde(rename_all = "snake_case")]
9pub enum OAuthTokenMode {
10 Cached,
11 Refresh,
12}
13
14#[cfg(unix)]
15mod imp {
16 use std::collections::HashMap;
17 use std::env;
18 use std::fs;
19 use std::io::{self, BufRead, BufReader, Write};
20 #[cfg(target_os = "macos")]
21 use std::os::unix::ffi::OsStringExt;
22 #[cfg(any(target_os = "linux", target_os = "macos"))]
23 use std::os::unix::fs::MetadataExt;
24 use std::os::unix::fs::PermissionsExt;
25 #[cfg(any(target_os = "linux", target_os = "macos"))]
26 use std::os::unix::io::AsRawFd;
27 use std::os::unix::net::{UnixListener, UnixStream};
28 use std::os::unix::process::CommandExt;
29 use std::path::{Path, PathBuf};
30 use std::process::{Command, Output, Stdio};
31 use std::thread;
32 use std::time::{Duration, Instant};
33
34 use reqwest::blocking::Client;
35 use serde::{Deserialize, Serialize};
36
37 use crate::error::ViaError;
38 use crate::redaction::Redactor;
39 use crate::secrets::SecretValue;
40
41 const CONNECT_WAIT: Duration = Duration::from_secs(5);
42 const CONNECT_POLL: Duration = Duration::from_millis(50);
43 const IDLE_TIMEOUT: Duration = Duration::from_secs(15 * 60);
44
45 pub fn resolve_onepassword_secret(
46 config_hash: &str,
47 ref_id: &str,
48 ttl_seconds: u64,
49 ) -> Result<SecretValue, ViaError> {
50 let span = crate::timing::span("1password daemon resolve");
51 let response = match request_with_autostart(DaemonRequest::Resolve {
52 config_hash: config_hash.to_owned(),
53 ref_id: ref_id.to_owned(),
54 ttl_seconds,
55 }) {
56 Ok(response) => {
57 span.finish(format!(
58 "cache={}",
59 response.cache.as_deref().unwrap_or("unknown")
60 ));
61 response
62 }
63 Err(error) => {
64 span.finish("failed");
65 return Err(error);
66 }
67 };
68
69 if response.ok {
70 return response
71 .value
72 .ok_or_else(|| ViaError::InvalidConfig("daemon returned no secret".to_owned()));
73 }
74
75 Err(ViaError::ExternalCommandFailed {
76 program: "via daemon".to_owned(),
77 status: None,
78 stderr: response
79 .error
80 .unwrap_or_else(|| "failed to resolve secret".to_owned()),
81 })
82 }
83
84 pub fn register_onepassword_refs(
85 config_hash: &str,
86 account: Option<&str>,
87 refs: Vec<super::AllowedOnePasswordRef>,
88 ) -> Result<(), ViaError> {
89 let response = request_with_autostart(DaemonRequest::Register {
90 config_hash: config_hash.to_owned(),
91 account: account.map(str::to_owned),
92 refs,
93 })?;
94 if response.ok {
95 Ok(())
96 } else {
97 Err(daemon_response_error(
98 response,
99 "failed to register 1Password references",
100 ))
101 }
102 }
103
104 pub fn oauth_access_token(
105 credential: &str,
106 mode: super::OAuthTokenMode,
107 ) -> Result<SecretValue, ViaError> {
108 let span = crate::timing::span("oauth daemon access token");
109 let response = match request_with_autostart(DaemonRequest::OAuthAccessToken {
110 credential: credential.to_owned(),
111 mode,
112 }) {
113 Ok(response) => response,
114 Err(error) => {
115 span.finish("failed");
116 return Err(error);
117 }
118 };
119 span.finish(format!(
120 "cache={}",
121 response.cache.as_deref().unwrap_or("unknown")
122 ));
123
124 oauth_access_token_from_response(response)
125 }
126
127 fn oauth_access_token_from_response(
128 response: ClientDaemonResponse,
129 ) -> Result<SecretValue, ViaError> {
130 if response.ok {
131 return response.value.ok_or_else(|| {
132 ViaError::InvalidConfig("daemon returned no OAuth access token".to_owned())
133 });
134 }
135
136 Err(daemon_response_error(
137 response,
138 "failed to resolve OAuth access token",
139 ))
140 }
141
142 pub fn serve() -> Result<(), ViaError> {
143 let path = socket_path()?;
144 let listener = bind_listener(&path)?;
145 run_server(listener, &path)
146 }
147
148 fn bind_listener(path: &Path) -> Result<UnixListener, ViaError> {
149 prepare_socket_parent(path)?;
150 remove_stale_socket(path)?;
151
152 let listener = UnixListener::bind(path)?;
153 fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
154 listener.set_nonblocking(true)?;
155 Ok(listener)
156 }
157
158 fn remove_stale_socket(path: &Path) -> Result<(), ViaError> {
159 if path.exists() {
160 if UnixStream::connect(path).is_ok() {
161 return Err(ViaError::InvalidConfig(
162 "via daemon is already running".to_owned(),
163 ));
164 }
165 fs::remove_file(path)?;
166 }
167
168 Ok(())
169 }
170
171 fn run_server(listener: UnixListener, path: &Path) -> Result<(), ViaError> {
172 let mut state = DaemonState::default();
173 let expected_client = daemon_executable_identity()?;
174 let mut last_activity = Instant::now();
175 loop {
176 match next_server_event(&listener, &mut last_activity)? {
177 ServerEvent::Connection(stream) => {
178 let action = handle_stream(stream, &mut state, expected_client.as_ref());
179 if action == DaemonAction::Stop {
180 break;
181 }
182 }
183 ServerEvent::NoConnection => {}
184 ServerEvent::IdleTimeout => break,
185 }
186 }
187
188 let _ = fs::remove_file(path);
189 Ok(())
190 }
191
192 fn next_server_event(
193 listener: &UnixListener,
194 last_activity: &mut Instant,
195 ) -> Result<ServerEvent, ViaError> {
196 match listener.accept() {
197 Ok((stream, _)) => {
198 *last_activity = Instant::now();
199 Ok(ServerEvent::Connection(stream))
200 }
201 Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
202 wait_for_connection(last_activity)
203 }
204 Err(error) => Err(error.into()),
205 }
206 }
207
208 fn wait_for_connection(last_activity: &Instant) -> Result<ServerEvent, ViaError> {
209 if last_activity.elapsed() >= IDLE_TIMEOUT {
210 Ok(ServerEvent::IdleTimeout)
211 } else {
212 thread::sleep(CONNECT_POLL);
213 Ok(ServerEvent::NoConnection)
214 }
215 }
216
217 pub fn status() -> Result<(), ViaError> {
218 control_request(DaemonRequest::Status, print_status, "status failed")
219 }
220
221 pub fn clear() -> Result<(), ViaError> {
222 control_request(
223 DaemonRequest::Clear,
224 |_| println!("via daemon: cache cleared"),
225 "clear failed",
226 )
227 }
228
229 pub fn stop() -> Result<(), ViaError> {
230 control_request(
231 DaemonRequest::Stop,
232 |_| println!("via daemon: stopped"),
233 "stop failed",
234 )
235 }
236
237 fn control_request(
238 daemon_request: DaemonRequest,
239 print_success: impl FnOnce(&ClientDaemonResponse),
240 fallback_error: &str,
241 ) -> Result<(), ViaError> {
242 match request(daemon_request) {
243 Ok(response) if response.ok => {
244 print_success(&response);
245 Ok(())
246 }
247 Ok(response) => Err(daemon_response_error(response, fallback_error)),
248 Err(error) if daemon_unavailable(&error) => {
249 println!("via daemon: stopped");
250 Ok(())
251 }
252 Err(error) => Err(error),
253 }
254 }
255
256 fn print_status(response: &ClientDaemonResponse) {
257 println!("via daemon: running");
258 println!("cached entries: {}", response.entries.unwrap_or(0));
259 }
260
261 fn daemon_response_error(response: ClientDaemonResponse, fallback: &str) -> ViaError {
262 ViaError::ExternalCommandFailed {
263 program: "via daemon".to_owned(),
264 status: None,
265 stderr: response.error.unwrap_or_else(|| fallback.to_owned()),
266 }
267 }
268
269 fn request_with_autostart(
270 daemon_request: DaemonRequest,
271 ) -> Result<ClientDaemonResponse, ViaError> {
272 match request(daemon_request.clone()) {
273 Ok(response) => Ok(response),
274 Err(error) if daemon_unavailable(&error) => {
275 start_daemon()?;
276 request(daemon_request)
277 }
278 Err(error) => Err(error),
279 }
280 }
281
282 fn request(request: DaemonRequest) -> Result<ClientDaemonResponse, ViaError> {
283 let path = socket_path()?;
284 let mut stream = UnixStream::connect(path)?;
285 let raw = SecretValue::new(serde_json::to_string(&request)?);
286 stream.write_all(raw.expose().as_bytes())?;
287 stream.write_all(b"\n")?;
288
289 let mut line = String::new();
290 BufReader::new(stream).read_line(&mut line)?;
291 if line.trim().is_empty() {
292 return Err(ViaError::InvalidConfig(
293 "daemon returned an empty response".to_owned(),
294 ));
295 }
296 let line = SecretValue::new(line);
297
298 serde_json::from_str(line.expose()).map_err(Into::into)
299 }
300
301 fn start_daemon() -> Result<(), ViaError> {
302 let exe = env::current_exe()?;
303 let path = socket_path()?;
304 let mut attempts = Vec::new();
305
306 match start_daemon_with_service_manager(&exe, &path) {
307 Ok(()) if wait_for_daemon(&path) => return Ok(()),
308 Ok(()) => attempts.push(StartAttempt {
309 name: service_manager_name(),
310 error: "started but socket did not become ready".to_owned(),
311 }),
312 Err(error) => attempts.push(error),
313 }
314
315 match start_daemon_with_direct_spawn(&exe, &path) {
316 Ok(()) if wait_for_daemon(&path) => return Ok(()),
317 Ok(()) => attempts.push(StartAttempt {
318 name: "direct spawn",
319 error: "started but socket did not become ready".to_owned(),
320 }),
321 Err(error) => attempts.push(error),
322 }
323
324 Err(ViaError::InvalidConfig(format!(
325 "timed out waiting for via daemon to start ({})",
326 format_start_attempts(&attempts)
327 )))
328 }
329
330 fn wait_for_daemon(path: &Path) -> bool {
331 let started = Instant::now();
332 while started.elapsed() < CONNECT_WAIT {
333 if UnixStream::connect(path).is_ok() {
334 return true;
335 }
336 thread::sleep(CONNECT_POLL);
337 }
338
339 false
340 }
341
342 fn start_daemon_with_direct_spawn(exe: &Path, path: &Path) -> Result<(), StartAttempt> {
343 let mut command = daemon_command(exe, path);
344 command.process_group(0);
345 command.spawn().map(|_| ()).map_err(|error| StartAttempt {
346 name: "direct spawn",
347 error: error.to_string(),
348 })
349 }
350
351 fn daemon_command(exe: &Path, path: &Path) -> Command {
352 let mut command = Command::new(exe);
353 command
354 .arg("daemon")
355 .arg("serve")
356 .stdin(Stdio::null())
357 .stdout(Stdio::null());
358 if crate::timing::enabled() {
359 command.stderr(Stdio::inherit());
360 } else {
361 command.stderr(Stdio::null());
362 }
363 for (name, value) in daemon_environment(path) {
364 command.env(name, value);
365 }
366 command
367 }
368
369 fn daemon_environment(path: &Path) -> Vec<(String, String)> {
370 let mut values = vec![
371 (
372 "VIA_DAEMON_SOCKET".to_owned(),
373 path.to_string_lossy().into_owned(),
374 ),
375 (
376 "OP_BIOMETRIC_UNLOCK_ENABLED".to_owned(),
377 env::var("OP_BIOMETRIC_UNLOCK_ENABLED").unwrap_or_else(|_| "true".to_owned()),
378 ),
379 ];
380 for name in [
381 "PATH",
382 "HOME",
383 "XDG_RUNTIME_DIR",
384 "DBUS_SESSION_BUS_ADDRESS",
385 "DISPLAY",
386 "WAYLAND_DISPLAY",
387 "XAUTHORITY",
388 ] {
389 if let Ok(value) = env::var(name) {
390 if !value.is_empty() {
391 values.push((name.to_owned(), value));
392 }
393 }
394 }
395 values
396 }
397
398 #[cfg(target_os = "linux")]
399 fn service_manager_name() -> &'static str {
400 "systemd user service"
401 }
402
403 #[cfg(target_os = "macos")]
404 fn service_manager_name() -> &'static str {
405 "launchd user agent"
406 }
407
408 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
409 fn service_manager_name() -> &'static str {
410 "user service manager"
411 }
412
413 #[cfg(target_os = "linux")]
414 fn start_daemon_with_service_manager(exe: &Path, path: &Path) -> Result<(), StartAttempt> {
415 let unit = format!("via-daemon-{}", socket_path_id(path));
416 let mut command = Command::new("systemd-run");
417 command
418 .arg("--user")
419 .arg("--collect")
420 .arg("--quiet")
421 .arg("--unit")
422 .arg(unit)
423 .arg("--description")
424 .arg("via daemon");
425 for (name, value) in daemon_environment(path) {
426 command.arg("--setenv").arg(format!("{name}={value}"));
427 }
428 command.arg(exe).arg("daemon").arg("serve");
429
430 run_start_command("systemd-run", command).map_err(|error| StartAttempt {
431 name: service_manager_name(),
432 error,
433 })
434 }
435
436 #[cfg(target_os = "macos")]
437 fn start_daemon_with_service_manager(exe: &Path, path: &Path) -> Result<(), StartAttempt> {
438 let label = format!("dev.tee8z.via.daemon.{}", socket_path_id(path));
439 let plist_path = launch_agent_path(&label).map_err(|error| StartAttempt {
440 name: service_manager_name(),
441 error,
442 })?;
443 write_launch_agent_plist(&plist_path, &label, exe, path).map_err(|error| StartAttempt {
444 name: service_manager_name(),
445 error,
446 })?;
447
448 let domain = format!("gui/{}", effective_user_id());
449 let target = format!("{domain}/{label}");
450 let mut bootstrap = Command::new("launchctl");
451 bootstrap.arg("bootstrap").arg(&domain).arg(&plist_path);
452 match run_start_command("launchctl bootstrap", bootstrap) {
453 Ok(()) => Ok(()),
454 Err(bootstrap_error) => {
455 let mut kickstart = Command::new("launchctl");
456 kickstart.arg("kickstart").arg("-k").arg(&target);
457 run_start_command("launchctl kickstart", kickstart).map_err(|kickstart_error| {
458 StartAttempt {
459 name: service_manager_name(),
460 error: format!("{bootstrap_error}; {kickstart_error}"),
461 }
462 })
463 }
464 }
465 }
466
467 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
468 fn start_daemon_with_service_manager(_exe: &Path, _path: &Path) -> Result<(), StartAttempt> {
469 Err(StartAttempt {
470 name: service_manager_name(),
471 error: "not supported on this platform".to_owned(),
472 })
473 }
474
475 #[cfg(target_os = "macos")]
476 fn launch_agent_path(label: &str) -> Result<PathBuf, String> {
477 let home = env_path("HOME").ok_or_else(|| "HOME is not set".to_owned())?;
478 let directory = home.join("Library").join("LaunchAgents");
479 fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
480 Ok(directory.join(format!("{label}.plist")))
481 }
482
483 #[cfg(target_os = "macos")]
484 fn write_launch_agent_plist(
485 path: &Path,
486 label: &str,
487 exe: &Path,
488 socket_path: &Path,
489 ) -> Result<(), String> {
490 let mut environment = String::new();
491 for (name, value) in daemon_environment(socket_path) {
492 environment.push_str(&format!(
493 "<key>{}</key><string>{}</string>\n",
494 xml_escape(&name),
495 xml_escape(&value)
496 ));
497 }
498 let plist = format!(
499 r#"<?xml version="1.0" encoding="UTF-8"?>
500<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
501<plist version="1.0">
502<dict>
503 <key>Label</key><string>{}</string>
504 <key>ProgramArguments</key>
505 <array>
506 <string>{}</string>
507 <string>daemon</string>
508 <string>serve</string>
509 </array>
510 <key>EnvironmentVariables</key>
511 <dict>
512 {}
513 </dict>
514 <key>RunAtLoad</key><true/>
515 <key>StandardOutPath</key><string>/dev/null</string>
516 <key>StandardErrorPath</key><string>/dev/null</string>
517</dict>
518</plist>
519"#,
520 xml_escape(label),
521 xml_escape(&exe.to_string_lossy()),
522 environment
523 );
524 fs::write(path, plist).map_err(|error| error.to_string())
525 }
526
527 #[cfg(target_os = "macos")]
528 fn xml_escape(value: &str) -> String {
529 value
530 .replace('&', "&")
531 .replace('<', "<")
532 .replace('>', ">")
533 .replace('"', """)
534 .replace('\'', "'")
535 }
536
537 #[cfg(target_os = "macos")]
538 fn effective_user_id() -> libc::uid_t {
539 unsafe { libc::geteuid() }
540 }
541
542 fn run_start_command(name: &'static str, mut command: Command) -> Result<(), String> {
543 let output = command.output().map_err(|error| {
544 if error.kind() == io::ErrorKind::NotFound {
545 format!("{name} not found")
546 } else {
547 format!("failed to start {name}: {error}")
548 }
549 })?;
550 if output.status.success() {
551 Ok(())
552 } else {
553 Err(format_command_failure(name, &output))
554 }
555 }
556
557 fn format_command_failure(name: &str, output: &Output) -> String {
558 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
559 if stderr.is_empty() {
560 format!("{name} exited with status {:?}", output.status.code())
561 } else {
562 format!(
563 "{name} exited with status {:?}: {stderr}",
564 output.status.code()
565 )
566 }
567 }
568
569 fn socket_path_id(path: &Path) -> String {
570 let mut hash = 0xcbf29ce484222325_u64;
571 for byte in path.to_string_lossy().as_bytes() {
572 hash ^= u64::from(*byte);
573 hash = hash.wrapping_mul(0x100000001b3);
574 }
575 format!("{hash:016x}")
576 }
577
578 struct StartAttempt {
579 name: &'static str,
580 error: String,
581 }
582
583 fn format_start_attempts(attempts: &[StartAttempt]) -> String {
584 attempts
585 .iter()
586 .map(|attempt| format!("{}: {}", attempt.name, attempt.error))
587 .collect::<Vec<_>>()
588 .join("; ")
589 }
590
591 fn handle_stream(
592 mut stream: UnixStream,
593 state: &mut DaemonState,
594 expected_client: Option<&ExecutableIdentity>,
595 ) -> DaemonAction {
596 let response = match verify_peer_executable(&stream, expected_client) {
597 Ok(()) => handle_verified_stream(&mut stream, state),
598 Err(error) => {
599 DaemonResponseInternal::error(format!("daemon client verification failed: {error}"))
600 }
601 };
602 let action = if response.stop {
603 DaemonAction::Stop
604 } else {
605 DaemonAction::Continue
606 };
607
608 write_daemon_response(&mut stream, response);
609
610 action
611 }
612
613 fn handle_verified_stream(
614 stream: &mut UnixStream,
615 state: &mut DaemonState,
616 ) -> DaemonResponseInternal {
617 let mut line = String::new();
618 let mut reader = BufReader::new(stream);
619 match reader.read_line(&mut line) {
620 Ok(_) => {
621 let line = SecretValue::new(line);
622 match serde_json::from_str(line.expose()) {
623 Ok(request) => state.handle(request),
624 Err(error) => {
625 DaemonResponseInternal::error(format!("invalid daemon request: {error}"))
626 }
627 }
628 }
629 Err(error) => {
630 DaemonResponseInternal::error(format!("failed to read daemon request: {error}"))
631 }
632 }
633 }
634
635 fn write_daemon_response(stream: &mut UnixStream, response: DaemonResponseInternal) {
636 if let Ok(raw) = serde_json::to_string(&response.into_public()) {
637 let raw = SecretValue::new(raw);
638 let _ = stream.write_all(raw.expose().as_bytes());
639 let _ = stream.write_all(b"\n");
640 }
641 }
642
643 #[derive(Clone, Deserialize, Serialize)]
644 #[serde(tag = "type", rename_all = "snake_case")]
645 enum DaemonRequest {
646 Register {
647 config_hash: String,
648 account: Option<String>,
649 refs: Vec<super::AllowedOnePasswordRef>,
650 },
651 Resolve {
652 config_hash: String,
653 ref_id: String,
654 ttl_seconds: u64,
655 },
656 OAuthAccessToken {
657 credential: String,
658 #[serde(default = "default_oauth_token_mode")]
659 mode: super::OAuthTokenMode,
660 },
661 Clear,
662 Status,
663 Stop,
664 }
665
666 fn default_oauth_token_mode() -> super::OAuthTokenMode {
667 super::OAuthTokenMode::Cached
668 }
669
670 #[derive(Default)]
671 struct DaemonState {
672 cache: HashMap<SecretCacheKey, SecretCacheEntry>,
673 oauth_cache: HashMap<String, crate::auth::oauth::CachedOAuthToken>,
674 registrations: HashMap<String, RegisteredConfig>,
675 }
676
677 impl DaemonState {
678 fn handle(&mut self, request: DaemonRequest) -> DaemonResponseInternal {
679 self.prune_expired();
680
681 match request {
682 DaemonRequest::Register {
683 config_hash,
684 account,
685 refs,
686 } => self.register(config_hash, account, refs),
687 DaemonRequest::Resolve {
688 config_hash,
689 ref_id,
690 ttl_seconds,
691 } => self.resolve(config_hash, ref_id, ttl_seconds),
692 DaemonRequest::OAuthAccessToken { credential, mode } => {
693 self.oauth_access_token(&credential, mode)
694 }
695 DaemonRequest::Clear => {
696 self.cache.clear();
697 self.oauth_cache.clear();
698 self.registrations.clear();
699 DaemonResponseInternal::ok()
700 }
701 DaemonRequest::Status => {
702 let mut response = DaemonResponseInternal::ok();
703 response.entries = Some(self.cache.len() + self.oauth_cache.len());
704 response
705 }
706 DaemonRequest::Stop => {
707 let mut response = DaemonResponseInternal::ok();
708 response.stop = true;
709 response
710 }
711 }
712 }
713
714 fn register(
715 &mut self,
716 config_hash: String,
717 account: Option<String>,
718 refs: Vec<super::AllowedOnePasswordRef>,
719 ) -> DaemonResponseInternal {
720 if config_hash.trim().is_empty() {
721 return DaemonResponseInternal::error("config hash must not be empty");
722 }
723
724 let refs = match normalize_allowed_refs(refs) {
725 Ok(refs) => refs,
726 Err(error) => return DaemonResponseInternal::error(error),
727 };
728 self.registrations
729 .insert(config_hash, RegisteredConfig { account, refs });
730 DaemonResponseInternal::ok()
731 }
732
733 fn resolve(
734 &mut self,
735 config_hash: String,
736 ref_id: String,
737 ttl_seconds: u64,
738 ) -> DaemonResponseInternal {
739 let Some(secret) = self.allowed_secret(&config_hash, &ref_id) else {
740 return DaemonResponseInternal::error(
741 "secret reference is not registered for this config",
742 );
743 };
744 let key = SecretCacheKey {
745 config_hash,
746 ref_id,
747 };
748 if let Some(entry) = self.cache.get(&key) {
749 let mut response = DaemonResponseInternal::ok();
750 response.value = Some(entry.value.clone());
751 response.cache = Some("hit".to_owned());
752 return response;
753 }
754
755 match op_read(secret.account.as_deref(), &secret.reference) {
756 Ok(value) => {
757 let ttl = Duration::from_secs(ttl_seconds.max(1));
758 let response_value = value.clone();
759 self.cache.insert(
760 key,
761 SecretCacheEntry {
762 value,
763 expires_at: Instant::now() + ttl,
764 },
765 );
766 let mut response = DaemonResponseInternal::ok();
767 response.value = Some(response_value);
768 response.cache = Some("miss".to_owned());
769 response
770 }
771 Err(error) => DaemonResponseInternal::error(error),
772 }
773 }
774
775 fn allowed_secret(&self, config_hash: &str, ref_id: &str) -> Option<AllowedSecret> {
776 let registration = self.registrations.get(config_hash)?;
777 let reference = registration.refs.get(ref_id)?;
778 Some(AllowedSecret {
779 account: registration.account.clone(),
780 reference: reference.clone(),
781 })
782 }
783
784 fn oauth_access_token(
785 &mut self,
786 credential: &str,
787 mode: super::OAuthTokenMode,
788 ) -> DaemonResponseInternal {
789 let bundle = match crate::auth::oauth::CredentialBundle::parse(credential) {
790 Ok(bundle) => bundle,
791 Err(error) => return DaemonResponseInternal::error(error.to_string()),
792 };
793 let key = crate::auth::oauth::cache_key(&bundle);
794 let now = match crate::auth::oauth::unix_timestamp() {
795 Ok(now) => now,
796 Err(error) => return DaemonResponseInternal::error(error.to_string()),
797 };
798
799 if matches!(mode, super::OAuthTokenMode::Cached) {
800 if let Some(access_token) =
801 crate::auth::oauth::cached_access_token(self.oauth_cache.get(&key), now)
802 {
803 let mut response = DaemonResponseInternal::ok();
804 response.value = Some(SecretValue::new(access_token));
805 response.cache = Some("hit".to_owned());
806 return response;
807 }
808 }
809
810 let cached = self.oauth_cache.get(&key).cloned();
811 let mut redactor = Redactor::new();
812 redactor.add(credential);
813 crate::auth::oauth::register_bundle_secrets(&bundle, &mut redactor);
814 crate::auth::oauth::register_cached_secrets(cached.as_ref(), &mut redactor);
815
816 let client = Client::new();
817 match crate::auth::oauth::exchange_access_token(
818 &client,
819 &bundle,
820 cached.as_ref(),
821 &mut redactor,
822 ) {
823 Ok(token) => {
824 self.oauth_cache.insert(
825 key,
826 crate::auth::oauth::CachedOAuthToken {
827 access_token: token.access_token.clone(),
828 expires_at: token.expires_at,
829 refresh_token: token.refresh_token.clone(),
830 },
831 );
832 let mut response = DaemonResponseInternal::ok();
833 response.value = Some(SecretValue::new(token.access_token));
834 response.cache = Some("miss".to_owned());
835 response
836 }
837 Err(error) => DaemonResponseInternal::error(redactor.redact(&error.to_string())),
838 }
839 }
840
841 fn prune_expired(&mut self) {
842 let now = Instant::now();
843 self.cache.retain(|_, entry| entry.expires_at > now);
844 if let Ok(now) = crate::auth::oauth::unix_timestamp() {
845 self.oauth_cache.retain(|_, entry| {
846 entry.refresh_token.is_some()
847 || crate::auth::oauth::cached_access_token(Some(entry), now).is_some()
848 });
849 }
850 }
851 }
852
853 #[derive(Hash, Eq, PartialEq)]
854 struct SecretCacheKey {
855 config_hash: String,
856 ref_id: String,
857 }
858
859 struct RegisteredConfig {
860 account: Option<String>,
861 refs: HashMap<String, String>,
862 }
863
864 struct AllowedSecret {
865 account: Option<String>,
866 reference: String,
867 }
868
869 struct SecretCacheEntry {
870 value: SecretValue,
871 expires_at: Instant,
872 }
873
874 fn normalize_allowed_refs(
875 refs: Vec<super::AllowedOnePasswordRef>,
876 ) -> Result<HashMap<String, String>, String> {
877 let mut normalized = HashMap::new();
878 for allowed_ref in refs {
879 if allowed_ref.id.trim().is_empty() {
880 return Err("registered secret reference id must not be empty".to_owned());
881 }
882 if !allowed_ref.reference.starts_with("op://") {
883 return Err("registered secret reference must start with op://".to_owned());
884 }
885 normalized.insert(allowed_ref.id, allowed_ref.reference);
886 }
887 Ok(normalized)
888 }
889
890 #[derive(Serialize)]
891 struct WireDaemonResponse {
892 ok: bool,
893 #[serde(
894 skip_serializing_if = "Option::is_none",
895 serialize_with = "serialize_secret_value_option"
896 )]
897 value: Option<SecretValue>,
898 #[serde(skip_serializing_if = "Option::is_none")]
899 cache: Option<String>,
900 #[serde(skip_serializing_if = "Option::is_none")]
901 entries: Option<usize>,
902 #[serde(skip_serializing_if = "Option::is_none")]
903 error: Option<String>,
904 }
905
906 #[derive(Deserialize)]
907 struct ClientDaemonResponse {
908 ok: bool,
909 value: Option<SecretValue>,
910 cache: Option<String>,
911 entries: Option<usize>,
912 error: Option<String>,
913 }
914
915 struct DaemonResponseInternal {
916 ok: bool,
917 value: Option<SecretValue>,
918 cache: Option<String>,
919 entries: Option<usize>,
920 error: Option<String>,
921 stop: bool,
922 }
923
924 impl DaemonResponseInternal {
925 fn ok() -> Self {
926 Self {
927 ok: true,
928 value: None,
929 cache: None,
930 entries: None,
931 error: None,
932 stop: false,
933 }
934 }
935
936 fn error(error: impl Into<String>) -> Self {
937 Self {
938 ok: false,
939 value: None,
940 cache: None,
941 entries: None,
942 error: Some(error.into()),
943 stop: false,
944 }
945 }
946
947 fn into_public(self) -> WireDaemonResponse {
948 WireDaemonResponse {
949 ok: self.ok,
950 value: self.value,
951 cache: self.cache,
952 entries: self.entries,
953 error: self.error,
954 }
955 }
956 }
957
958 fn serialize_secret_value_option<S>(
959 value: &Option<SecretValue>,
960 serializer: S,
961 ) -> Result<S::Ok, S::Error>
962 where
963 S: serde::Serializer,
964 {
965 match value {
966 Some(value) => serializer.serialize_some(value.expose()),
967 None => serializer.serialize_none(),
968 }
969 }
970
971 fn op_read(account: Option<&str>, reference: &str) -> Result<SecretValue, String> {
972 let mut command = Command::new("op");
973 command.arg("read").arg(reference);
974 if let Some(account) = account {
975 command.arg("--account").arg(account);
976 }
977
978 let output = command
979 .output()
980 .map_err(|source| format!("program `op` was not found: {source}"))?;
981
982 if !output.status.success() {
983 return Err(format!(
984 "program `op` failed with status {:?}: {}",
985 output.status.code(),
986 String::from_utf8_lossy(&output.stderr).trim()
987 ));
988 }
989
990 Ok(SecretValue::from_utf8_lossy_trimmed(output.stdout))
991 }
992
993 fn socket_path() -> Result<PathBuf, ViaError> {
994 if let Some(path) = env_path("VIA_DAEMON_SOCKET") {
995 return Ok(path);
996 }
997
998 if let Some(runtime) = env_path("XDG_RUNTIME_DIR") {
999 return Ok(runtime.join("via").join("daemon.sock"));
1000 }
1001
1002 Ok(env::temp_dir()
1003 .join(format!("via-{}", user_id()))
1004 .join("daemon.sock"))
1005 }
1006
1007 fn prepare_socket_parent(path: &Path) -> Result<(), ViaError> {
1008 let parent = path.parent().ok_or_else(|| {
1009 ViaError::InvalidConfig("daemon socket path has no parent".to_owned())
1010 })?;
1011 fs::create_dir_all(parent)?;
1012 fs::set_permissions(parent, fs::Permissions::from_mode(0o700))?;
1013 Ok(())
1014 }
1015
1016 fn env_path(name: &str) -> Option<PathBuf> {
1017 env::var_os(name)
1018 .filter(|value| !value.as_os_str().is_empty())
1019 .map(PathBuf::from)
1020 }
1021
1022 fn user_id() -> String {
1023 env::var("UID")
1024 .ok()
1025 .filter(|value| !value.trim().is_empty())
1026 .unwrap_or_else(|| {
1027 env::var("USER")
1028 .ok()
1029 .map(|value| sanitize_path_part(&value))
1030 .filter(|value| !value.is_empty())
1031 .unwrap_or_else(|| "unknown".to_owned())
1032 })
1033 }
1034
1035 fn sanitize_path_part(value: &str) -> String {
1036 value
1037 .chars()
1038 .filter(|character| character.is_ascii_alphanumeric() || *character == '_')
1039 .collect()
1040 }
1041
1042 fn daemon_unavailable(error: &ViaError) -> bool {
1043 matches!(error, ViaError::Io(source) if matches!(
1044 source.kind(),
1045 io::ErrorKind::NotFound
1046 | io::ErrorKind::ConnectionRefused
1047 | io::ErrorKind::ConnectionReset
1048 | io::ErrorKind::BrokenPipe
1049 ))
1050 }
1051
1052 #[derive(Clone)]
1053 struct ExecutableIdentity {
1054 path: PathBuf,
1055 device: u64,
1056 inode: u64,
1057 }
1058
1059 impl ExecutableIdentity {
1060 fn matches(&self, other: &Self) -> bool {
1061 self.path == other.path || (self.device == other.device && self.inode == other.inode)
1062 }
1063 }
1064
1065 #[cfg(any(target_os = "linux", target_os = "macos"))]
1066 fn daemon_executable_identity() -> Result<Option<ExecutableIdentity>, ViaError> {
1067 Ok(Some(executable_identity_from_path(&env::current_exe()?)?))
1068 }
1069
1070 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1071 fn daemon_executable_identity() -> Result<Option<ExecutableIdentity>, ViaError> {
1072 Ok(None)
1073 }
1074
1075 #[cfg(any(target_os = "linux", target_os = "macos"))]
1076 fn verify_peer_executable(
1077 stream: &UnixStream,
1078 expected: Option<&ExecutableIdentity>,
1079 ) -> Result<(), ViaError> {
1080 let Some(expected) = expected else {
1081 return Ok(());
1082 };
1083 let peer = peer_executable_identity(stream)?;
1084 if expected.matches(&peer) {
1085 Ok(())
1086 } else {
1087 Err(ViaError::InvalidConfig(
1088 "daemon refused connection from executable other than via".to_owned(),
1089 ))
1090 }
1091 }
1092
1093 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1094 fn verify_peer_executable(
1095 _stream: &UnixStream,
1096 _expected: Option<&ExecutableIdentity>,
1097 ) -> Result<(), ViaError> {
1098 Ok(())
1099 }
1100
1101 #[cfg(any(target_os = "linux", target_os = "macos"))]
1102 fn executable_identity_from_path(path: &Path) -> Result<ExecutableIdentity, ViaError> {
1103 let metadata = fs::metadata(path)?;
1104 Ok(executable_identity_from_parts(path.to_path_buf(), metadata))
1105 }
1106
1107 #[cfg(any(target_os = "linux", target_os = "macos"))]
1108 fn executable_identity_from_parts(path: PathBuf, metadata: fs::Metadata) -> ExecutableIdentity {
1109 let path = fs::canonicalize(&path).unwrap_or(path);
1110 ExecutableIdentity {
1111 path,
1112 device: metadata.dev(),
1113 inode: metadata.ino(),
1114 }
1115 }
1116
1117 #[cfg(target_os = "linux")]
1118 fn peer_executable_identity(stream: &UnixStream) -> Result<ExecutableIdentity, ViaError> {
1119 let pid = linux_peer_pid(stream)?;
1120 let proc_exe = PathBuf::from(format!("/proc/{pid}/exe"));
1121 let metadata = fs::metadata(&proc_exe)?;
1122 let path = fs::read_link(&proc_exe).unwrap_or_else(|_| proc_exe.clone());
1123 Ok(executable_identity_from_parts(path, metadata))
1124 }
1125
1126 #[cfg(target_os = "linux")]
1127 fn linux_peer_pid(stream: &UnixStream) -> Result<libc::pid_t, ViaError> {
1128 let mut credentials = std::mem::MaybeUninit::<libc::ucred>::uninit();
1129 let mut length = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
1130 let result = unsafe {
1133 libc::getsockopt(
1134 stream.as_raw_fd(),
1135 libc::SOL_SOCKET,
1136 libc::SO_PEERCRED,
1137 credentials.as_mut_ptr().cast(),
1138 &mut length,
1139 )
1140 };
1141 if result != 0 {
1142 return Err(io::Error::last_os_error().into());
1143 }
1144 if length as usize != std::mem::size_of::<libc::ucred>() {
1145 return Err(ViaError::InvalidConfig(
1146 "daemon could not read peer process credentials".to_owned(),
1147 ));
1148 }
1149
1150 Ok(unsafe { credentials.assume_init() }.pid)
1152 }
1153
1154 #[cfg(target_os = "macos")]
1155 fn peer_executable_identity(stream: &UnixStream) -> Result<ExecutableIdentity, ViaError> {
1156 let pid = macos_peer_pid(stream)?;
1157 let mut buffer = vec![0_u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
1158 let length =
1160 unsafe { libc::proc_pidpath(pid, buffer.as_mut_ptr().cast(), buffer.len() as u32) };
1161 if length <= 0 {
1162 return Err(io::Error::last_os_error().into());
1163 }
1164 buffer.truncate(length as usize);
1165 let path = PathBuf::from(std::ffi::OsString::from_vec(buffer));
1166 executable_identity_from_path(&path)
1167 }
1168
1169 #[cfg(target_os = "macos")]
1170 fn macos_peer_pid(stream: &UnixStream) -> Result<libc::pid_t, ViaError> {
1171 let mut pid = std::mem::MaybeUninit::<libc::pid_t>::uninit();
1172 let mut length = std::mem::size_of::<libc::pid_t>() as libc::socklen_t;
1173 let result = unsafe {
1176 libc::getsockopt(
1177 stream.as_raw_fd(),
1178 libc::SOL_LOCAL,
1179 libc::LOCAL_PEERPID,
1180 pid.as_mut_ptr().cast(),
1181 &mut length,
1182 )
1183 };
1184 if result != 0 {
1185 return Err(io::Error::last_os_error().into());
1186 }
1187 if length as usize != std::mem::size_of::<libc::pid_t>() {
1188 return Err(ViaError::InvalidConfig(
1189 "daemon could not read peer process id".to_owned(),
1190 ));
1191 }
1192
1193 Ok(unsafe { pid.assume_init() })
1195 }
1196
1197 #[derive(PartialEq, Eq)]
1198 enum DaemonAction {
1199 Continue,
1200 Stop,
1201 }
1202
1203 enum ServerEvent {
1204 Connection(UnixStream),
1205 NoConnection,
1206 IdleTimeout,
1207 }
1208
1209 #[cfg(test)]
1210 mod tests {
1211 use super::*;
1212 use std::io::{Read, Write};
1213 use std::net::TcpListener;
1214 use std::thread;
1215
1216 #[test]
1217 fn rejects_unregistered_resolve_request() {
1218 let mut state = DaemonState::default();
1219
1220 let response = state.handle(DaemonRequest::Resolve {
1221 config_hash: "config".to_owned(),
1222 ref_id: "secret".to_owned(),
1223 ttl_seconds: 300,
1224 });
1225
1226 assert!(!response.ok);
1227 assert!(response
1228 .error
1229 .as_deref()
1230 .unwrap()
1231 .contains("not registered"));
1232 }
1233
1234 #[test]
1235 fn rejects_registered_non_op_reference() {
1236 let mut state = DaemonState::default();
1237
1238 let response = state.handle(DaemonRequest::Register {
1239 config_hash: "config".to_owned(),
1240 account: None,
1241 refs: vec![super::super::AllowedOnePasswordRef {
1242 id: "secret".to_owned(),
1243 reference: "plaintext".to_owned(),
1244 }],
1245 });
1246
1247 assert!(!response.ok);
1248 assert!(response
1249 .error
1250 .as_deref()
1251 .unwrap()
1252 .contains("must start with op://"));
1253 }
1254
1255 #[test]
1256 fn resolves_registered_ref_id_from_cache() {
1257 let mut state = DaemonState::default();
1258 let register = state.handle(DaemonRequest::Register {
1259 config_hash: "config".to_owned(),
1260 account: None,
1261 refs: vec![super::super::AllowedOnePasswordRef {
1262 id: "secret".to_owned(),
1263 reference: "op://Private/Example/token".to_owned(),
1264 }],
1265 });
1266 assert!(register.ok);
1267 state.cache.insert(
1268 SecretCacheKey {
1269 config_hash: "config".to_owned(),
1270 ref_id: "secret".to_owned(),
1271 },
1272 SecretCacheEntry {
1273 value: SecretValue::new("cached-secret".to_owned()),
1274 expires_at: Instant::now() + Duration::from_secs(300),
1275 },
1276 );
1277
1278 let response = state.handle(DaemonRequest::Resolve {
1279 config_hash: "config".to_owned(),
1280 ref_id: "secret".to_owned(),
1281 ttl_seconds: 300,
1282 });
1283
1284 assert!(response.ok);
1285 assert_eq!(response.cache.as_deref(), Some("hit"));
1286 assert_eq!(
1287 response.value.as_ref().map(SecretValue::expose),
1288 Some("cached-secret")
1289 );
1290 }
1291
1292 #[test]
1293 fn clear_drops_cached_values_and_registered_refs() {
1294 let mut state = DaemonState::default();
1295 let register = state.handle(DaemonRequest::Register {
1296 config_hash: "config".to_owned(),
1297 account: None,
1298 refs: vec![super::super::AllowedOnePasswordRef {
1299 id: "secret".to_owned(),
1300 reference: "op://Private/Example/token".to_owned(),
1301 }],
1302 });
1303 assert!(register.ok);
1304 state.cache.insert(
1305 SecretCacheKey {
1306 config_hash: "config".to_owned(),
1307 ref_id: "secret".to_owned(),
1308 },
1309 SecretCacheEntry {
1310 value: SecretValue::new("cached-secret".to_owned()),
1311 expires_at: Instant::now() + Duration::from_secs(300),
1312 },
1313 );
1314
1315 let clear = state.handle(DaemonRequest::Clear);
1316 assert!(clear.ok);
1317 let response = state.handle(DaemonRequest::Resolve {
1318 config_hash: "config".to_owned(),
1319 ref_id: "secret".to_owned(),
1320 ttl_seconds: 300,
1321 });
1322
1323 assert!(!response.ok);
1324 assert!(state.cache.is_empty());
1325 assert!(state.oauth_cache.is_empty());
1326 assert!(state.registrations.is_empty());
1327 }
1328
1329 #[test]
1330 fn oauth_access_token_is_cached_in_daemon_memory() {
1331 let response_body = serde_json::json!({
1332 "access_token": "fresh-access-token",
1333 "token_type": "Bearer",
1334 "expires_in": 3600,
1335 "refresh_token": "rotated-refresh-token",
1336 })
1337 .to_string();
1338 let (token_url, server) = token_server(response_body);
1339 let mut state = DaemonState::default();
1340 let credential = serde_json::json!({
1341 "type": "service_oauth",
1342 "token_url": token_url,
1343 "grant_type": "refresh_token",
1344 "client_id": "client-id",
1345 "client_secret": "client-secret",
1346 "refresh_token": "configured-refresh-token",
1347 })
1348 .to_string();
1349
1350 let response = state.handle(DaemonRequest::OAuthAccessToken {
1351 credential: credential.clone(),
1352 mode: crate::daemon::OAuthTokenMode::Cached,
1353 });
1354 let request = server.join().unwrap();
1355 let cached_response = state.handle(DaemonRequest::OAuthAccessToken {
1356 credential,
1357 mode: crate::daemon::OAuthTokenMode::Cached,
1358 });
1359
1360 assert!(response.ok);
1361 assert_eq!(response.cache.as_deref(), Some("miss"));
1362 assert_eq!(
1363 response.value.as_ref().map(SecretValue::expose),
1364 Some("fresh-access-token")
1365 );
1366 assert!(request.contains("grant_type=refresh_token"));
1367 assert!(request.contains("refresh_token=configured-refresh-token"));
1368 assert_eq!(state.oauth_cache.len(), 1);
1369 assert!(cached_response.ok);
1370 assert_eq!(cached_response.cache.as_deref(), Some("hit"));
1371 assert_eq!(
1372 cached_response.value.as_ref().map(SecretValue::expose),
1373 Some("fresh-access-token")
1374 );
1375 }
1376
1377 #[test]
1378 fn oauth_access_token_refresh_mode_skips_unexpired_cache() {
1379 let response_body = serde_json::json!({
1380 "access_token": "fresh-access-token",
1381 "token_type": "Bearer",
1382 "expires_in": 3600,
1383 })
1384 .to_string();
1385 let (token_url, server) = token_server(response_body);
1386 let mut state = DaemonState::default();
1387 let credential = serde_json::json!({
1388 "type": "service_oauth",
1389 "token_url": token_url,
1390 "grant_type": "client_credentials",
1391 "client_id": "client-id",
1392 "client_secret": "client-secret",
1393 "scope": "read,issues:create",
1394 })
1395 .to_string();
1396 let bundle = crate::auth::oauth::CredentialBundle::parse(&credential).unwrap();
1397 state.oauth_cache.insert(
1398 crate::auth::oauth::cache_key(&bundle),
1399 crate::auth::oauth::CachedOAuthToken {
1400 access_token: "cached-access-token".to_owned(),
1401 expires_at: crate::auth::oauth::unix_timestamp().unwrap() + 3_600,
1402 refresh_token: None,
1403 },
1404 );
1405
1406 let response = state.handle(DaemonRequest::OAuthAccessToken {
1407 credential,
1408 mode: crate::daemon::OAuthTokenMode::Refresh,
1409 });
1410 let request = server.join().unwrap();
1411
1412 assert!(response.ok);
1413 assert_eq!(response.cache.as_deref(), Some("miss"));
1414 assert_eq!(
1415 response.value.as_ref().map(SecretValue::expose),
1416 Some("fresh-access-token")
1417 );
1418 assert!(request.contains("grant_type=client_credentials"));
1419 }
1420
1421 #[test]
1422 fn prune_expired_keeps_rotated_oauth_refresh_tokens() {
1423 let mut state = DaemonState::default();
1424 state.oauth_cache.insert(
1425 "oauth".to_owned(),
1426 crate::auth::oauth::CachedOAuthToken {
1427 access_token: "expired-access-token".to_owned(),
1428 expires_at: 0,
1429 refresh_token: Some("rotated-refresh-token".to_owned()),
1430 },
1431 );
1432
1433 state.prune_expired();
1434
1435 assert_eq!(state.oauth_cache.len(), 1);
1436 assert_eq!(
1437 state
1438 .oauth_cache
1439 .values()
1440 .next()
1441 .and_then(|entry| entry.refresh_token.as_deref()),
1442 Some("rotated-refresh-token")
1443 );
1444 }
1445
1446 fn token_server(response_body: String) -> (String, thread::JoinHandle<String>) {
1447 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1448 let address = listener.local_addr().unwrap();
1449 let handle = thread::spawn(move || {
1450 let (mut stream, _) = listener.accept().unwrap();
1451 let mut buffer = [0_u8; 8192];
1452 let read = stream.read(&mut buffer).unwrap();
1453 let request = String::from_utf8_lossy(&buffer[..read]).to_string();
1454 let response = format!(
1455 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1456 response_body.len(),
1457 response_body
1458 );
1459 stream.write_all(response.as_bytes()).unwrap();
1460 request
1461 });
1462
1463 (format!("http://{address}/oauth/token"), handle)
1464 }
1465 }
1466}
1467
1468#[cfg(not(unix))]
1469mod imp {
1470 use crate::error::ViaError;
1471 use crate::secrets::SecretValue;
1472
1473 pub fn resolve_onepassword_secret(
1474 _config_hash: &str,
1475 _ref_id: &str,
1476 _ttl_seconds: u64,
1477 ) -> Result<SecretValue, ViaError> {
1478 Err(ViaError::InvalidConfig(
1479 "via daemon cache is only supported on Unix-like platforms".to_owned(),
1480 ))
1481 }
1482
1483 pub fn register_onepassword_refs(
1484 _config_hash: &str,
1485 _account: Option<&str>,
1486 _refs: Vec<super::AllowedOnePasswordRef>,
1487 ) -> Result<(), ViaError> {
1488 Err(ViaError::InvalidConfig(
1489 "via daemon cache is only supported on Unix-like platforms".to_owned(),
1490 ))
1491 }
1492
1493 pub fn oauth_access_token(
1494 _credential: &str,
1495 _mode: super::OAuthTokenMode,
1496 ) -> Result<SecretValue, ViaError> {
1497 Err(ViaError::InvalidConfig(
1498 "OAuth auth requires the via daemon, which is only supported on Unix-like platforms"
1499 .to_owned(),
1500 ))
1501 }
1502
1503 pub fn serve() -> Result<(), ViaError> {
1504 Err(ViaError::InvalidConfig(
1505 "via daemon cache is only supported on Unix-like platforms".to_owned(),
1506 ))
1507 }
1508
1509 pub fn status() -> Result<(), ViaError> {
1510 println!("via daemon: unsupported");
1511 Ok(())
1512 }
1513
1514 pub fn clear() -> Result<(), ViaError> {
1515 println!("via daemon: unsupported");
1516 Ok(())
1517 }
1518
1519 pub fn stop() -> Result<(), ViaError> {
1520 println!("via daemon: unsupported");
1521 Ok(())
1522 }
1523}
1524
1525pub use imp::{
1526 clear, oauth_access_token, register_onepassword_refs, resolve_onepassword_secret, serve,
1527 status, stop,
1528};