1#[derive(Clone, serde::Deserialize, serde::Serialize)]
2pub struct AllowedOnePasswordRef {
3 pub id: String,
4 pub reference: String,
5}
6
7#[cfg(unix)]
8mod imp {
9 use std::collections::HashMap;
10 use std::env;
11 use std::fs;
12 use std::io::{self, BufRead, BufReader, Write};
13 #[cfg(target_os = "macos")]
14 use std::os::unix::ffi::OsStringExt;
15 #[cfg(any(target_os = "linux", target_os = "macos"))]
16 use std::os::unix::fs::MetadataExt;
17 use std::os::unix::fs::PermissionsExt;
18 #[cfg(any(target_os = "linux", target_os = "macos"))]
19 use std::os::unix::io::AsRawFd;
20 use std::os::unix::net::{UnixListener, UnixStream};
21 use std::path::{Path, PathBuf};
22 use std::process::{Command, Stdio};
23 use std::thread;
24 use std::time::{Duration, Instant};
25
26 use serde::{Deserialize, Serialize};
27
28 use crate::error::ViaError;
29 use crate::secrets::SecretValue;
30
31 const CONNECT_WAIT: Duration = Duration::from_secs(2);
32 const CONNECT_POLL: Duration = Duration::from_millis(50);
33 const IDLE_TIMEOUT: Duration = Duration::from_secs(15 * 60);
34
35 pub fn resolve_onepassword_secret(
36 config_hash: &str,
37 ref_id: &str,
38 ttl_seconds: u64,
39 ) -> Result<SecretValue, ViaError> {
40 let span = crate::timing::span("1password daemon resolve");
41 let response = match request_with_autostart(DaemonRequest::Resolve {
42 config_hash: config_hash.to_owned(),
43 ref_id: ref_id.to_owned(),
44 ttl_seconds,
45 }) {
46 Ok(response) => {
47 span.finish(format!(
48 "cache={}",
49 response.cache.as_deref().unwrap_or("unknown")
50 ));
51 response
52 }
53 Err(error) => {
54 span.finish("failed");
55 return Err(error);
56 }
57 };
58
59 if response.ok {
60 return response
61 .value
62 .ok_or_else(|| ViaError::InvalidConfig("daemon returned no secret".to_owned()));
63 }
64
65 Err(ViaError::ExternalCommandFailed {
66 program: "via daemon".to_owned(),
67 status: None,
68 stderr: response
69 .error
70 .unwrap_or_else(|| "failed to resolve secret".to_owned()),
71 })
72 }
73
74 pub fn register_onepassword_refs(
75 config_hash: &str,
76 account: Option<&str>,
77 refs: Vec<super::AllowedOnePasswordRef>,
78 ) -> Result<(), ViaError> {
79 let response = request_with_autostart(DaemonRequest::Register {
80 config_hash: config_hash.to_owned(),
81 account: account.map(str::to_owned),
82 refs,
83 })?;
84 if response.ok {
85 Ok(())
86 } else {
87 Err(daemon_response_error(
88 response,
89 "failed to register 1Password references",
90 ))
91 }
92 }
93
94 pub fn serve() -> Result<(), ViaError> {
95 let path = socket_path()?;
96 let listener = bind_listener(&path)?;
97 run_server(listener, &path)
98 }
99
100 fn bind_listener(path: &Path) -> Result<UnixListener, ViaError> {
101 prepare_socket_parent(path)?;
102 remove_stale_socket(path)?;
103
104 let listener = UnixListener::bind(path)?;
105 fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
106 listener.set_nonblocking(true)?;
107 Ok(listener)
108 }
109
110 fn remove_stale_socket(path: &Path) -> Result<(), ViaError> {
111 if path.exists() {
112 if UnixStream::connect(path).is_ok() {
113 return Err(ViaError::InvalidConfig(
114 "via daemon is already running".to_owned(),
115 ));
116 }
117 fs::remove_file(path)?;
118 }
119
120 Ok(())
121 }
122
123 fn run_server(listener: UnixListener, path: &Path) -> Result<(), ViaError> {
124 let mut state = DaemonState::default();
125 let expected_client = daemon_executable_identity()?;
126 let mut last_activity = Instant::now();
127 loop {
128 match next_server_event(&listener, &mut last_activity)? {
129 ServerEvent::Connection(stream) => {
130 let action = handle_stream(stream, &mut state, expected_client.as_ref());
131 if action == DaemonAction::Stop {
132 break;
133 }
134 }
135 ServerEvent::NoConnection => {}
136 ServerEvent::IdleTimeout => break,
137 }
138 }
139
140 let _ = fs::remove_file(path);
141 Ok(())
142 }
143
144 fn next_server_event(
145 listener: &UnixListener,
146 last_activity: &mut Instant,
147 ) -> Result<ServerEvent, ViaError> {
148 match listener.accept() {
149 Ok((stream, _)) => {
150 *last_activity = Instant::now();
151 Ok(ServerEvent::Connection(stream))
152 }
153 Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
154 wait_for_connection(last_activity)
155 }
156 Err(error) => Err(error.into()),
157 }
158 }
159
160 fn wait_for_connection(last_activity: &Instant) -> Result<ServerEvent, ViaError> {
161 if last_activity.elapsed() >= IDLE_TIMEOUT {
162 Ok(ServerEvent::IdleTimeout)
163 } else {
164 thread::sleep(CONNECT_POLL);
165 Ok(ServerEvent::NoConnection)
166 }
167 }
168
169 pub fn status() -> Result<(), ViaError> {
170 control_request(DaemonRequest::Status, print_status, "status failed")
171 }
172
173 pub fn clear() -> Result<(), ViaError> {
174 control_request(
175 DaemonRequest::Clear,
176 |_| println!("via daemon: cache cleared"),
177 "clear failed",
178 )
179 }
180
181 pub fn stop() -> Result<(), ViaError> {
182 control_request(
183 DaemonRequest::Stop,
184 |_| println!("via daemon: stopped"),
185 "stop failed",
186 )
187 }
188
189 fn control_request(
190 daemon_request: DaemonRequest,
191 print_success: impl FnOnce(&ClientDaemonResponse),
192 fallback_error: &str,
193 ) -> Result<(), ViaError> {
194 match request(daemon_request) {
195 Ok(response) if response.ok => {
196 print_success(&response);
197 Ok(())
198 }
199 Ok(response) => Err(daemon_response_error(response, fallback_error)),
200 Err(error) if daemon_unavailable(&error) => {
201 println!("via daemon: stopped");
202 Ok(())
203 }
204 Err(error) => Err(error),
205 }
206 }
207
208 fn print_status(response: &ClientDaemonResponse) {
209 println!("via daemon: running");
210 println!("cached secrets: {}", response.entries.unwrap_or(0));
211 }
212
213 fn daemon_response_error(response: ClientDaemonResponse, fallback: &str) -> ViaError {
214 ViaError::ExternalCommandFailed {
215 program: "via daemon".to_owned(),
216 status: None,
217 stderr: response.error.unwrap_or_else(|| fallback.to_owned()),
218 }
219 }
220
221 fn request_with_autostart(
222 daemon_request: DaemonRequest,
223 ) -> Result<ClientDaemonResponse, ViaError> {
224 match request(daemon_request.clone()) {
225 Ok(response) => Ok(response),
226 Err(error) if daemon_unavailable(&error) => {
227 start_daemon()?;
228 request(daemon_request)
229 }
230 Err(error) => Err(error),
231 }
232 }
233
234 fn request(request: DaemonRequest) -> Result<ClientDaemonResponse, ViaError> {
235 let path = socket_path()?;
236 let mut stream = UnixStream::connect(path)?;
237 let raw = SecretValue::new(serde_json::to_string(&request)?);
238 stream.write_all(raw.expose().as_bytes())?;
239 stream.write_all(b"\n")?;
240
241 let mut line = String::new();
242 BufReader::new(stream).read_line(&mut line)?;
243 if line.trim().is_empty() {
244 return Err(ViaError::InvalidConfig(
245 "daemon returned an empty response".to_owned(),
246 ));
247 }
248 let line = SecretValue::new(line);
249
250 serde_json::from_str(line.expose()).map_err(Into::into)
251 }
252
253 fn start_daemon() -> Result<(), ViaError> {
254 let exe = env::current_exe()?;
255 let mut command = Command::new(exe);
256 command
257 .arg("daemon")
258 .arg("serve")
259 .stdin(Stdio::null())
260 .stdout(Stdio::null());
261 if crate::timing::enabled() {
262 command.stderr(Stdio::inherit());
263 } else {
264 command.stderr(Stdio::null());
265 }
266 command.spawn()?;
267
268 let started = Instant::now();
269 while started.elapsed() < CONNECT_WAIT {
270 if UnixStream::connect(socket_path()?).is_ok() {
271 return Ok(());
272 }
273 thread::sleep(CONNECT_POLL);
274 }
275
276 Err(ViaError::InvalidConfig(
277 "timed out waiting for via daemon to start".to_owned(),
278 ))
279 }
280
281 fn handle_stream(
282 mut stream: UnixStream,
283 state: &mut DaemonState,
284 expected_client: Option<&ExecutableIdentity>,
285 ) -> DaemonAction {
286 let response = match verify_peer_executable(&stream, expected_client) {
287 Ok(()) => handle_verified_stream(&mut stream, state),
288 Err(error) => {
289 DaemonResponseInternal::error(format!("daemon client verification failed: {error}"))
290 }
291 };
292 let action = if response.stop {
293 DaemonAction::Stop
294 } else {
295 DaemonAction::Continue
296 };
297
298 write_daemon_response(&mut stream, response);
299
300 action
301 }
302
303 fn handle_verified_stream(
304 stream: &mut UnixStream,
305 state: &mut DaemonState,
306 ) -> DaemonResponseInternal {
307 let mut line = String::new();
308 let mut reader = BufReader::new(stream);
309 match reader.read_line(&mut line) {
310 Ok(_) => {
311 let line = SecretValue::new(line);
312 match serde_json::from_str(line.expose()) {
313 Ok(request) => state.handle(request),
314 Err(error) => {
315 DaemonResponseInternal::error(format!("invalid daemon request: {error}"))
316 }
317 }
318 }
319 Err(error) => {
320 DaemonResponseInternal::error(format!("failed to read daemon request: {error}"))
321 }
322 }
323 }
324
325 fn write_daemon_response(stream: &mut UnixStream, response: DaemonResponseInternal) {
326 if let Ok(raw) = serde_json::to_string(&response.into_public()) {
327 let raw = SecretValue::new(raw);
328 let _ = stream.write_all(raw.expose().as_bytes());
329 let _ = stream.write_all(b"\n");
330 }
331 }
332
333 #[derive(Clone, Deserialize, Serialize)]
334 #[serde(tag = "type", rename_all = "snake_case")]
335 enum DaemonRequest {
336 Register {
337 config_hash: String,
338 account: Option<String>,
339 refs: Vec<super::AllowedOnePasswordRef>,
340 },
341 Resolve {
342 config_hash: String,
343 ref_id: String,
344 ttl_seconds: u64,
345 },
346 Clear,
347 Status,
348 Stop,
349 }
350
351 #[derive(Default)]
352 struct DaemonState {
353 cache: HashMap<SecretCacheKey, SecretCacheEntry>,
354 registrations: HashMap<String, RegisteredConfig>,
355 }
356
357 impl DaemonState {
358 fn handle(&mut self, request: DaemonRequest) -> DaemonResponseInternal {
359 self.prune_expired();
360
361 match request {
362 DaemonRequest::Register {
363 config_hash,
364 account,
365 refs,
366 } => self.register(config_hash, account, refs),
367 DaemonRequest::Resolve {
368 config_hash,
369 ref_id,
370 ttl_seconds,
371 } => self.resolve(config_hash, ref_id, ttl_seconds),
372 DaemonRequest::Clear => {
373 self.cache.clear();
374 self.registrations.clear();
375 DaemonResponseInternal::ok()
376 }
377 DaemonRequest::Status => {
378 let mut response = DaemonResponseInternal::ok();
379 response.entries = Some(self.cache.len());
380 response
381 }
382 DaemonRequest::Stop => {
383 let mut response = DaemonResponseInternal::ok();
384 response.stop = true;
385 response
386 }
387 }
388 }
389
390 fn register(
391 &mut self,
392 config_hash: String,
393 account: Option<String>,
394 refs: Vec<super::AllowedOnePasswordRef>,
395 ) -> DaemonResponseInternal {
396 if config_hash.trim().is_empty() {
397 return DaemonResponseInternal::error("config hash must not be empty");
398 }
399
400 let refs = match normalize_allowed_refs(refs) {
401 Ok(refs) => refs,
402 Err(error) => return DaemonResponseInternal::error(error),
403 };
404 self.registrations
405 .insert(config_hash, RegisteredConfig { account, refs });
406 DaemonResponseInternal::ok()
407 }
408
409 fn resolve(
410 &mut self,
411 config_hash: String,
412 ref_id: String,
413 ttl_seconds: u64,
414 ) -> DaemonResponseInternal {
415 let Some(secret) = self.allowed_secret(&config_hash, &ref_id) else {
416 return DaemonResponseInternal::error(
417 "secret reference is not registered for this config",
418 );
419 };
420 let key = SecretCacheKey {
421 config_hash,
422 ref_id,
423 };
424 if let Some(entry) = self.cache.get(&key) {
425 let mut response = DaemonResponseInternal::ok();
426 response.value = Some(entry.value.clone());
427 response.cache = Some("hit".to_owned());
428 return response;
429 }
430
431 match op_read(secret.account.as_deref(), &secret.reference) {
432 Ok(value) => {
433 let ttl = Duration::from_secs(ttl_seconds.max(1));
434 let response_value = value.clone();
435 self.cache.insert(
436 key,
437 SecretCacheEntry {
438 value,
439 expires_at: Instant::now() + ttl,
440 },
441 );
442 let mut response = DaemonResponseInternal::ok();
443 response.value = Some(response_value);
444 response.cache = Some("miss".to_owned());
445 response
446 }
447 Err(error) => DaemonResponseInternal::error(error),
448 }
449 }
450
451 fn allowed_secret(&self, config_hash: &str, ref_id: &str) -> Option<AllowedSecret> {
452 let registration = self.registrations.get(config_hash)?;
453 let reference = registration.refs.get(ref_id)?;
454 Some(AllowedSecret {
455 account: registration.account.clone(),
456 reference: reference.clone(),
457 })
458 }
459
460 fn prune_expired(&mut self) {
461 let now = Instant::now();
462 self.cache.retain(|_, entry| entry.expires_at > now);
463 }
464 }
465
466 #[derive(Hash, Eq, PartialEq)]
467 struct SecretCacheKey {
468 config_hash: String,
469 ref_id: String,
470 }
471
472 struct RegisteredConfig {
473 account: Option<String>,
474 refs: HashMap<String, String>,
475 }
476
477 struct AllowedSecret {
478 account: Option<String>,
479 reference: String,
480 }
481
482 struct SecretCacheEntry {
483 value: SecretValue,
484 expires_at: Instant,
485 }
486
487 fn normalize_allowed_refs(
488 refs: Vec<super::AllowedOnePasswordRef>,
489 ) -> Result<HashMap<String, String>, String> {
490 let mut normalized = HashMap::new();
491 for allowed_ref in refs {
492 if allowed_ref.id.trim().is_empty() {
493 return Err("registered secret reference id must not be empty".to_owned());
494 }
495 if !allowed_ref.reference.starts_with("op://") {
496 return Err("registered secret reference must start with op://".to_owned());
497 }
498 normalized.insert(allowed_ref.id, allowed_ref.reference);
499 }
500 Ok(normalized)
501 }
502
503 #[derive(Serialize)]
504 struct WireDaemonResponse {
505 ok: bool,
506 #[serde(
507 skip_serializing_if = "Option::is_none",
508 serialize_with = "serialize_secret_value_option"
509 )]
510 value: Option<SecretValue>,
511 #[serde(skip_serializing_if = "Option::is_none")]
512 cache: Option<String>,
513 #[serde(skip_serializing_if = "Option::is_none")]
514 entries: Option<usize>,
515 #[serde(skip_serializing_if = "Option::is_none")]
516 error: Option<String>,
517 }
518
519 #[derive(Deserialize)]
520 struct ClientDaemonResponse {
521 ok: bool,
522 value: Option<SecretValue>,
523 cache: Option<String>,
524 entries: Option<usize>,
525 error: Option<String>,
526 }
527
528 struct DaemonResponseInternal {
529 ok: bool,
530 value: Option<SecretValue>,
531 cache: Option<String>,
532 entries: Option<usize>,
533 error: Option<String>,
534 stop: bool,
535 }
536
537 impl DaemonResponseInternal {
538 fn ok() -> Self {
539 Self {
540 ok: true,
541 value: None,
542 cache: None,
543 entries: None,
544 error: None,
545 stop: false,
546 }
547 }
548
549 fn error(error: impl Into<String>) -> Self {
550 Self {
551 ok: false,
552 value: None,
553 cache: None,
554 entries: None,
555 error: Some(error.into()),
556 stop: false,
557 }
558 }
559
560 fn into_public(self) -> WireDaemonResponse {
561 WireDaemonResponse {
562 ok: self.ok,
563 value: self.value,
564 cache: self.cache,
565 entries: self.entries,
566 error: self.error,
567 }
568 }
569 }
570
571 fn serialize_secret_value_option<S>(
572 value: &Option<SecretValue>,
573 serializer: S,
574 ) -> Result<S::Ok, S::Error>
575 where
576 S: serde::Serializer,
577 {
578 match value {
579 Some(value) => serializer.serialize_some(value.expose()),
580 None => serializer.serialize_none(),
581 }
582 }
583
584 fn op_read(account: Option<&str>, reference: &str) -> Result<SecretValue, String> {
585 let mut command = Command::new("op");
586 command.arg("read").arg(reference);
587 if let Some(account) = account {
588 command.arg("--account").arg(account);
589 }
590
591 let output = command
592 .output()
593 .map_err(|source| format!("program `op` was not found: {source}"))?;
594
595 if !output.status.success() {
596 return Err(format!(
597 "program `op` failed with status {:?}: {}",
598 output.status.code(),
599 String::from_utf8_lossy(&output.stderr).trim()
600 ));
601 }
602
603 Ok(SecretValue::from_utf8_lossy_trimmed(output.stdout))
604 }
605
606 fn socket_path() -> Result<PathBuf, ViaError> {
607 if let Some(path) = env_path("VIA_DAEMON_SOCKET") {
608 return Ok(path);
609 }
610
611 if let Some(runtime) = env_path("XDG_RUNTIME_DIR") {
612 return Ok(runtime.join("via").join("daemon.sock"));
613 }
614
615 Ok(env::temp_dir()
616 .join(format!("via-{}", user_id()))
617 .join("daemon.sock"))
618 }
619
620 fn prepare_socket_parent(path: &Path) -> Result<(), ViaError> {
621 let parent = path.parent().ok_or_else(|| {
622 ViaError::InvalidConfig("daemon socket path has no parent".to_owned())
623 })?;
624 fs::create_dir_all(parent)?;
625 fs::set_permissions(parent, fs::Permissions::from_mode(0o700))?;
626 Ok(())
627 }
628
629 fn env_path(name: &str) -> Option<PathBuf> {
630 env::var_os(name)
631 .filter(|value| !value.as_os_str().is_empty())
632 .map(PathBuf::from)
633 }
634
635 fn user_id() -> String {
636 env::var("UID")
637 .ok()
638 .filter(|value| !value.trim().is_empty())
639 .unwrap_or_else(|| {
640 env::var("USER")
641 .ok()
642 .map(|value| sanitize_path_part(&value))
643 .filter(|value| !value.is_empty())
644 .unwrap_or_else(|| "unknown".to_owned())
645 })
646 }
647
648 fn sanitize_path_part(value: &str) -> String {
649 value
650 .chars()
651 .filter(|character| character.is_ascii_alphanumeric() || *character == '_')
652 .collect()
653 }
654
655 fn daemon_unavailable(error: &ViaError) -> bool {
656 matches!(error, ViaError::Io(source) if matches!(
657 source.kind(),
658 io::ErrorKind::NotFound
659 | io::ErrorKind::ConnectionRefused
660 | io::ErrorKind::ConnectionReset
661 | io::ErrorKind::BrokenPipe
662 ))
663 }
664
665 #[derive(Clone)]
666 struct ExecutableIdentity {
667 path: PathBuf,
668 device: u64,
669 inode: u64,
670 }
671
672 impl ExecutableIdentity {
673 fn matches(&self, other: &Self) -> bool {
674 self.path == other.path || (self.device == other.device && self.inode == other.inode)
675 }
676 }
677
678 #[cfg(any(target_os = "linux", target_os = "macos"))]
679 fn daemon_executable_identity() -> Result<Option<ExecutableIdentity>, ViaError> {
680 Ok(Some(executable_identity_from_path(&env::current_exe()?)?))
681 }
682
683 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
684 fn daemon_executable_identity() -> Result<Option<ExecutableIdentity>, ViaError> {
685 Ok(None)
686 }
687
688 #[cfg(any(target_os = "linux", target_os = "macos"))]
689 fn verify_peer_executable(
690 stream: &UnixStream,
691 expected: Option<&ExecutableIdentity>,
692 ) -> Result<(), ViaError> {
693 let Some(expected) = expected else {
694 return Ok(());
695 };
696 let peer = peer_executable_identity(stream)?;
697 if expected.matches(&peer) {
698 Ok(())
699 } else {
700 Err(ViaError::InvalidConfig(
701 "daemon refused connection from executable other than via".to_owned(),
702 ))
703 }
704 }
705
706 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
707 fn verify_peer_executable(
708 _stream: &UnixStream,
709 _expected: Option<&ExecutableIdentity>,
710 ) -> Result<(), ViaError> {
711 Ok(())
712 }
713
714 #[cfg(any(target_os = "linux", target_os = "macos"))]
715 fn executable_identity_from_path(path: &Path) -> Result<ExecutableIdentity, ViaError> {
716 let metadata = fs::metadata(path)?;
717 Ok(executable_identity_from_parts(path.to_path_buf(), metadata))
718 }
719
720 #[cfg(any(target_os = "linux", target_os = "macos"))]
721 fn executable_identity_from_parts(path: PathBuf, metadata: fs::Metadata) -> ExecutableIdentity {
722 let path = fs::canonicalize(&path).unwrap_or(path);
723 ExecutableIdentity {
724 path,
725 device: metadata.dev(),
726 inode: metadata.ino(),
727 }
728 }
729
730 #[cfg(target_os = "linux")]
731 fn peer_executable_identity(stream: &UnixStream) -> Result<ExecutableIdentity, ViaError> {
732 let pid = linux_peer_pid(stream)?;
733 let proc_exe = PathBuf::from(format!("/proc/{pid}/exe"));
734 let metadata = fs::metadata(&proc_exe)?;
735 let path = fs::read_link(&proc_exe).unwrap_or_else(|_| proc_exe.clone());
736 Ok(executable_identity_from_parts(path, metadata))
737 }
738
739 #[cfg(target_os = "linux")]
740 fn linux_peer_pid(stream: &UnixStream) -> Result<libc::pid_t, ViaError> {
741 let mut credentials = std::mem::MaybeUninit::<libc::ucred>::uninit();
742 let mut length = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
743 let result = unsafe {
746 libc::getsockopt(
747 stream.as_raw_fd(),
748 libc::SOL_SOCKET,
749 libc::SO_PEERCRED,
750 credentials.as_mut_ptr().cast(),
751 &mut length,
752 )
753 };
754 if result != 0 {
755 return Err(io::Error::last_os_error().into());
756 }
757 if length as usize != std::mem::size_of::<libc::ucred>() {
758 return Err(ViaError::InvalidConfig(
759 "daemon could not read peer process credentials".to_owned(),
760 ));
761 }
762
763 Ok(unsafe { credentials.assume_init() }.pid)
765 }
766
767 #[cfg(target_os = "macos")]
768 fn peer_executable_identity(stream: &UnixStream) -> Result<ExecutableIdentity, ViaError> {
769 let pid = macos_peer_pid(stream)?;
770 let mut buffer = vec![0_u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
771 let length =
773 unsafe { libc::proc_pidpath(pid, buffer.as_mut_ptr().cast(), buffer.len() as u32) };
774 if length <= 0 {
775 return Err(io::Error::last_os_error().into());
776 }
777 buffer.truncate(length as usize);
778 let path = PathBuf::from(std::ffi::OsString::from_vec(buffer));
779 executable_identity_from_path(&path)
780 }
781
782 #[cfg(target_os = "macos")]
783 fn macos_peer_pid(stream: &UnixStream) -> Result<libc::pid_t, ViaError> {
784 let mut pid = std::mem::MaybeUninit::<libc::pid_t>::uninit();
785 let mut length = std::mem::size_of::<libc::pid_t>() as libc::socklen_t;
786 let result = unsafe {
789 libc::getsockopt(
790 stream.as_raw_fd(),
791 libc::SOL_LOCAL,
792 libc::LOCAL_PEERPID,
793 pid.as_mut_ptr().cast(),
794 &mut length,
795 )
796 };
797 if result != 0 {
798 return Err(io::Error::last_os_error().into());
799 }
800 if length as usize != std::mem::size_of::<libc::pid_t>() {
801 return Err(ViaError::InvalidConfig(
802 "daemon could not read peer process id".to_owned(),
803 ));
804 }
805
806 Ok(unsafe { pid.assume_init() })
808 }
809
810 #[derive(PartialEq, Eq)]
811 enum DaemonAction {
812 Continue,
813 Stop,
814 }
815
816 enum ServerEvent {
817 Connection(UnixStream),
818 NoConnection,
819 IdleTimeout,
820 }
821
822 #[cfg(test)]
823 mod tests {
824 use super::*;
825
826 #[test]
827 fn rejects_unregistered_resolve_request() {
828 let mut state = DaemonState::default();
829
830 let response = state.handle(DaemonRequest::Resolve {
831 config_hash: "config".to_owned(),
832 ref_id: "secret".to_owned(),
833 ttl_seconds: 300,
834 });
835
836 assert!(!response.ok);
837 assert!(response
838 .error
839 .as_deref()
840 .unwrap()
841 .contains("not registered"));
842 }
843
844 #[test]
845 fn rejects_registered_non_op_reference() {
846 let mut state = DaemonState::default();
847
848 let response = state.handle(DaemonRequest::Register {
849 config_hash: "config".to_owned(),
850 account: None,
851 refs: vec![super::super::AllowedOnePasswordRef {
852 id: "secret".to_owned(),
853 reference: "plaintext".to_owned(),
854 }],
855 });
856
857 assert!(!response.ok);
858 assert!(response
859 .error
860 .as_deref()
861 .unwrap()
862 .contains("must start with op://"));
863 }
864
865 #[test]
866 fn resolves_registered_ref_id_from_cache() {
867 let mut state = DaemonState::default();
868 let register = state.handle(DaemonRequest::Register {
869 config_hash: "config".to_owned(),
870 account: None,
871 refs: vec![super::super::AllowedOnePasswordRef {
872 id: "secret".to_owned(),
873 reference: "op://Private/Example/token".to_owned(),
874 }],
875 });
876 assert!(register.ok);
877 state.cache.insert(
878 SecretCacheKey {
879 config_hash: "config".to_owned(),
880 ref_id: "secret".to_owned(),
881 },
882 SecretCacheEntry {
883 value: SecretValue::new("cached-secret".to_owned()),
884 expires_at: Instant::now() + Duration::from_secs(300),
885 },
886 );
887
888 let response = state.handle(DaemonRequest::Resolve {
889 config_hash: "config".to_owned(),
890 ref_id: "secret".to_owned(),
891 ttl_seconds: 300,
892 });
893
894 assert!(response.ok);
895 assert_eq!(response.cache.as_deref(), Some("hit"));
896 assert_eq!(
897 response.value.as_ref().map(SecretValue::expose),
898 Some("cached-secret")
899 );
900 }
901
902 #[test]
903 fn clear_drops_cached_values_and_registered_refs() {
904 let mut state = DaemonState::default();
905 let register = state.handle(DaemonRequest::Register {
906 config_hash: "config".to_owned(),
907 account: None,
908 refs: vec![super::super::AllowedOnePasswordRef {
909 id: "secret".to_owned(),
910 reference: "op://Private/Example/token".to_owned(),
911 }],
912 });
913 assert!(register.ok);
914 state.cache.insert(
915 SecretCacheKey {
916 config_hash: "config".to_owned(),
917 ref_id: "secret".to_owned(),
918 },
919 SecretCacheEntry {
920 value: SecretValue::new("cached-secret".to_owned()),
921 expires_at: Instant::now() + Duration::from_secs(300),
922 },
923 );
924
925 let clear = state.handle(DaemonRequest::Clear);
926 assert!(clear.ok);
927 let response = state.handle(DaemonRequest::Resolve {
928 config_hash: "config".to_owned(),
929 ref_id: "secret".to_owned(),
930 ttl_seconds: 300,
931 });
932
933 assert!(!response.ok);
934 assert!(state.cache.is_empty());
935 assert!(state.registrations.is_empty());
936 }
937 }
938}
939
940#[cfg(not(unix))]
941mod imp {
942 use crate::error::ViaError;
943 use crate::secrets::SecretValue;
944
945 pub fn resolve_onepassword_secret(
946 _config_hash: &str,
947 _ref_id: &str,
948 _ttl_seconds: u64,
949 ) -> Result<SecretValue, ViaError> {
950 Err(ViaError::InvalidConfig(
951 "via daemon cache is only supported on Unix-like platforms".to_owned(),
952 ))
953 }
954
955 pub fn register_onepassword_refs(
956 _config_hash: &str,
957 _account: Option<&str>,
958 _refs: Vec<super::AllowedOnePasswordRef>,
959 ) -> Result<(), ViaError> {
960 Err(ViaError::InvalidConfig(
961 "via daemon cache is only supported on Unix-like platforms".to_owned(),
962 ))
963 }
964
965 pub fn serve() -> Result<(), ViaError> {
966 Err(ViaError::InvalidConfig(
967 "via daemon cache is only supported on Unix-like platforms".to_owned(),
968 ))
969 }
970
971 pub fn status() -> Result<(), ViaError> {
972 println!("via daemon: unsupported");
973 Ok(())
974 }
975
976 pub fn clear() -> Result<(), ViaError> {
977 println!("via daemon: unsupported");
978 Ok(())
979 }
980
981 pub fn stop() -> Result<(), ViaError> {
982 println!("via daemon: unsupported");
983 Ok(())
984 }
985}
986
987pub use imp::{clear, register_onepassword_refs, resolve_onepassword_secret, serve, status, stop};