1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! Thin client that routes CLI commands through a running daemon when one is
//! up, falling back to the file session otherwise. The functions return an
//! `Option`: `Some(..)` means the daemon handled it, `None` means "no daemon —
//! fall back to the file-session path." On non-Unix everything returns `None`.
/// Result of asking the daemon for a secret.
pub enum GetOutcome {
/// The daemon returned the value.
Value(String),
/// The daemon is up but the vault isn't unlocked — caller should fall back.
NotUnlocked,
/// The daemon is up and the vault unlocked, but the secret doesn't exist.
NotFound,
}
#[cfg(unix)]
mod imp {
use super::GetOutcome;
use crate::daemon::{self, Request, Response};
use anyhow::{anyhow, Result};
use std::path::PathBuf;
fn base() -> PathBuf {
daemon::base_dir()
}
/// True when a daemon is running for this project.
pub fn available() -> bool {
daemon::is_running(&base())
}
/// Cache a vault's key in the daemon. `None` = no daemon (fall back).
///
/// The key is derived (and the passphrase validated) **client-side** by
/// opening the vault here; only the 32-byte derived key crosses the socket,
/// never the passphrase (finding #3). A wrong passphrase fails locally and
/// never reaches the daemon.
pub fn unlock(vault: &str, passphrase: &str) -> Option<Result<()>> {
if !available() {
return None;
}
let dir = base().join(vault);
let key_hex = match crate::vault::Vault::open(&dir, passphrase) {
Ok(v) => hex::encode(v.key().bytes()),
Err(e) => return Some(Err(e)),
};
let req = Request::Unlock {
vault: vault.to_string(),
key: key_hex,
};
Some(match daemon::send(&base(), &req) {
Ok(Response::Unlocked) => Ok(()),
Ok(Response::Error { message }) => Err(anyhow!(message)),
Ok(other) => Err(anyhow!("unexpected daemon response: {other:?}")),
Err(e) => Err(e),
})
}
/// Drop one vault's key. `None` = no daemon.
pub fn lock(vault: &str) -> Option<usize> {
if !available() {
return None;
}
match daemon::send(
&base(),
&Request::Lock {
vault: vault.to_string(),
},
) {
Ok(Response::Locked { count }) => Some(count),
_ => Some(0),
}
}
/// Drop every cached key. `None` = no daemon.
pub fn lock_all() -> Option<usize> {
if !available() {
return None;
}
match daemon::send(&base(), &Request::LockAll) {
Ok(Response::Locked { count }) => Some(count),
_ => Some(0),
}
}
/// Read a secret via the daemon. `None` = no daemon, or a protocol error
/// the caller should treat as "fall back".
pub fn get(vault: &str, secret: &str) -> Option<GetOutcome> {
if !available() {
return None;
}
let req = Request::Get {
vault: vault.to_string(),
secret: secret.to_string(),
};
match daemon::send(&base(), &req) {
Ok(Response::Secret { value }) => Some(GetOutcome::Value(value)),
Ok(Response::NotUnlocked) => Some(GetOutcome::NotUnlocked),
Ok(Response::NotFound) => Some(GetOutcome::NotFound),
_ => None,
}
}
/// Names of vaults currently unlocked in the daemon (empty if none / down).
pub fn unlocked_vaults() -> Vec<String> {
if !available() {
return Vec::new();
}
match daemon::send(&base(), &Request::Status) {
Ok(Response::Status { vaults }) => vaults.into_iter().map(|v| v.name).collect(),
_ => Vec::new(),
}
}
}
#[cfg(not(unix))]
mod imp {
use super::GetOutcome;
use anyhow::Result;
pub fn unlock(_vault: &str, _passphrase: &str) -> Option<Result<()>> {
None
}
pub fn lock(_vault: &str) -> Option<usize> {
None
}
pub fn lock_all() -> Option<usize> {
None
}
pub fn get(_vault: &str, _secret: &str) -> Option<GetOutcome> {
None
}
pub fn unlocked_vaults() -> Vec<String> {
Vec::new()
}
}
pub use imp::{get, lock, lock_all, unlock, unlocked_vaults};