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