use std::collections::HashMap;
use log::warn;
use zvariant::{OwnedObjectPath, OwnedValue, Str};
use crate::ConnectionError;
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecretAgentFlags: u32 {
const ALLOW_INTERACTION = 0x1;
const REQUEST_NEW = 0x2;
const USER_REQUESTED = 0x4;
const WPS_PBC_ACTIVE = 0x8;
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecretAgentCapabilities: u32 {
const VPN_HINTS = 0x1;
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum SecretSetting {
WifiPsk {
ssid: String,
},
WifiEap {
identity: Option<String>,
method: Option<String>,
},
Vpn {
service_type: String,
user_name: Option<String>,
},
Gsm,
Cdma,
Pppoe,
Other(String),
}
#[non_exhaustive]
pub struct SecretRequest {
pub connection_uuid: String,
pub connection_id: String,
pub connection_type: String,
pub connection_path: OwnedObjectPath,
pub setting: SecretSetting,
pub hints: Vec<String>,
pub flags: SecretAgentFlags,
pub responder: SecretResponder,
}
impl std::fmt::Debug for SecretRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecretRequest")
.field("connection_uuid", &self.connection_uuid)
.field("connection_id", &self.connection_id)
.field("connection_type", &self.connection_type)
.field("connection_path", &self.connection_path)
.field("setting", &self.setting)
.field("hints", &self.hints)
.field("flags", &self.flags)
.finish_non_exhaustive()
}
}
pub struct SecretResponder {
reply_tx: Option<futures::channel::oneshot::Sender<SecretReply>>,
setting_name: String,
}
impl std::fmt::Debug for SecretResponder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecretResponder")
.field("setting_name", &self.setting_name)
.field("consumed", &self.reply_tx.is_none())
.finish()
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct CancelReason {
pub connection_path: OwnedObjectPath,
pub setting_name: String,
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum SecretStoreEvent {
Save {
connection_path: OwnedObjectPath,
},
Delete {
connection_path: OwnedObjectPath,
},
}
pub(crate) type ConnectionDict = HashMap<String, HashMap<String, OwnedValue>>;
pub(crate) enum SecretReply {
Secrets(ConnectionDict),
UserCanceled,
NoSecrets,
}
impl SecretResponder {
pub(crate) fn new(
reply_tx: futures::channel::oneshot::Sender<SecretReply>,
setting_name: String,
) -> Self {
Self {
reply_tx: Some(reply_tx),
setting_name,
}
}
pub async fn wifi_psk(mut self, psk: impl Into<String>) -> crate::Result<()> {
let mut inner = HashMap::new();
inner.insert("psk".to_owned(), OwnedValue::from(Str::from(psk.into())));
let mut outer = HashMap::new();
outer.insert("802-11-wireless-security".to_owned(), inner);
self.send_reply(SecretReply::Secrets(outer))
}
pub async fn wifi_eap(
mut self,
identity: Option<String>,
password: String,
) -> crate::Result<()> {
let mut inner = HashMap::new();
inner.insert("password".to_owned(), OwnedValue::from(Str::from(password)));
if let Some(id) = identity {
inner.insert("identity".to_owned(), OwnedValue::from(Str::from(id)));
}
let mut outer = HashMap::new();
outer.insert("802-1x".to_owned(), inner);
self.send_reply(SecretReply::Secrets(outer))
}
pub async fn vpn_secrets(mut self, secrets: HashMap<String, String>) -> crate::Result<()> {
let mut inner = HashMap::new();
inner.insert("secrets".to_owned(), OwnedValue::from(secrets));
let mut outer = HashMap::new();
outer.insert("vpn".to_owned(), inner);
self.send_reply(SecretReply::Secrets(outer))
}
pub async fn raw(
mut self,
setting_name: impl Into<String>,
data: HashMap<String, OwnedValue>,
) -> crate::Result<()> {
let mut outer = HashMap::new();
outer.insert(setting_name.into(), data);
self.send_reply(SecretReply::Secrets(outer))
}
pub async fn cancel(mut self) -> crate::Result<()> {
self.send_reply(SecretReply::UserCanceled)
}
pub async fn no_secrets(mut self) -> crate::Result<()> {
self.send_reply(SecretReply::NoSecrets)
}
fn send_reply(&mut self, reply: SecretReply) -> crate::Result<()> {
let tx = self
.reply_tx
.take()
.ok_or(ConnectionError::AgentNotRegistered)?;
let _ = tx.send(reply);
Ok(())
}
}
impl Drop for SecretResponder {
fn drop(&mut self) {
if let Some(tx) = self.reply_tx.take() {
warn!("SecretResponder dropped without responding; auto-replying NoSecrets");
let _ = tx.send(SecretReply::NoSecrets);
}
}
}
pub(crate) fn extract_setting_string(
connection: &ConnectionDict,
section: &str,
key: &str,
) -> Option<String> {
let section_dict = connection.get(section)?;
let value = section_dict.get(key)?;
<&str>::try_from(value).ok().map(String::from)
}
pub(crate) fn extract_ssid(connection: &ConnectionDict) -> Option<String> {
let wireless = connection.get("802-11-wireless")?;
let ssid_value = wireless.get("ssid")?;
if let Ok(bytes) = <Vec<u8>>::try_from(ssid_value.clone()) {
return Some(String::from_utf8_lossy(&bytes).into_owned());
}
<&str>::try_from(ssid_value).ok().map(String::from)
}
pub(crate) fn parse_secret_setting(
connection: &ConnectionDict,
setting_name: &str,
) -> SecretSetting {
match setting_name {
"802-11-wireless-security" => SecretSetting::WifiPsk {
ssid: extract_ssid(connection).unwrap_or_default(),
},
"802-1x" => SecretSetting::WifiEap {
identity: extract_setting_string(connection, "802-1x", "identity"),
method: extract_setting_string(connection, "802-1x", "eap"),
},
"vpn" => SecretSetting::Vpn {
service_type: extract_setting_string(connection, "vpn", "service-type")
.unwrap_or_default(),
user_name: extract_setting_string(connection, "vpn", "user-name"),
},
"gsm" => SecretSetting::Gsm,
"cdma" => SecretSetting::Cdma,
"pppoe" => SecretSetting::Pppoe,
other => SecretSetting::Other(other.to_owned()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flags_from_bits() {
let flags = SecretAgentFlags::from_bits_truncate(0x5);
assert!(flags.contains(SecretAgentFlags::ALLOW_INTERACTION));
assert!(flags.contains(SecretAgentFlags::USER_REQUESTED));
assert!(!flags.contains(SecretAgentFlags::REQUEST_NEW));
}
#[test]
fn capabilities_bits_round_trip() {
let caps = SecretAgentCapabilities::VPN_HINTS;
assert_eq!(caps.bits(), 0x1);
}
#[test]
fn parse_wifi_psk_setting() {
let connection = HashMap::new();
let setting = parse_secret_setting(&connection, "802-11-wireless-security");
assert!(matches!(setting, SecretSetting::WifiPsk { .. }));
}
#[test]
fn parse_vpn_setting() {
let connection = HashMap::new();
let setting = parse_secret_setting(&connection, "vpn");
assert!(matches!(setting, SecretSetting::Vpn { .. }));
}
#[test]
fn parse_unknown_setting() {
let connection = HashMap::new();
let setting = parse_secret_setting(&connection, "some-custom-thing");
assert!(matches!(setting, SecretSetting::Other(s) if s == "some-custom-thing"));
}
#[test]
fn responder_drop_sends_no_secrets() {
let (tx, mut rx) = futures::channel::oneshot::channel();
let responder = SecretResponder::new(tx, "test".into());
drop(responder);
let reply = rx.try_recv().expect("should have received a reply");
assert!(reply.is_some(), "drop should have sent a reply");
assert!(matches!(reply.unwrap(), SecretReply::NoSecrets));
}
}