cloud_sdk_reqwest/shared/
basic.rs1use core::fmt;
2use std::vec::Vec;
3
4use base64_ng::{STANDARD, checked_encoded_len};
5use cloud_sdk_sanitization::{SecretBuffer, sanitize_bytes};
6use reqwest::header::HeaderValue;
7
8use super::{BasicCredentialScope, sensitive_header_value};
9
10pub const MAX_BASIC_USERNAME_BYTES: usize = 256;
12pub const MAX_BASIC_PASSWORD_BYTES: usize = 2048;
14pub const MAX_BASIC_AUTHORIZATION_BYTES: usize = 4096;
16
17const BASIC_PREFIX: &[u8] = b"Basic ";
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum BasicUsernameError {
22 Empty,
24 TooLong,
26 InvalidByte,
28 AllocationFailed,
30}
31
32impl_static_error!(BasicUsernameError,
33 Self::Empty => "Basic username is empty",
34 Self::TooLong => "Basic username exceeds the length limit",
35 Self::InvalidByte => "Basic username contains an invalid byte",
36 Self::AllocationFailed => "Basic username allocation failed",
37);
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum BasicPasswordError {
42 Empty,
44 TooLong,
46 InvalidByte,
48 AllocationFailed,
50}
51
52impl_static_error!(BasicPasswordError,
53 Self::Empty => "Basic password is empty",
54 Self::TooLong => "Basic password exceeds the length limit",
55 Self::InvalidByte => "Basic password contains an invalid byte",
56 Self::AllocationFailed => "Basic password allocation failed",
57);
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub enum BasicCredentialError {
62 UsernameRejected(BasicUsernameError),
64 PasswordRejected(BasicPasswordError),
66 AuthorizationTooLong,
68 AllocationFailed,
70 EncodingFailed,
72}
73
74impl fmt::Display for BasicCredentialError {
75 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76 formatter.write_str(match self {
77 Self::UsernameRejected(_) => "Basic username was rejected",
78 Self::PasswordRejected(_) => "Basic password was rejected",
79 Self::AuthorizationTooLong => "Basic authorization exceeds the length limit",
80 Self::AllocationFailed => "Basic authorization allocation failed",
81 Self::EncodingFailed => "Basic authorization encoding failed",
82 })
83 }
84}
85
86impl core::error::Error for BasicCredentialError {
87 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
88 match self {
89 Self::UsernameRejected(error) => Some(error),
90 Self::PasswordRejected(error) => Some(error),
91 Self::AuthorizationTooLong | Self::AllocationFailed | Self::EncodingFailed => None,
92 }
93 }
94}
95
96pub struct BasicUsername(SecretBytes);
98
99impl BasicUsername {
100 pub fn new(value: &str) -> Result<Self, BasicUsernameError> {
102 Self::from_bytes(value.as_bytes())
103 }
104
105 pub fn from_mut_bytes(value: &mut [u8]) -> Result<Self, BasicUsernameError> {
107 let result = Self::from_bytes(value);
108 sanitize_bytes(value);
109 result
110 }
111
112 pub fn from_secret_buffer(value: SecretBuffer<'_>) -> Result<Self, BasicUsernameError> {
114 Self::from_bytes(value.as_slice())
115 }
116
117 fn from_bytes(value: &[u8]) -> Result<Self, BasicUsernameError> {
118 validate_username(value)?;
119 SecretBytes::copy_from(value)
120 .map(Self)
121 .map_err(|()| BasicUsernameError::AllocationFailed)
122 }
123}
124
125impl fmt::Debug for BasicUsername {
126 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127 formatter.write_str("BasicUsername([redacted])")
128 }
129}
130
131pub struct BasicPassword(SecretBytes);
133
134impl BasicPassword {
135 pub fn new(value: &str) -> Result<Self, BasicPasswordError> {
137 Self::from_bytes(value.as_bytes())
138 }
139
140 pub fn from_mut_bytes(value: &mut [u8]) -> Result<Self, BasicPasswordError> {
142 let result = Self::from_bytes(value);
143 sanitize_bytes(value);
144 result
145 }
146
147 pub fn from_secret_buffer(value: SecretBuffer<'_>) -> Result<Self, BasicPasswordError> {
149 Self::from_bytes(value.as_slice())
150 }
151
152 fn from_bytes(value: &[u8]) -> Result<Self, BasicPasswordError> {
153 validate_password(value)?;
154 SecretBytes::copy_from(value)
155 .map(Self)
156 .map_err(|()| BasicPasswordError::AllocationFailed)
157 }
158}
159
160impl fmt::Debug for BasicPassword {
161 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
162 formatter.write_str("BasicPassword([redacted])")
163 }
164}
165
166pub struct BasicCredential {
168 authorization: SecretBytes,
169 pub(crate) scope: BasicCredentialScope,
170}
171
172impl BasicCredential {
173 pub fn new(
175 username: BasicUsername,
176 password: BasicPassword,
177 scope: BasicCredentialScope,
178 ) -> Result<Self, BasicCredentialError> {
179 let mut user_pass = SecretBytes::with_capacity(
180 username
181 .0
182 .len()
183 .checked_add(1)
184 .and_then(|len| len.checked_add(password.0.len()))
185 .ok_or(BasicCredentialError::AuthorizationTooLong)?,
186 )
187 .map_err(|()| BasicCredentialError::AllocationFailed)?;
188 user_pass.extend(username.0.as_ref())?;
189 user_pass.push(b':')?;
190 user_pass.extend(password.0.as_ref())?;
191 let encoded_len = checked_encoded_len(user_pass.len(), true)
192 .ok_or(BasicCredentialError::AuthorizationTooLong)?;
193 let total_len = BASIC_PREFIX
194 .len()
195 .checked_add(encoded_len)
196 .ok_or(BasicCredentialError::AuthorizationTooLong)?;
197 if total_len > MAX_BASIC_AUTHORIZATION_BYTES {
198 return Err(BasicCredentialError::AuthorizationTooLong);
199 }
200 let mut authorization = SecretBytes::with_capacity(total_len)
201 .map_err(|()| BasicCredentialError::AllocationFailed)?;
202 authorization.extend(BASIC_PREFIX)?;
203 authorization.resize(total_len)?;
204 let destination = authorization
205 .as_mut()
206 .get_mut(BASIC_PREFIX.len()..)
207 .ok_or(BasicCredentialError::EncodingFailed)?;
208 let written = STANDARD
209 .encode_slice(user_pass.as_ref(), destination)
210 .map_err(|_| BasicCredentialError::EncodingFailed)?;
211 if written != encoded_len {
212 return Err(BasicCredentialError::EncodingFailed);
213 }
214 Ok(Self {
215 authorization,
216 scope,
217 })
218 }
219
220 pub fn from_mut_bytes(
222 username: &mut [u8],
223 password: &mut [u8],
224 scope: BasicCredentialScope,
225 ) -> Result<Self, BasicCredentialError> {
226 let username =
227 BasicUsername::from_mut_bytes(username).map_err(BasicCredentialError::UsernameRejected);
228 let password =
229 BasicPassword::from_mut_bytes(password).map_err(BasicCredentialError::PasswordRejected);
230 Self::new(username?, password?, scope)
231 }
232
233 pub(crate) fn header_value(&self) -> Result<HeaderValue, ()> {
234 sensitive_header_value(self.authorization.as_ref())
235 }
236
237 pub(crate) const fn scope(&self) -> &BasicCredentialScope {
238 &self.scope
239 }
240
241 #[cfg(test)]
242 pub(crate) fn owned_bytes(&self) -> &[u8] {
243 self.authorization.as_ref()
244 }
245}
246
247impl fmt::Debug for BasicCredential {
248 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
249 formatter.write_str("BasicCredential([redacted])")
250 }
251}
252
253struct SecretBytes(Vec<u8>);
254
255impl SecretBytes {
256 fn copy_from(value: &[u8]) -> Result<Self, ()> {
257 let mut bytes = Vec::new();
258 bytes.try_reserve_exact(value.len()).map_err(|_| ())?;
259 bytes.extend_from_slice(value);
260 Ok(Self(bytes))
261 }
262
263 fn with_capacity(capacity: usize) -> Result<Self, ()> {
264 let mut bytes = Vec::new();
265 bytes.try_reserve_exact(capacity).map_err(|_| ())?;
266 Ok(Self(bytes))
267 }
268
269 fn extend(&mut self, value: &[u8]) -> Result<(), BasicCredentialError> {
270 self.0
271 .try_reserve(value.len())
272 .map_err(|_| BasicCredentialError::AllocationFailed)?;
273 self.0.extend_from_slice(value);
274 Ok(())
275 }
276
277 fn push(&mut self, value: u8) -> Result<(), BasicCredentialError> {
278 self.0
279 .try_reserve(1)
280 .map_err(|_| BasicCredentialError::AllocationFailed)?;
281 self.0.push(value);
282 Ok(())
283 }
284
285 fn resize(&mut self, len: usize) -> Result<(), BasicCredentialError> {
286 let additional = len.saturating_sub(self.0.len());
287 self.0
288 .try_reserve(additional)
289 .map_err(|_| BasicCredentialError::AllocationFailed)?;
290 self.0.resize(len, 0);
291 Ok(())
292 }
293
294 fn len(&self) -> usize {
295 self.0.len()
296 }
297
298 fn as_mut(&mut self) -> &mut [u8] {
299 &mut self.0
300 }
301}
302
303impl AsRef<[u8]> for SecretBytes {
304 fn as_ref(&self) -> &[u8] {
305 &self.0
306 }
307}
308
309impl Drop for SecretBytes {
310 fn drop(&mut self) {
311 sanitize_bytes(&mut self.0);
312 }
313}
314
315fn validate_username(value: &[u8]) -> Result<(), BasicUsernameError> {
316 if value.is_empty() {
317 return Err(BasicUsernameError::Empty);
318 }
319 if value.len() > MAX_BASIC_USERNAME_BYTES {
320 return Err(BasicUsernameError::TooLong);
321 }
322 if !value
323 .iter()
324 .all(|byte| matches!(byte, b'!'..=b'~') && *byte != b':')
325 {
326 return Err(BasicUsernameError::InvalidByte);
327 }
328 Ok(())
329}
330
331fn validate_password(value: &[u8]) -> Result<(), BasicPasswordError> {
332 if value.is_empty() {
333 return Err(BasicPasswordError::Empty);
334 }
335 if value.len() > MAX_BASIC_PASSWORD_BYTES {
336 return Err(BasicPasswordError::TooLong);
337 }
338 if !value.iter().all(|byte| matches!(byte, b' '..=b'~')) {
339 return Err(BasicPasswordError::InvalidByte);
340 }
341 Ok(())
342}
343
344#[cfg(test)]
345mod tests {
346 use std::format;
347
348 use cloud_sdk::transport::CustomEndpointAcknowledgement;
349
350 use super::{
351 BasicCredential, BasicCredentialError, BasicPassword, BasicPasswordError, BasicUsername,
352 BasicUsernameError, MAX_BASIC_PASSWORD_BYTES, MAX_BASIC_USERNAME_BYTES,
353 };
354 use crate::shared::{BasicCredentialScope, HttpsEndpoint};
355
356 fn test_scope() -> Option<BasicCredentialScope> {
357 let endpoint = HttpsEndpoint::new_custom(
358 "https://robot-ws.your-server.de",
359 CustomEndpointAcknowledgement::trusted_operator_configuration(),
360 )
361 .ok()?;
362 Some(BasicCredentialScope::new(
363 cloud_sdk::provider_id!("hetzner"),
364 cloud_sdk::service_id!("robot"),
365 endpoint,
366 ))
367 }
368
369 #[test]
370 fn rfc_vector_is_exact_bounded_sensitive_and_redacted() {
371 let username = BasicUsername::new("Aladdin");
372 let password = BasicPassword::new("open sesame");
373 let (Ok(username), Ok(password), Some(scope)) = (username, password, test_scope()) else {
374 return;
375 };
376 let credential = BasicCredential::new(username, password, scope);
377 assert!(credential.is_ok());
378 if let Ok(credential) = credential {
379 assert_eq!(
380 credential.owned_bytes(),
381 b"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
382 );
383 assert!(!format!("{credential:?}").contains("Aladdin"));
384 let header = credential.header_value();
385 assert!(header.as_ref().is_ok_and(|value| value.is_sensitive()));
386 }
387 }
388
389 #[test]
390 fn username_rejects_colon_space_controls_non_ascii_and_bounds() {
391 for value in ["", "user:name", "user name", "user\nname", "anv\u{e4}ndare"] {
392 let result = BasicUsername::new(value);
393 let expected = if value.is_empty() {
394 BasicUsernameError::Empty
395 } else {
396 BasicUsernameError::InvalidByte
397 };
398 assert_eq!(result.map(|_| ()), Err(expected));
399 }
400 let accepted = [b'u'; MAX_BASIC_USERNAME_BYTES];
401 assert!(BasicUsername::from_bytes(&accepted).is_ok());
402 let rejected = [b'u'; MAX_BASIC_USERNAME_BYTES + 1];
403 assert_eq!(
404 BasicUsername::from_bytes(&rejected).map(|_| ()),
405 Err(BasicUsernameError::TooLong)
406 );
407 }
408
409 #[test]
410 fn password_allows_spaces_and_colons_but_rejects_controls_non_ascii_and_bounds() {
411 assert!(BasicPassword::new("open sesame:again").is_ok());
412 assert_eq!(
413 BasicPassword::new("").map(|_| ()),
414 Err(BasicPasswordError::Empty)
415 );
416 for value in ["line\nbreak", "l\u{f6}senord"] {
417 assert_eq!(
418 BasicPassword::new(value).map(|_| ()),
419 Err(BasicPasswordError::InvalidByte)
420 );
421 }
422 let accepted = [b'p'; MAX_BASIC_PASSWORD_BYTES];
423 assert!(BasicPassword::from_bytes(&accepted).is_ok());
424 let rejected = [b'p'; MAX_BASIC_PASSWORD_BYTES + 1];
425 assert_eq!(
426 BasicPassword::from_bytes(&rejected).map(|_| ()),
427 Err(BasicPasswordError::TooLong)
428 );
429 }
430
431 #[test]
432 fn mutable_sources_clear_on_success_and_rejection() {
433 let mut username = *b"robot-user";
434 let mut password = *b"secret-pass";
435 let Some(scope) = test_scope() else {
436 return;
437 };
438 assert!(BasicCredential::from_mut_bytes(&mut username, &mut password, scope).is_ok());
439 assert_eq!(username, [0; 10]);
440 assert_eq!(password, [0; 11]);
441
442 let mut invalid_username = *b"bad:user";
443 let mut valid_password = *b"password";
444 let Some(scope) = test_scope() else {
445 return;
446 };
447 assert_eq!(
448 BasicCredential::from_mut_bytes(&mut invalid_username, &mut valid_password, scope,)
449 .map(|_| ()),
450 Err(BasicCredentialError::UsernameRejected(
451 BasicUsernameError::InvalidByte
452 ))
453 );
454 assert_eq!(invalid_username, [0; 8]);
455 assert_eq!(valid_password, [0; 8]);
456 }
457
458 #[test]
459 fn exact_individual_bounds_fit_the_aggregate_authorization_limit() {
460 let username = [b'u'; MAX_BASIC_USERNAME_BYTES];
461 let password = [b'p'; MAX_BASIC_PASSWORD_BYTES];
462 let (Ok(username), Ok(password), Some(scope)) = (
463 BasicUsername::from_bytes(&username),
464 BasicPassword::from_bytes(&password),
465 test_scope(),
466 ) else {
467 return;
468 };
469 let credential = BasicCredential::new(username, password, scope);
470 assert!(credential.is_ok());
471 if let Ok(credential) = credential {
472 assert_eq!(credential.owned_bytes().len(), 3_082);
473 }
474 }
475}