Skip to main content

ap_client/
types.rs

1//! Types for the remote client protocol
2
3use std::fmt;
4
5use ap_noise::Psk;
6use ap_proxy_protocol::IdentityFingerprint;
7use serde::{Deserialize, Serialize};
8
9/// A stable identifier for a PSK, derived from `hex(SHA256(psk)[0..8])`.
10///
11/// Used as a lookup key to match incoming handshakes to the correct pending
12/// pairing. Today this maps to in-memory pending pairings; in the future it
13/// could index persistent/reusable PSKs from a `PskStore`.
14pub type PskId = String;
15
16/// What kind of credential to look up.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub enum CredentialQuery {
20    /// Look up by domain / URL.
21    Domain(String),
22    /// Look up by vault item ID.
23    Id(String),
24    /// Free-text search.
25    Search(String),
26}
27
28impl CredentialQuery {
29    /// Extract the inner search string from any query variant.
30    pub fn search_string(&self) -> &str {
31        match self {
32            Self::Domain(d) => d.as_str(),
33            Self::Id(id) => id.as_str(),
34            Self::Search(s) => s.as_str(),
35        }
36    }
37}
38
39impl fmt::Display for CredentialQuery {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            CredentialQuery::Domain(d) => write!(f, "domain: {d}"),
43            CredentialQuery::Id(id) => write!(f, "id: {id}"),
44            CredentialQuery::Search(s) => write!(f, "search: {s}"),
45        }
46    }
47}
48
49/// Connection mode for establishing a connection
50#[derive(Debug, Clone)]
51pub enum ConnectionMode {
52    /// New connection requiring rendezvous code pairing
53    New { rendezvous_code: String },
54    /// New connection using PSK authentication
55    NewPsk {
56        psk: Psk,
57        remote_fingerprint: IdentityFingerprint,
58    },
59    /// Existing connection using cached remote fingerprint
60    Existing {
61        remote_fingerprint: IdentityFingerprint,
62    },
63}
64
65/// Credential data returned from a request
66#[derive(Clone, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct CredentialData {
69    /// Username for the credential
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub username: Option<String>,
72    /// Password for the credential
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub password: Option<String>,
75    /// TOTP code if available
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub totp: Option<String>,
78    /// URI associated with the credential
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub uri: Option<String>,
81    /// Additional notes
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub notes: Option<String>,
84    /// Vault item ID
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub credential_id: Option<String>,
87    /// Domain associated with this credential
88    #[serde(default)]
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub domain: Option<String>,
91}
92
93impl std::fmt::Debug for CredentialData {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.debug_struct("CredentialData")
96            .field("domain", &self.domain)
97            .field("username", &self.username)
98            .field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
99            .field("totp", &self.totp.as_ref().map(|_| "[REDACTED]"))
100            .field("notes", &self.notes.as_ref().map(|_| "[REDACTED]"))
101            .field("credential_id", &self.credential_id)
102            .finish()
103    }
104}
105
106/// Internal protocol messages sent over WebSocket
107#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(tag = "type", rename_all = "kebab-case")]
109pub(crate) enum ProtocolMessage {
110    /// Noise handshake init (initiator -> responder)
111    #[serde(rename = "handshake-init")]
112    HandshakeInit {
113        data: String,
114        ciphersuite: String,
115        /// PSK identifier — `Some(id)` for PSK mode, `None` for rendezvous mode.
116        /// Backward-compatible: old clients omit this field (deserialized as `None`).
117        #[serde(default, skip_serializing_if = "Option::is_none")]
118        psk_id: Option<PskId>,
119    },
120    /// Noise handshake response (responder -> initiator)
121    #[serde(rename = "handshake-response")]
122    HandshakeResponse { data: String, ciphersuite: String },
123    /// Encrypted credential request
124    CredentialRequest { encrypted: String },
125    /// Encrypted credential response
126    CredentialResponse { encrypted: String },
127}
128
129/// Internal credential request structure (encrypted in transit)
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub(crate) struct CredentialRequestPayload {
132    #[serde(rename = "type")]
133    pub request_type: String,
134    pub query: CredentialQuery,
135    pub timestamp: u64,
136    #[serde(rename = "requestId")]
137    pub request_id: String,
138}
139
140/// Internal credential response structure (encrypted in transit)
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub(crate) struct CredentialResponsePayload {
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub credential: Option<CredentialData>,
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub error: Option<String>,
147    #[serde(rename = "requestId")]
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub request_id: Option<String>,
150}