refactor: split the session manager into modules
sessions.rs (1 730 lines) becomes sessions/ with mod.rs (types, session lifecycle, I/O, recording, tests), credentials.rs (secret resolution, prompts, host-key checks), protocols.rs (SSH, GitHub, IRC, VNC, RDP connects and SSH tunnels), local.rs (local shells, login scripts) and extras.rs (per-protocol accessors). Code moves unchanged; helpers that crossed a module boundary became pub(super).
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
//! Credential resolution (saved, session-only, ask-every-time), prompts and host-key checks.
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(super) type SessionSecrets = Arc<Mutex<SecretState>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct SecretState {
|
||||
/// `SecretMode::SessionOnly` values, asked once per session. Wiped from memory on drop.
|
||||
pub(super) session_only: HashMap<String, Zeroizing<String>>,
|
||||
/// `SecretMode::Saved` values typed during this attempt; stored only once it connects, so a
|
||||
/// typo never ends up saved.
|
||||
pub(super) pending: HashMap<String, Zeroizing<String>>,
|
||||
/// Saved values this attempt used.
|
||||
pub(super) used: HashSet<String>,
|
||||
/// Saved values the server rejected: the next attempt asks again instead of reusing them.
|
||||
pub(super) rejected: HashSet<String>,
|
||||
}
|
||||
|
||||
/// After a failed attempt, typed credentials are dropped and, when the server refused them, the
|
||||
/// saved ones used are skipped next time so the user can enter a replacement.
|
||||
pub(super) fn attempt_failed(state: &SessionSecrets, error: &str) {
|
||||
let mut st = state.lock();
|
||||
st.pending.clear();
|
||||
if is_auth_error(error) {
|
||||
let used = std::mem::take(&mut st.used);
|
||||
st.rejected.extend(used);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a connect error means the server refused the credentials (as opposed to a network or
|
||||
/// host-key failure).
|
||||
pub fn is_auth_error(msg: &str) -> bool {
|
||||
let m = msg.to_lowercase();
|
||||
// The last two are CredSSP's (RDP with NLA) wording for a rejected password.
|
||||
["unable to authenticate", "authentication failed", "permission denied", "no supported methods", "logondenied", "logon failure"]
|
||||
.iter()
|
||||
.any(|p| m.contains(p))
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
// --- credentials --------------------------------------------------------------------
|
||||
|
||||
/// Resolves a credential per its SecretMode (Go's buildSecretCallback): saved values come from
|
||||
/// the keychain then the vault (unless the server just rejected them), and newly typed ones
|
||||
/// are saved once the connection succeeds; session-only values are asked once per session and
|
||||
/// reused on reconnects (so e.g. a NickServ password entered once keeps identifying after a
|
||||
/// drop).
|
||||
pub fn secret_fn(&self, session: &str, profile_id: &str, kind: SecretKind, mode: SecretMode, prompt: Prompt) -> SecretFn {
|
||||
let key = kind.key(profile_id);
|
||||
let (secrets, store) = (self.secrets.clone(), self.store.clone());
|
||||
let (pid, kind) = (profile_id.to_string(), kind.as_str());
|
||||
let state: SessionSecrets = self.sessions.lock().get(session).map(|s| s.session_secrets.clone()).unwrap_or_default();
|
||||
Arc::new(move || {
|
||||
if mode == SecretMode::Saved {
|
||||
let mut st = state.lock();
|
||||
if let Some(v) = st.pending.get(&key) {
|
||||
return Ok(v.to_string());
|
||||
}
|
||||
if !st.rejected.remove(&key)
|
||||
&& let Some(v) = secrets.get(&key)
|
||||
{
|
||||
st.used.insert(key.clone());
|
||||
store.add_audit("secret.access", &pid, "", &format!("Accessed {kind} credential"));
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
if mode == SecretMode::SessionOnly
|
||||
&& let Some(v) = state.lock().session_only.get(&key).map(|v| v.to_string())
|
||||
{
|
||||
return Ok(v);
|
||||
}
|
||||
let v = prompt(&format!("Enter {kind}:"))?;
|
||||
let mut st = state.lock();
|
||||
match mode {
|
||||
SecretMode::Saved => {
|
||||
st.pending.insert(key.clone(), Zeroizing::new(v.clone()));
|
||||
}
|
||||
SecretMode::SessionOnly => {
|
||||
st.session_only.insert(key.clone(), Zeroizing::new(v.clone()));
|
||||
}
|
||||
SecretMode::AskEveryTime => {}
|
||||
}
|
||||
Ok(v)
|
||||
})
|
||||
}
|
||||
|
||||
/// Blocks until the user answers a credential prompt (5 minutes, or cancel).
|
||||
pub fn prompt(&self, session: &str, title: &str, message: &str, timeout: Duration) -> Result<String> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.emit(Event::SecretPrompt { session: session.into(), title: title.into(), message: message.into(), reply: tx });
|
||||
match rx.recv_timeout(timeout) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => err("secret prompt timed out".into()),
|
||||
Err(_) => err("user cancelled secret prompt".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The standard interactive prompt for a session's credentials.
|
||||
pub fn prompt_for(self: &Arc<Self>, session: &str, title: &str, timeout: Duration) -> Prompt {
|
||||
let (me, sid, title) = (self.clone(), session.to_string(), title.to_string());
|
||||
Arc::new(move |msg| me.prompt(&sid, &title, msg, timeout))
|
||||
}
|
||||
|
||||
/// Host-key policy check against the known-hosts table (Go's HostKeyVerifier). Under the
|
||||
/// `ask` policy both unknown and changed keys go to the user, who can reject, accept once
|
||||
/// (connect without touching the record) or accept and save/update the record.
|
||||
pub fn host_key_check(self: &Arc<Self>, session: &str, policy: HostKeyPolicy) -> HostKeyCheck {
|
||||
let (me, sid) = (self.clone(), session.to_string());
|
||||
Arc::new(move |k: &HostKeyInfo| {
|
||||
let stored = me.store.get_known_host(&k.host, k.port as i64).map_err(|e| format!("failed to query known hosts: {e}"))?;
|
||||
let save = || {
|
||||
let n = now();
|
||||
me.store
|
||||
.save_known_host(&KnownHost {
|
||||
id: String::new(),
|
||||
host: k.host.clone(),
|
||||
port: k.port as i64,
|
||||
key_type: k.key_type.clone(),
|
||||
fingerprint: k.fingerprint.clone(),
|
||||
public_key: k.public_key_b64.clone(),
|
||||
first_seen: n,
|
||||
last_seen: n,
|
||||
trusted: true,
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
};
|
||||
let touch = |mut s: KnownHost| {
|
||||
s.last_seen = now();
|
||||
let _ = me.store.save_known_host(&s);
|
||||
Ok(())
|
||||
};
|
||||
let changed = |s: &KnownHost| {
|
||||
format!("host key for {} has changed! stored fingerprint: {}, received: {} — possible MITM attack", k.host, s.fingerprint, k.fingerprint)
|
||||
};
|
||||
let ask = |stored: Option<&KnownHost>| -> std::result::Result<HostKeyDecision, String> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
me.emit(Event::HostKeyVerify {
|
||||
session: sid.clone(),
|
||||
host: k.host.clone(),
|
||||
key_type: k.key_type.clone(),
|
||||
fingerprint: k.fingerprint.clone(),
|
||||
stored: stored.map(|s| s.fingerprint.clone()),
|
||||
reply: tx,
|
||||
});
|
||||
match rx.recv_timeout(Duration::from_secs(60)) {
|
||||
Ok(d) => Ok(d),
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => Ok(HostKeyDecision::Reject),
|
||||
Err(_) => Err("host key prompt failed: host key prompt timed out waiting for user response".into()),
|
||||
}
|
||||
};
|
||||
match (policy, stored) {
|
||||
(HostKeyPolicy::Insecure, None) | (HostKeyPolicy::AcceptNew, None) => save(),
|
||||
(HostKeyPolicy::Insecure, Some(s)) => touch(s),
|
||||
(HostKeyPolicy::Strict, None) => Err(format!("host key for {}:{} is not known (policy: strict)", k.host, k.port)),
|
||||
(HostKeyPolicy::Ask, None) => match ask(None)? {
|
||||
HostKeyDecision::AcceptAndSave => save(),
|
||||
HostKeyDecision::AcceptOnce => Ok(()),
|
||||
HostKeyDecision::Reject => Err(format!("host key for {}:{} rejected by user", k.host, k.port)),
|
||||
},
|
||||
(_, Some(s)) if s.fingerprint == k.fingerprint => touch(s),
|
||||
(HostKeyPolicy::Ask, Some(s)) => match ask(Some(&s))? {
|
||||
HostKeyDecision::AcceptAndSave => {
|
||||
me.store.delete_known_host(&s.id).map_err(|e| e.to_string())?;
|
||||
save()?;
|
||||
me.store.add_audit(
|
||||
"hostkey.update",
|
||||
"",
|
||||
&sid,
|
||||
&format!("Replaced host key for {}:{} ({} -> {})", k.host, k.port, s.fingerprint, k.fingerprint),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
HostKeyDecision::AcceptOnce => {
|
||||
me.store.add_audit(
|
||||
"hostkey.accept-once",
|
||||
"",
|
||||
&sid,
|
||||
&format!("Accepted changed host key for {}:{} once ({})", k.host, k.port, k.fingerprint),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
HostKeyDecision::Reject => Err(format!("{} (rejected by user)", changed(&s))),
|
||||
},
|
||||
(_, Some(s)) => Err(changed(&s)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user