1use std::fmt;
8use std::io::Write;
9
10use percent_encoding::percent_decode_str;
11use thiserror::Error;
12
13use crate::types::DeviceId;
14
15pub const PHONE_AUTHENTICATION_MAX_QUERY_BYTES: usize = 1_024;
17pub const PHONE_AUTHENTICATION_MAX_USER_ID_BYTES: usize = 128;
19pub const PHONE_AUTHENTICATION_MAX_PASSWORD_BYTES: usize = 256;
21pub const PHONE_AUTHENTICATION_MAX_RESPONSE_BYTES: usize = 256;
23
24const AUTHORIZED: &[u8] = b"AUTHORIZED";
25const UNAUTHORIZED: &[u8] = b"UN-AUTHORIZED";
26
27#[derive(Clone, Eq, Hash, PartialEq)]
29pub struct PhoneAuthenticationUserId(String);
30
31impl PhoneAuthenticationUserId {
32 pub fn new(value: impl Into<String>) -> Result<Self, PhoneAuthenticationError> {
34 let value = value.into();
35 validate_credential(
36 "authentication user identifier",
37 &value,
38 PHONE_AUTHENTICATION_MAX_USER_ID_BYTES,
39 )?;
40 Ok(Self(value))
41 }
42
43 pub fn expose_secret(&self) -> &str {
45 &self.0
46 }
47}
48
49impl fmt::Debug for PhoneAuthenticationUserId {
50 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51 formatter.write_str("PhoneAuthenticationUserId(<redacted>)")
52 }
53}
54
55impl TryFrom<String> for PhoneAuthenticationUserId {
56 type Error = PhoneAuthenticationError;
57
58 fn try_from(value: String) -> Result<Self, Self::Error> {
59 Self::new(value)
60 }
61}
62
63#[derive(Clone, Eq, Hash, PartialEq)]
65pub struct PhoneAuthenticationPassword(String);
66
67impl PhoneAuthenticationPassword {
68 pub fn new(value: impl Into<String>) -> Result<Self, PhoneAuthenticationError> {
70 let value = value.into();
71 validate_credential(
72 "authentication password",
73 &value,
74 PHONE_AUTHENTICATION_MAX_PASSWORD_BYTES,
75 )?;
76 Ok(Self(value))
77 }
78
79 pub fn expose_secret(&self) -> &str {
81 &self.0
82 }
83}
84
85impl fmt::Debug for PhoneAuthenticationPassword {
86 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
87 formatter.write_str("PhoneAuthenticationPassword(<redacted>)")
88 }
89}
90
91impl TryFrom<String> for PhoneAuthenticationPassword {
92 type Error = PhoneAuthenticationError;
93
94 fn try_from(value: String) -> Result<Self, Self::Error> {
95 Self::new(value)
96 }
97}
98
99#[derive(Clone, Eq, PartialEq)]
104pub struct PhoneAuthenticationRequest {
105 pub user_id: PhoneAuthenticationUserId,
106 pub password: PhoneAuthenticationPassword,
107 pub device_id: DeviceId,
108}
109
110impl PhoneAuthenticationRequest {
111 pub fn parse_query(query: &[u8]) -> Result<Self, PhoneAuthenticationError> {
114 if query.len() > PHONE_AUTHENTICATION_MAX_QUERY_BYTES {
115 return Err(PhoneAuthenticationError::QueryExceedsLimit);
116 }
117 let query =
118 std::str::from_utf8(query).map_err(|_| PhoneAuthenticationError::InvalidEncoding)?;
119 validate_encoded_form(query)?;
120 Self::from_fields(
121 form_urlencoded::parse(query.as_bytes())
122 .map(|(name, value)| (name.into_owned(), value.into_owned())),
123 )
124 }
125
126 pub fn from_fields<I, N, V>(fields: I) -> Result<Self, PhoneAuthenticationError>
128 where
129 I: IntoIterator<Item = (N, V)>,
130 N: AsRef<str>,
131 V: AsRef<str>,
132 {
133 let mut user_id = None;
134 let mut password = None;
135 let mut device_id = None;
136 for (name, value) in fields {
137 let name = name.as_ref();
138 let value = value.as_ref();
139 match name {
140 "UserID" => set_once(
141 &mut user_id,
142 "UserID",
143 PhoneAuthenticationUserId::new(value)?,
144 )?,
145 "Password" => set_once(
146 &mut password,
147 "Password",
148 PhoneAuthenticationPassword::new(value)?,
149 )?,
150 "devicename" => {
151 if value.trim() != value || value.chars().any(char::is_control) {
152 return Err(PhoneAuthenticationError::InvalidDeviceName);
153 }
154 let parsed = DeviceId::new(value)
155 .map_err(|_| PhoneAuthenticationError::InvalidDeviceName)?;
156 set_once(&mut device_id, "devicename", parsed)?;
157 }
158 _ => return Err(PhoneAuthenticationError::UnknownField),
159 }
160 }
161 Ok(Self {
162 user_id: user_id.ok_or(PhoneAuthenticationError::MissingField("UserID"))?,
163 password: password.ok_or(PhoneAuthenticationError::MissingField("Password"))?,
164 device_id: device_id.ok_or(PhoneAuthenticationError::MissingField("devicename"))?,
165 })
166 }
167}
168
169impl fmt::Debug for PhoneAuthenticationRequest {
170 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171 formatter
172 .debug_struct("PhoneAuthenticationRequest")
173 .field("user_id", &"<redacted>")
174 .field("password", &"<redacted>")
175 .field("device_id", &self.device_id)
176 .finish()
177 }
178}
179
180#[derive(Clone, Eq, PartialEq)]
182pub struct OpaquePhoneAuthenticationResponse(Vec<u8>);
183
184impl OpaquePhoneAuthenticationResponse {
185 pub fn new(value: Vec<u8>) -> Result<Self, PhoneAuthenticationError> {
187 validate_response_size(value.len())?;
188 Ok(Self(value))
189 }
190
191 pub fn as_bytes(&self) -> &[u8] {
192 &self.0
193 }
194}
195
196impl fmt::Debug for OpaquePhoneAuthenticationResponse {
197 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
198 formatter
199 .debug_struct("OpaquePhoneAuthenticationResponse")
200 .field("bytes", &self.0.len())
201 .finish()
202 }
203}
204
205#[derive(Clone, Eq, PartialEq)]
207pub enum PhoneAuthenticationResponse {
208 Authorized,
209 Unauthorized,
210 Opaque(OpaquePhoneAuthenticationResponse),
212}
213
214impl PhoneAuthenticationResponse {
215 pub fn from_bytes(value: &[u8]) -> Result<Self, PhoneAuthenticationError> {
217 validate_response_size(value.len())?;
218 let trimmed = value.trim_ascii();
219 Ok(match trimmed {
220 AUTHORIZED => Self::Authorized,
221 UNAUTHORIZED => Self::Unauthorized,
222 _ => Self::Opaque(OpaquePhoneAuthenticationResponse(value.to_vec())),
223 })
224 }
225
226 pub fn as_bytes(&self) -> &[u8] {
228 match self {
229 Self::Authorized => AUTHORIZED,
230 Self::Unauthorized => UNAUTHORIZED,
231 Self::Opaque(value) => value.as_bytes(),
232 }
233 }
234
235 pub fn to_bytes(&self) -> Vec<u8> {
236 self.as_bytes().to_vec()
237 }
238
239 pub fn write_to(&self, mut writer: impl Write) -> Result<(), PhoneAuthenticationError> {
241 writer
242 .write_all(self.as_bytes())
243 .map_err(|_| PhoneAuthenticationError::Write)
244 }
245}
246
247impl fmt::Debug for PhoneAuthenticationResponse {
248 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
249 match self {
250 Self::Authorized => formatter.write_str("Authorized"),
251 Self::Unauthorized => formatter.write_str("Unauthorized"),
252 Self::Opaque(value) => formatter.debug_tuple("Opaque").field(value).finish(),
253 }
254 }
255}
256
257#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
259pub enum PhoneAuthenticationError {
260 #[error("phone authentication query exceeds its byte limit")]
262 QueryExceedsLimit,
263 #[error("phone authentication form is not valid UTF-8 or percent encoding")]
265 InvalidEncoding,
266 #[error("phone authentication form contains an unknown field")]
267 UnknownField,
268 #[error("phone authentication form repeats field {0}")]
269 DuplicateField(&'static str),
270 #[error("phone authentication form is missing field {0}")]
271 MissingField(&'static str),
272 #[error("phone authentication credential {field} exceeds its bound or contains controls")]
274 InvalidCredential { field: &'static str },
275 #[error("phone authentication device name is invalid")]
277 InvalidDeviceName,
278 #[error("phone authentication response exceeds its byte limit")]
280 ResponseExceedsLimit,
281 #[error("unable to write phone authentication response")]
282 Write,
283}
284
285fn validate_credential(
286 field: &'static str,
287 value: &str,
288 maximum: usize,
289) -> Result<(), PhoneAuthenticationError> {
290 if value.len() > maximum || value.chars().any(char::is_control) {
291 return Err(PhoneAuthenticationError::InvalidCredential { field });
292 }
293 Ok(())
294}
295
296fn validate_response_size(actual: usize) -> Result<(), PhoneAuthenticationError> {
297 if actual > PHONE_AUTHENTICATION_MAX_RESPONSE_BYTES {
298 return Err(PhoneAuthenticationError::ResponseExceedsLimit);
299 }
300 Ok(())
301}
302
303fn set_once<T>(
304 target: &mut Option<T>,
305 field: &'static str,
306 value: T,
307) -> Result<(), PhoneAuthenticationError> {
308 if target.replace(value).is_some() {
309 return Err(PhoneAuthenticationError::DuplicateField(field));
310 }
311 Ok(())
312}
313
314fn validate_encoded_form(query: &str) -> Result<(), PhoneAuthenticationError> {
315 if query.is_empty() {
316 return Ok(());
317 }
318 for field in query.split('&') {
319 if field.is_empty() {
320 return Err(PhoneAuthenticationError::InvalidEncoding);
321 }
322 let (name, value) = field.split_once('=').unwrap_or((field, ""));
323 validate_percent_triplets(name)?;
324 validate_percent_triplets(value)?;
325 for component in [name, value] {
326 let decoded = percent_decode_str(component)
327 .decode_utf8()
328 .map_err(|_| PhoneAuthenticationError::InvalidEncoding)?;
329 if decoded.chars().any(char::is_control) {
330 return Err(PhoneAuthenticationError::InvalidEncoding);
331 }
332 }
333 }
334 Ok(())
335}
336
337fn validate_percent_triplets(value: &str) -> Result<(), PhoneAuthenticationError> {
338 let bytes = value.as_bytes();
339 let mut index = 0;
340 while index < bytes.len() {
341 if bytes[index] == b'%' {
342 let Some(pair) = bytes.get(index + 1..index + 3) else {
343 return Err(PhoneAuthenticationError::InvalidEncoding);
344 };
345 if !pair.iter().all(u8::is_ascii_hexdigit) {
346 return Err(PhoneAuthenticationError::InvalidEncoding);
347 }
348 index += 3;
349 } else {
350 index += 1;
351 }
352 }
353 Ok(())
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use std::io;
360
361 #[test]
362 fn request_decodes_exact_form_fields_and_redacts_credentials() {
363 let request = PhoneAuthenticationRequest::parse_query(
364 b"UserID=alex%40example.test&Password=p%40ss+word%26more&devicename=sep001122334455",
365 )
366 .unwrap();
367 assert_eq!(request.user_id.expose_secret(), "alex@example.test");
368 assert_eq!(request.password.expose_secret(), "p@ss word&more");
369 assert_eq!(request.device_id.as_str(), "SEP001122334455");
370
371 let debug = format!("{request:?}");
372 assert!(!debug.contains("alex"));
373 assert!(!debug.contains("p@ss"));
374 assert!(debug.contains("<redacted>"));
375 assert_eq!(
376 format!("{:?}", request.user_id),
377 "PhoneAuthenticationUserId(<redacted>)"
378 );
379 assert_eq!(
380 format!("{:?}", request.password),
381 "PhoneAuthenticationPassword(<redacted>)"
382 );
383 }
384
385 #[test]
386 fn request_requires_exact_unique_fields_and_secret_safe_bounds() {
387 for query in [
388 "UserId=private-user&Password=private-pass&devicename=SEP001122334455",
389 "UserID=private-user&password=private-pass&devicename=SEP001122334455",
390 "UserID=private-user&Password=private-pass&DeviceName=SEP001122334455",
391 "UserID=private-user&Password=private-pass",
392 "UserID=private-user&UserID=other&Password=private-pass&devicename=SEP001122334455",
393 "UserID=private%Q0user&Password=private-pass&devicename=SEP001122334455",
394 "UserID=private%0Auser&Password=private-pass&devicename=SEP001122334455",
395 "UserID=private-user&Password=private-pass&devicename=../../secret",
396 ] {
397 let error = PhoneAuthenticationRequest::parse_query(query.as_bytes()).unwrap_err();
398 let text = error.to_string();
399 assert!(!text.contains("private-user"), "{text}");
400 assert!(!text.contains("private-pass"), "{text}");
401 }
402
403 let oversized = format!(
404 "UserID={}&Password=secret&devicename=SEP001122334455",
405 "u".repeat(PHONE_AUTHENTICATION_MAX_USER_ID_BYTES + 1)
406 );
407 let error = PhoneAuthenticationRequest::parse_query(oversized.as_bytes()).unwrap_err();
408 assert!(!error.to_string().contains(&"u".repeat(32)));
409 let oversized = format!(
410 "UserID=user&Password={}&devicename=SEP001122334455",
411 "p".repeat(PHONE_AUTHENTICATION_MAX_PASSWORD_BYTES + 1)
412 );
413 let error = PhoneAuthenticationRequest::parse_query(oversized.as_bytes()).unwrap_err();
414 assert!(!error.to_string().contains(&"p".repeat(32)));
415 assert!(matches!(
416 PhoneAuthenticationRequest::parse_query(&vec![
417 b'x';
418 PHONE_AUTHENTICATION_MAX_QUERY_BYTES + 1
419 ]),
420 Err(PhoneAuthenticationError::QueryExceedsLimit)
421 ));
422 assert!(matches!(
423 PhoneAuthenticationRequest::parse_query(&[0xff]),
424 Err(PhoneAuthenticationError::InvalidEncoding)
425 ));
426 }
427
428 #[test]
429 fn empty_credentials_are_typed_for_policy_driven_denial() {
430 let request = PhoneAuthenticationRequest::parse_query(
431 b"UserID=&Password=&devicename=SEP001122334455",
432 )
433 .unwrap();
434 assert!(request.user_id.expose_secret().is_empty());
435 assert!(request.password.expose_secret().is_empty());
436 }
437
438 #[test]
439 fn response_round_trips_exact_tokens_and_preserves_unknown_bodies_opaquely() {
440 for expected in [
441 PhoneAuthenticationResponse::Authorized,
442 PhoneAuthenticationResponse::Unauthorized,
443 ] {
444 assert_eq!(
445 PhoneAuthenticationResponse::from_bytes(expected.as_bytes()).unwrap(),
446 expected
447 );
448 }
449 assert_eq!(
450 PhoneAuthenticationResponse::from_bytes(b"AUTHORIZED\r\n").unwrap(),
451 PhoneAuthenticationResponse::Authorized
452 );
453
454 for unknown in [
455 b"MAYBE".as_slice(),
456 b"<!DOCTYPE auth [<!ENTITY secret 'private'>]><auth>&secret;</auth>".as_slice(),
457 b"<auth><nested><result>AUTHORIZED</result></nested></auth>".as_slice(),
458 b"<auth><".as_slice(),
459 &[0xff],
460 ] {
461 let response = PhoneAuthenticationResponse::from_bytes(unknown).unwrap();
462 let PhoneAuthenticationResponse::Opaque(value) = response else {
463 panic!("unknown authentication body must remain opaque");
464 };
465 assert_eq!(value.as_bytes(), unknown);
466 let debug = format!("{value:?}");
467 assert!(debug.contains(&unknown.len().to_string()));
468 assert!(!debug.contains("private"));
469 assert!(!debug.contains("AUTHORIZED"));
470 }
471 let nested = format!("<auth>{}{}</auth>", "<n>".repeat(33), "</n>".repeat(33));
472 assert!(nested.len() <= PHONE_AUTHENTICATION_MAX_RESPONSE_BYTES);
473 assert!(matches!(
474 PhoneAuthenticationResponse::from_bytes(nested.as_bytes()).unwrap(),
475 PhoneAuthenticationResponse::Opaque(_)
476 ));
477 assert!(matches!(
478 PhoneAuthenticationResponse::from_bytes(&vec![
479 b'x';
480 PHONE_AUTHENTICATION_MAX_RESPONSE_BYTES
481 + 1
482 ]),
483 Err(PhoneAuthenticationError::ResponseExceedsLimit)
484 ));
485 }
486
487 #[test]
488 fn response_writer_propagates_failures_without_body_data() {
489 #[derive(Debug)]
490 struct FailingWriter;
491 impl Write for FailingWriter {
492 fn write(&mut self, _buffer: &[u8]) -> io::Result<usize> {
493 Err(io::Error::other("sensitive downstream context"))
494 }
495
496 fn flush(&mut self) -> io::Result<()> {
497 Ok(())
498 }
499 }
500
501 let mut body = Vec::new();
502 PhoneAuthenticationResponse::Authorized
503 .write_to(&mut body)
504 .unwrap();
505 assert_eq!(body, AUTHORIZED);
506 let error = PhoneAuthenticationResponse::Unauthorized
507 .write_to(FailingWriter)
508 .unwrap_err();
509 assert_eq!(error, PhoneAuthenticationError::Write);
510 assert!(!error.to_string().contains("sensitive"));
511 }
512}