1use std::collections::HashMap;
33use std::str;
34
35use hmac::{Hmac, Mac};
36use http::HeaderMap;
37use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
38use sha2::{Digest, Sha256};
39use time::{OffsetDateTime, macros::format_description};
40use url::Url;
41
42use crate::LONG_DATETIME;
43use crate::error::S3Error;
44use crate::region::Region;
45
46use std::fmt::Write as _;
47
48const SHORT_DATE: &[time::format_description::FormatItem<'static>] =
49 format_description!("[year][month][day]");
50
51pub type HmacSha256 = Hmac<Sha256>;
52
53pub const FRAGMENT: &AsciiSet = &CONTROLS
55 .add(b':')
57 .add(b'?')
58 .add(b'#')
59 .add(b'[')
60 .add(b']')
61 .add(b'@')
62 .add(b'!')
63 .add(b'$')
64 .add(b'&')
65 .add(b'\'')
66 .add(b'(')
67 .add(b')')
68 .add(b'*')
69 .add(b'+')
70 .add(b',')
71 .add(b';')
72 .add(b'=')
73 .add(b'"')
75 .add(b' ')
76 .add(b'<')
77 .add(b'>')
78 .add(b'%')
79 .add(b'{')
80 .add(b'}')
81 .add(b'|')
82 .add(b'\\')
83 .add(b'^')
84 .add(b'`');
85
86pub const FRAGMENT_SLASH: &AsciiSet = &FRAGMENT.add(b'/');
87
88pub fn uri_encode(string: &str, encode_slash: bool) -> String {
90 if encode_slash {
91 utf8_percent_encode(string, FRAGMENT_SLASH).to_string()
92 } else {
93 utf8_percent_encode(string, FRAGMENT).to_string()
94 }
95}
96
97pub fn canonical_uri_string(uri: &Url) -> String {
99 let decoded = percent_encoding::percent_decode_str(uri.path()).decode_utf8_lossy();
102 uri_encode(&decoded, false)
103}
104
105pub fn canonical_query_string(uri: &Url) -> String {
107 let mut keyvalues: Vec<(String, String)> = uri
108 .query_pairs()
109 .map(|(key, value)| (key.to_string(), value.to_string()))
110 .collect();
111 keyvalues.sort();
112 let keyvalues: Vec<String> = keyvalues
113 .iter()
114 .map(|(k, v)| {
115 format!(
116 "{}={}",
117 utf8_percent_encode(k, FRAGMENT_SLASH),
118 utf8_percent_encode(v, FRAGMENT_SLASH)
119 )
120 })
121 .collect();
122 keyvalues.join("&")
123}
124
125pub fn canonical_header_string(headers: &HeaderMap) -> Result<String, S3Error> {
127 let mut keyvalues = vec![];
128 for (key, value) in headers.iter() {
129 keyvalues.push(format!(
130 "{}:{}",
131 key.as_str().to_lowercase(),
132 value.to_str()?.trim()
133 ))
134 }
135 keyvalues.sort();
136 Ok(keyvalues.join("\n"))
137}
138
139pub fn signed_header_string(headers: &HeaderMap) -> String {
141 let mut keys = headers
142 .keys()
143 .map(|key| key.as_str().to_lowercase())
144 .collect::<Vec<String>>();
145 keys.sort();
146 keys.join(";")
147}
148
149pub fn canonical_request(
151 method: &str,
152 url: &Url,
153 headers: &HeaderMap,
154 sha256: &str,
155) -> Result<String, S3Error> {
156 Ok(format!(
157 "{method}\n{uri}\n{query_string}\n{headers}\n\n{signed}\n{sha256}",
158 method = method,
159 uri = canonical_uri_string(url),
160 query_string = canonical_query_string(url),
161 headers = canonical_header_string(headers)?,
162 signed = signed_header_string(headers),
163 sha256 = sha256
164 ))
165}
166
167pub fn scope_string(datetime: &OffsetDateTime, region: &Region) -> Result<String, S3Error> {
169 Ok(format!(
170 "{date}/{region}/s3/aws4_request",
171 date = datetime.format(SHORT_DATE)?,
172 region = region
173 ))
174}
175
176pub fn string_to_sign(
179 datetime: &OffsetDateTime,
180 region: &Region,
181 canonical_req: &str,
182) -> Result<String, S3Error> {
183 let mut hasher = Sha256::default();
184 hasher.update(canonical_req.as_bytes());
185 let string_to = format!(
186 "AWS4-HMAC-SHA256\n{timestamp}\n{scope}\n{hash}",
187 timestamp = datetime.format(LONG_DATETIME)?,
188 scope = scope_string(datetime, region)?,
189 hash = hex::encode(hasher.finalize().as_slice())
190 );
191 Ok(string_to)
192}
193
194pub fn signing_key(
197 datetime: &OffsetDateTime,
198 secret_key: &str,
199 region: &Region,
200 service: &str,
201) -> Result<Vec<u8>, S3Error> {
202 let secret = format!("AWS4{}", secret_key);
203 let mut date_hmac = HmacSha256::new_from_slice(secret.as_bytes())?;
204 date_hmac.update(datetime.format(SHORT_DATE)?.as_bytes());
205 let mut region_hmac = HmacSha256::new_from_slice(&date_hmac.finalize().into_bytes())?;
206 region_hmac.update(region.to_string().as_bytes());
207 let mut service_hmac = HmacSha256::new_from_slice(®ion_hmac.finalize().into_bytes())?;
208 service_hmac.update(service.as_bytes());
209 let mut signing_hmac = HmacSha256::new_from_slice(&service_hmac.finalize().into_bytes())?;
210 signing_hmac.update(b"aws4_request");
211 Ok(signing_hmac.finalize().into_bytes().to_vec())
212}
213
214pub fn authorization_header(
216 access_key: &str,
217 datetime: &OffsetDateTime,
218 region: &Region,
219 signed_headers: &str,
220 signature: &str,
221) -> Result<String, S3Error> {
222 Ok(format!(
223 "AWS4-HMAC-SHA256 Credential={access_key}/{scope},\
224 SignedHeaders={signed_headers},Signature={signature}",
225 access_key = access_key,
226 scope = scope_string(datetime, region)?,
227 signed_headers = signed_headers,
228 signature = signature
229 ))
230}
231
232pub fn authorization_query_params_no_sig(
233 access_key: &str,
234 datetime: &OffsetDateTime,
235 region: &Region,
236 expires: u32,
237 custom_headers: Option<&HeaderMap>,
238 token: Option<&String>,
239) -> Result<String, S3Error> {
240 let credentials = format!("{}/{}", access_key, scope_string(datetime, region)?);
241 let credentials = utf8_percent_encode(&credentials, FRAGMENT_SLASH);
242
243 let mut signed_headers = vec!["host".to_string()];
244
245 if let Some(custom_headers) = &custom_headers {
246 for k in custom_headers.keys() {
247 signed_headers.push(k.to_string())
248 }
249 }
250
251 signed_headers.sort();
252 let signed_headers = signed_headers.join(";");
253 let signed_headers = utf8_percent_encode(&signed_headers, FRAGMENT_SLASH);
254
255 let mut query_params = format!(
256 "?X-Amz-Algorithm=AWS4-HMAC-SHA256\
257 &X-Amz-Credential={credentials}\
258 &X-Amz-Date={long_date}\
259 &X-Amz-Expires={expires}\
260 &X-Amz-SignedHeaders={signed_headers}",
261 credentials = credentials,
262 long_date = datetime.format(LONG_DATETIME)?,
263 expires = expires,
264 signed_headers = signed_headers,
265 );
266
267 if let Some(token) = token {
268 write!(
269 query_params,
270 "&X-Amz-Security-Token={}",
271 utf8_percent_encode(token, FRAGMENT_SLASH)
272 )
273 .expect("Could not write token");
274 }
275
276 Ok(query_params)
277}
278
279pub fn flatten_queries(queries: Option<&HashMap<String, String>>) -> Result<String, S3Error> {
280 match queries {
281 None => Ok(String::new()),
282 Some(queries) => {
283 let mut query_str = String::new();
284 for (k, v) in queries {
285 write!(
286 query_str,
287 "&{}={}",
288 utf8_percent_encode(k, FRAGMENT_SLASH),
289 utf8_percent_encode(v, FRAGMENT_SLASH),
290 )?;
291 }
292 Ok(query_str)
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use std::convert::TryInto;
300 use std::str;
301
302 use http::HeaderMap;
303 use http::header::{HOST, HeaderName, RANGE};
304 use time::Date;
305 use url::Url;
306
307 use crate::serde_types::ListBucketResult;
308
309 use super::*;
310
311 #[test]
312 fn test_base_url_encode() {
313 let url = Url::parse("http://s3.amazonaws.com/examplebucket///foo//bar//baz").unwrap();
316 let canonical = canonical_uri_string(&url);
317 assert_eq!("/examplebucket///foo//bar//baz", canonical);
318 }
319
320 #[test]
321 fn test_path_encode() {
322 let url = Url::parse("http://s3.amazonaws.com/bucket/Filename (xx)%=").unwrap();
323 let canonical = canonical_uri_string(&url);
324 assert_eq!("/bucket/Filename%20%28xx%29%25%3D", canonical);
325 }
326
327 #[test]
328 fn test_path_slash_encode() {
329 let url =
330 Url::parse("http://s3.amazonaws.com/bucket/Folder (xx)%=/Filename (xx)%=").unwrap();
331 let canonical = canonical_uri_string(&url);
332 assert_eq!(
333 "/bucket/Folder%20%28xx%29%25%3D/Filename%20%28xx%29%25%3D",
334 canonical
335 );
336 }
337
338 #[test]
339 fn test_query_string_encode() {
340 let url = Url::parse(
341 "http://s3.amazonaws.com/examplebucket?\
342 prefix=somePrefix&marker=someMarker&max-keys=20",
343 )
344 .unwrap();
345 let canonical = canonical_query_string(&url);
346 assert_eq!("marker=someMarker&max-keys=20&prefix=somePrefix", canonical);
347
348 let url = Url::parse("http://s3.amazonaws.com/examplebucket?acl").unwrap();
349 let canonical = canonical_query_string(&url);
350 assert_eq!("acl=", canonical);
351
352 let url = Url::parse(
353 "http://s3.amazonaws.com/examplebucket?\
354 key=with%20space&also+space=with+plus",
355 )
356 .unwrap();
357 let canonical = canonical_query_string(&url);
358 assert_eq!("also%20space=with%20plus&key=with%20space", canonical);
359
360 let url =
361 Url::parse("http://s3.amazonaws.com/examplebucket?key-with-postfix=something&key=")
362 .unwrap();
363 let canonical = canonical_query_string(&url);
364 assert_eq!("key=&key-with-postfix=something", canonical);
365
366 let url = Url::parse("http://s3.amazonaws.com/examplebucket?key=c&key=a&key=b").unwrap();
367 let canonical = canonical_query_string(&url);
368 assert_eq!("key=a&key=b&key=c", canonical);
369 }
370
371 #[test]
372 fn test_headers_encode() {
373 let mut headers = HeaderMap::new();
374 headers.insert(
375 HeaderName::from_static("x-amz-date"),
376 "20130708T220855Z".parse().unwrap(),
377 );
378 headers.insert(HeaderName::from_static("foo"), "bAr".parse().unwrap());
379 headers.insert(HOST, "s3.amazonaws.com".parse().unwrap());
380 let canonical = canonical_header_string(&headers).unwrap();
381 let expected = "foo:bAr\nhost:s3.amazonaws.com\nx-amz-date:20130708T220855Z";
382 assert_eq!(expected, canonical);
383
384 let signed = signed_header_string(&headers);
385 assert_eq!("foo;host;x-amz-date", signed);
386 }
387
388 #[test]
389 fn test_aws_signing_key() {
390 let key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
391 let expected = "c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9";
392 let datetime = Date::from_calendar_date(2015, 8.try_into().unwrap(), 30)
393 .unwrap()
394 .with_hms(0, 0, 0)
395 .unwrap()
396 .assume_utc();
397 let signature = signing_key(&datetime, key, &"us-east-1".parse().unwrap(), "iam").unwrap();
398 assert_eq!(expected, hex::encode(signature));
399 }
400
401 const EXPECTED_SHA: &str = "e3b0c44298fc1c149afbf4c8996fb924\
402 27ae41e4649b934ca495991b7852b855";
403
404 #[rustfmt::skip]
405 const EXPECTED_CANONICAL_REQUEST: &str =
406 "GET\n\
407 /test.txt\n\
408 \n\
409 host:examplebucket.s3.amazonaws.com\n\
410 range:bytes=0-9\n\
411 x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n\
412 x-amz-date:20130524T000000Z\n\
413 \n\
414 host;range;x-amz-content-sha256;x-amz-date\n\
415 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
416
417 #[rustfmt::skip]
418 const EXPECTED_STRING_TO_SIGN: &str =
419 "AWS4-HMAC-SHA256\n\
420 20130524T000000Z\n\
421 20130524/us-east-1/s3/aws4_request\n\
422 7344ae5b7ee6c3e7e6b0fe0640412a37625d1fbfff95c48bbb2dc43964946972";
423
424 #[test]
425 fn test_signing() {
426 let url = Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap();
427 let mut headers = HeaderMap::new();
428 headers.insert(
429 HeaderName::from_static("x-amz-date"),
430 "20130524T000000Z".parse().unwrap(),
431 );
432 headers.insert(RANGE, "bytes=0-9".parse().unwrap());
433 headers.insert(HOST, "examplebucket.s3.amazonaws.com".parse().unwrap());
434 headers.insert(
435 HeaderName::from_static("x-amz-content-sha256"),
436 EXPECTED_SHA.parse().unwrap(),
437 );
438 let canonical = canonical_request("GET", &url, &headers, EXPECTED_SHA).unwrap();
439 assert_eq!(EXPECTED_CANONICAL_REQUEST, canonical);
440
441 let datetime = Date::from_calendar_date(2013, 5.try_into().unwrap(), 24)
442 .unwrap()
443 .with_hms(0, 0, 0)
444 .unwrap()
445 .assume_utc();
446 let string_to_sign =
447 string_to_sign(&datetime, &"us-east-1".parse().unwrap(), &canonical).unwrap();
448 assert_eq!(EXPECTED_STRING_TO_SIGN, string_to_sign);
449
450 let expected = "f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41";
451 let secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
452 let signing_key = signing_key(&datetime, secret, &"us-east-1".parse().unwrap(), "s3");
453 let mut hmac = Hmac::<Sha256>::new_from_slice(&signing_key.unwrap()).unwrap();
454 hmac.update(string_to_sign.as_bytes());
455 assert_eq!(expected, hex::encode(hmac.finalize().into_bytes()));
456 }
457
458 #[test]
459 fn test_parse_list_bucket_result() {
460 let result_string = r###"
461 <?xml version="1.0" encoding="UTF-8"?>
462 <ListBucketResult
463 xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
464 <Name>RelationalAI</Name>
465 <Prefix>/</Prefix>
466 <KeyCount>0</KeyCount>
467 <MaxKeys>1000</MaxKeys>
468 <IsTruncated>true</IsTruncated>
469 </ListBucketResult>
470 "###;
471 let deserialized: ListBucketResult =
472 quick_xml::de::from_reader(result_string.as_bytes()).expect("Parse error!");
473 assert!(deserialized.is_truncated);
474 }
475
476 #[test]
477 fn test_uri_encode() {
478 assert_eq!(
479 uri_encode(r#"~!@#$%^&*()-_=+[]\{}|;:'",.<>? привет 你好"#, true),
480 "~%21%40%23%24%25%5E%26%2A%28%29-_%3D%2B%5B%5D%5C%7B%7D%7C%3B%3A%27%22%2C.%3C%3E%3F%20%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%E4%BD%A0%E5%A5%BD"
481 );
482 }
483}