diff options
Diffstat (limited to 'src/api')
-rw-r--r-- | src/api/Cargo.toml | 1 | ||||
-rw-r--r-- | src/api/s3/encryption.rs | 199 | ||||
-rw-r--r-- | src/api/s3/error.rs | 6 | ||||
-rw-r--r-- | src/api/s3/mod.rs | 1 |
4 files changed, 207 insertions, 0 deletions
diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 3b555b8b..c759d5a4 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -21,6 +21,7 @@ garage_net.workspace = true garage_util.workspace = true garage_rpc.workspace = true +aes-gcm.workspace = true async-trait.workspace = true base64.workspace = true bytes.workspace = true 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; |