Skip to main content

cloud_sdk_reqwest/shared/
basic.rs

1use core::fmt;
2use std::vec::Vec;
3
4use aws_lc_rs::rand::{SecureRandom, SystemRandom};
5use base64_ng::{STANDARD, checked_encoded_len};
6use cloud_sdk::authentication::{CREDENTIAL_BINDING_BYTES, CredentialBinding};
7use cloud_sdk_sanitization::{SecretBuffer, sanitize_bytes};
8use reqwest::header::HeaderValue;
9
10use super::{BasicCredentialScope, sensitive_header_value};
11
12/// Maximum Basic username bytes accepted by the adapter.
13pub const MAX_BASIC_USERNAME_BYTES: usize = 256;
14/// Maximum Basic password bytes accepted by the adapter.
15pub const MAX_BASIC_PASSWORD_BYTES: usize = 2048;
16/// Maximum complete `Authorization: Basic ...` value bytes.
17pub const MAX_BASIC_AUTHORIZATION_BYTES: usize = 4096;
18
19const BASIC_PREFIX: &[u8] = b"Basic ";
20
21/// Basic username validation failure.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum BasicUsernameError {
24    /// Usernames must not be empty.
25    Empty,
26    /// Usernames exceed [`MAX_BASIC_USERNAME_BYTES`].
27    TooLong,
28    /// Usernames must be visible ASCII and must not contain a colon.
29    InvalidByte,
30    /// Adapter-owned secret storage could not be allocated.
31    AllocationFailed,
32}
33
34impl_static_error!(BasicUsernameError,
35    Self::Empty => "Basic username is empty",
36    Self::TooLong => "Basic username exceeds the length limit",
37    Self::InvalidByte => "Basic username contains an invalid byte",
38    Self::AllocationFailed => "Basic username allocation failed",
39);
40
41/// Basic password validation failure.
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum BasicPasswordError {
44    /// Passwords must not be empty.
45    Empty,
46    /// Passwords exceed [`MAX_BASIC_PASSWORD_BYTES`].
47    TooLong,
48    /// Passwords must use the source-locked ASCII interoperability profile.
49    InvalidByte,
50    /// Adapter-owned secret storage could not be allocated.
51    AllocationFailed,
52}
53
54impl_static_error!(BasicPasswordError,
55    Self::Empty => "Basic password is empty",
56    Self::TooLong => "Basic password exceeds the length limit",
57    Self::InvalidByte => "Basic password contains an invalid byte",
58    Self::AllocationFailed => "Basic password allocation failed",
59);
60
61/// Basic credential construction failure.
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum BasicCredentialError {
64    /// The username was rejected.
65    UsernameRejected(BasicUsernameError),
66    /// The password was rejected.
67    PasswordRejected(BasicPasswordError),
68    /// The encoded authorization value exceeds its aggregate cap.
69    AuthorizationTooLong,
70    /// Adapter-owned authorization storage could not be allocated.
71    AllocationFailed,
72    /// The admitted RFC 4648 encoder rejected the exact-sized destination.
73    EncodingFailed,
74    /// The operating-system CSPRNG could not mint a credential binding.
75    BindingGenerationFailed,
76}
77
78impl fmt::Display for BasicCredentialError {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        formatter.write_str(match self {
81            Self::UsernameRejected(_) => "Basic username was rejected",
82            Self::PasswordRejected(_) => "Basic password was rejected",
83            Self::AuthorizationTooLong => "Basic authorization exceeds the length limit",
84            Self::AllocationFailed => "Basic authorization allocation failed",
85            Self::EncodingFailed => "Basic authorization encoding failed",
86            Self::BindingGenerationFailed => "Basic credential binding generation failed",
87        })
88    }
89}
90
91impl core::error::Error for BasicCredentialError {
92    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
93        match self {
94            Self::UsernameRejected(error) => Some(error),
95            Self::PasswordRejected(error) => Some(error),
96            Self::AuthorizationTooLong
97            | Self::AllocationFailed
98            | Self::EncodingFailed
99            | Self::BindingGenerationFailed => None,
100        }
101    }
102}
103
104/// Owned Basic username with redacted diagnostics and drop-time cleanup.
105pub struct BasicUsername(SecretBytes);
106
107impl BasicUsername {
108    /// Validates and copies an immutable username.
109    pub fn new(value: &str) -> Result<Self, BasicUsernameError> {
110        Self::from_bytes(value.as_bytes())
111    }
112
113    /// Validates mutable input and clears the complete source on return.
114    pub fn from_mut_bytes(value: &mut [u8]) -> Result<Self, BasicUsernameError> {
115        let result = Self::from_bytes(value);
116        sanitize_bytes(value);
117        result
118    }
119
120    /// Consumes guarded input, which clears its complete source on return.
121    pub fn from_secret_buffer(value: SecretBuffer<'_>) -> Result<Self, BasicUsernameError> {
122        Self::from_bytes(value.as_slice())
123    }
124
125    fn from_bytes(value: &[u8]) -> Result<Self, BasicUsernameError> {
126        validate_username(value)?;
127        SecretBytes::copy_from(value)
128            .map(Self)
129            .map_err(|()| BasicUsernameError::AllocationFailed)
130    }
131}
132
133impl fmt::Debug for BasicUsername {
134    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135        formatter.write_str("BasicUsername([redacted])")
136    }
137}
138
139/// Owned Basic password with redacted diagnostics and drop-time cleanup.
140pub struct BasicPassword(SecretBytes);
141
142impl BasicPassword {
143    /// Validates and copies an immutable password.
144    pub fn new(value: &str) -> Result<Self, BasicPasswordError> {
145        Self::from_bytes(value.as_bytes())
146    }
147
148    /// Validates mutable input and clears the complete source on return.
149    pub fn from_mut_bytes(value: &mut [u8]) -> Result<Self, BasicPasswordError> {
150        let result = Self::from_bytes(value);
151        sanitize_bytes(value);
152        result
153    }
154
155    /// Consumes guarded input, which clears its complete source on return.
156    pub fn from_secret_buffer(value: SecretBuffer<'_>) -> Result<Self, BasicPasswordError> {
157        Self::from_bytes(value.as_slice())
158    }
159
160    fn from_bytes(value: &[u8]) -> Result<Self, BasicPasswordError> {
161        validate_password(value)?;
162        SecretBytes::copy_from(value)
163            .map(Self)
164            .map_err(|()| BasicPasswordError::AllocationFailed)
165    }
166}
167
168impl fmt::Debug for BasicPassword {
169    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170        formatter.write_str("BasicPassword([redacted])")
171    }
172}
173
174/// Type-separated Basic credential and immutable authentication scope.
175pub struct BasicCredential {
176    authorization: SecretBytes,
177    pub(crate) scope: BasicCredentialScope,
178    binding: CredentialBinding,
179}
180
181impl BasicCredential {
182    /// Encodes a validated username/password pair with RFC 4648 padding.
183    pub fn new(
184        username: BasicUsername,
185        password: BasicPassword,
186        scope: BasicCredentialScope,
187    ) -> Result<Self, BasicCredentialError> {
188        let mut user_pass = SecretBytes::with_capacity(
189            username
190                .0
191                .len()
192                .checked_add(1)
193                .and_then(|len| len.checked_add(password.0.len()))
194                .ok_or(BasicCredentialError::AuthorizationTooLong)?,
195        )
196        .map_err(|()| BasicCredentialError::AllocationFailed)?;
197        user_pass.extend(username.0.as_ref())?;
198        user_pass.push(b':')?;
199        user_pass.extend(password.0.as_ref())?;
200        let encoded_len = checked_encoded_len(user_pass.len(), true)
201            .ok_or(BasicCredentialError::AuthorizationTooLong)?;
202        let total_len = BASIC_PREFIX
203            .len()
204            .checked_add(encoded_len)
205            .ok_or(BasicCredentialError::AuthorizationTooLong)?;
206        if total_len > MAX_BASIC_AUTHORIZATION_BYTES {
207            return Err(BasicCredentialError::AuthorizationTooLong);
208        }
209        let mut authorization = SecretBytes::with_capacity(total_len)
210            .map_err(|()| BasicCredentialError::AllocationFailed)?;
211        authorization.extend(BASIC_PREFIX)?;
212        authorization.resize(total_len)?;
213        let destination = authorization
214            .as_mut()
215            .get_mut(BASIC_PREFIX.len()..)
216            .ok_or(BasicCredentialError::EncodingFailed)?;
217        let written = STANDARD
218            .encode_slice(user_pass.as_ref(), destination)
219            .map_err(|_| BasicCredentialError::EncodingFailed)?;
220        if written != encoded_len {
221            return Err(BasicCredentialError::EncodingFailed);
222        }
223        let mut binding = [0_u8; CREDENTIAL_BINDING_BYTES];
224        SystemRandom::new()
225            .fill(&mut binding)
226            .map_err(|_| BasicCredentialError::BindingGenerationFailed)?;
227        let binding = CredentialBinding::new(binding)
228            .map_err(|_| BasicCredentialError::BindingGenerationFailed)?;
229        Ok(Self {
230            authorization,
231            scope,
232            binding,
233        })
234    }
235
236    /// Validates mutable sources, clears both, and constructs one credential.
237    pub fn from_mut_bytes(
238        username: &mut [u8],
239        password: &mut [u8],
240        scope: BasicCredentialScope,
241    ) -> Result<Self, BasicCredentialError> {
242        let username =
243            BasicUsername::from_mut_bytes(username).map_err(BasicCredentialError::UsernameRejected);
244        let password =
245            BasicPassword::from_mut_bytes(password).map_err(BasicCredentialError::PasswordRejected);
246        Self::new(username?, password?, scope)
247    }
248
249    pub(crate) fn header_value(&self) -> Result<HeaderValue, ()> {
250        sensitive_header_value(self.authorization.as_ref())
251    }
252
253    pub(crate) const fn scope(&self) -> &BasicCredentialScope {
254        &self.scope
255    }
256
257    pub(crate) const fn binding(&self) -> CredentialBinding {
258        self.binding
259    }
260
261    #[cfg(test)]
262    pub(crate) fn owned_bytes(&self) -> &[u8] {
263        self.authorization.as_ref()
264    }
265}
266
267impl fmt::Debug for BasicCredential {
268    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
269        formatter.write_str("BasicCredential([redacted])")
270    }
271}
272
273struct SecretBytes(Vec<u8>);
274
275impl SecretBytes {
276    fn copy_from(value: &[u8]) -> Result<Self, ()> {
277        let mut bytes = Vec::new();
278        bytes.try_reserve_exact(value.len()).map_err(|_| ())?;
279        bytes.extend_from_slice(value);
280        Ok(Self(bytes))
281    }
282
283    fn with_capacity(capacity: usize) -> Result<Self, ()> {
284        let mut bytes = Vec::new();
285        bytes.try_reserve_exact(capacity).map_err(|_| ())?;
286        Ok(Self(bytes))
287    }
288
289    fn extend(&mut self, value: &[u8]) -> Result<(), BasicCredentialError> {
290        self.0
291            .try_reserve(value.len())
292            .map_err(|_| BasicCredentialError::AllocationFailed)?;
293        self.0.extend_from_slice(value);
294        Ok(())
295    }
296
297    fn push(&mut self, value: u8) -> Result<(), BasicCredentialError> {
298        self.0
299            .try_reserve(1)
300            .map_err(|_| BasicCredentialError::AllocationFailed)?;
301        self.0.push(value);
302        Ok(())
303    }
304
305    fn resize(&mut self, len: usize) -> Result<(), BasicCredentialError> {
306        let additional = len.saturating_sub(self.0.len());
307        self.0
308            .try_reserve(additional)
309            .map_err(|_| BasicCredentialError::AllocationFailed)?;
310        self.0.resize(len, 0);
311        Ok(())
312    }
313
314    fn len(&self) -> usize {
315        self.0.len()
316    }
317
318    fn as_mut(&mut self) -> &mut [u8] {
319        &mut self.0
320    }
321}
322
323impl AsRef<[u8]> for SecretBytes {
324    fn as_ref(&self) -> &[u8] {
325        &self.0
326    }
327}
328
329impl Drop for SecretBytes {
330    fn drop(&mut self) {
331        sanitize_bytes(&mut self.0);
332    }
333}
334
335fn validate_username(value: &[u8]) -> Result<(), BasicUsernameError> {
336    if value.is_empty() {
337        return Err(BasicUsernameError::Empty);
338    }
339    if value.len() > MAX_BASIC_USERNAME_BYTES {
340        return Err(BasicUsernameError::TooLong);
341    }
342    if !value
343        .iter()
344        .all(|byte| matches!(byte, b'!'..=b'~') && *byte != b':')
345    {
346        return Err(BasicUsernameError::InvalidByte);
347    }
348    Ok(())
349}
350
351fn validate_password(value: &[u8]) -> Result<(), BasicPasswordError> {
352    if value.is_empty() {
353        return Err(BasicPasswordError::Empty);
354    }
355    if value.len() > MAX_BASIC_PASSWORD_BYTES {
356        return Err(BasicPasswordError::TooLong);
357    }
358    if !value.iter().all(|byte| matches!(byte, b' '..=b'~')) {
359        return Err(BasicPasswordError::InvalidByte);
360    }
361    Ok(())
362}
363
364#[cfg(test)]
365mod tests;