secrets manager
git clone https://git.lucas.co/cce-secrets.git
src/bin/cce-keyring-sync/daemon.rs (3K)
1 //! `daemon` — the resident parent that keeps the `op` authorization alive.
2 //!
3 //! Phase 0 (KEYRING-SYNC.md) measured the rule this loop lives by: the
4 //! app's authorization is keyed to the calling process's parent and lapses
5 //! after ~10 idle minutes, but use extends it indefinitely. So this process
6 //! stays up for the session, ticks every [`TICK`], and every tick is one
7 //! `op item list` under its own pid. One Authorize dialog per login, then
8 //! none, as long as nothing (suspend, the app locking) opens a gap.
9 //!
10 //! A dialog nobody answers costs a 60-second hang and comes back as
11 //! `authorization prompt dismissed`; re-offering one every five minutes to an
12 //! empty chair is the annoyance the timer design was rejected for, so after a
13 //! dismissal the tick backs off (15 → 30 → 60 minutes) until something asks:
14 //! `SIGUSR1`, which cce-secrets sends from its Sync button and after a save.
15
16 use std::time::Duration;
17
18 use tokio::signal::unix::{signal, SignalKind};
19
20 use crate::op::{is_dismissed, OnePassword};
21 use crate::sync::sync_remote;
22 use crate::{now_unix, State};
23
24 /// Inside the ~10-minute idle window with margin.
25 pub const TICK: Duration = Duration::from_secs(5 * 60);
26 const BACKOFF: [Duration; 3] = [Duration::from_secs(15 * 60), Duration::from_secs(30 * 60), Duration::from_secs(60 * 60)];
27
28 pub async fn daemon(state_path: &std::path::Path) {
29 let mut usr1 = signal(SignalKind::user_defined1()).expect("SIGUSR1 handler");
30 let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler");
31 let mut dismissed = 0usize;
32 println!("cce-keyring-sync daemon: tick every {}s, SIGUSR1 syncs now", TICK.as_secs());
33
34 loop {
35 // Re-read every tick: adopt or a manual sync may have moved the base.
36 let mut state: State = std::fs::read_to_string(state_path)
37 .ok()
38 .and_then(|s| serde_json::from_str(&s).ok())
39 .unwrap_or_default();
40 let wait = if state.backend != "onepassword" {
41 eprintln!("{}: base is not 1Password's; idling until `adopt` runs", now_unix());
42 TICK
43 } else {
44 let mut remote = OnePassword::new(&state.vault);
45 match sync_remote(&mut remote, state_path, &mut state, false).await {
46 Ok(_) => {
47 dismissed = 0;
48 TICK
49 }
50 Err(e) if is_dismissed(&e) => {
51 let w = BACKOFF[dismissed.min(BACKOFF.len() - 1)];
52 dismissed += 1;
53 eprintln!("authorization dialog unanswered; next try in {}m (or SIGUSR1)", w.as_secs() / 60);
54 w
55 }
56 Err(e) => {
57 eprintln!("sync: {e}");
58 TICK
59 }
60 }
61 };
62 tokio::select! {
63 _ = tokio::time::sleep(wait) => {}
64 _ = usr1.recv() => { dismissed = 0; }
65 _ = term.recv() => { println!("cce-keyring-sync daemon: stopping"); return; }
66 }
67 }
68 }