Merge pull request 'sse-c: use different object encryption key for each object (fix #848)' (#990) from fix-ssec-crypto into next-v2
Reviewed-on: #990
This commit is contained in:
commit
fb6db494cc
11 changed files with 266 additions and 61 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1403,6 +1403,7 @@ dependencies = [
|
|||
"garage_table",
|
||||
"garage_util",
|
||||
"hex",
|
||||
"hmac",
|
||||
"http 1.2.0",
|
||||
"http-body-util",
|
||||
"http-range",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)?;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, ¶ms)?;
|
||||
let encryption = EncryptionParams::new_from_headers(
|
||||
&garage,
|
||||
¶ms,
|
||||
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),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue