use axum::{
http::{HeaderMap, StatusCode},
response::Response,
};
use base64::Engine as _;
use crate::api::utils::error_response;
use crate::AppState;
pub enum SseDecision {
None,
Aes256,
SseC {
key: Box<[u8; 32]>,
key_md5: String,
},
SseKms {
key_id: String,
},
}
pub fn format_kms_arn(key_id: &str) -> String {
format!("arn:aws:kms:us-east-1:000000000000:key/{}", key_id)
}
pub async fn resolve_sse(
state: &AppState,
bucket: &str,
headers: &HeaderMap,
) -> Result<SseDecision, Response> {
if headers
.get("x-amz-server-side-encryption-customer-algorithm")
.is_some()
{
let resource = format!("/{}", bucket);
let algo = headers
.get("x-amz-server-side-encryption-customer-algorithm")
.and_then(|v| v.to_str().ok())
.unwrap_or_default();
if algo != "AES256" {
return Err(error_response(
StatusCode::BAD_REQUEST,
"InvalidArgument",
&format!(
"Unsupported SSE-C algorithm: '{}'. Only AES256 is supported.",
algo
),
&resource,
));
}
let key_b64 = headers
.get("x-amz-server-side-encryption-customer-key")
.and_then(|v| v.to_str().ok())
.unwrap_or_default();
let key_bytes = base64::engine::general_purpose::STANDARD
.decode(key_b64)
.map_err(|_| {
error_response(
StatusCode::BAD_REQUEST,
"InvalidArgument",
"x-amz-server-side-encryption-customer-key is not valid base64",
&resource,
)
})?;
if key_bytes.len() != 32 {
return Err(error_response(
StatusCode::BAD_REQUEST,
"InvalidArgument",
&format!(
"SSE-C customer key must be 32 bytes for AES256, got {}",
key_bytes.len()
),
&resource,
));
}
let computed_md5_bytes = md5::compute(key_bytes.as_slice()).0;
let computed_md5_b64 = base64::engine::general_purpose::STANDARD.encode(computed_md5_bytes);
let provided_md5 = headers
.get("x-amz-server-side-encryption-customer-key-MD5")
.and_then(|v| v.to_str().ok())
.unwrap_or_default();
if provided_md5 != computed_md5_b64 {
return Err(error_response(
StatusCode::BAD_REQUEST,
"InvalidArgument",
"The MD5 you specified did not match the calculated MD5 for the customer-provided key",
&resource,
));
}
let mut key_array = [0u8; 32];
key_array.copy_from_slice(&key_bytes);
return Ok(SseDecision::SseC {
key: Box::new(key_array),
key_md5: computed_md5_b64,
});
}
if let Some(h) = headers.get("x-amz-server-side-encryption") {
let algo_str = h.to_str().unwrap_or_default();
match algo_str {
"AES256" => return Ok(SseDecision::Aes256),
"aws:kms" => {
let resource = format!("/{}", bucket);
let requested_key_id = headers
.get("x-amz-server-side-encryption-aws-kms-key-id")
.and_then(|v| v.to_str().ok());
let key_id = state
.encryption
.resolve_kms_key_id(requested_key_id)
.await
.map_err(|_| {
error_response(
StatusCode::BAD_REQUEST,
"InvalidArgument",
"KMS key not found",
&resource,
)
})?;
return Ok(SseDecision::SseKms { key_id });
}
"aws:kms:dsse" => {
return Err(error_response(
StatusCode::NOT_IMPLEMENTED,
"NotImplemented",
"SSE-KMS double-layer (aws:kms:dsse) is not supported.",
&format!("/{}", bucket),
));
}
algo => {
return Err(error_response(
StatusCode::BAD_REQUEST,
"InvalidArgument",
&format!("Unknown server-side encryption algorithm: {}", algo),
&format!("/{}", bucket),
));
}
}
}
if let Ok(cfg) = state.storage.get_bucket_encryption(bucket).await {
if let Some(rule) = cfg.rules.first() {
match rule.sse_algorithm.as_str() {
"AES256" => return Ok(SseDecision::Aes256),
"aws:kms" => {
let resource = format!("/{}", bucket);
let key_id = state
.encryption
.resolve_kms_key_id(None)
.await
.map_err(|_| {
error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"InternalError",
"Failed to resolve default KMS key",
&resource,
)
})?;
return Ok(SseDecision::SseKms { key_id });
}
_ => return Ok(SseDecision::None),
}
}
}
Ok(SseDecision::None)
}
pub async fn resolve_sse_c_copy_source(
headers: &HeaderMap,
) -> Result<Option<Box<[u8; 32]>>, Response> {
let algo_hv = match headers.get("x-amz-copy-source-server-side-encryption-customer-algorithm") {
Some(v) => v,
None => return Ok(None),
};
let algo = algo_hv.to_str().unwrap_or_default();
if algo != "AES256" {
return Err(error_response(
StatusCode::BAD_REQUEST,
"InvalidArgument",
&format!(
"Unsupported copy-source SSE-C algorithm: '{}'. Only AES256 is supported.",
algo
),
"/copy-source",
));
}
let key_b64 = headers
.get("x-amz-copy-source-server-side-encryption-customer-key")
.and_then(|v| v.to_str().ok())
.unwrap_or_default();
let key_bytes = base64::engine::general_purpose::STANDARD
.decode(key_b64)
.map_err(|_| {
error_response(
StatusCode::BAD_REQUEST,
"InvalidArgument",
"x-amz-copy-source-server-side-encryption-customer-key is not valid base64",
"/copy-source",
)
})?;
if key_bytes.len() != 32 {
return Err(error_response(
StatusCode::BAD_REQUEST,
"InvalidArgument",
&format!(
"SSE-C copy-source customer key must be 32 bytes for AES256, got {}",
key_bytes.len()
),
"/copy-source",
));
}
let computed_md5_bytes = md5::compute(key_bytes.as_slice()).0;
let computed_md5_b64 = base64::engine::general_purpose::STANDARD.encode(computed_md5_bytes);
let provided_md5 = headers
.get("x-amz-copy-source-server-side-encryption-customer-key-MD5")
.and_then(|v| v.to_str().ok())
.unwrap_or_default();
if provided_md5 != computed_md5_b64 {
return Err(error_response(
StatusCode::BAD_REQUEST,
"InvalidArgument",
"The MD5 you specified did not match the calculated MD5 for the copy-source customer-provided key",
"/copy-source",
));
}
let mut key_array = [0u8; 32];
key_array.copy_from_slice(&key_bytes);
Ok(Some(Box::new(key_array)))
}