aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock103
-rw-r--r--Cargo.toml1
-rw-r--r--src/api/Cargo.toml1
-rw-r--r--src/api/s3/encryption.rs199
-rw-r--r--src/api/s3/error.rs6
-rw-r--r--src/api/s3/mod.rs1
-rw-r--r--src/model/s3/object_table.rs3
7 files changed, 313 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 7ce1ca1d..0ed568c3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -18,6 +18,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
+[[package]]
name = "ahash"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -750,6 +785,16 @@ dependencies = [
]
[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
name = "clap"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -917,10 +962,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
+ "rand_core",
"typenum",
]
[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
name = "darling"
version = "0.20.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1321,6 +1376,7 @@ dependencies = [
name = "garage_api"
version = "0.10.0"
dependencies = [
+ "aes-gcm",
"async-trait",
"base64 0.21.7",
"bytes",
@@ -1602,6 +1658,16 @@ dependencies = [
]
[[package]]
+name = "ghash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
name = "gimli"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2051,6 +2117,15 @@ dependencies = [
]
[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2631,6 +2706,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2957,6 +3038,18 @@ dependencies = [
]
[[package]]
+name = "polyval"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4376,6 +4469,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
name = "unsafe-libyaml"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index ec89c325..648ddcd7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -65,6 +65,7 @@ sha2 = "0.10"
timeago = { version = "0.4", default-features = false }
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
+aes-gcm = { version = "0.10", features = ["aes", "stream"] }
sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" }
kuska-handshake = { version = "0.2.0", features = ["default", "async_std"] }
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;
diff --git a/src/model/s3/object_table.rs b/src/model/s3/object_table.rs
index 91f83419..20824e8d 100644
--- a/src/model/s3/object_table.rs
+++ b/src/model/s3/object_table.rs
@@ -285,7 +285,8 @@ mod v010 {
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Serialize, Deserialize)]
pub enum ObjectVersionEncryption {
SseC {
- /// Encrypted serialized ObjectVersionHeaders struct
+ /// Encrypted serialized ObjectVersionHeaders struct.
+ /// This is never compressed, just encrypted using AES256-GCM.
#[serde(with = "serde_bytes")]
headers: Vec<u8>,
/// Whether data blocks are compressed in addition to being encrypted