aboutsummaryrefslogtreecommitdiff
path: root/src/api/s3
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/s3')
-rw-r--r--src/api/s3/encryption.rs199
-rw-r--r--src/api/s3/error.rs6
-rw-r--r--src/api/s3/mod.rs1
3 files changed, 206 insertions, 0 deletions
diff --git a/src/api/s3/encryption.rs b/src/api/s3/encryption.rs
new file mode 100644
index 00000000..4055a67f
--- /dev/null
+++ b/src/api/s3/encryption.rs
@@ -0,0 +1,199 @@
+use std::borrow::Cow;
+
+use aes_gcm::{
+ aead::{Aead, AeadCore, KeyInit, OsRng},
+ Aes256Gcm, Key, Nonce,
+};
+
+use http::header::{HeaderName, HeaderValue};
+use hyper::{body::Body, Request};
+
+use garage_model::garage::Garage;
+use garage_model::s3::object_table::{ObjectVersionEncryption, ObjectVersionHeaders};
+
+use crate::s3::error::Error;
+
+const X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM: HeaderName =
+ HeaderName::from_static("x-amz-server-side-encryption-customer-algorithm");
+const X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY: HeaderName =
+ HeaderName::from_static("x-amz-server-side-encryption-customer-key");
+const X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5: HeaderName =
+ HeaderName::from_static("x-amz-server-side-encryption-customer-key-MD5");
+
+const X_AMZ_COPY_SOURCE_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM: HeaderName =
+ HeaderName::from_static("x-amz-copy-source-server-side-encryption-customer-algorithm");
+const X_AMZ_COPY_SOURCE_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY: HeaderName =
+ HeaderName::from_static("x-amz-copy-source-server-side-encryption-customer-key");
+const X_AMZ_COPY_SOURCE_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5: HeaderName =
+ HeaderName::from_static("x-amz-copy-source-server-side-encryption-customer-key-MD5");
+
+const CUSTOMER_ALGORITHM_AES256: HeaderValue = HeaderValue::from_static("AES256");
+
+pub enum EncryptionParams {
+ Plaintext,
+ SseC {
+ client_key: Key<Aes256Gcm>,
+ compression_level: Option<i32>,
+ },
+}
+
+impl EncryptionParams {
+ pub fn new_from_req(
+ garage: &Garage,
+ req: &Request<impl Body>,
+ ) -> Result<EncryptionParams, Error> {
+ let key = parse_request_headers(
+ req,
+ &X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM,
+ &X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY,
+ &X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5,
+ )?;
+ match key {
+ Some(client_key) => Ok(EncryptionParams::SseC {
+ client_key: parse_and_check_key(req)?,
+ compression_level: garage.config.compression_level,
+ }),
+ None => Ok(EncryptionParams::Plaintext),
+ }
+ }
+
+ pub fn check_decrypt_for_get<'a>(
+ garage: &Garage,
+ req: &Request<impl Body>,
+ obj_enc: &'a ObjectVersionEncryption,
+ ) -> Result<(Self, Cow<'a, ObjectVersionHeaders>), Error> {
+ let key = parse_request_headers(
+ req,
+ &X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM,
+ &X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY,
+ &X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5,
+ )?;
+ Self::check_decrypt(garage, key, obj_enc)
+ }
+
+ pub fn check_decrypt_for_copy_source<'a>(
+ garage: &Garage,
+ req: &Request<impl Body>,
+ obj_enc: &'a ObjectVersionEncryption,
+ ) -> Result<(Self, Cow<'a, ObjectVersionHeaders>), Error> {
+ let key = parse_request_headers(
+ req,
+ &X_AMZ_COPY_SOURCE_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM,
+ &X_AMZ_COPY_SOURCE_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY,
+ &X_AMZ_COPY_SOURCE_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5,
+ )?;
+ Self::check_decrypt(garage, key, obj_enc)
+ }
+
+ fn check_decrypt<'a>(
+ garage: &Garage,
+ key: Option<Key<Aes256Gcm>>,
+ obj_enc: &'a ObjectVersionEncryption,
+ ) -> Result<(Self, Cow<'a, ObjectVersionHeaders>), Error> {
+ match (key, obj_enc) {
+ (
+ Some(client_key),
+ ObjectVersionEncryption::SseC {
+ headers,
+ compressed,
+ },
+ ) => {
+ let cipher = Aes256Gcm::new(&client_key);
+ let nonce: Nonce<Aes256Gcm::NonceSize> = headers
+ .get(..12)
+ .ok_or_internal_error("invalid encrypted data")?
+ .try_into()
+ .unwrap();
+ let plaintext = cipher.decrypt(&nonce, &headers[12..]).ok_or_bad_request(
+ "Invalid encryption key, could not decrypt object metadata.",
+ )?;
+ let headers = ObjectVersionHeaders::decode(&plaintext)?;
+ let enc = Self::SseC {
+ client_key,
+ compression_level: if compressed {
+ Some(garage.config.compression_level.unwrap_or(1))
+ } else {
+ None
+ },
+ };
+ Ok((enc, headers.into()))
+ }
+ (None, ObjectVersionEncryption::Plaintext { headers }) => {
+ Ok((Self::Plaintext, headers.into()))
+ }
+ (_, ObjectVersionEncryption::SseC { .. }) => {
+ Err(Error::bad_request("Object is encrypted"))
+ }
+ (Some(_), _) => {
+ // TODO: should this be an OK scenario?
+ Err(Error::bad_request("Trying to decrypt a plaintext object"))
+ }
+ }
+ }
+
+ pub fn encrypt_headers(
+ &self,
+ h: ObjectVersionHeaders,
+ ) -> Result<ObjectVersionEncryption, Error> {
+ match self {
+ Self::SseC {
+ client_key,
+ compression_level,
+ } => {
+ let cipher = Aes256Gcm::new(&client_key);
+ let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
+ let plaintext = h.encode()?;
+ let ciphertext = cipher
+ .encrypt(&nonce, &plaintext)
+ .ok_or_internal_error("Encryption failed")?;
+ let headers_enc = [nonce.to_vec(), ciphertext].concat();
+ Ok(ObjectVersionEncryption::SseC {
+ headers: headers_enc,
+ compressed: compression_level.is_some(),
+ })
+ }
+ Self::Plaintext => Ok(ObjectVersionEncryption::Plaintext { headers: h }),
+ }
+ }
+}
+
+fn parse_request_headers(
+ req: &Request<impl Body>,
+ alg_header: &HeaderName,
+ key_header: &HeaderName,
+ md5_header: &HeaderName,
+) -> Result<Option<Key<Aes256Gcm>>, Error> {
+ match req.headers().get(alg_header) {
+ Some(CUSTOMER_ALGORITHM_AES256) => {
+ use md5::{Digest, Md5};
+
+ let key_b64 = req
+ .headers()
+ .get(key_header)
+ .ok_or_bad_request(format!("Missing {} header", key_header))?;
+ let key_bytes: [u8; 32] = base64::decode(&key_b64)
+ .ok_or_bad_request(format!("Invalid {} header", key_header))?
+ .try_into()
+ .ok_or_bad_request(format!("Invalid {} header", key_header))?;
+
+ let md5_b64 = req
+ .headers()
+ .get(md5_header)
+ .ok_or_bad_request(format!("Missing {} header", md5_header))?;
+ let md5_bytes = base64::decode(&md5_b64)
+ .ok_or_bad_request(format!("Invalid {} header", md5_header))?;
+
+ let mut hasher = Md5::new();
+ hasher.update(&key_bytes[..]);
+ if hasher.finalize() != md5_bytes {
+ return Err(Error::bad_request(
+ "Encryption key MD5 checksum does not match",
+ ));
+ }
+
+ Ok(Some(key_bytes.into()))
+ }
+ Some(alg) => Err(Error::InvalidEncryptionAlgorithm(alg.to_string())),
+ None => Ok(None),
+ }
+}
diff --git a/src/api/s3/error.rs b/src/api/s3/error.rs
index f86c19a6..5cb5d04e 100644
--- a/src/api/s3/error.rs
+++ b/src/api/s3/error.rs
@@ -65,6 +65,10 @@ pub enum Error {
#[error(display = "Invalid HTTP range: {:?}", _0)]
InvalidRange(#[error(from)] (http_range::HttpRangeParseError, u64)),
+ /// The client sent a range header with invalid value
+ #[error(display = "Invalid encryption algorithm: {:?}, should be AES256", _0)]
+ InvalidEncryptionAlgorithm(String),
+
/// The client sent a request for an action not supported by garage
#[error(display = "Unimplemented action: {}", _0)]
NotImplemented(String),
@@ -126,6 +130,7 @@ impl Error {
Error::InvalidXml(_) => "MalformedXML",
Error::InvalidRange(_) => "InvalidRange",
Error::InvalidUtf8Str(_) | Error::InvalidUtf8String(_) => "InvalidRequest",
+ Error::InvalidEncryptionAlgorithm(_) => "InvalidEncryptionAlgorithmError",
}
}
}
@@ -143,6 +148,7 @@ impl ApiError for Error {
| Error::InvalidPart
| Error::InvalidPartOrder
| Error::EntityTooSmall
+ | Error::InvalidEncryptionAlgorithm(_)
| Error::InvalidXml(_)
| Error::InvalidUtf8Str(_)
| Error::InvalidUtf8String(_) => StatusCode::BAD_REQUEST,
diff --git a/src/api/s3/mod.rs b/src/api/s3/mod.rs
index cbdb94ab..1eb95d40 100644
--- a/src/api/s3/mod.rs
+++ b/src/api/s3/mod.rs
@@ -13,5 +13,6 @@ mod post_object;
mod put;
mod website;
+mod encryption;
mod router;
pub mod xml;