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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
use sha2::Digest as _;
/// How long a Touch ID authorization remains valid for a given session
/// before the agent prompts again. Bumped on every access, so the window
/// is idle time, not a hard upper bound on total command duration.
const TOUCHID_SESSION_TTL: std::time::Duration =
std::time::Duration::from_secs(60);
pub struct State {
pub priv_key: Option<bwx::locked::Keys>,
pub org_keys:
Option<std::collections::HashMap<String, bwx::locked::Keys>>,
pub timeout: crate::timeout::Timeout,
pub timeout_duration: std::time::Duration,
pub sync_timeout: crate::timeout::Timeout,
pub sync_timeout_duration: std::time::Duration,
pub notifications_handler: crate::notifications::Handler,
pub master_password_reprompt: std::collections::HashSet<[u8; 32]>,
pub master_password_reprompt_initialized: bool,
/// Session tokens that have cleared a Touch ID prompt, mapped to the
/// last time activity was seen on that session. Bumped on every
/// authorized access so a long-running command doesn't time out
/// mid-execution. Cleared on `Lock`.
pub touchid_sessions:
std::collections::HashMap<String, std::time::Instant>,
// stored here for the ssh agent, because requests made to the ssh
// agent don't include an environment, so the pinentry process can't
// be properly initialized. workaround: reuse the last environment
// seen by the main agent (there should be at least one in most
// cases, since starting the bwx agent is what brings up the ssh
// agent socket, and that initial request comes with an environment).
//
// should not be used for requests on the main agent, those should
// all send their own environment over.
pub last_environment: bwx::protocol::Environment,
#[cfg(feature = "clipboard")]
pub clipboard: Option<arboard::Clipboard>,
}
impl State {
pub fn key(&self, org_id: Option<&str>) -> Option<&bwx::locked::Keys> {
org_id.map_or(self.priv_key.as_ref(), |id| {
self.org_keys.as_ref().and_then(|h| h.get(id))
})
}
pub fn needs_unlock(&self) -> bool {
self.priv_key.is_none() || self.org_keys.is_none()
}
pub fn set_timeout(&self) {
self.timeout.set(self.timeout_duration);
}
pub fn clear(&mut self) {
self.priv_key = None;
self.org_keys = None;
self.timeout.clear();
self.clear_touchid_sessions();
}
/// True if `session_id` has been recorded within
/// `TOUCHID_SESSION_TTL`; such sessions may skip the biometric
/// prompt on subsequent requests within the same `bwx <command>`.
pub fn touchid_session_is_fresh(&self, session_id: &str) -> bool {
self.touchid_sessions
.get(session_id)
.is_some_and(|ts| ts.elapsed() < TOUCHID_SESSION_TTL)
}
pub fn record_touchid_session(&mut self, session_id: &str) {
self.touchid_sessions
.insert(session_id.to_string(), std::time::Instant::now());
self.prune_touchid_sessions();
}
pub fn clear_touchid_sessions(&mut self) {
self.touchid_sessions.clear();
}
fn prune_touchid_sessions(&mut self) {
self.touchid_sessions
.retain(|_, ts| ts.elapsed() < TOUCHID_SESSION_TTL);
}
pub fn set_sync_timeout(&self) {
self.sync_timeout.set(self.sync_timeout_duration);
}
// the way we structure the client/agent split in bwx makes the master
// password reprompt feature a bit complicated to implement - it would be
// a lot easier to just have the client do the prompting, but that would
// leave it open to someone reading the cipherstring from the local
// database and passing it to the agent directly, bypassing the client.
// the agent is the thing that holds the unlocked secrets, so it also
// needs to be the thing guarding access to master password reprompt
// entries. we only pass individual cipherstrings to the agent though, so
// the agent needs to be able to recognize the cipherstrings that need
// reprompting, without the additional context of the entry they came
// from. in addition, because the reprompt state is stored in the sync db
// in plaintext, we can't just read it from the db directly, because
// someone could just edit the file on disk before making the request.
//
// therefore, the solution we choose here is to keep an in-memory set of
// cipherstrings that we know correspond to entries with master password
// reprompt enabled. this set is only updated when the agent itself does
// a sync, so it can't be bypassed by editing the on-disk file directly.
// if the agent gets a request for any of those cipherstrings that it saw
// marked as master password reprompt during the most recent sync, it
// forces a reprompt.
pub fn set_master_password_reprompt(
&mut self,
entries: &[bwx::db::Entry],
) {
self.master_password_reprompt.clear();
let mut hasher = sha2::Sha256::new();
let mut insert = |s: Option<&str>| {
if let Some(s) = s {
if !s.is_empty() {
hasher.update(s);
self.master_password_reprompt
.insert(hasher.finalize_reset().into());
}
}
};
for entry in entries {
if !entry.master_password_reprompt() {
continue;
}
match &entry.data {
bwx::db::EntryData::Login { password, totp, .. } => {
insert(password.as_deref());
insert(totp.as_deref());
}
bwx::db::EntryData::Card { number, code, .. } => {
insert(number.as_deref());
insert(code.as_deref());
}
bwx::db::EntryData::Identity {
ssn,
passport_number,
..
} => {
insert(ssn.as_deref());
insert(passport_number.as_deref());
}
bwx::db::EntryData::SecureNote => {}
bwx::db::EntryData::SshKey { private_key, .. } => {
insert(private_key.as_deref());
}
}
for field in &entry.fields {
if field.ty == Some(bwx::api::FieldType::Hidden) {
insert(field.value.as_deref());
}
}
}
self.master_password_reprompt_initialized = true;
}
pub fn master_password_reprompt_initialized(&self) -> bool {
self.master_password_reprompt_initialized
}
pub fn last_environment(&self) -> &bwx::protocol::Environment {
&self.last_environment
}
pub fn set_last_environment(
&mut self,
environment: bwx::protocol::Environment,
) {
self.last_environment = environment;
}
}