1use std::fmt;
4
5use ap_noise::Psk;
6use ap_proxy_protocol::IdentityFingerprint;
7use serde::{Deserialize, Serialize};
8use zeroize::Zeroizing;
9
10pub type PskId = String;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub enum CredentialQuery {
21 Domain(String),
23 Id(String),
25 Search(String),
27}
28
29impl CredentialQuery {
30 pub fn search_string(&self) -> &str {
32 match self {
33 Self::Domain(d) => d.as_str(),
34 Self::Id(id) => id.as_str(),
35 Self::Search(s) => s.as_str(),
36 }
37 }
38}
39
40impl fmt::Display for CredentialQuery {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 CredentialQuery::Domain(d) => write!(f, "domain: {d}"),
44 CredentialQuery::Id(id) => write!(f, "id: {id}"),
45 CredentialQuery::Search(s) => write!(f, "search: {s}"),
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
65pub struct PskToken {
66 psk: Psk,
67 fingerprint: IdentityFingerprint,
68}
69
70impl PskToken {
71 pub fn new(psk: Psk, fingerprint: IdentityFingerprint) -> Self {
73 Self { psk, fingerprint }
74 }
75
76 pub fn parse(token: &str) -> Result<Self, crate::error::ClientError> {
82 if token.len() != 129 {
83 return Err(crate::error::ClientError::InvalidPairingCode(format!(
84 "PSK token must be exactly 129 characters, got {}",
85 token.len()
86 )));
87 }
88
89 let (psk_hex, rest) = token.split_at(64);
90 let (sep, fp_hex) = rest.split_at(1);
91 if sep != "_" {
92 return Err(crate::error::ClientError::InvalidPairingCode(
93 "PSK token must have '_' separator at position 64".to_string(),
94 ));
95 }
96
97 let psk = Psk::from_hex(psk_hex).map_err(|e| {
98 crate::error::ClientError::InvalidPairingCode(format!("Invalid PSK: {e}"))
99 })?;
100 let fingerprint = IdentityFingerprint::from_hex(fp_hex).map_err(|e| {
101 crate::error::ClientError::InvalidPairingCode(format!("Invalid fingerprint: {e}"))
102 })?;
103
104 Ok(Self { psk, fingerprint })
105 }
106
107 pub fn looks_like_psk_token(s: &str) -> bool {
109 s.len() == 129 && s.as_bytes()[64] == b'_'
110 }
111
112 pub fn psk(&self) -> &Psk {
114 &self.psk
115 }
116
117 pub fn fingerprint(&self) -> &IdentityFingerprint {
119 &self.fingerprint
120 }
121
122 pub fn into_parts(self) -> (Psk, IdentityFingerprint) {
124 (self.psk, self.fingerprint)
125 }
126
127 pub fn into_connection_mode(self) -> ConnectionMode {
129 ConnectionMode::NewPsk {
130 psk: self.psk,
131 remote_fingerprint: self.fingerprint,
132 }
133 }
134}
135
136impl fmt::Display for PskToken {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 write!(f, "{}_{}", self.psk.to_hex(), self.fingerprint.to_hex())
139 }
140}
141
142#[derive(Debug, Clone)]
144pub enum ConnectionMode {
145 New { rendezvous_code: String },
147 NewPsk {
149 psk: Psk,
150 remote_fingerprint: IdentityFingerprint,
151 },
152 Existing {
154 remote_fingerprint: IdentityFingerprint,
155 },
156}
157
158#[derive(Clone, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct CredentialData {
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub username: Option<String>,
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub password: Option<Zeroizing<String>>,
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub totp: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub uri: Option<String>,
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub notes: Option<String>,
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub credential_id: Option<String>,
180 #[serde(default)]
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub domain: Option<String>,
184}
185
186impl std::fmt::Debug for CredentialData {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 f.debug_struct("CredentialData")
189 .field("domain", &self.domain)
190 .field("username", &self.username)
191 .field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
192 .field("totp", &self.totp.as_ref().map(|_| "[REDACTED]"))
193 .field("notes", &self.notes.as_ref().map(|_| "[REDACTED]"))
194 .field("credential_id", &self.credential_id)
195 .finish()
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201#[serde(tag = "type", rename_all = "kebab-case")]
202pub(crate) enum ProtocolMessage {
203 #[serde(rename = "handshake-init")]
205 HandshakeInit {
206 data: String,
207 ciphersuite: String,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
211 psk_id: Option<PskId>,
212 },
213 #[serde(rename = "handshake-response")]
215 HandshakeResponse { data: String, ciphersuite: String },
216 CredentialRequest { encrypted: String },
218 CredentialResponse { encrypted: String },
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub(crate) struct CredentialRequestPayload {
225 #[serde(rename = "type")]
226 pub request_type: String,
227 pub query: CredentialQuery,
228 pub timestamp: u64,
229 #[serde(rename = "requestId")]
230 pub request_id: String,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub(crate) struct CredentialResponsePayload {
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub credential: Option<CredentialData>,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub error: Option<String>,
240 #[serde(rename = "requestId")]
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub request_id: Option<String>,
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn psk_token_roundtrip() {
251 let psk = Psk::generate();
252 let fp = IdentityFingerprint([0xcd; 32]);
253 let token = PskToken::new(psk.clone(), fp);
254 let s = token.to_string();
255 assert_eq!(s.len(), 129);
256 assert_eq!(s.as_bytes()[64], b'_');
257
258 let parsed = PskToken::parse(&s).expect("should parse");
259 assert_eq!(parsed.fingerprint(), &fp);
260 assert_eq!(parsed.psk().to_hex(), psk.to_hex());
261 }
262
263 #[test]
264 fn psk_token_parse_wrong_length() {
265 assert!(PskToken::parse("too_short").is_err());
266 }
267
268 #[test]
269 fn psk_token_looks_like() {
270 let valid = format!("{}_{}", "ab".repeat(32), "cd".repeat(32));
271 assert!(PskToken::looks_like_psk_token(&valid));
272 assert!(!PskToken::looks_like_psk_token("ABC-DEF-GHI"));
273 }
274
275 #[test]
276 fn psk_token_into_connection_mode() {
277 let psk = Psk::generate();
278 let fp = IdentityFingerprint([0x01; 32]);
279 let token = PskToken::new(psk, fp);
280 let mode = token.into_connection_mode();
281 assert!(matches!(mode, ConnectionMode::NewPsk { .. }));
282 }
283}