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)| {
110 (
111 utf8_percent_encode(&key, FRAGMENT_SLASH).to_string(),
112 utf8_percent_encode(&value, FRAGMENT_SLASH).to_string(),
113 )
114 })
115 .collect();
116 keyvalues.sort();
117 let keyvalues: Vec<String> = keyvalues.iter().map(|(k, v)| format!("{k}={v}")).collect();
118 keyvalues.join("&")
119}
120
121pub fn canonical_header_string(headers: &HeaderMap) -> Result<String, S3Error> {
123 let mut keyvalues = vec![];
124 for (key, value) in headers.iter() {
125 keyvalues.push(format!(
126 "{}:{}",
127 key.as_str().to_lowercase(),
128 value.to_str()?.trim()
129 ))
130 }
131 keyvalues.sort();
132 Ok(keyvalues.join("\n"))
133}
134
135pub fn signed_header_string(headers: &HeaderMap) -> String {
137 let mut keys = headers
138 .keys()
139 .map(|key| key.as_str().to_lowercase())
140 .collect::<Vec<String>>();
141 keys.sort();
142 keys.join(";")
143}
144
145pub fn canonical_request(
147 method: &str,
148 url: &Url,
149 headers: &HeaderMap,
150 sha256: &str,
151) -> Result<String, S3Error> {
152 Ok(format!(
153 "{method}\n{uri}\n{query_string}\n{headers}\n\n{signed}\n{sha256}",
154 method = method,
155 uri = canonical_uri_string(url),
156 query_string = canonical_query_string(url),
157 headers = canonical_header_string(headers)?,
158 signed = signed_header_string(headers),
159 sha256 = sha256
160 ))
161}
162
163pub fn scope_string(datetime: &OffsetDateTime, region: &Region) -> Result<String, S3Error> {
165 Ok(format!(
166 "{date}/{region}/s3/aws4_request",
167 date = datetime.format(SHORT_DATE)?,
168 region = region
169 ))
170}
171
172pub fn string_to_sign(
175 datetime: &OffsetDateTime,
176 region: &Region,
177 canonical_req: &str,
178) -> Result<String, S3Error> {
179 let mut hasher = Sha256::default();
180 hasher.update(canonical_req.as_bytes());
181 let string_to = format!(
182 "AWS4-HMAC-SHA256\n{timestamp}\n{scope}\n{hash}",
183 timestamp = datetime.format(LONG_DATETIME)?,
184 scope = scope_string(datetime, region)?,
185 hash = hex::encode(hasher.finalize().as_slice())
186 );
187 Ok(string_to)
188}
189
190pub fn signing_key(
193 datetime: &OffsetDateTime,
194 secret_key: &str,
195 region: &Region,
196 service: &str,
197) -> Result<Vec<u8>, S3Error> {
198 let secret = format!("AWS4{}", secret_key);
199 let mut date_hmac = HmacSha256::new_from_slice(secret.as_bytes())?;
200 date_hmac.update(datetime.format(SHORT_DATE)?.as_bytes());
201 let mut region_hmac = HmacSha256::new_from_slice(&date_hmac.finalize().into_bytes())?;
202 region_hmac.update(region.to_string().as_bytes());
203 let mut service_hmac = HmacSha256::new_from_slice(®ion_hmac.finalize().into_bytes())?;
204 service_hmac.update(service.as_bytes());
205 let mut signing_hmac = HmacSha256::new_from_slice(&service_hmac.finalize().into_bytes())?;
206 signing_hmac.update(b"aws4_request");
207 Ok(signing_hmac.finalize().into_bytes().to_vec())
208}
209
210pub fn authorization_header(
212 access_key: &str,
213 datetime: &OffsetDateTime,
214 region: &Region,
215 signed_headers: &str,
216 signature: &str,
217) -> Result<String, S3Error> {
218 Ok(format!(
219 "AWS4-HMAC-SHA256 Credential={access_key}/{scope},\
220 SignedHeaders={signed_headers},Signature={signature}",
221 access_key = access_key,
222 scope = scope_string(datetime, region)?,
223 signed_headers = signed_headers,
224 signature = signature
225 ))
226}
227
228pub fn authorization_query_params_no_sig(
229 access_key: &str,
230 datetime: &OffsetDateTime,
231 region: &Region,
232 expires: u32,
233 custom_headers: Option<&HeaderMap>,
234 token: Option<&String>,
235) -> Result<String, S3Error> {
236 let credentials = format!("{}/{}", access_key, scope_string(datetime, region)?);
237 let credentials = utf8_percent_encode(&credentials, FRAGMENT_SLASH);
238
239 let mut signed_headers = vec!["host".to_string()];
240
241 if let Some(custom_headers) = &custom_headers {
242 for k in custom_headers.keys() {
243 signed_headers.push(k.to_string())
244 }
245 }
246
247 signed_headers.sort();
248 let signed_headers = signed_headers.join(";");
249 let signed_headers = utf8_percent_encode(&signed_headers, FRAGMENT_SLASH);
250
251 let mut query_params = format!(
252 "?X-Amz-Algorithm=AWS4-HMAC-SHA256\
253 &X-Amz-Credential={credentials}\
254 &X-Amz-Date={long_date}\
255 &X-Amz-Expires={expires}\
256 &X-Amz-SignedHeaders={signed_headers}",
257 credentials = credentials,
258 long_date = datetime.format(LONG_DATETIME)?,
259 expires = expires,
260 signed_headers = signed_headers,
261 );
262
263 if let Some(token) = token {
264 write!(
265 query_params,
266 "&X-Amz-Security-Token={}",
267 utf8_percent_encode(token, FRAGMENT_SLASH)
268 )
269 .expect("Could not write token");
270 }
271
272 Ok(query_params)
273}
274
275pub fn flatten_queries(queries: Option<&HashMap<String, String>>) -> Result<String, S3Error> {
276 match queries {
277 None => Ok(String::new()),
278 Some(queries) => {
279 let mut query_str = String::new();
280 for (k, v) in queries {
281 write!(
282 query_str,
283 "&{}={}",
284 utf8_percent_encode(k, FRAGMENT_SLASH),
285 utf8_percent_encode(v, FRAGMENT_SLASH),
286 )?;
287 }
288 Ok(query_str)
289 }
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use std::convert::TryInto;
296 use std::str;
297
298 use http::HeaderMap;
299 use http::header::{HOST, HeaderName, RANGE};
300 use time::Date;
301 use url::Url;
302
303 use crate::serde_types::ListBucketResult;
304
305 use super::*;
306
307 #[test]
308 fn test_base_url_encode() {
309 let url = Url::parse("http://s3.amazonaws.com/examplebucket///foo//bar//baz").unwrap();
312 let canonical = canonical_uri_string(&url);
313 assert_eq!("/examplebucket///foo//bar//baz", canonical);
314 }
315
316 #[test]
317 fn test_path_encode() {
318 let url = Url::parse("http://s3.amazonaws.com/bucket/Filename (xx)%=").unwrap();
319 let canonical = canonical_uri_string(&url);
320 assert_eq!("/bucket/Filename%20%28xx%29%25%3D", canonical);
321 }
322
323 #[test]
324 fn test_path_slash_encode() {
325 let url =
326 Url::parse("http://s3.amazonaws.com/bucket/Folder (xx)%=/Filename (xx)%=").unwrap();
327 let canonical = canonical_uri_string(&url);
328 assert_eq!(
329 "/bucket/Folder%20%28xx%29%25%3D/Filename%20%28xx%29%25%3D",
330 canonical
331 );
332 }
333
334 #[test]
335 fn test_query_string_encode() {
336 let url = Url::parse(
337 "http://s3.amazonaws.com/examplebucket?\
338 prefix=somePrefix&marker=someMarker&max-keys=20",
339 )
340 .unwrap();
341 let canonical = canonical_query_string(&url);
342 assert_eq!("marker=someMarker&max-keys=20&prefix=somePrefix", canonical);
343
344 let url = Url::parse("http://s3.amazonaws.com/examplebucket?acl").unwrap();
345 let canonical = canonical_query_string(&url);
346 assert_eq!("acl=", canonical);
347
348 let url = Url::parse(
349 "http://s3.amazonaws.com/examplebucket?\
350 key=with%20space&also+space=with+plus",
351 )
352 .unwrap();
353 let canonical = canonical_query_string(&url);
354 assert_eq!("also%20space=with%20plus&key=with%20space", canonical);
355
356 let url =
357 Url::parse("http://s3.amazonaws.com/examplebucket?key-with-postfix=something&key=")
358 .unwrap();
359 let canonical = canonical_query_string(&url);
360 assert_eq!("key=&key-with-postfix=something", canonical);
361
362 let url = Url::parse("http://s3.amazonaws.com/examplebucket?key=c&key=a&key=b").unwrap();
363 let canonical = canonical_query_string(&url);
364 assert_eq!("key=a&key=b&key=c", canonical);
365 }
366
367 #[test]
368 fn test_query_string_sorts_encoded_pairs() {
369 let url = Url::parse(
370 "http://s3.amazonaws.com/examplebucket?z=last&%C3%A9=accent&-=dash&%2F=slash&same=/&same=-&same=%C3%A9",
371 )
372 .unwrap();
373 let original_pairs = url
374 .query_pairs()
375 .map(|(key, value)| (key.into_owned(), value.into_owned()))
376 .collect::<Vec<_>>();
377 let canonical = canonical_query_string(&url);
378
379 assert_eq!(
380 "%2F=slash&%C3%A9=accent&-=dash&same=%2F&same=%C3%A9&same=-&z=last",
381 canonical
382 );
383 let mut canonical_pairs = Url::parse(&format!("http://example.com/?{canonical}"))
384 .unwrap()
385 .query_pairs()
386 .map(|(key, value)| (key.into_owned(), value.into_owned()))
387 .collect::<Vec<_>>();
388 let mut sorted_original_pairs = original_pairs;
389 canonical_pairs.sort();
390 sorted_original_pairs.sort();
391 assert_eq!(sorted_original_pairs, canonical_pairs);
392 assert_eq!(
393 vec![
394 ("z".to_owned(), "last".to_owned()),
395 ("é".to_owned(), "accent".to_owned()),
396 ("-".to_owned(), "dash".to_owned()),
397 ("/".to_owned(), "slash".to_owned()),
398 ("same".to_owned(), "/".to_owned()),
399 ("same".to_owned(), "-".to_owned()),
400 ("same".to_owned(), "é".to_owned()),
401 ],
402 url.query_pairs()
403 .map(|(key, value)| (key.into_owned(), value.into_owned()))
404 .collect::<Vec<_>>()
405 );
406 }
407
408 #[test]
409 fn test_headers_encode() {
410 let mut headers = HeaderMap::new();
411 headers.insert(
412 HeaderName::from_static("x-amz-date"),
413 "20130708T220855Z".parse().unwrap(),
414 );
415 headers.insert(HeaderName::from_static("foo"), "bAr".parse().unwrap());
416 headers.insert(HOST, "s3.amazonaws.com".parse().unwrap());
417 let canonical = canonical_header_string(&headers).unwrap();
418 let expected = "foo:bAr\nhost:s3.amazonaws.com\nx-amz-date:20130708T220855Z";
419 assert_eq!(expected, canonical);
420
421 let signed = signed_header_string(&headers);
422 assert_eq!("foo;host;x-amz-date", signed);
423 }
424
425 #[test]
426 fn test_aws_signing_key() {
427 let key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
428 let expected = "c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9";
429 let datetime = Date::from_calendar_date(2015, 8.try_into().unwrap(), 30)
430 .unwrap()
431 .with_hms(0, 0, 0)
432 .unwrap()
433 .assume_utc();
434 let signature = signing_key(&datetime, key, &"us-east-1".parse().unwrap(), "iam").unwrap();
435 assert_eq!(expected, hex::encode(signature));
436 }
437
438 const EXPECTED_SHA: &str = "e3b0c44298fc1c149afbf4c8996fb924\
439 27ae41e4649b934ca495991b7852b855";
440
441 #[rustfmt::skip]
442 const EXPECTED_CANONICAL_REQUEST: &str =
443 "GET\n\
444 /test.txt\n\
445 \n\
446 host:examplebucket.s3.amazonaws.com\n\
447 range:bytes=0-9\n\
448 x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n\
449 x-amz-date:20130524T000000Z\n\
450 \n\
451 host;range;x-amz-content-sha256;x-amz-date\n\
452 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
453
454 #[rustfmt::skip]
455 const EXPECTED_STRING_TO_SIGN: &str =
456 "AWS4-HMAC-SHA256\n\
457 20130524T000000Z\n\
458 20130524/us-east-1/s3/aws4_request\n\
459 7344ae5b7ee6c3e7e6b0fe0640412a37625d1fbfff95c48bbb2dc43964946972";
460
461 #[test]
462 fn test_signing() {
463 let url = Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap();
464 let mut headers = HeaderMap::new();
465 headers.insert(
466 HeaderName::from_static("x-amz-date"),
467 "20130524T000000Z".parse().unwrap(),
468 );
469 headers.insert(RANGE, "bytes=0-9".parse().unwrap());
470 headers.insert(HOST, "examplebucket.s3.amazonaws.com".parse().unwrap());
471 headers.insert(
472 HeaderName::from_static("x-amz-content-sha256"),
473 EXPECTED_SHA.parse().unwrap(),
474 );
475 let canonical = canonical_request("GET", &url, &headers, EXPECTED_SHA).unwrap();
476 assert_eq!(EXPECTED_CANONICAL_REQUEST, canonical);
477
478 let datetime = Date::from_calendar_date(2013, 5.try_into().unwrap(), 24)
479 .unwrap()
480 .with_hms(0, 0, 0)
481 .unwrap()
482 .assume_utc();
483 let string_to_sign =
484 string_to_sign(&datetime, &"us-east-1".parse().unwrap(), &canonical).unwrap();
485 assert_eq!(EXPECTED_STRING_TO_SIGN, string_to_sign);
486
487 let expected = "f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41";
488 let secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
489 let signing_key = signing_key(&datetime, secret, &"us-east-1".parse().unwrap(), "s3");
490 let mut hmac = Hmac::<Sha256>::new_from_slice(&signing_key.unwrap()).unwrap();
491 hmac.update(string_to_sign.as_bytes());
492 assert_eq!(expected, hex::encode(hmac.finalize().into_bytes()));
493 }
494
495 #[test]
496 fn test_parse_list_bucket_result() {
497 let result_string = r###"
498 <?xml version="1.0" encoding="UTF-8"?>
499 <ListBucketResult
500 xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
501 <Name>RelationalAI</Name>
502 <Prefix>/</Prefix>
503 <KeyCount>0</KeyCount>
504 <MaxKeys>1000</MaxKeys>
505 <IsTruncated>true</IsTruncated>
506 </ListBucketResult>
507 "###;
508 let deserialized: ListBucketResult =
509 quick_xml::de::from_reader(result_string.as_bytes()).expect("Parse error!");
510 assert!(deserialized.is_truncated);
511 }
512
513 #[test]
514 fn test_uri_encode() {
515 assert_eq!(
516 uri_encode(r#"~!@#$%^&*()-_=+[]\{}|;:'",.<>? привет 你好"#, true),
517 "~%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"
518 );
519 }
520}