1#![cfg_attr(not(feature = "std"), no_std)]
2#![doc = include_str!("../README.md")]
3
4#[macro_use]
5extern crate alloc;
6
7pub use pink::chain_extension::HttpResponse;
8
9use scale::{Decode, Encode};
10use alloc::{
14 borrow::ToOwned,
15 string::{String, ToString},
16 vec::Vec,
17};
18use hmac::{Hmac, Mac};
19use sha2::Digest;
20use sha2::Sha256;
21
22#[derive(Encode, Decode, Debug, PartialEq, Eq, Copy, Clone)]
23#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
24pub enum Error {
25 RequestFailed(u16),
26 InvalidEndpoint,
27}
28
29pub struct S3<'a> {
31 endpoint: &'a str,
32 region: &'a str,
33 access_key: &'a str,
34 secret_key: &'a str,
35 virtual_host_mode: bool,
36}
37
38impl<'a> S3<'a> {
39 pub fn new(
41 endpoint: &'a str,
42 region: &'a str,
43 access_key: &'a str,
44 secret_key: &'a str,
45 ) -> Result<Self, Error> {
46 Ok(Self {
47 endpoint,
48 region,
49 access_key,
50 secret_key,
51 virtual_host_mode: false,
52 })
53 }
54
55 pub fn virtual_host_mode(self) -> Self {
59 Self {
60 virtual_host_mode: true,
61 ..self
62 }
63 }
64
65 pub fn head(&self, bucket_name: &str, object_key: &str) -> Result<HttpResponse, Error> {
69 self.request("HEAD", bucket_name, object_key, None)
70 }
71
72 pub fn get(&self, bucket_name: &str, object_key: &str) -> Result<HttpResponse, Error> {
76 self.request("GET", bucket_name, object_key, None)
77 }
78
79 pub fn put(
81 &self,
82 bucket_name: &str,
83 object_key: &str,
84 value: &[u8],
85 ) -> Result<HttpResponse, Error> {
86 self.request("PUT", bucket_name, object_key, Some(value))
87 }
88
89 pub fn delete(&self, bucket_name: &str, object_key: &str) -> Result<HttpResponse, Error> {
93 self.request("DELETE", bucket_name, object_key, None)
94 }
95
96 fn request(
97 &self,
98 method: &str,
99 bucket_name: &str,
100 object_key: &str,
101 value: Option<&[u8]>,
102 ) -> Result<HttpResponse, Error> {
103 let service = "s3";
105 let payload_hash = format!("{:x}", Sha256::digest(value.unwrap_or_default()));
106
107 let host = if self.virtual_host_mode {
108 format!("{bucket_name}.{}", self.endpoint)
109 } else {
110 self.endpoint.to_owned()
111 };
112
113 let (datestamp, amz_date) = times();
115
116 let canonical_uri = if self.virtual_host_mode {
118 format!("/{object_key}")
119 } else {
120 format!("/{bucket_name}/{object_key}")
121 };
122 let canonical_querystring = "";
123 let canonical_headers =
124 format!("host:{host}\nx-amz-content-sha256:{payload_hash}\nx-amz-date:{amz_date}\n");
125 let signed_headers = "host;x-amz-content-sha256;x-amz-date";
126 let canonical_request = format!(
127 "{method}\n{canonical_uri}\n{canonical_querystring}\n{canonical_headers}\n{signed_headers}\n{payload_hash}"
128 );
129
130 let algorithm = "AWS4-HMAC-SHA256";
132 let credential_scope = format!("{datestamp}/{}/{service}/aws4_request", self.region);
133 let canonical_request_hash = format!("{:x}", Sha256::digest(canonical_request.as_bytes()));
134 let string_to_sign =
135 format!("{algorithm}\n{amz_date}\n{credential_scope}\n{canonical_request_hash}");
136
137 let signature_key = get_signature_key(
139 self.secret_key.as_bytes(),
140 datestamp.as_bytes(),
141 self.region.as_bytes(),
142 service.as_bytes(),
143 );
144 let signature_bytes = hmac_sign(&signature_key, string_to_sign.as_bytes());
145 let signature = base16::encode_lower(&signature_bytes);
146
147 let authorization_header = format!(
149 "{} Credential={}/{}, SignedHeaders={}, Signature={}",
150 algorithm, self.access_key, credential_scope, signed_headers, signature
151 );
152
153 let mut headers: Vec<(String, String)> = vec![
154 ("Authorization".into(), authorization_header),
155 ("x-amz-content-sha256".into(), payload_hash),
156 ("x-amz-date".into(), amz_date),
157 ];
158
159 let body = if let Some(value) = value {
160 headers.push(("Content-Length".into(), format!("{}", &value.len())));
161 headers.push(("Content-Type".into(), "binary/octet-stream".into()));
162 value
163 } else {
164 &[]
165 };
166
167 let request_url = format!("https://{host}{canonical_uri}");
169 let response = pink::http_req!(method, request_url, body.to_vec(), headers);
170
171 if response.status_code / 100 != 2 {
172 return Err(Error::RequestFailed(response.status_code));
173 }
174
175 Ok(response)
176 }
177}
178
179fn times() -> (String, String) {
180 #[cfg(test)]
182 let datetime = chrono::Utc::now();
183 #[cfg(not(test))]
184 let datetime = {
185 use chrono::{TimeZone, Utc};
186 let time = pink::ext().untrusted_millis_since_unix_epoch() / 1000;
187 Utc.timestamp_opt(time as _, 0)
188 .earliest()
189 .expect("Could not convert timestamp to Utc")
190 };
191
192 let datestamp = datetime.format("%Y%m%d").to_string();
194 let datetimestamp = datetime.format("%Y%m%dT%H%M%SZ").to_string();
195
196 (datestamp, datetimestamp)
197}
198
199type HmacSha256 = Hmac<Sha256>;
201
202fn hmac_sign(key: &[u8], msg: &[u8]) -> Vec<u8> {
204 let mut mac =
205 <HmacSha256 as Mac>::new_from_slice(key).expect("Could not instantiate HMAC instance");
206 mac.update(msg);
207 let result = mac.finalize().into_bytes();
208 result.to_vec()
209}
210
211fn get_signature_key(
213 key: &[u8],
214 datestamp: &[u8],
215 region_name: &[u8],
216 service_name: &[u8],
217) -> Vec<u8> {
218 let k_date = hmac_sign(&[b"AWS4", key].concat(), datestamp);
219 let k_region = hmac_sign(&k_date, region_name);
220 let k_service = hmac_sign(&k_region, service_name);
221 hmac_sign(&k_service, b"aws4_request")
222}