1use std::fmt;
4
5use ap_noise::Psk;
6use ap_proxy_protocol::IdentityFingerprint;
7use serde::{Deserialize, Serialize};
8
9pub type PskId = String;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub enum CredentialQuery {
20 Domain(String),
22 Id(String),
24 Search(String),
26}
27
28impl CredentialQuery {
29 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#[derive(Debug, Clone)]
64pub struct PskToken {
65 psk: Psk,
66 fingerprint: IdentityFingerprint,
67}
68
69impl PskToken {
70 pub fn new(psk: Psk, fingerprint: IdentityFingerprint) -> Self {
72 Self { psk, fingerprint }
73 }
74
75 pub fn parse(token: &str) -> Result<Self, crate::error::ClientError> {
81 if token.len() != 129 {
82 return Err(crate::error::ClientError::InvalidPairingCode(format!(
83 "PSK token must be exactly 129 characters, got {}",
84 token.len()
85 )));
86 }
87
88 let (psk_hex, rest) = token.split_at(64);
89 let (sep, fp_hex) = rest.split_at(1);
90 if sep != "_" {
91 return Err(crate::error::ClientError::InvalidPairingCode(
92 "PSK token must have '_' separator at position 64".to_string(),
93 ));
94 }
95
96 let psk = Psk::from_hex(psk_hex).map_err(|e| {
97 crate::error::ClientError::InvalidPairingCode(format!("Invalid PSK: {e}"))
98 })?;
99 let fingerprint = IdentityFingerprint::from_hex(fp_hex).map_err(|e| {
100 crate::error::ClientError::InvalidPairingCode(format!("Invalid fingerprint: {e}"))
101 })?;
102
103 Ok(Self { psk, fingerprint })
104 }
105
106 pub fn looks_like_psk_token(s: &str) -> bool {
108 s.len() == 129 && s.as_bytes()[64] == b'_'
109 }
110
111 pub fn psk(&self) -> &Psk {
113 &self.psk
114 }
115
116 pub fn fingerprint(&self) -> &IdentityFingerprint {
118 &self.fingerprint
119 }
120
121 pub fn into_parts(self) -> (Psk, IdentityFingerprint) {
123 (self.psk, self.fingerprint)
124 }
125
126 pub fn into_connection_mode(self) -> ConnectionMode {
128 ConnectionMode::NewPsk {
129 psk: self.psk,
130 remote_fingerprint: self.fingerprint,
131 }
132 }
133}
134
135impl fmt::Display for PskToken {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "{}_{}", self.psk.to_hex(), self.fingerprint.to_hex())
138 }
139}
140
141#[derive(Debug, Clone)]
143pub enum ConnectionMode {
144 New { rendezvous_code: String },
146 NewPsk {
148 psk: Psk,
149 remote_fingerprint: IdentityFingerprint,
150 },
151 Existing {
153 remote_fingerprint: IdentityFingerprint,
154 },
155}
156
157#[derive(Clone, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct CredentialData {
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub username: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub password: Option<String>,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub totp: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub uri: Option<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub notes: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub credential_id: Option<String>,
179 #[serde(default)]
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub domain: Option<String>,
183}
184
185impl std::fmt::Debug for CredentialData {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 f.debug_struct("CredentialData")
188 .field("domain", &self.domain)
189 .field("username", &self.username)
190 .field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
191 .field("totp", &self.totp.as_ref().map(|_| "[REDACTED]"))
192 .field("notes", &self.notes.as_ref().map(|_| "[REDACTED]"))
193 .field("credential_id", &self.credential_id)
194 .finish()
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200#[serde(tag = "type", rename_all = "kebab-case")]
201pub(crate) enum ProtocolMessage {
202 #[serde(rename = "handshake-init")]
204 HandshakeInit {
205 data: String,
206 ciphersuite: String,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
210 psk_id: Option<PskId>,
211 },
212 #[serde(rename = "handshake-response")]
214 HandshakeResponse { data: String, ciphersuite: String },
215 CredentialRequest { encrypted: String },
217 CredentialResponse { encrypted: String },
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
223pub(crate) struct CredentialRequestPayload {
224 #[serde(rename = "type")]
225 pub request_type: String,
226 pub query: CredentialQuery,
227 pub timestamp: u64,
228 #[serde(rename = "requestId")]
229 pub request_id: String,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub(crate) struct CredentialResponsePayload {
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub credential: Option<CredentialData>,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub error: Option<String>,
239 #[serde(rename = "requestId")]
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub request_id: Option<String>,
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn psk_token_roundtrip() {
250 let psk = Psk::generate();
251 let fp = IdentityFingerprint([0xcd; 32]);
252 let token = PskToken::new(psk.clone(), fp);
253 let s = token.to_string();
254 assert_eq!(s.len(), 129);
255 assert_eq!(s.as_bytes()[64], b'_');
256
257 let parsed = PskToken::parse(&s).expect("should parse");
258 assert_eq!(parsed.fingerprint(), &fp);
259 assert_eq!(parsed.psk().to_hex(), psk.to_hex());
260 }
261
262 #[test]
263 fn psk_token_parse_wrong_length() {
264 assert!(PskToken::parse("too_short").is_err());
265 }
266
267 #[test]
268 fn psk_token_looks_like() {
269 let valid = format!("{}_{}", "ab".repeat(32), "cd".repeat(32));
270 assert!(PskToken::looks_like_psk_token(&valid));
271 assert!(!PskToken::looks_like_psk_token("ABC-DEF-GHI"));
272 }
273
274 #[test]
275 fn psk_token_into_connection_mode() {
276 let psk = Psk::generate();
277 let fp = IdentityFingerprint([0x01; 32]);
278 let token = PskToken::new(psk, fp);
279 let mode = token.into_connection_mode();
280 assert!(matches!(mode, ConnectionMode::NewPsk { .. }));
281 }
282}