Merge pull request 'sse-c: use different object encryption key for each object (fix #848)' (#990) from fix-ssec-crypto into next-v2
All checks were successful
ci/woodpecker/push/debug Pipeline was successful
ci/woodpecker/pr/debug Pipeline was successful

Reviewed-on: #990
This commit is contained in:
Alex 2025-03-19 13:12:24 +00:00
commit fb6db494cc
11 changed files with 266 additions and 61 deletions

1
Cargo.lock generated
View file

@ -1403,6 +1403,7 @@ dependencies = [
"garage_table",
"garage_util",
"hex",
"hmac",
"http 1.2.0",
"http-body-util",
"http-range",

View file

@ -30,6 +30,11 @@ elif (echo $OLD_VERSION | grep 'v0\.9\.') || (echo $OLD_VERSION | grep 'v1\.');
export GARAGE_OLDVER=v1
fi
if echo $OLD_VERSION | grep 'v1\.'; then
DO_SSEC_TEST=1
fi
SSEC_KEY="u8zCfnEyt5Imo/krN+sxA1DQXxLWtPJavU6T6gOVj1Y="
echo "⏳ Setup cluster using old version"
$GARAGE_BIN --version
${SCRIPT_FOLDER}/dev-clean.sh
@ -40,7 +45,23 @@ ${SCRIPT_FOLDER}/dev-bucket.sh
echo "🛠️ Inserting data in old cluster"
source ${SCRIPT_FOLDER}/dev-env-rclone.sh
rclone copy "${SCRIPT_FOLDER}/../.git/" garage:eprouvette/test_dotgit --stats=1s --stats-log-level=NOTICE --stats-one-line
rclone copy "${SCRIPT_FOLDER}/../.git/" garage:eprouvette/test_dotgit \
--stats=1s --stats-log-level=NOTICE --stats-one-line
if [ "$DO_SSEC_TEST" = "1" ]; then
# upload small file (should be single part)
rclone copy "${SCRIPT_FOLDER}/test-upgrade.sh" garage:eprouvette/test-ssec \
--s3-sse-customer-algorithm AES256 \
--s3-sse-customer-key-base64 "$SSEC_KEY" \
--stats=1s --stats-log-level=NOTICE --stats-one-line
# do a multipart upload
dd if=/dev/urandom of=/tmp/randfile-for-upgrade bs=5M count=5
rclone copy "/tmp/randfile-for-upgrade" garage:eprouvette/test-ssec \
--s3-chunk-size 5M \
--s3-sse-customer-algorithm AES256 \
--s3-sse-customer-key-base64 "$SSEC_KEY" \
--stats=1s --stats-log-level=NOTICE --stats-one-line
fi
echo "🏁 Stopping old cluster"
killall -INT old_garage
@ -63,7 +84,8 @@ ${SCRIPT_FOLDER}/dev-cluster.sh >> /tmp/garage.log 2>&1 &
sleep 3
echo "🛠️ Retrieving data from old cluster"
rclone copy garage:eprouvette/test_dotgit /tmp/test_dotgit --stats=1s --stats-log-level=NOTICE --stats-one-line --fast-list
rclone copy garage:eprouvette/test_dotgit /tmp/test_dotgit \
--stats=1s --stats-log-level=NOTICE --stats-one-line --fast-list
if ! diff <(find "${SCRIPT_FOLDER}/../.git" -type f | xargs md5sum | cut -d ' ' -f 1 | sort) <(find /tmp/test_dotgit -type f | xargs md5sum | cut -d ' ' -f 1 | sort); then
echo "TEST FAILURE: directories are different"
@ -71,6 +93,23 @@ if ! diff <(find "${SCRIPT_FOLDER}/../.git" -type f | xargs md5sum | cut -d ' '
fi
rm -r /tmp/test_dotgit
if [ "$DO_SSEC_TEST" = "1" ]; then
rclone copy garage:eprouvette/test-ssec /tmp/test_ssec_out \
--s3-sse-customer-algorithm AES256 \
--s3-sse-customer-key-base64 "$SSEC_KEY" \
--stats=1s --stats-log-level=NOTICE --stats-one-line
if ! diff "/tmp/test_ssec_out/test-upgrade.sh" "${SCRIPT_FOLDER}/test-upgrade.sh"; then
echo "SSEC-FAILURE (small file)"
exit 1
fi
if ! diff "/tmp/test_ssec_out/randfile-for-upgrade" "/tmp/randfile-for-upgrade"; then
echo "SSEC-FAILURE (big file)"
exit 1
fi
rm -r /tmp/test_ssec_out
rm /tmp/randfile-for-upgrade
fi
echo "🏁 Teardown"
rm -rf /tmp/garage-{data,meta}-*
rm -rf /tmp/config.*.toml

View file

@ -31,6 +31,7 @@ crc32fast.workspace = true
crc32c.workspace = true
err-derive.workspace = true
hex.workspace = true
hmac.workspace = true
tracing.workspace = true
md-5.workspace = true
pin-project.workspace = true

View file

@ -24,7 +24,7 @@ use garage_api_common::helpers::*;
use garage_api_common::signature::checksum::*;
use crate::api_server::{ReqBody, ResBody};
use crate::encryption::EncryptionParams;
use crate::encryption::{EncryptionParams, OekDerivationInfo};
use crate::error::*;
use crate::get::{full_object_byte_stream, PreconditionHeaders};
use crate::multipart;
@ -65,8 +65,18 @@ pub async fn handle_copy(
&ctx.garage,
req.headers(),
&source_version_meta.encryption,
OekDerivationInfo::for_object(&source_object, source_version),
)?;
let dest_encryption = EncryptionParams::new_from_headers(&ctx.garage, req.headers())?;
let dest_uuid = gen_uuid();
let dest_encryption = EncryptionParams::new_from_headers(
&ctx.garage,
req.headers(),
OekDerivationInfo {
bucket_id: ctx.bucket_id,
version_id: dest_uuid,
object_key: dest_key,
},
)?;
// Extract source checksum info before source_object_meta_inner is consumed
let source_checksum = source_object_meta_inner.checksum;
@ -115,6 +125,7 @@ pub async fn handle_copy(
handle_copy_metaonly(
ctx,
dest_key,
dest_uuid,
dest_object_meta,
dest_encryption,
source_version,
@ -138,6 +149,7 @@ pub async fn handle_copy(
handle_copy_reencrypt(
ctx,
dest_key,
dest_uuid,
dest_object_meta,
dest_encryption,
source_version,
@ -169,6 +181,7 @@ pub async fn handle_copy(
async fn handle_copy_metaonly(
ctx: ReqCtx,
dest_key: &str,
dest_uuid: Uuid,
dest_object_meta: ObjectVersionMetaInner,
dest_encryption: EncryptionParams,
source_version: &ObjectVersion,
@ -182,7 +195,6 @@ async fn handle_copy_metaonly(
} = ctx;
// Generate parameters for copied object
let new_uuid = gen_uuid();
let new_timestamp = now_msec();
let new_meta = ObjectVersionMeta {
@ -192,7 +204,7 @@ async fn handle_copy_metaonly(
};
let res = SaveStreamResult {
version_uuid: new_uuid,
version_uuid: dest_uuid,
version_timestamp: new_timestamp,
etag: new_meta.etag.clone(),
};
@ -204,7 +216,7 @@ async fn handle_copy_metaonly(
// bytes is either plaintext before&after or encrypted with the
// same keys, so it's ok to just copy it as is
let dest_object_version = ObjectVersion {
uuid: new_uuid,
uuid: dest_uuid,
timestamp: new_timestamp,
state: ObjectVersionState::Complete(ObjectVersionData::Inline(
new_meta,
@ -230,7 +242,7 @@ async fn handle_copy_metaonly(
// This holds a reference to the object in the Version table
// so that it won't be deleted, e.g. by repair_versions.
let tmp_dest_object_version = ObjectVersion {
uuid: new_uuid,
uuid: dest_uuid,
timestamp: new_timestamp,
state: ObjectVersionState::Uploading {
encryption: new_meta.encryption.clone(),
@ -250,7 +262,7 @@ async fn handle_copy_metaonly(
// marked as deleted (they are marked as deleted only if the Version
// doesn't exist or is marked as deleted).
let mut dest_version = Version::new(
new_uuid,
dest_uuid,
VersionBacklink::Object {
bucket_id: dest_bucket_id,
key: dest_key.to_string(),
@ -269,7 +281,7 @@ async fn handle_copy_metaonly(
.iter()
.map(|b| BlockRef {
block: b.1.hash,
version: new_uuid,
version: dest_uuid,
deleted: false.into(),
})
.collect::<Vec<_>>();
@ -285,7 +297,7 @@ async fn handle_copy_metaonly(
// with the stuff before, the block's reference counts could be decremented before
// they are incremented again for the new version, leading to data being deleted.
let dest_object_version = ObjectVersion {
uuid: new_uuid,
uuid: dest_uuid,
timestamp: new_timestamp,
state: ObjectVersionState::Complete(ObjectVersionData::FirstBlock(
new_meta,
@ -307,6 +319,7 @@ async fn handle_copy_metaonly(
async fn handle_copy_reencrypt(
ctx: ReqCtx,
dest_key: &str,
dest_uuid: Uuid,
dest_object_meta: ObjectVersionMetaInner,
dest_encryption: EncryptionParams,
source_version: &ObjectVersion,
@ -326,6 +339,7 @@ async fn handle_copy_reencrypt(
save_stream(
&ctx,
dest_uuid,
dest_object_meta,
dest_encryption,
source_stream.map_err(|e| Error::from(GarageError::from(e))),
@ -349,7 +363,7 @@ pub async fn handle_upload_part_copy(
let dest_upload_id = multipart::decode_upload_id(upload_id)?;
let dest_key = dest_key.to_string();
let (source_object, (_, dest_version, mut dest_mpu)) = futures::try_join!(
let (source_object, (dest_object, dest_version, mut dest_mpu)) = futures::try_join!(
get_copy_source(&ctx, req),
multipart::get_upload(&ctx, &dest_key, &dest_upload_id)
)?;
@ -367,7 +381,10 @@ pub async fn handle_upload_part_copy(
&garage,
req.headers(),
&source_version_meta.encryption,
OekDerivationInfo::for_object(&source_object, source_object_version),
)?;
let dest_oek_params = OekDerivationInfo::for_object(&dest_object, &dest_version);
let (dest_object_encryption, dest_object_checksum_algorithm) = match dest_version.state {
ObjectVersionState::Uploading {
encryption,
@ -376,8 +393,12 @@ pub async fn handle_upload_part_copy(
} => (encryption, checksum_algorithm),
_ => unreachable!(),
};
let (dest_encryption, _) =
EncryptionParams::check_decrypt(&garage, req.headers(), &dest_object_encryption)?;
let (dest_encryption, _) = EncryptionParams::check_decrypt(
&garage,
req.headers(),
&dest_object_encryption,
dest_oek_params,
)?;
let same_encryption = EncryptionParams::is_same(&source_encryption, &dest_encryption);
// Check source range is valid

View file

@ -11,6 +11,7 @@ use aes_gcm::{
};
use base64::prelude::*;
use bytes::Bytes;
use sha2::Sha256;
use futures::stream::Stream;
use futures::task;
@ -21,12 +22,12 @@ use http::header::{HeaderMap, HeaderName, HeaderValue};
use garage_net::bytes_buf::BytesBuf;
use garage_net::stream::{stream_asyncread, ByteStream};
use garage_rpc::rpc_helper::OrderTag;
use garage_util::data::Hash;
use garage_util::data::{Hash, Uuid};
use garage_util::error::Error as GarageError;
use garage_util::migrate::Migrate;
use garage_model::garage::Garage;
use garage_model::s3::object_table::{ObjectVersionEncryption, ObjectVersionMetaInner};
use garage_model::s3::object_table::*;
use garage_api_common::common_error::*;
use garage_api_common::signature::checksum::Md5Checksum;
@ -64,32 +65,45 @@ const STREAM_ENC_CYPER_CHUNK_SIZE: usize = STREAM_ENC_PLAIN_CHUNK_SIZE + 16;
pub enum EncryptionParams {
Plaintext,
SseC {
/// the value of x-amz-server-side-encryption-customer-key
client_key: Key<Aes256Gcm>,
/// the value of x-amz-server-side-encryption-customer-key-md5
client_key_md5: Md5Output,
/// the object encryption key, for uploads created in garage v2+
object_key: Option<Key<Aes256Gcm>>,
/// the compression level used for compressing data blocks
compression_level: Option<i32>,
},
}
#[derive(Clone, Copy)]
pub struct OekDerivationInfo<'a> {
pub bucket_id: Uuid,
pub version_id: Uuid,
pub object_key: &'a str,
}
impl EncryptionParams {
pub fn is_encrypted(&self) -> bool {
!matches!(self, Self::Plaintext)
}
pub fn is_same(a: &Self, b: &Self) -> bool {
let relevant_info = |x: &Self| match x {
Self::Plaintext => None,
Self::SseC {
client_key,
compression_level,
..
} => Some((*client_key, compression_level.is_some())),
};
relevant_info(a) == relevant_info(b)
// This function is used in CopyObject and UploadPartCopy to determine
// whether the object must be re-encrypted. If this returns true,
// data blocks are reused as-is. Since Garage v2, we are using
// object-specific encryption keys, so we know that if both source
// and destination are encrypted, it can't be with the same key.
match (a, b) {
(Self::Plaintext, Self::Plaintext) => true,
_ => false,
}
}
pub fn new_from_headers(
garage: &Garage,
headers: &HeaderMap,
oek_info: OekDerivationInfo<'_>,
) -> Result<EncryptionParams, Error> {
let key = parse_request_headers(
headers,
@ -101,6 +115,7 @@ impl EncryptionParams {
Some((client_key, client_key_md5)) => Ok(EncryptionParams::SseC {
client_key,
client_key_md5,
object_key: Some(oek_info.derive_oek(&client_key)),
compression_level: garage.config.compression_level,
}),
None => Ok(EncryptionParams::Plaintext),
@ -126,6 +141,7 @@ impl EncryptionParams {
garage: &Garage,
headers: &HeaderMap,
obj_enc: &'a ObjectVersionEncryption,
oek_info: OekDerivationInfo<'_>,
) -> Result<(Self, Cow<'a, ObjectVersionMetaInner>), Error> {
let key = parse_request_headers(
headers,
@ -133,13 +149,14 @@ impl EncryptionParams {
&X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY,
&X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5,
)?;
Self::check_decrypt_common(garage, key, obj_enc)
Self::check_decrypt_common(garage, key, obj_enc, oek_info)
}
pub fn check_decrypt_for_copy_source<'a>(
garage: &Garage,
headers: &HeaderMap,
obj_enc: &'a ObjectVersionEncryption,
oek_info: OekDerivationInfo<'_>,
) -> Result<(Self, Cow<'a, ObjectVersionMetaInner>), Error> {
let key = parse_request_headers(
headers,
@ -147,22 +164,32 @@ impl EncryptionParams {
&X_AMZ_COPY_SOURCE_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY,
&X_AMZ_COPY_SOURCE_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5,
)?;
Self::check_decrypt_common(garage, key, obj_enc)
Self::check_decrypt_common(garage, key, obj_enc, oek_info)
}
fn check_decrypt_common<'a>(
garage: &Garage,
key: Option<(Key<Aes256Gcm>, Md5Output)>,
obj_enc: &'a ObjectVersionEncryption,
oek_info: OekDerivationInfo<'_>,
) -> Result<(Self, Cow<'a, ObjectVersionMetaInner>), Error> {
match (key, &obj_enc) {
(
Some((client_key, client_key_md5)),
ObjectVersionEncryption::SseC { inner, compressed },
ObjectVersionEncryption::SseC {
inner,
compressed,
use_oek,
},
) => {
let enc = Self::SseC {
client_key,
client_key_md5,
object_key: if *use_oek {
Some(oek_info.derive_oek(&client_key))
} else {
None
},
compression_level: if *compressed {
Some(garage.config.compression_level.unwrap_or(1))
} else {
@ -193,13 +220,16 @@ impl EncryptionParams {
) -> Result<ObjectVersionEncryption, Error> {
match self {
Self::SseC {
compression_level, ..
compression_level,
object_key,
..
} => {
let plaintext = meta.encode().map_err(GarageError::from)?;
let ciphertext = self.encrypt_blob(&plaintext)?;
Ok(ObjectVersionEncryption::SseC {
inner: ciphertext.into_owned(),
compressed: compression_level.is_some(),
use_oek: object_key.is_some(),
})
}
Self::Plaintext => Ok(ObjectVersionEncryption::Plaintext { inner: meta }),
@ -228,24 +258,37 @@ impl EncryptionParams {
// This is used for encrypting object metadata and inlined data for small objects.
// This does not compress anything.
pub fn encrypt_blob<'a>(&self, blob: &'a [u8]) -> Result<Cow<'a, [u8]>, Error> {
fn cipher(&self) -> Option<Aes256Gcm> {
match self {
Self::SseC { client_key, .. } => {
let cipher = Aes256Gcm::new(&client_key);
Self::SseC {
object_key: Some(oek),
..
} => Some(Aes256Gcm::new(&oek)),
Self::SseC {
client_key,
object_key: None,
..
} => Some(Aes256Gcm::new(&client_key)),
Self::Plaintext => None,
}
}
pub fn encrypt_blob<'a>(&self, blob: &'a [u8]) -> Result<Cow<'a, [u8]>, Error> {
match self.cipher() {
Some(cipher) => {
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let ciphertext = cipher
.encrypt(&nonce, blob)
.ok_or_internal_error("Encryption failed")?;
Ok(Cow::Owned([nonce.to_vec(), ciphertext].concat()))
}
Self::Plaintext => Ok(Cow::Borrowed(blob)),
None => Ok(Cow::Borrowed(blob)),
}
}
pub fn decrypt_blob<'a>(&self, blob: &'a [u8]) -> Result<Cow<'a, [u8]>, Error> {
match self {
Self::SseC { client_key, .. } => {
let cipher = Aes256Gcm::new(&client_key);
match self.cipher() {
Some(cipher) => {
let nonce_size = <Aes256Gcm as AeadCore>::NonceSize::to_usize();
let nonce = Nonce::from_slice(
blob.get(..nonce_size)
@ -258,7 +301,7 @@ impl EncryptionParams {
)?;
Ok(Cow::Owned(plaintext))
}
Self::Plaintext => Ok(Cow::Borrowed(blob)),
None => Ok(Cow::Borrowed(blob)),
}
}
@ -284,10 +327,12 @@ impl EncryptionParams {
Self::Plaintext => stream,
Self::SseC {
client_key,
object_key,
compression_level,
..
} => {
let plaintext = DecryptStream::new(stream, *client_key);
let key = object_key.as_ref().unwrap_or(client_key);
let plaintext = DecryptStream::new(stream, *key);
if compression_level.is_some() {
let reader = stream_asyncread(Box::pin(plaintext));
let reader = BufReader::new(reader);
@ -307,9 +352,12 @@ impl EncryptionParams {
Self::Plaintext => Ok(block),
Self::SseC {
client_key,
object_key,
compression_level,
..
} => {
let key = object_key.as_ref().unwrap_or(client_key);
let block = if let Some(level) = compression_level {
Cow::Owned(
garage_block::zstd_encode(block.as_ref(), *level)
@ -325,7 +373,7 @@ impl EncryptionParams {
OsRng.fill_bytes(&mut nonce);
ret.extend_from_slice(nonce.as_slice());
let mut cipher = EncryptorLE31::<Aes256Gcm>::new(&client_key, &nonce);
let mut cipher = EncryptorLE31::<Aes256Gcm>::new(key, &nonce);
let mut iter = block.chunks(STREAM_ENC_PLAIN_CHUNK_SIZE).peekable();
if iter.peek().is_none() {
@ -361,6 +409,13 @@ impl EncryptionParams {
}
}
pub fn has_encryption_header(headers: &HeaderMap) -> bool {
match headers.get(X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM) {
Some(h) => h.as_bytes() == CUSTOMER_ALGORITHM_AES256,
None => false,
}
}
fn parse_request_headers(
headers: &HeaderMap,
alg_header: &HeaderName,
@ -420,6 +475,30 @@ fn parse_request_headers(
}
}
impl<'a> OekDerivationInfo<'a> {
pub fn for_object<'b>(object: &'a Object, version: &'b ObjectVersion) -> Self {
Self {
bucket_id: object.bucket_id,
version_id: version.uuid,
object_key: &object.key,
}
}
fn derive_oek(&self, client_key: &Key<Aes256Gcm>) -> Key<Aes256Gcm> {
use hmac::{Hmac, Mac};
// info = bucket_id + object_name + version_uuid + "garage-object-encryption-key"
// oek = hmac_sha256(ssec_key, info)
let mut hmac = <Hmac<Sha256> as Mac>::new_from_slice(client_key.as_slice())
.expect("create hmac-sha256");
hmac.update(b"garage-object-encryption-key");
hmac.update(self.bucket_id.as_slice());
hmac.update(self.version_id.as_slice());
hmac.update(self.object_key.as_bytes());
hmac.finalize().into_bytes()
}
}
// ---- encrypt & decrypt streams ----
#[pin_project::pin_project]
@ -569,6 +648,7 @@ mod tests {
let enc = EncryptionParams::SseC {
client_key: Aes256Gcm::generate_key(&mut OsRng),
client_key_md5: Default::default(), // not needed
object_key: Some(Aes256Gcm::generate_key(&mut OsRng)),
compression_level,
};

View file

@ -30,7 +30,7 @@ use garage_api_common::signature::checksum::{add_checksum_response_headers, X_AM
use crate::api_server::ResBody;
use crate::copy::*;
use crate::encryption::EncryptionParams;
use crate::encryption::{EncryptionParams, OekDerivationInfo};
use crate::error::*;
const X_AMZ_MP_PARTS_COUNT: HeaderName = HeaderName::from_static("x-amz-mp-parts-count");
@ -181,8 +181,12 @@ pub async fn handle_head_without_ctx(
return Ok(res);
}
let (encryption, headers) =
EncryptionParams::check_decrypt(&garage, req.headers(), &version_meta.encryption)?;
let (encryption, headers) = EncryptionParams::check_decrypt(
&garage,
req.headers(),
&version_meta.encryption,
OekDerivationInfo::for_object(&object, object_version),
)?;
let checksum_mode = checksum_mode(&req);
@ -303,8 +307,12 @@ pub async fn handle_get_without_ctx(
return Ok(res);
}
let (enc, headers) =
EncryptionParams::check_decrypt(&garage, req.headers(), &last_v_meta.encryption)?;
let (enc, headers) = EncryptionParams::check_decrypt(
&garage,
req.headers(),
&last_v_meta.encryption,
OekDerivationInfo::for_object(&object, last_v),
)?;
let checksum_mode = checksum_mode(&req);

View file

@ -17,7 +17,7 @@ use garage_api_common::encoding::*;
use garage_api_common::helpers::*;
use crate::api_server::{ReqBody, ResBody};
use crate::encryption::EncryptionParams;
use crate::encryption::{EncryptionParams, OekDerivationInfo};
use crate::error::*;
use crate::multipart as s3_multipart;
use crate::xml as s3_xml;
@ -285,8 +285,16 @@ pub async fn handle_list_parts(
ObjectVersionState::Uploading { encryption, .. } => encryption,
_ => unreachable!(),
};
let encryption_res =
EncryptionParams::check_decrypt(&ctx.garage, req.headers(), &object_encryption);
let encryption_res = EncryptionParams::check_decrypt(
&ctx.garage,
req.headers(),
&object_encryption,
OekDerivationInfo {
bucket_id: ctx.bucket_id,
version_id: upload_id,
object_key: &query.key,
},
);
let (info, next) = fetch_part_info(query, &mpu)?;

View file

@ -26,7 +26,7 @@ use garage_api_common::helpers::*;
use garage_api_common::signature::checksum::*;
use crate::api_server::{ReqBody, ResBody};
use crate::encryption::EncryptionParams;
use crate::encryption::{has_encryption_header, EncryptionParams, OekDerivationInfo};
use crate::error::*;
use crate::put::*;
use crate::xml as s3_xml;
@ -56,7 +56,15 @@ pub async fn handle_create_multipart_upload(
};
// Determine whether object should be encrypted, and if so the key
let encryption = EncryptionParams::new_from_headers(&garage, req.headers())?;
let encryption = EncryptionParams::new_from_headers(
&garage,
req.headers(),
OekDerivationInfo {
bucket_id: *bucket_id,
version_id: upload_id,
object_key: &key,
},
)?;
let object_encryption = encryption.encrypt_meta(meta)?;
let checksum_algorithm = request_checksum_algorithm(req.headers())?;
@ -120,8 +128,7 @@ pub async fn handle_put_part(
// Before we stream the body, configure the needed checksums.
req_body.add_expected_checksums(expected_checksums.clone());
// TODO: avoid parsing encryption headers twice...
if !EncryptionParams::new_from_headers(&garage, &req_head.headers)?.is_encrypted() {
if !has_encryption_header(&req_head.headers) {
// For non-encrypted objects, we need to compute the md5sum in all cases
// (even if content-md5 is not set), because it is used as an etag of the
// part, which is in turn used in the etag computation of the whole object
@ -134,10 +141,11 @@ pub async fn handle_put_part(
let mut chunker = StreamChunker::new(stream, garage.config.block_size);
// Read first chuck, and at the same time try to get object to see if it exists
let ((_, object_version, mut mpu), first_block) =
let ((object, object_version, mut mpu), first_block) =
futures::try_join!(get_upload(&ctx, &key, &upload_id), chunker.next(),)?;
// Check encryption params
let oek_params = OekDerivationInfo::for_object(&object, &object_version);
let (object_encryption, checksum_algorithm) = match object_version.state {
ObjectVersionState::Uploading {
encryption,
@ -146,8 +154,12 @@ pub async fn handle_put_part(
} => (encryption, checksum_algorithm),
_ => unreachable!(),
};
let (encryption, _) =
EncryptionParams::check_decrypt(&garage, &req_head.headers, &object_encryption)?;
let (encryption, _) = EncryptionParams::check_decrypt(
&garage,
&req_head.headers,
&object_encryption,
oek_params,
)?;
// Check object is valid and part can be accepted
let first_block = first_block.ok_or_bad_request("Empty body")?;
@ -297,6 +309,7 @@ pub async fn handle_complete_multipart_upload(
return Err(Error::bad_request("No data was uploaded"));
}
let oek_params = OekDerivationInfo::for_object(&object, &object_version);
let (object_encryption, checksum_algorithm) = match object_version.state {
ObjectVersionState::Uploading {
encryption,
@ -417,8 +430,12 @@ pub async fn handle_complete_multipart_upload(
let object_encryption = match checksum_algorithm {
None => object_encryption,
Some(_) => {
let (encryption, meta) =
EncryptionParams::check_decrypt(&garage, &req_head.headers, &object_encryption)?;
let (encryption, meta) = EncryptionParams::check_decrypt(
&garage,
&req_head.headers,
&object_encryption,
oek_params,
)?;
let new_meta = ObjectVersionMetaInner {
headers: meta.into_owned().headers,
checksum: checksum_extra,

View file

@ -15,6 +15,7 @@ use serde::Deserialize;
use garage_model::garage::Garage;
use garage_model::s3::object_table::*;
use garage_util::data::gen_uuid;
use garage_api_common::cors::*;
use garage_api_common::helpers::*;
@ -22,7 +23,7 @@ use garage_api_common::signature::checksum::*;
use garage_api_common::signature::payload::{verify_v4, Authorization};
use crate::api_server::ResBody;
use crate::encryption::EncryptionParams;
use crate::encryption::{EncryptionParams, OekDerivationInfo};
use crate::error::*;
use crate::put::{extract_metadata_headers, save_stream, ChecksumMode};
use crate::xml as s3_xml;
@ -231,12 +232,22 @@ pub async fn handle_post_object(
.transpose()?,
};
let version_uuid = gen_uuid();
let meta = ObjectVersionMetaInner {
headers,
checksum: expected_checksums.extra,
};
let encryption = EncryptionParams::new_from_headers(&garage, &params)?;
let encryption = EncryptionParams::new_from_headers(
&garage,
&params,
OekDerivationInfo {
bucket_id,
version_id: version_uuid,
object_key: &key,
},
)?;
let stream = file_field.map(|r| r.map_err(Into::into));
let ctx = ReqCtx {
@ -249,6 +260,7 @@ pub async fn handle_post_object(
let res = save_stream(
&ctx,
version_uuid,
meta,
encryption,
StreamLimiter::new(stream, conditions.content_length),

View file

@ -35,7 +35,7 @@ use garage_api_common::signature::body::StreamingChecksumReceiver;
use garage_api_common::signature::checksum::*;
use crate::api_server::{ReqBody, ResBody};
use crate::encryption::EncryptionParams;
use crate::encryption::{EncryptionParams, OekDerivationInfo};
use crate::error::*;
use crate::website::X_AMZ_WEBSITE_REDIRECT_LOCATION;
@ -62,6 +62,10 @@ pub async fn handle_put(
req: Request<ReqBody>,
key: &String,
) -> Result<Response<ResBody>, Error> {
// Generate version uuid now, because it is necessary to compute SSE-C
// encryption parameters
let version_uuid = gen_uuid();
// Retrieve interesting headers from request
let headers = extract_metadata_headers(req.headers())?;
debug!("Object headers: {:?}", headers);
@ -82,7 +86,15 @@ pub async fn handle_put(
};
// Determine whether object should be encrypted, and if so the key
let encryption = EncryptionParams::new_from_headers(&ctx.garage, req.headers())?;
let encryption = EncryptionParams::new_from_headers(
&ctx.garage,
req.headers(),
OekDerivationInfo {
bucket_id: ctx.bucket_id,
version_id: version_uuid,
object_key: &key,
},
)?;
// The request body is a special ReqBody object (see garage_api_common::signature::body)
// which supports calculating checksums while streaming the data.
@ -100,6 +112,7 @@ pub async fn handle_put(
let res = save_stream(
&ctx,
version_uuid,
meta,
encryption,
stream,
@ -121,6 +134,7 @@ pub async fn handle_put(
pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
ctx: &ReqCtx,
version_uuid: Uuid,
mut meta: ObjectVersionMetaInner,
encryption: EncryptionParams,
body: S,
@ -140,7 +154,6 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
let first_block = first_block_opt.unwrap_or_default();
// Generate identity of new version
let version_uuid = gen_uuid();
let version_timestamp = next_timestamp(existing_object.as_ref());
let mut checksummer = match &checksum_mode {

View file

@ -257,6 +257,11 @@ mod v010 {
/// (compression happens before encryption, whereas for non-encrypted
/// objects, compression is handled at the level of the block manager)
compressed: bool,
/// Whether the encryption uses an Object Encryption Key derived
/// from the master SSE-C key, instead of the master SSE-C key itself.
/// This is the case of objects created in Garage v2+
#[serde(default)]
use_oek: bool,
},
Plaintext {
/// Plain-text headers