}
Other commands and APIs will report object and bucket sizes based on their
-uncompressed data.
+uncompressed data.
-The ``size_utilized`` and ``size_kb_utilized`` fields represent the total
-size of compressed data, in bytes and kilobytes respectively.
+The ``size_utilized`` and ``size_kb_utilized`` fields represent the actual
+bytes stored on disk, in bytes and kilobytes respectively. This value reflects
+the effects of both compression and encryption:
+
+* With compression only: size after compression
+* With AEAD encryption only (e.g., AES-256-GCM): size after encryption
+ (includes authentication tag overhead)
+* With both compression and encryption: size after compression then encryption
+
+See :ref:`radosgw-encryption` for details on encryption algorithms.
===============================
.. confval:: rgw_crypt_s3_kms_backend
+.. confval:: rgw_crypt_sse_algorithm
Barbican Settings
=================
.. note:: Server-side encryption keys must be 256-bit long and base64 encoded.
+Encryption Algorithm
+====================
+
+.. versionadded:: Umbrella
+
+The Ceph Object Gateway supports two AES-256 encryption algorithms for
+server-side encryption:
+
+**AES-256-CBC** (Cipher Block Chaining)
+ The legacy encryption algorithm. This mode is compatible with older Ceph
+ releases and is the default for backward compatibility. CBC mode encrypts
+ data but does not provide built-in integrity verification.
+
+**AES-256-GCM** (Galois/Counter Mode)
+ A modern authenticated encryption algorithm that provides both
+ confidentiality and integrity protection. GCM mode detects any tampering
+ or corruption of encrypted data. This is the recommended algorithm for
+ new deployments.
+
+The encryption algorithm for new objects can be configured with::
+
+ rgw crypt sse algorithm = aes-256-cbc # default, for backward compatibility
+ rgw crypt sse algorithm = aes-256-gcm # recommended for new deployments
+
+.. note:: This setting only affects newly encrypted objects. Existing objects
+ are always decrypted using the algorithm that was used when they
+ were encrypted, regardless of the current setting. This allows
+ CBC-encrypted and GCM-encrypted objects to coexist in the same
+ cluster.
+
+.. important:: When upgrading from an older Ceph release, keep the default
+ ``aes-256-cbc`` setting until all RGW instances have been
+ upgraded. Once all instances support GCM, you can enable
+ ``aes-256-gcm`` for new uploads.
+
+GCM Encryption Format
+---------------------
+
+AES-256-GCM encrypts data in 4 KB (4096 byte) chunks. Each chunk produces
+4112 bytes of ciphertext (4096 bytes of encrypted data plus a 16-byte
+authentication tag).
+
+.. list-table:: GCM Size Calculation Example
+ :header-rows: 1
+ :widths: 40 30 30
+
+ * - Description
+ - Size
+ - Notes
+ * - Original plaintext
+ - 10,000 bytes
+ - User's data
+ * - Number of chunks
+ - 3
+ - ⌈10000 ÷ 4096⌉
+ * - Authentication tags
+ - 48 bytes
+ - 3 chunks × 16 bytes
+ * - **Encrypted on disk**
+ - **10,048 bytes**
+ - plaintext + tags
+
+The storage overhead is approximately 0.4% (16 bytes per 4 KB). S3 API
+responses always report the original plaintext size, so this overhead is
+transparent to clients.
+
Customer-Provided Keys
======================
services:
- rgw
with_legacy: true
+- name: rgw_crypt_sse_algorithm
+ type: str
+ level: advanced
+ desc: Default encryption algorithm for server-side encryption
+ long_desc: Specifies the default AES encryption algorithm to use for new objects
+ encrypted with SSE-C, SSE-KMS, SSE-S3, or RGW-AUTO modes. Valid values are
+ aes-256-cbc (legacy, compatible with older RGW versions) and aes-256-gcm
+ (recommended, provides authenticated encryption). Existing encrypted objects
+ are always decrypted using the algorithm specified in their metadata,
+ regardless of this setting.
+ default: aes-256-cbc
+ services:
+ - rgw
+ see_also:
+ - rgw_crypt_require_ssl
+ enum_values:
+ - aes-256-cbc
+ - aes-256-gcm
+ with_legacy: true
- name: rgw_crypt_sse_s3_backend
type: str
level: advanced
#include "rgw_rest_conn.h"
#include "rgw_cr_rados.h"
#include "rgw_cr_rest.h"
+#include "rgw_crypt.h"
#include "rgw_datalog.h"
#include "rgw_putobj_processor.h"
#include "rgw_lc_tier.h"
ldpp_dout(rctx.dpp, 0) << "ERROR: complete_atomic_modification returned r=" << r << dendl;
}
+ /**
+ * For AEAD encryption (GCM), set bucket index sizes:
+ * index_size = encrypted bytes on disk (for size_utilized)
+ * index_accounted_size = plaintext bytes (for quota/listings)
+ *
+ * This is consistent with compression behavior where:
+ * index_size = compressed bytes, index_accounted_size = uncompressed bytes
+ *
+ * For non-AEAD (CBC or unencrypted), both remain as passed in.
+ */
+ uint64_t index_size, index_accounted_size;
+ {
+ index_size = size;
+ index_accounted_size = accounted_size;
+ uint64_t original_size = 0;
+ if (rgw_get_aead_original_size(rctx.dpp, attrs, &original_size)) {
+ index_accounted_size = original_size;
+ }
+ }
+
tracepoint(rgw_rados, complete_enter, req_id.c_str());
- r = index_op->complete(rctx.dpp, poolid, epoch, size, accounted_size,
+ r = index_op->complete(rctx.dpp, poolid, epoch, index_size, index_accounted_size,
meta.set_mtime, etag, content_type,
storage_class, meta.owner,
meta.category, meta.remove_objs, rctx.y,
src_attrs.erase(RGW_ATTR_OBJ_REPLICATION_TIMESTAMP);
src_attrs.erase(RGW_ATTR_OBJ_REPLICATION_STATUS);
- // drop encryption attributes
- // will be generated by copy_obj_data() if encryption is requested
+ /**
+ * Drop encryption attributes - will be generated by copy_obj_data() if
+ * encryption is requested. CRYPT_ORIGINAL_SIZE and CRYPT_PARTS are preserved
+ * for size calculations. CRYPT_PART_NUMS must be erased because copy writes
+ * data as a single stream (part 0), so stale part numbers from a multipart
+ * source would cause wrong key derivation.
+ */
src_attrs.erase(RGW_ATTR_CRYPT_KEYSEL);
src_attrs.erase(RGW_ATTR_CRYPT_CONTEXT);
src_attrs.erase(RGW_ATTR_CRYPT_MODE);
src_attrs.erase(RGW_ATTR_CRYPT_KEYID);
src_attrs.erase(RGW_ATTR_CRYPT_KEYMD5);
src_attrs.erase(RGW_ATTR_CRYPT_DATAKEY);
+ src_attrs.erase(RGW_ATTR_CRYPT_NONCE);
+ src_attrs.erase(RGW_ATTR_CRYPT_PART_NUMS);
set_copy_attrs(src_attrs, attrs, attrs_mod);
attrs.erase(RGW_ATTR_ID_TAG);
if (copy_data) { /* refcounting tail wouldn't work here, just copy the data */
attrs.erase(RGW_ATTR_TAIL_TAG);
+ // Data is rewritten as a single stream; drop stale multipart boundaries
+ attrs.erase(RGW_ATTR_CRYPT_PARTS);
+ attrs.erase(RGW_ATTR_CRYPT_PART_NUMS);
return copy_obj_data(dest_obj_ctx, owner, dest_bucket_info, dest_placement, read_op, obj_size - 1, dest_obj,
mtime, real_time(), attrs, olh_epoch, delete_at, petag, dp_factory, dpp, y);
}
s->fake_tag = true;
}
}
+
+ /**
+ * For AEAD encryption: adjust accounted_size to original size.
+ * Helpers return false for non-AEAD modes (including CBC), so this is a no-op
+ * outside of AEAD.
+ * Must be after manifest handling since manifest->get_obj_size() returns
+ * encrypted size and re-sets accounted_size.
+ */
+ if (!compressed && s->accounted_size == s->size) {
+ uint64_t original_size = 0;
+ if (rgw_get_aead_original_size(dpp, s->attrset, &original_size)) {
+ s->accounted_size = original_size;
+ } else {
+ uint64_t decrypted_size = 0;
+ if (rgw_get_aead_decrypted_size(dpp, s->attrset, s->size, &decrypted_size)) {
+ s->accounted_size = decrypted_size;
+ }
+ }
+ }
if (iter = s->attrset.find(RGW_ATTR_PG_VER); iter != s->attrset.end()) {
const bufferlist& pg_ver_bl = iter->second;
if (pg_ver_bl.length()) {
}
for (auto& iter : src_attrset) {
+ /**
+ * Skip object-level encryption attributes when reading individual parts.
+ * These attrs describe the complete multipart object, not this part:
+ * - ORIGINAL_SIZE: would cause Content-Length mismatch
+ * - PARTS: contains sizes of all parts, not applicable to single part
+ * - PART_NUMS: maps part indices to S3 part numbers for the full object
+ */
+ if (iter.first == RGW_ATTR_CRYPT_ORIGINAL_SIZE ||
+ iter.first == RGW_ATTR_CRYPT_PARTS ||
+ iter.first == RGW_ATTR_CRYPT_PART_NUMS) {
+ ldpp_dout(dpp, 4) << "skip crypt attr for part read: " << iter.first << dendl;
+ continue;
+ }
ldpp_dout(dpp, 4) << "copy crypt attr: " << iter.first << dendl;
if (astate->attrset.find(iter.first) == astate->attrset.end()) {
astate->attrset[iter.first] = std::move(iter.second);
#include "rgw_aio_throttle.h"
#include "rgw_bucket.h"
#include "rgw_bucket_logging.h"
+#include "rgw_crypt.h"
#include "rgw_bl_rados.h"
#include "rgw_lc.h"
#include "rgw_lc_tier.h"
auto etags_iter = part_etags.begin();
rgw::sal::Attrs& attrs = target_obj->get_attrs();
+ // Check if this is AEAD encryption to track plaintext size
+ bool is_aead = false;
+ uint64_t plaintext_ofs = 0;
+ auto mode_iter = attrs.find(RGW_ATTR_CRYPT_MODE);
+ if (mode_iter != attrs.end()) {
+ std::string crypt_mode = mode_iter->second.to_str();
+ is_aead = is_aead_mode(crypt_mode);
+ }
+
do {
ret = list_parts(dpp, cct, max_parts, marker, &marker, &truncated, y);
if (ret == -ENOENT) {
ofs += obj_part.size;
accounted_size += obj_part.accounted_size;
+
+ // Track plaintext size for AEAD encryption
+ if (is_aead) {
+ if (part_compressed) {
+ // For compressed parts, use the uncompressed size directly
+ plaintext_ofs += obj_part.accounted_size;
+ } else {
+ plaintext_ofs += aead_encrypted_to_plaintext_size(obj_part.size);
+ }
+ }
}
} while (truncated);
hash.Final((unsigned char *)final_etag);
attrs[RGW_ATTR_COMPRESSION] = tmp;
}
+ // For AEAD encryption: store total plaintext size (calculated during loop)
+ if (is_aead) {
+ bufferlist bl;
+ bl.append(std::to_string(plaintext_ofs));
+ attrs[RGW_ATTR_CRYPT_ORIGINAL_SIZE] = std::move(bl);
+
+ // Store actual S3 part numbers for correct IV/key derivation during decrypt
+ std::vector<uint32_t> part_nums;
+ part_nums.reserve(part_etags.size());
+ for (const auto& part : part_etags) {
+ part_nums.push_back(static_cast<uint32_t>(part.first));
+ }
+ bufferlist part_nums_bl;
+ using ceph::encode;
+ encode(part_nums, part_nums_bl);
+ attrs[RGW_ATTR_CRYPT_PART_NUMS] = std::move(part_nums_bl);
+ ldpp_dout(dpp, 20) << "Stored CRYPT_PART_NUMS with " << part_nums.size()
+ << " parts" << dendl;
+ }
+
target_obj->set_atomic(true);
const RGWBucketInfo& bucket_info = target_obj->get_bucket()->get_info();
#define RGW_ATTR_CRYPT_CONTEXT RGW_ATTR_CRYPT_PREFIX "context"
#define RGW_ATTR_CRYPT_DATAKEY RGW_ATTR_CRYPT_PREFIX "datakey"
#define RGW_ATTR_CRYPT_PARTS RGW_ATTR_CRYPT_PREFIX "part-lengths"
+#define RGW_ATTR_CRYPT_PART_NUMS RGW_ATTR_CRYPT_PREFIX "part-numbers"
+#define RGW_ATTR_CRYPT_NONCE RGW_ATTR_CRYPT_PREFIX "nonce"
+#define RGW_ATTR_CRYPT_ORIGINAL_SIZE RGW_ATTR_CRYPT_PREFIX "original-size"
/* SSE-S3 Encryption Attributes */
#define RGW_ATTR_BUCKET_ENCRYPTION_PREFIX RGW_ATTR_PREFIX "sse-s3."
#include <unicode/normalizer2.h> // libicu
#include <openssl/evp.h>
+#include "common/ceph_crypto.h"
#define dout_context g_ceph_context
#define dout_subsys ceph_subsys_rgw
{ 'a', 'e', 's', '2', '5', '6', 'i', 'v', '_', 'c', 't', 'r', '1', '3', '3', '7' };
+/**
+ * AES-256-GCM encryption implementation
+ * Provides authenticated encryption with 96-bit IVs and 128-bit authentication tags
+ */
+class AES_256_GCM : public BlockCrypt {
+public:
+ static const size_t AES_256_KEYSIZE = 256 / 8; // 32 bytes
+ static const size_t AES_256_IVSIZE = 96 / 8; // 12 bytes (GCM standard)
+ static const size_t GCM_TAG_SIZE = 128 / 8; // 16 bytes
+ static const size_t CHUNK_SIZE = 4096;
+ static const size_t ENCRYPTED_CHUNK_SIZE = CHUNK_SIZE + GCM_TAG_SIZE; // 4112
+
+ /**
+ * Combined index layout for IV derivation:
+ * - Upper 24 bits: part_number (supports up to 16M parts; S3 limit is 10K)
+ * - Lower 40 bits: chunk_index (supports up to 4 PB per part at 4KB chunks)
+ */
+ static constexpr unsigned CHUNK_INDEX_BITS = 40;
+ static constexpr uint64_t MAX_CHUNK_INDEX = (1ULL << CHUNK_INDEX_BITS) - 1;
+
+ const DoutPrefixProvider* dpp;
+private:
+ CephContext* cct;
+ uint8_t key[AES_256_KEYSIZE];
+ uint8_t base_key[AES_256_KEYSIZE]; // For SSE-C: stores object key before part derivation
+ bool has_base_key = false; // True if base_key is valid (SSE-C with key derivation)
+ uint8_t base_nonce[AES_256_IVSIZE];
+ bool nonce_initialized = false;
+ uint32_t part_number_ = 0; // For multipart: ensures unique IVs across parts
+
+public:
+ explicit AES_256_GCM(const DoutPrefixProvider* dpp, CephContext* cct)
+ : dpp(dpp), cct(cct) {
+ memset(base_nonce, 0, AES_256_IVSIZE);
+ }
+
+ ~AES_256_GCM() {
+ ::ceph::crypto::zeroize_for_security(key, AES_256_KEYSIZE);
+ ::ceph::crypto::zeroize_for_security(base_key, AES_256_KEYSIZE);
+ ::ceph::crypto::zeroize_for_security(base_nonce, AES_256_IVSIZE);
+ }
+
+ bool set_key(const uint8_t* _key, size_t key_size) {
+ if (key_size != AES_256_KEYSIZE) {
+ return false;
+ }
+ memcpy(key, _key, AES_256_KEYSIZE);
+ return true;
+ }
+
+ /**
+ * Generate a random base nonce for this object.
+ * Called during encryption to create a unique nonce per object.
+ */
+ bool generate_nonce() {
+ cct->random()->get_bytes(reinterpret_cast<char*>(base_nonce), AES_256_IVSIZE);
+ nonce_initialized = true;
+ return true;
+ }
+
+ /**
+ * Set the base nonce from a stored value.
+ * Called during decryption to restore the object's nonce.
+ */
+ bool set_nonce(const uint8_t* nonce, size_t len) {
+ if (len != AES_256_IVSIZE) {
+ return false;
+ }
+ memcpy(base_nonce, nonce, AES_256_IVSIZE);
+ nonce_initialized = true;
+ return true;
+ }
+
+ /**
+ * Get the base nonce for storage in object attributes.
+ */
+ std::string get_nonce() const {
+ return std::string(reinterpret_cast<const char*>(base_nonce), AES_256_IVSIZE);
+ }
+
+ bool is_nonce_initialized() const {
+ return nonce_initialized;
+ }
+
+ /**
+ * Set part number for multipart IV derivation and key derivation (SSE-C).
+ * Must be called before encrypt/decrypt for multipart uploads.
+ *
+ * For SSE-C mode (has_base_key=true): also re-derives the part-specific key
+ * from base_key, enabling correct decryption when switching between parts
+ * during multipart GET operations.
+ */
+ void set_part_number(uint32_t part_number) override {
+ this->part_number_ = part_number;
+
+ // For SSE-C mode, also derive the correct part key
+ if (has_base_key && part_number > 0) {
+ // Restore base key, then derive part key
+ memcpy(this->key, this->base_key, AES_256_KEYSIZE);
+ derive_part_key(part_number);
+ } else if (has_base_key && part_number == 0) {
+ // Part 0 means single-part or init - use base key directly
+ memcpy(this->key, this->base_key, AES_256_KEYSIZE);
+ }
+ // For non-SSE-C modes (has_base_key=false), only IV derivation uses part_number
+ }
+
+ /**
+ * Derive object-specific encryption key from user key + object identity.
+ * This provides cryptographic binding between ciphertext and object identity:
+ * - If object is moved/renamed at RADOS level → wrong key → decrypt fails
+ *
+ * Key derivation formula:
+ * ObjectKey = HMAC-SHA256(key, nonce || domain || bucket || object)
+ *
+ * @param user_key The user-provided encryption key (32 bytes)
+ * @param key_len Length of user_key (must be 32)
+ * @param bucket Bucket name
+ * @param object Object name
+ * @param part_number Part number for multipart uploads (0 for non-multipart)
+ * @param domain Domain separator string for algorithm binding (e.g., "SSE-C-GCM", "RGW-AUTO-GCM")
+ * @return true on success
+ */
+ bool derive_object_key(
+ const uint8_t* user_key,
+ size_t key_len,
+ const std::string& bucket,
+ const std::string& object,
+ uint32_t part_number,
+ const std::string& domain = "SSE-C-GCM")
+ {
+ if (key_len != AES_256_KEYSIZE) {
+ ldpp_dout(dpp, 0) << "ERROR: derive_object_key: invalid key length "
+ << key_len << ", expected " << AES_256_KEYSIZE << dendl;
+ return false;
+ }
+ if (!nonce_initialized) {
+ ldpp_dout(dpp, 0) << "ERROR: derive_object_key: nonce not initialized" << dendl;
+ return false;
+ }
+
+ // HMAC-SHA256(user_key, nonce || domain || bucket || object)
+ try {
+ ceph::crypto::HMACSHA256 hmac(user_key, key_len);
+
+ // Helper to encode length as 4-byte big-endian and update HMAC
+ // Length-prefixing prevents ambiguous concatenation attacks
+ auto hmac_update_with_length = [&hmac](const std::string_view& data) {
+ uint32_t len = static_cast<uint32_t>(data.size());
+ uint8_t len_buf[4] = {
+ static_cast<uint8_t>((len >> 24) & 0xFF),
+ static_cast<uint8_t>((len >> 16) & 0xFF),
+ static_cast<uint8_t>((len >> 8) & 0xFF),
+ static_cast<uint8_t>(len & 0xFF)
+ };
+ hmac.Update(len_buf, 4);
+ if (!data.empty()) {
+ hmac.Update(reinterpret_cast<const uint8_t*>(data.data()), data.size());
+ }
+ };
+
+ // Include nonce in key derivation
+ hmac.Update(base_nonce, AES_256_IVSIZE);
+
+ // Domain separator (length-prefixed for consistency)
+ hmac_update_with_length(domain);
+
+ // Include bucket/object with length prefixes
+ // Format: nonce || len(domain) || domain || len(bucket) || bucket || len(object) || object
+ hmac_update_with_length(bucket);
+ hmac_update_with_length(object);
+
+ hmac.Final(this->key);
+ } catch (const ceph::crypto::DigestException& e) {
+ ldpp_dout(dpp, 0) << "ERROR: derive_object_key: HMAC failed: " << e.what() << dendl;
+ ::ceph::crypto::zeroize_for_security(this->key, AES_256_KEYSIZE);
+ return false;
+ }
+
+ // Store part_number for IV derivation (ensures unique IVs across parts)
+ this->part_number_ = part_number;
+
+ // Save base key for later part key derivation (needed for multipart GET)
+ // This allows set_part_number() to re-derive the correct part key
+ memcpy(this->base_key, this->key, AES_256_KEYSIZE);
+ this->has_base_key = true;
+
+ ldpp_dout(dpp, 20) << "derive_object_key: derived key for bucket=" << bucket
+ << " object=" << object
+ << " part_number=" << part_number << dendl;
+
+ // For multipart, derive part-specific key
+ if (part_number > 0) {
+ return derive_part_key(part_number);
+ }
+ return true;
+ }
+
+ /**
+ * Derive part-specific key for multipart uploads.
+ * This prevents part reordering/swapping attacks.
+ *
+ * Formula: PartKey = HMAC-SHA256(ObjectKey, part_number)
+ *
+ * @param part_number Part number (1-based, as per S3 multipart API)
+ * @return true on success
+ */
+ bool derive_part_key(uint32_t part_number) {
+ // Encode part number as big-endian 4 bytes
+ uint8_t part_bytes[4];
+ part_bytes[0] = (part_number >> 24) & 0xFF;
+ part_bytes[1] = (part_number >> 16) & 0xFF;
+ part_bytes[2] = (part_number >> 8) & 0xFF;
+ part_bytes[3] = part_number & 0xFF;
+
+ uint8_t derived[AES_256_KEYSIZE];
+
+ try {
+ ceph::crypto::HMACSHA256 hmac(this->key, AES_256_KEYSIZE);
+ hmac.Update(part_bytes, 4);
+ hmac.Final(derived);
+ } catch (const ceph::crypto::DigestException& e) {
+ ldpp_dout(dpp, 0) << "ERROR: derive_part_key: HMAC failed: " << e.what() << dendl;
+ ::ceph::crypto::zeroize_for_security(derived, AES_256_KEYSIZE);
+ ::ceph::crypto::zeroize_for_security(this->key, AES_256_KEYSIZE);
+ return false;
+ }
+
+ memcpy(this->key, derived, AES_256_KEYSIZE);
+ ::ceph::crypto::zeroize_for_security(derived, AES_256_KEYSIZE);
+
+ ldpp_dout(dpp, 20) << "derive_part_key: derived key for part " << part_number << dendl;
+ return true;
+ }
+
+ size_t get_block_size() override {
+ return CHUNK_SIZE;
+ }
+
+ size_t get_encrypted_block_size() override {
+ return ENCRYPTED_CHUNK_SIZE;
+ }
+
+ /**
+ * Encode chunk index as 8-byte big-endian AAD.
+ * Binds ciphertext to stream position, preventing chunk reordering attacks.
+ */
+ static void encode_chunk_aad(uint8_t (&aad)[8], uint64_t chunk_index) {
+ aad[0] = (chunk_index >> 56) & 0xFF;
+ aad[1] = (chunk_index >> 48) & 0xFF;
+ aad[2] = (chunk_index >> 40) & 0xFF;
+ aad[3] = (chunk_index >> 32) & 0xFF;
+ aad[4] = (chunk_index >> 24) & 0xFF;
+ aad[5] = (chunk_index >> 16) & 0xFF;
+ aad[6] = (chunk_index >> 8) & 0xFF;
+ aad[7] = chunk_index & 0xFF;
+ }
+
+ bool gcm_encrypt_chunk(unsigned char* out,
+ const unsigned char* in,
+ size_t size,
+ const unsigned char (&iv)[AES_256_IVSIZE],
+ const unsigned char (&key)[AES_256_KEYSIZE],
+ unsigned char* tag,
+ uint64_t chunk_index)
+ {
+ using pctx_t = std::unique_ptr<EVP_CIPHER_CTX, decltype(&::EVP_CIPHER_CTX_free)>;
+ pctx_t pctx{ EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free };
+
+ if (!pctx) {
+ ldpp_dout(dpp, 5) << "EVP: failed to create cipher context" << dendl;
+ return false;
+ }
+
+ // 1st init: set cipher type
+ if (1 != EVP_EncryptInit_ex(pctx.get(), EVP_aes_256_gcm(),
+ nullptr, nullptr, nullptr)) {
+ ldpp_dout(dpp, 5) << "EVP: failed to initialize GCM" << dendl;
+ return false;
+ }
+
+ // Verify IV size (should be 12 bytes for GCM)
+ if (EVP_CIPHER_CTX_iv_length(pctx.get()) != AES_256_IVSIZE) {
+ ldpp_dout(dpp, 5) << "EVP: unexpected IV length "
+ << EVP_CIPHER_CTX_iv_length(pctx.get())
+ << " expected " << AES_256_IVSIZE << dendl;
+ return false;
+ }
+
+ // 2nd init: set key and IV
+ if (1 != EVP_EncryptInit_ex(pctx.get(), nullptr, nullptr, key, iv)) {
+ ldpp_dout(dpp, 5) << "EVP: failed to set key/IV" << dendl;
+ return false;
+ }
+
+ // Add AAD for chunk ordering protection
+ uint8_t aad[8];
+ encode_chunk_aad(aad, chunk_index);
+ int aad_len = 0;
+ if (1 != EVP_EncryptUpdate(pctx.get(), nullptr, &aad_len, aad, sizeof(aad))) {
+ ldpp_dout(dpp, 5) << "EVP: failed to set AAD" << dendl;
+ return false;
+ }
+
+ // Encrypt data (size is at most CHUNK_SIZE, well within int range for EVP API)
+ int written = 0;
+ ceph_assert(size <= CHUNK_SIZE);
+ if (1 != EVP_EncryptUpdate(pctx.get(), out, &written, in, size)) {
+ ldpp_dout(dpp, 5) << "EVP: EncryptUpdate failed" << dendl;
+ return false;
+ }
+
+ // Finalize (GCM doesn't add padding, so finally_written should be 0)
+ int finally_written = 0;
+ if (1 != EVP_EncryptFinal_ex(pctx.get(), out + written, &finally_written)) {
+ ldpp_dout(dpp, 5) << "EVP: EncryptFinal_ex failed" << dendl;
+ return false;
+ }
+
+ // Get authentication tag
+ if (1 != EVP_CIPHER_CTX_ctrl(pctx.get(), EVP_CTRL_GCM_GET_TAG,
+ GCM_TAG_SIZE, tag)) {
+ ldpp_dout(dpp, 5) << "EVP: failed to get GCM tag" << dendl;
+ return false;
+ }
+
+ return (written + finally_written) == static_cast<int>(size);
+ }
+
+ bool gcm_decrypt_chunk(unsigned char* out,
+ const unsigned char* in,
+ size_t size,
+ const unsigned char (&iv)[AES_256_IVSIZE],
+ const unsigned char (&key)[AES_256_KEYSIZE],
+ const unsigned char* tag,
+ uint64_t chunk_index)
+ {
+ using pctx_t = std::unique_ptr<EVP_CIPHER_CTX, decltype(&::EVP_CIPHER_CTX_free)>;
+ pctx_t pctx{ EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free };
+
+ if (!pctx) {
+ ldpp_dout(dpp, 5) << "EVP: failed to create cipher context" << dendl;
+ return false;
+ }
+
+ // 1st init: set cipher type
+ if (1 != EVP_DecryptInit_ex(pctx.get(), EVP_aes_256_gcm(),
+ nullptr, nullptr, nullptr)) {
+ ldpp_dout(dpp, 5) << "EVP: failed to initialize GCM" << dendl;
+ return false;
+ }
+
+ // 2nd init: set key and IV
+ if (1 != EVP_DecryptInit_ex(pctx.get(), nullptr, nullptr, key, iv)) {
+ ldpp_dout(dpp, 5) << "EVP: failed to set key/IV" << dendl;
+ return false;
+ }
+
+ // Add AAD for chunk ordering protection (must match encryption)
+ uint8_t aad[8];
+ encode_chunk_aad(aad, chunk_index);
+ int aad_len = 0;
+ if (1 != EVP_DecryptUpdate(pctx.get(), nullptr, &aad_len, aad, sizeof(aad))) {
+ ldpp_dout(dpp, 5) << "EVP: failed to set AAD" << dendl;
+ return false;
+ }
+
+ // Decrypt data (size is at most CHUNK_SIZE, well within int range for EVP API)
+ int written = 0;
+ ceph_assert(size <= CHUNK_SIZE);
+ if (1 != EVP_DecryptUpdate(pctx.get(), out, &written, in, size)) {
+ ldpp_dout(dpp, 5) << "EVP: DecryptUpdate failed" << dendl;
+ return false;
+ }
+
+ // Set expected tag for verification
+ if (1 != EVP_CIPHER_CTX_ctrl(pctx.get(), EVP_CTRL_GCM_SET_TAG,
+ GCM_TAG_SIZE, const_cast<unsigned char*>(tag))) {
+ ldpp_dout(dpp, 5) << "EVP: failed to set GCM tag" << dendl;
+ return false;
+ }
+
+ // Finalize - this verifies the tag
+ int finally_written = 0;
+ if (1 != EVP_DecryptFinal_ex(pctx.get(), out + written, &finally_written)) {
+ ldpp_dout(dpp, 5) << "EVP: DecryptFinal_ex failed - authentication failure" << dendl;
+ return false; // Tag verification failed
+ }
+
+ return (written + finally_written) == static_cast<int>(size);
+ }
+
+ bool encrypt(bufferlist& input,
+ off_t in_ofs,
+ size_t size,
+ bufferlist& output,
+ off_t stream_offset,
+ optional_yield y) override
+ {
+ output.clear();
+
+ // Calculate output size: each CHUNK_SIZE plaintext becomes CHUNK_SIZE + GCM_TAG_SIZE
+ size_t num_full_chunks = size / CHUNK_SIZE;
+ size_t remainder = size % CHUNK_SIZE;
+ size_t output_size = num_full_chunks * ENCRYPTED_CHUNK_SIZE;
+ if (remainder > 0) {
+ output_size += remainder + GCM_TAG_SIZE;
+ }
+
+ buffer::ptr buf(output_size);
+ unsigned char* buf_raw = reinterpret_cast<unsigned char*>(buf.c_str());
+ const unsigned char* input_raw = reinterpret_cast<const unsigned char*>(
+ input.c_str() + in_ofs);
+
+ size_t out_pos = 0;
+
+ // Process full chunks
+ for (size_t offset = 0; offset < num_full_chunks * CHUNK_SIZE; offset += CHUNK_SIZE) {
+ unsigned char iv[AES_256_IVSIZE];
+ if (!prepare_iv(iv, stream_offset + offset)) {
+ return false;
+ }
+ uint64_t chunk_index = (stream_offset + offset) / CHUNK_SIZE;
+
+ unsigned char* ciphertext = buf_raw + out_pos;
+ unsigned char* tag = buf_raw + out_pos + CHUNK_SIZE;
+
+ if (!gcm_encrypt_chunk(ciphertext, input_raw + offset, CHUNK_SIZE,
+ iv, key, tag, chunk_index)) {
+ ldpp_dout(dpp, 5) << "Failed to encrypt chunk at offset " << offset << dendl;
+ return false;
+ }
+
+ out_pos += ENCRYPTED_CHUNK_SIZE;
+ }
+
+ // Process remainder (if any)
+ if (remainder > 0) {
+ unsigned char iv[AES_256_IVSIZE];
+ if (!prepare_iv(iv, stream_offset + num_full_chunks * CHUNK_SIZE)) {
+ return false;
+ }
+ uint64_t chunk_index = (stream_offset + num_full_chunks * CHUNK_SIZE) / CHUNK_SIZE;
+
+ unsigned char* ciphertext = buf_raw + out_pos;
+ unsigned char* tag = buf_raw + out_pos + remainder;
+
+ if (!gcm_encrypt_chunk(ciphertext, input_raw + num_full_chunks * CHUNK_SIZE,
+ remainder, iv, key, tag, chunk_index)) {
+ ldpp_dout(dpp, 5) << "Failed to encrypt final chunk" << dendl;
+ return false;
+ }
+ }
+
+ ldpp_dout(dpp, 25) << "GCM: Encrypted " << size << " bytes to "
+ << output_size << " bytes" << dendl;
+ buf.set_length(output_size);
+ output.append(buf);
+ return true;
+ }
+
+ bool decrypt(bufferlist& input,
+ off_t in_ofs,
+ size_t size,
+ bufferlist& output,
+ off_t stream_offset,
+ optional_yield y) override
+ {
+ output.clear();
+
+ // Input is organized as encrypted chunks (ciphertext + tag)
+ size_t num_full_chunks = size / ENCRYPTED_CHUNK_SIZE;
+ size_t remainder = size % ENCRYPTED_CHUNK_SIZE;
+ size_t output_size = num_full_chunks * CHUNK_SIZE;
+
+ if (remainder > 0) {
+ if (remainder <= GCM_TAG_SIZE) {
+ ldpp_dout(dpp, 5) << "GCM: Invalid encrypted data size: " << size << dendl;
+ return false;
+ }
+ output_size += remainder - GCM_TAG_SIZE;
+ }
+
+ buffer::ptr buf(output_size);
+ unsigned char* buf_raw = reinterpret_cast<unsigned char*>(buf.c_str());
+ unsigned char* input_raw = reinterpret_cast<unsigned char*>(
+ input.c_str() + in_ofs);
+
+ size_t in_pos = 0;
+ size_t out_pos = 0;
+
+ // Process full chunks
+ for (size_t i = 0; i < num_full_chunks; i++) {
+ unsigned char iv[AES_256_IVSIZE];
+ if (!prepare_iv(iv, stream_offset + i * CHUNK_SIZE)) {
+ return false;
+ }
+ uint64_t chunk_index = (stream_offset + i * CHUNK_SIZE) / CHUNK_SIZE;
+
+ unsigned char* ciphertext = input_raw + in_pos;
+ unsigned char* tag = input_raw + in_pos + CHUNK_SIZE;
+
+ if (!gcm_decrypt_chunk(buf_raw + out_pos, ciphertext, CHUNK_SIZE,
+ iv, key, tag, chunk_index)) {
+ ldpp_dout(dpp, 5) << "GCM: Failed to decrypt chunk " << i
+ << " - authentication failed" << dendl;
+ return false;
+ }
+
+ in_pos += ENCRYPTED_CHUNK_SIZE;
+ out_pos += CHUNK_SIZE;
+ }
+
+ // Process remainder (if any)
+ if (remainder > 0) {
+ size_t plaintext_size = remainder - GCM_TAG_SIZE;
+ unsigned char iv[AES_256_IVSIZE];
+ if (!prepare_iv(iv, stream_offset + num_full_chunks * CHUNK_SIZE)) {
+ return false;
+ }
+ uint64_t chunk_index = (stream_offset + num_full_chunks * CHUNK_SIZE) / CHUNK_SIZE;
+
+ unsigned char* ciphertext = input_raw + in_pos;
+ unsigned char* tag = input_raw + in_pos + plaintext_size;
+
+ if (!gcm_decrypt_chunk(buf_raw + out_pos, ciphertext, plaintext_size,
+ iv, key, tag, chunk_index)) {
+ ldpp_dout(dpp, 5) << "GCM: Failed to decrypt final chunk - authentication failed" << dendl;
+ return false;
+ }
+ }
+
+ ldpp_dout(dpp, 25) << "GCM: Decrypted " << size << " bytes to "
+ << output_size << " bytes" << dendl;
+ buf.set_length(output_size);
+ output.append(buf);
+ return true;
+ }
+
+ /**
+ * Derive per-chunk nonce from base_nonce + combined index.
+ * This ensures each chunk has a unique nonce while maintaining
+ * the "number used once" property required by GCM.
+ *
+ * For multipart uploads, we combine part_number and chunk_index to ensure
+ * unique IVs across all parts (since each part's offset starts at 0).
+ *
+ * Combined index layout (64 bits):
+ * - Upper 24 bits: part_number (supports up to 16M parts; S3 limit is 10K)
+ * - Lower 40 bits: chunk_index (supports up to 1T chunks per part)
+ */
+ bool prepare_iv(unsigned char (&iv)[AES_256_IVSIZE], off_t offset) {
+ ceph_assert(nonce_initialized);
+
+ // Combine part_number and chunk_index to ensure unique IVs across parts
+ // Without this, multipart parts would reuse IVs (all start at offset 0)
+ uint64_t chunk_index = offset / CHUNK_SIZE;
+
+ // Validate chunk_index fits in CHUNK_INDEX_BITS (supports up to 4 PB per part at 4KB chunks)
+ // This is a runtime check to prevent IV reuse in release builds
+ if (chunk_index > MAX_CHUNK_INDEX) {
+ ldpp_dout(dpp, 0) << "ERROR: chunk_index " << chunk_index
+ << " exceeds maximum " << MAX_CHUNK_INDEX
+ << " - IV collision risk, refusing to encrypt" << dendl;
+ return false;
+ }
+
+ uint64_t combined_index = (static_cast<uint64_t>(part_number_) << CHUNK_INDEX_BITS) | chunk_index;
+
+ // Derive IV: base_nonce + combined_index (with carry propagation)
+ int i = AES_256_IVSIZE - 1;
+ unsigned int val;
+ unsigned int carry = 0;
+
+ while (i >= 0) {
+ val = (combined_index & 0xff) + base_nonce[i] + carry;
+ iv[i] = static_cast<unsigned char>(val);
+ carry = val >> 8;
+ combined_index = combined_index >> 8;
+ i--;
+ }
+ return true;
+ }
+};
+
+
+/**
+ * Create an AES-256-GCM BlockCrypt instance.
+ *
+ * For encryption: Pass nonce=nullptr to generate a random nonce.
+ * After creation, call get_nonce() to retrieve it for storage.
+ *
+ * For decryption: Pass the stored nonce from RGW_ATTR_CRYPT_NONCE.
+ */
+std::unique_ptr<BlockCrypt> AES_256_GCM_create(const DoutPrefixProvider* dpp,
+ CephContext* cct,
+ const uint8_t* key,
+ size_t key_len,
+ const uint8_t* nonce,
+ size_t nonce_len,
+ uint32_t part_number)
+{
+ // Validate key_len to prevent OOB read if caller passes smaller buffer
+ if (key_len != AES_256_GCM::AES_256_KEYSIZE) {
+ ldpp_dout(dpp, 5) << "AES_256_GCM_create: invalid key size " << key_len
+ << ", expected " << AES_256_GCM::AES_256_KEYSIZE << dendl;
+ return nullptr;
+ }
+
+ auto gcm = std::unique_ptr<AES_256_GCM>(new AES_256_GCM(dpp, cct));
+ if (!gcm->set_key(key, key_len)) {
+ return nullptr;
+ }
+
+ // Set part_number for multipart IV derivation (ensures unique IVs across parts)
+ gcm->set_part_number(part_number);
+
+ if (nonce != nullptr) {
+ // Decryption path: use the provided stored nonce
+ if (!gcm->set_nonce(nonce, nonce_len)) {
+ ldpp_dout(dpp, 5) << "AES_256_GCM_create: invalid nonce size " << nonce_len << dendl;
+ return nullptr;
+ }
+ } else {
+ // Encryption path: generate a random nonce
+ gcm->generate_nonce();
+ }
+
+ return gcm;
+}
+
+
+/**
+ * Retrieve the nonce from a BlockCrypt instance for storage.
+ * Returns empty string if the BlockCrypt is not an AES_256_GCM instance.
+ */
+std::string AES_256_GCM_get_nonce(BlockCrypt* block_crypt)
+{
+ auto* gcm = dynamic_cast<AES_256_GCM*>(block_crypt);
+ if (gcm && gcm->is_nonce_initialized()) {
+ return gcm->get_nonce();
+ }
+ return {};
+}
+
+/**
+ * Test helper: derive object key for a BlockCrypt instance.
+ * Returns true on success.
+ */
+bool AES_256_GCM_derive_object_key(BlockCrypt* block_crypt,
+ const uint8_t* user_key,
+ size_t key_len,
+ const std::string& bucket,
+ const std::string& object,
+ uint32_t part_number,
+ const std::string& domain)
+{
+ auto* gcm = dynamic_cast<AES_256_GCM*>(block_crypt);
+ if (!gcm) {
+ return false;
+ }
+ return gcm->derive_object_key(user_key, key_len, bucket, object,
+ part_number, domain);
+}
+
+
bool AES_256_ECB_encrypt(const DoutPrefixProvider* dpp,
CephContext* cct,
const uint8_t* key,
RGWGetObj_Filter* next,
std::unique_ptr<BlockCrypt> crypt,
std::vector<size_t> parts_len,
+ std::vector<uint32_t> part_nums,
+ off_t encrypted_total_size,
+ bool has_compression,
optional_yield y)
:
RGWGetObj_Filter(next),
crypt(std::move(crypt)),
enc_begin_skip(0),
ofs(0),
+ enc_ofs(0),
end(0),
+ encrypted_total_size(encrypted_total_size),
+ has_compression(has_compression),
cache(),
y(y),
- parts_len(std::move(parts_len))
+ parts_len(std::move(parts_len)),
+ part_nums(std::move(part_nums)),
+ current_part_num(0)
{
block_size = this->crypt->get_block_size();
+ encrypted_block_size = this->crypt->get_encrypted_block_size();
+
+ /**
+ * Sanity check: when BOTH part_nums and parts_len are populated, they must
+ * match in size. A mismatch indicates data corruption or a bug.
+ *
+ * When parts_len is empty (e.g., GET ?partNumber=N where CRYPT_PARTS is
+ * intentionally skipped and the part object has no manifest), we can only
+ * trust a single fallback part number.
+ */
+ if (!this->part_nums.empty() && !this->parts_len.empty() &&
+ this->part_nums.size() != this->parts_len.size()) {
+ ldpp_dout(dpp, 0) << "ERROR: part_nums.size()=" << this->part_nums.size()
+ << " != parts_len.size()=" << this->parts_len.size()
+ << " - possible data corruption" << dendl;
+ this->part_nums.clear();
+ }
+ if (this->parts_len.empty() && this->part_nums.size() > 1) {
+ ldpp_dout(dpp, 0) << "ERROR: part_nums.size()=" << this->part_nums.size()
+ << " but parts_len is empty - cannot map part boundaries"
+ << dendl;
+ this->part_nums.clear();
+ }
+
+ // Initialize with first part's key if multipart
+ if (!this->part_nums.empty()) {
+ current_part_num = this->part_nums[0];
+ this->crypt->set_part_number(current_part_num);
+ }
}
RGWGetObj_BlockDecrypt::~RGWGetObj_BlockDecrypt() {
}
int RGWGetObj_BlockDecrypt::fixup_range(off_t& bl_ofs, off_t& bl_end) {
- /*
- * Cascade to the next filter first so it sees the original
- * plaintext range before we block-align for decryption.
- */
- if (next)
- next->fixup_range(bl_ofs, bl_end);
-
off_t inp_ofs = bl_ofs;
off_t inp_end = bl_end;
- if (parts_len.size() > 0) {
- off_t in_ofs = bl_ofs;
- off_t in_end = bl_end;
- size_t i = 0;
- while (i<parts_len.size() && (in_ofs >= (off_t)parts_len[i])) {
- in_ofs -= parts_len[i];
- i++;
- }
- //in_ofs is inside block i
- size_t j = 0;
- while (j<(parts_len.size() - 1) && (in_end >= (off_t)parts_len[j])) {
- in_end -= parts_len[j];
- j++;
- }
- //in_end is inside part j, OR j is the last part
-
- size_t rounded_end = ( in_end & ~(block_size - 1) ) + (block_size - 1);
- if (rounded_end > parts_len[j]) {
- rounded_end = parts_len[j] - 1;
+ // If compression is present, let the decompression filter remap the range
+ // from plaintext to compressed domain first. We'll then map compressed -> encrypted.
+ if (has_compression && next) {
+ int r = next->fixup_range(bl_ofs, bl_end);
+ if (r < 0) {
+ return r;
}
+ }
- enc_begin_skip = in_ofs & (block_size - 1);
+ if (parts_len.size() > 0) {
+ // Multipart object: find parts containing start and end offsets
+ PartLocation start_loc = find_part_for_plaintext_offset(bl_ofs, false);
+ PartLocation end_loc = find_part_for_plaintext_offset(bl_end, true); // clamp to last
+
+ // Block-align end within its part (in plaintext space)
+ size_t part_plaintext_end = encrypted_to_plaintext_size(parts_len[end_loc.part_idx]);
+ off_t rounded_end = std::min(
+ (off_t)((end_loc.offset_in_part & ~(block_size - 1)) + (block_size - 1)),
+ (off_t)(part_plaintext_end - 1));
+
+ // enc_begin_skip is offset within the starting block
+ enc_begin_skip = start_loc.offset_in_part & (block_size - 1);
ofs = bl_ofs - enc_begin_skip;
end = bl_end;
- bl_end += rounded_end - in_end;
- bl_ofs = std::min(bl_ofs - enc_begin_skip, bl_end);
+
+ // Convert end offset: plaintext -> encrypted, then align to encrypted block
+ off_t enc_end = align_to_encrypted_block_end(logical_to_encrypted_offset(rounded_end));
+ enc_end = std::min(enc_end, (off_t)(parts_len[end_loc.part_idx] - 1));
+ bl_end = end_loc.cumulative_encrypted + enc_end;
+
+ // Convert start offset: align in plaintext, then convert to encrypted
+ off_t aligned_start = std::max((off_t)0, start_loc.offset_in_part - enc_begin_skip);
+ bl_ofs = start_loc.cumulative_encrypted + logical_to_encrypted_offset(aligned_start);
+
+ // Clamp start to end (handles invalid ranges)
+ bl_ofs = std::min(bl_ofs, bl_end);
+ enc_ofs = bl_ofs;
}
else
{
+ // Simple object (no multipart)
enc_begin_skip = bl_ofs & (block_size - 1);
ofs = bl_ofs & ~(block_size - 1);
end = bl_end;
- bl_ofs = bl_ofs & ~(block_size - 1);
- bl_end = ( bl_end & ~(block_size - 1) ) + (block_size - 1);
+
+ // Calculate block-aligned logical range
+ off_t aligned_start = bl_ofs & ~(block_size - 1);
+ off_t aligned_end = (bl_end & ~(block_size - 1)) + (block_size - 1);
+
+ // Convert to encrypted offsets
+ bl_ofs = logical_to_encrypted_offset(aligned_start);
+ bl_end = align_to_encrypted_block_end(logical_to_encrypted_offset(aligned_end));
+ enc_ofs = bl_ofs;
+
+ // Clamp to actual encrypted object size
+ if (encrypted_total_size > 0 && bl_end >= encrypted_total_size) {
+ bl_end = encrypted_total_size - 1;
+ }
}
+
ldpp_dout(this->dpp, 20) << "fixup_range [" << inp_ofs << "," << inp_end
- << "] => [" << bl_ofs << "," << bl_end << "]" << dendl;
+ << "] => [" << bl_ofs << "," << bl_end << "]"
+ << " (block_size=" << block_size
+ << ", encrypted_block_size=" << encrypted_block_size << ")" << dendl;
+
+ if (next && !has_compression)
+ return next->fixup_range(bl_ofs, bl_end);
return 0;
}
if (!crypt->decrypt(in, 0, size, data, part_ofs, y)) {
return -ERR_INTERNAL_ERROR;
}
- off_t send_size = size - enc_begin_skip;
+
+ /**
+ * data.length() is the decrypted (plaintext) size.
+ * For GCM: data.length() < size (auth tags removed during decryption)
+ * For CBC: data.length() == size (no overhead)
+ */
+ off_t decrypted_size = data.length();
+
+ off_t send_size = decrypted_size - enc_begin_skip;
if (ofs + enc_begin_skip + send_size > end + 1) {
send_size = end + 1 - ofs - enc_begin_skip;
}
int res = next->handle_data(data, enc_begin_skip, send_size);
enc_begin_skip = 0;
- ofs += size;
- in.splice(0, size);
+ ofs += decrypted_size; // Advance plaintext position
+ enc_ofs += size; // Advance encrypted position (for part boundary tracking)
+ in.splice(0, size); // Remove encrypted data from input buffer
return res;
}
-int RGWGetObj_BlockDecrypt::handle_data(bufferlist& bl, off_t bl_ofs, off_t bl_len) {
- ldpp_dout(this->dpp, 25) << "Decrypt " << bl_len << " bytes" << dendl;
- bl.begin(bl_ofs).copy(bl_len, cache);
-
+int RGWGetObj_BlockDecrypt::process_part_boundaries(size_t& plain_part_ofs_out) {
+ size_t enc_part_ofs = enc_ofs;
+ size_t plain_part_ofs = ofs;
+ const bool is_multipart = !part_nums.empty();
+ uint32_t part_idx = 0;
int res = 0;
- size_t part_ofs = ofs;
+
for (size_t part : parts_len) {
- if (part_ofs >= part) {
- part_ofs -= part;
- } else if (part_ofs + cache.length() >= part) {
- // flush data up to part boundaries, aligned or not
- res = process(cache, part_ofs, part - part_ofs);
+ // Get actual S3 part number from attribute (not calculated!)
+ uint32_t this_part_num = 0;
+ if (is_multipart && part_idx < part_nums.size()) {
+ this_part_num = part_nums[part_idx];
+ }
+
+ if (enc_part_ofs >= part) {
+ // Past this part entirely, skip to next
+ enc_part_ofs -= part;
+ plain_part_ofs -= encrypted_to_plaintext_size(part);
+ part_idx++;
+ } else if (enc_part_ofs + cache.length() >= part) {
+ // Ensure cipher has correct part number
+ if (is_multipart && current_part_num != this_part_num) {
+ current_part_num = this_part_num;
+ crypt->set_part_number(current_part_num);
+ }
+
+ // Data crosses part boundary - process up to boundary
+ size_t enc_bytes_this_part = part - enc_part_ofs;
+ res = process(cache, plain_part_ofs, enc_bytes_this_part);
if (res < 0) {
return res;
}
- part_ofs = 0;
+
+ // Move to next part
+ part_idx++;
+ uint32_t next_part_num = 0;
+ if (is_multipart && part_idx < part_nums.size()) {
+ next_part_num = part_nums[part_idx];
+ }
+ if (is_multipart && part_idx < parts_len.size() && current_part_num != next_part_num) {
+ current_part_num = next_part_num;
+ crypt->set_part_number(current_part_num);
+ }
+
+ enc_part_ofs = 0;
+ plain_part_ofs = 0;
} else {
+ // Ensure cipher has correct part number
+ if (is_multipart && current_part_num != this_part_num) {
+ current_part_num = this_part_num;
+ crypt->set_part_number(current_part_num);
+ }
break;
}
}
- // write up to block boundaries, aligned only
- off_t aligned_size = cache.length() & ~(block_size - 1);
+
+ plain_part_ofs_out = plain_part_ofs;
+ return 0;
+}
+
+RGWGetObj_BlockDecrypt::PartLocation
+RGWGetObj_BlockDecrypt::find_part_for_plaintext_offset(off_t plaintext_ofs, bool clamp_to_last) const
+{
+ PartLocation loc = {0, plaintext_ofs, 0};
+
+ // If clamp_to_last, stop at second-to-last part (used for end offsets)
+ size_t limit = (clamp_to_last && parts_len.size() > 0)
+ ? (parts_len.size() - 1)
+ : parts_len.size();
+
+ while (loc.part_idx < limit) {
+ size_t part_plain = encrypted_to_plaintext_size(parts_len[loc.part_idx]);
+ if (loc.offset_in_part < (off_t)part_plain) {
+ break; // Found the part containing this offset
+ }
+ loc.offset_in_part -= part_plain;
+ loc.cumulative_encrypted += parts_len[loc.part_idx];
+ loc.part_idx++;
+ }
+
+ return loc;
+}
+
+int RGWGetObj_BlockDecrypt::handle_data(bufferlist& bl, off_t bl_ofs, off_t bl_len) {
+ ldpp_dout(this->dpp, 25) << "Decrypt " << bl_len << " bytes"
+ << " (encrypted_block_size=" << encrypted_block_size << ")" << dendl;
+ bl.begin(bl_ofs).copy(bl_len, cache);
+
+ size_t plain_part_ofs;
+ int res = process_part_boundaries(plain_part_ofs);
+ if (res < 0) return res;
+
+ // Process up to encrypted block boundaries
+ off_t aligned_size = (cache.length() / encrypted_block_size) * encrypted_block_size;
if (aligned_size > 0) {
- res = process(cache, part_ofs, aligned_size);
+ res = process(cache, plain_part_ofs, aligned_size);
}
return res;
}
*/
int RGWGetObj_BlockDecrypt::flush() {
ldpp_dout(this->dpp, 25) << "Decrypt flushing " << cache.length() << " bytes" << dendl;
- int res = 0;
- size_t part_ofs = ofs;
- for (size_t part : parts_len) {
- if (part_ofs >= part) {
- part_ofs -= part;
- } else if (part_ofs + cache.length() >= part) {
- // flush data up to part boundaries, aligned or not
- res = process(cache, part_ofs, part - part_ofs);
- if (res < 0) {
- return res;
- }
- part_ofs = 0;
- } else {
- break;
- }
- }
- // flush up to block boundaries, aligned or not
+
+ size_t plain_part_ofs;
+ int res = process_part_boundaries(plain_part_ofs);
+ if (res < 0) return res;
+
+ // Flush remaining data (possibly unaligned final block)
if (cache.length() > 0) {
- res = process(cache, part_ofs, cache.length());
- if (res < 0) {
- return res;
- }
+ res = process(cache, plain_part_ofs, cache.length());
+ if (res < 0) return res;
}
- if (next)
- return next->flush();
-
- return 0;
+ return next ? next->flush() : 0;
}
RGWPutObj_BlockEncrypt::RGWPutObj_BlockEncrypt(const DoutPrefixProvider *dpp,
if (!crypt->encrypt(in, 0, proc_size, out, logical_offset, y)) {
return -ERR_INTERNAL_ERROR;
}
- int r = Pipe::process(std::move(out), logical_offset);
- logical_offset += proc_size;
+ // For size-expanding ciphers (GCM), out.length() > proc_size
+ // Use encrypted_offset for downstream writes, not plaintext logical_offset
+ int r = Pipe::process(std::move(out), encrypted_offset);
+ encrypted_offset += out.length();
if (r < 0)
return r;
}
if (flush) {
/*replicate 0-sized handle_data*/
- return Pipe::process({}, logical_offset);
+ return Pipe::process({}, encrypted_offset);
}
return 0;
}
break;
}
}
- if (res != 0) {
- ldpp_dout(s, 5) << "ERROR: unable to save new key_id on bucket" << dendl;
- s->err.message = "Server side error - unable to save key_id";
- return res;
+ if (res != 0) {
+ ldpp_dout(s, 5) << "ERROR: unable to save new key_id on bucket" << dendl;
+ s->err.message = "Server side error - unable to save key_id";
+ return res;
+ }
+ }
+ return 0;
+}
+
+
+/**
+ * Generate random GCM nonce and store in attributes.
+ */
+static std::string generate_gcm_nonce(
+ req_state* s,
+ std::map<std::string, ceph::bufferlist>& attrs)
+{
+ std::string nonce(AES_256_GCM_NONCE_SIZE, '\0');
+ s->cct->random()->get_bytes(nonce.data(), AES_256_GCM_NONCE_SIZE);
+ set_attr(attrs, RGW_ATTR_CRYPT_NONCE, nonce);
+ return nonce;
+}
+
+/**
+ * Store plaintext size for GCM objects (needed for size accounting).
+ * Note: Only set when content_length is actually known (> 0).
+ * For chunked uploads without x-amz-decoded-content-length, content_length
+ * is 0 and we must NOT set ORIGINAL_SIZE here - it will be set later in
+ * RGWPutObj::execute() after all data is processed.
+ */
+static void set_gcm_plaintext_size(
+ req_state* s,
+ std::map<std::string, ceph::bufferlist>& attrs,
+ bool is_copy)
+{
+ if (s->content_length > 0 && !is_copy) {
+ set_attr(attrs, RGW_ATTR_CRYPT_ORIGINAL_SIZE,
+ std::to_string(s->content_length));
+ }
+}
+
+/**
+ * Retrieve and validate stored GCM nonce for decryption.
+ * Returns empty string on error (caller should return -EIO).
+ */
+static std::string get_gcm_nonce(
+ const DoutPrefixProvider* dpp,
+ req_state* s,
+ const std::map<std::string, ceph::bufferlist>& attrs,
+ std::string_view mode_name)
+{
+ std::string stored_nonce = get_str_attribute(attrs, RGW_ATTR_CRYPT_NONCE);
+ if (stored_nonce.empty()) {
+ ldpp_dout(dpp, 5) << "ERROR: " << mode_name << " decryption failed: "
+ << "nonce attribute is missing" << dendl;
+ s->err.message = "Object encryption metadata is corrupted.";
+ return {};
+ }
+ if (stored_nonce.size() != AES_256_GCM_NONCE_SIZE) {
+ ldpp_dout(dpp, 5) << "ERROR: " << mode_name << " decryption failed: "
+ << "stored nonce has invalid size " << stored_nonce.size()
+ << " (expected " << AES_256_GCM_NONCE_SIZE << ")" << dendl;
+ s->err.message = "Object encryption metadata is corrupted.";
+ return {};
+ }
+ return stored_nonce;
+}
+
+bool rgw_get_aead_original_size(const DoutPrefixProvider* dpp,
+ const std::map<std::string, bufferlist>& attrs,
+ uint64_t* original_size)
+{
+ if (!original_size) {
+ return false;
+ }
+
+ const auto mode = get_str_attribute(attrs, RGW_ATTR_CRYPT_MODE);
+ if (!is_aead_mode(mode)) {
+ return false;
+ }
+
+ auto p = attrs.find(RGW_ATTR_CRYPT_ORIGINAL_SIZE);
+ if (p == attrs.end()) {
+ return false;
+ }
+
+ try {
+ *original_size = std::stoull(p->second.to_str());
+ return true;
+ } catch (const std::exception& e) {
+ if (dpp) {
+ ldpp_dout(dpp, 0) << "ERROR: invalid RGW_ATTR_CRYPT_ORIGINAL_SIZE: "
+ << e.what() << dendl;
+ }
+ return false;
+ }
+}
+
+bool rgw_get_aead_decrypted_size(const DoutPrefixProvider* dpp,
+ const std::map<std::string, bufferlist>& attrs,
+ uint64_t encrypted_size,
+ uint64_t* decrypted_size)
+{
+ if (!decrypted_size) {
+ return false;
+ }
+
+ const auto mode = get_str_attribute(attrs, RGW_ATTR_CRYPT_MODE);
+ if (!is_aead_mode(mode)) {
+ return false;
+ }
+
+ /* Try CRYPT_PARTS first (more accurate for multipart) */
+ if (auto i = attrs.find(RGW_ATTR_CRYPT_PARTS); i != attrs.end()) {
+ std::vector<size_t> parts_len;
+ try {
+ auto iter = i->second.cbegin();
+ using ceph::decode;
+ decode(parts_len, iter);
+ } catch (const buffer::error&) {
+ if (dpp) {
+ ldpp_dout(dpp, 1) << "failed to decode RGW_ATTR_CRYPT_PARTS" << dendl;
+ }
+ parts_len.clear();
+ }
+ if (!parts_len.empty()) {
+ uint64_t total = 0;
+ for (size_t enc_part : parts_len) {
+ total += aead_encrypted_to_plaintext_size(enc_part, dpp);
+ }
+ *decrypted_size = total;
+ return true;
}
}
- return 0;
+
+ /* Fallback: calculate from total encrypted size */
+ *decrypted_size = aead_encrypted_to_plaintext_size(encrypted_size, dpp);
+ if (dpp) {
+ ldpp_dout(dpp, 20) << "AEAD: calculated decrypted size " << *decrypted_size
+ << " from encrypted " << encrypted_size << dendl;
+ }
+ return true;
}
+
int rgw_s3_prepare_encrypt(req_state* s, optional_yield y,
std::map<std::string, ceph::bufferlist>& attrs,
std::unique_ptr<BlockCrypt>* block_crypt,
- std::map<std::string, std::string>& crypt_http_responses)
+ std::map<std::string, std::string>& crypt_http_responses,
+ uint32_t part_number)
{
int res = 0;
CryptAttributes crypt_attributes { s };
+ const bool is_copy = (s->op_type == RGW_OP_COPY_OBJ);
crypt_http_responses.clear();
{
return -EINVAL;
}
- set_attr(attrs, RGW_ATTR_CRYPT_MODE, "SSE-C-AES256");
set_attr(attrs, RGW_ATTR_CRYPT_KEYMD5, keymd5_bin);
- if (block_crypt) {
- auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s, s->cct));
- aes->set_key(reinterpret_cast<const uint8_t*>(key_bin.c_str()), AES_256_KEYSIZE);
- *block_crypt = std::move(aes);
+ // Check config for encryption algorithm preference
+ const bool use_gcm = (s->cct->_conf->rgw_crypt_sse_algorithm == "aes-256-gcm");
+
+ if (use_gcm) {
+ set_attr(attrs, RGW_ATTR_CRYPT_MODE, "SSE-C-AES256-GCM");
+ std::string nonce = generate_gcm_nonce(s, attrs);
+ set_gcm_plaintext_size(s, attrs, is_copy);
+
+ if (block_crypt) {
+ auto gcm = std::unique_ptr<AES_256_GCM>(new AES_256_GCM(s, s->cct));
+ if (!gcm->set_nonce(reinterpret_cast<const uint8_t*>(nonce.c_str()), nonce.size())) {
+ ldpp_dout(s, 5) << "ERROR: SSE-C-AES256-GCM encryption failed: "
+ << "could not initialize nonce" << dendl;
+ ::ceph::crypto::zeroize_for_security(key_bin.data(), key_bin.length());
+ return -EIO;
+ }
+ // Derive encryption key from user key + object identity
+ if (!gcm->derive_object_key(
+ reinterpret_cast<const uint8_t*>(key_bin.c_str()),
+ AES_256_KEYSIZE,
+ s->bucket->get_name(),
+ s->object->get_name(),
+ part_number)) {
+ ldpp_dout(s, 5) << "ERROR: SSE-C-AES256-GCM key derivation failed for "
+ << s->bucket->get_name() << "/" << s->object->get_name() << dendl;
+ s->err.message = "Failed to derive encryption key.";
+ ::ceph::crypto::zeroize_for_security(key_bin.data(), key_bin.length());
+ return -EIO;
+ }
+ *block_crypt = std::move(gcm);
+ }
+ } else {
+ set_attr(attrs, RGW_ATTR_CRYPT_MODE, "SSE-C-AES256");
+ if (block_crypt) {
+ auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s, s->cct));
+ aes->set_key(reinterpret_cast<const uint8_t*>(key_bin.c_str()), AES_256_KEYSIZE);
+ *block_crypt = std::move(aes);
+ }
}
crypt_http_responses["x-amz-server-side-encryption-customer-algorithm"] = "AES256";
std::string key_selector = create_random_key_selector(s->cct);
set_attr(attrs, RGW_ATTR_CRYPT_KEYSEL, key_selector);
}
- set_attr(attrs, RGW_ATTR_CRYPT_MODE, "SSE-KMS");
set_attr(attrs, RGW_ATTR_CRYPT_KEYID, key_id);
set_attr(attrs, RGW_ATTR_CRYPT_CONTEXT, cooked_context);
std::string actual_key;
return -EINVAL;
}
- if (block_crypt) {
- auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s, s->cct));
- aes->set_key(reinterpret_cast<const uint8_t*>(actual_key.c_str()), AES_256_KEYSIZE);
- *block_crypt = std::move(aes);
+ const bool use_gcm = (s->cct->_conf->rgw_crypt_sse_algorithm == "aes-256-gcm");
+
+ if (use_gcm) {
+ set_attr(attrs, RGW_ATTR_CRYPT_MODE, "SSE-KMS-GCM");
+ std::string nonce = generate_gcm_nonce(s, attrs);
+ set_gcm_plaintext_size(s, attrs, is_copy);
+
+ if (block_crypt) {
+ auto aes = AES_256_GCM_create(s, s->cct,
+ reinterpret_cast<const uint8_t*>(actual_key.c_str()),
+ AES_256_KEYSIZE,
+ reinterpret_cast<const uint8_t*>(nonce.c_str()),
+ nonce.size(),
+ part_number);
+ if (!aes) {
+ ldpp_dout(s, 5) << "ERROR: Failed to create AES-256-GCM instance" << dendl;
+ ::ceph::crypto::zeroize_for_security(actual_key.data(), actual_key.length());
+ return -EIO;
+ }
+ *block_crypt = std::move(aes);
+ }
+ } else {
+ set_attr(attrs, RGW_ATTR_CRYPT_MODE, "SSE-KMS");
+ if (block_crypt) {
+ auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s, s->cct));
+ aes->set_key(reinterpret_cast<const uint8_t*>(actual_key.c_str()), AES_256_KEYSIZE);
+ *block_crypt = std::move(aes);
+ }
}
::ceph::crypto::zeroize_for_security(actual_key.data(), actual_key.length());
}
set_attr(attrs, RGW_ATTR_CRYPT_CONTEXT, cooked_context);
- set_attr(attrs, RGW_ATTR_CRYPT_MODE, "AES256");
set_attr(attrs, RGW_ATTR_CRYPT_KEYID, key_id);
std::string actual_key;
res = make_actual_key_from_sse_s3(s, attrs, y, actual_key);
return -EINVAL;
}
- if (block_crypt) {
- auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s, s->cct));
- aes->set_key(reinterpret_cast<const uint8_t*>(actual_key.c_str()), AES_256_KEYSIZE);
- *block_crypt = std::move(aes);
+ const bool use_gcm = (s->cct->_conf->rgw_crypt_sse_algorithm == "aes-256-gcm");
+
+ if (use_gcm) {
+ set_attr(attrs, RGW_ATTR_CRYPT_MODE, "AES256-GCM");
+ std::string nonce = generate_gcm_nonce(s, attrs);
+ set_gcm_plaintext_size(s, attrs, is_copy);
+
+ if (block_crypt) {
+ auto aes = AES_256_GCM_create(s, s->cct,
+ reinterpret_cast<const uint8_t*>(actual_key.c_str()),
+ AES_256_KEYSIZE,
+ reinterpret_cast<const uint8_t*>(nonce.c_str()),
+ nonce.size(),
+ part_number);
+ if (!aes) {
+ ldpp_dout(s, 5) << "ERROR: Failed to create AES-256-GCM instance" << dendl;
+ ::ceph::crypto::zeroize_for_security(actual_key.data(), actual_key.length());
+ return -EIO;
+ }
+ *block_crypt = std::move(aes);
+ }
+ } else {
+ set_attr(attrs, RGW_ATTR_CRYPT_MODE, "AES256");
+ if (block_crypt) {
+ auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s, s->cct));
+ aes->set_key(reinterpret_cast<const uint8_t*>(actual_key.c_str()), AES_256_KEYSIZE);
+ *block_crypt = std::move(aes);
+ }
}
::ceph::crypto::zeroize_for_security(actual_key.data(), actual_key.length());
return 0;
}
- set_attr(attrs, RGW_ATTR_CRYPT_MODE, "RGW-AUTO");
- std::string key_selector = create_random_key_selector(s->cct);
- set_attr(attrs, RGW_ATTR_CRYPT_KEYSEL, key_selector);
+ const bool use_gcm = (s->cct->_conf->rgw_crypt_sse_algorithm == "aes-256-gcm");
+
+ if (use_gcm) {
+ set_attr(attrs, RGW_ATTR_CRYPT_MODE, "RGW-AUTO-GCM");
+ std::string nonce = generate_gcm_nonce(s, attrs);
+ set_gcm_plaintext_size(s, attrs, is_copy);
+
+ if (block_crypt) {
+ auto gcm = std::unique_ptr<AES_256_GCM>(new AES_256_GCM(s, s->cct));
+ if (!gcm->set_nonce(reinterpret_cast<const uint8_t*>(nonce.c_str()), nonce.size())) {
+ ldpp_dout(s, 5) << "ERROR: RGW-AUTO-GCM: could not initialize nonce" << dendl;
+ return -EIO;
+ }
+
+ // Derive encryption key using HMAC-SHA256 with context binding
+ // Key = HMAC-SHA256(master_key, nonce || "RGW-AUTO-GCM" || bucket || object)
+ if (!gcm->derive_object_key(
+ reinterpret_cast<const uint8_t*>(master_encryption_key.c_str()),
+ AES_256_KEYSIZE,
+ s->bucket->get_name(),
+ s->object->get_name(),
+ part_number,
+ "RGW-AUTO-GCM")) {
+ ldpp_dout(s, 5) << "ERROR: RGW-AUTO-GCM key derivation failed for "
+ << s->bucket->get_name() << "/" << s->object->get_name() << dendl;
+ return -EIO;
+ }
+ *block_crypt = std::move(gcm);
+ }
+ } else {
+ // CBC mode: use AES-ECB key derivation (legacy approach)
+ std::string key_selector = create_random_key_selector(s->cct);
+ set_attr(attrs, RGW_ATTR_CRYPT_KEYSEL, key_selector);
+
+ uint8_t actual_key[AES_256_KEYSIZE];
+ if (AES_256_ECB_encrypt(s, s->cct,
+ reinterpret_cast<const uint8_t*>(master_encryption_key.c_str()), AES_256_KEYSIZE,
+ reinterpret_cast<const uint8_t*>(key_selector.c_str()),
+ actual_key, AES_256_KEYSIZE) != true) {
+ ::ceph::crypto::zeroize_for_security(actual_key, sizeof(actual_key));
+ return -EIO;
+ }
- uint8_t actual_key[AES_256_KEYSIZE];
- if (AES_256_ECB_encrypt(s, s->cct,
- reinterpret_cast<const uint8_t*>(master_encryption_key.c_str()), AES_256_KEYSIZE,
- reinterpret_cast<const uint8_t*>(key_selector.c_str()),
- actual_key, AES_256_KEYSIZE) != true) {
+ set_attr(attrs, RGW_ATTR_CRYPT_MODE, "RGW-AUTO");
+ if (block_crypt) {
+ auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s, s->cct));
+ aes->set_key(actual_key, AES_256_KEYSIZE);
+ *block_crypt = std::move(aes);
+ }
::ceph::crypto::zeroize_for_security(actual_key, sizeof(actual_key));
- return -EIO;
}
- if (block_crypt) {
- auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s, s->cct));
- aes->set_key(reinterpret_cast<const uint8_t*>(actual_key), AES_256_KEYSIZE);
- *block_crypt = std::move(aes);
- }
- ::ceph::crypto::zeroize_for_security(actual_key, sizeof(actual_key));
return 0;
}
}
}
+static void pick_gcm_identity(req_state* s,
+ bool copy_source,
+ const rgw_crypt_src_identity* src_identity,
+ std::string& bucket_name,
+ std::string& object_name)
+{
+ if (copy_source) {
+ if (src_identity && src_identity->valid()) {
+ bucket_name = std::string(src_identity->bucket);
+ object_name = std::string(src_identity->object);
+ return;
+ }
+ if (s->src_object) {
+ bucket_name = s->src_bucket_name;
+ object_name = s->src_object->get_name();
+ return;
+ }
+ }
+ bucket_name = s->bucket->get_name();
+ object_name = s->object->get_name();
+}
+
int rgw_s3_prepare_decrypt(req_state* s, optional_yield y,
map<string, bufferlist>& attrs,
std::unique_ptr<BlockCrypt>* block_crypt,
std::map<std::string, std::string>* crypt_http_responses,
- bool copy_source)
+ bool copy_source,
+ uint32_t part_number,
+ const rgw_crypt_src_identity* src_identity)
{
int res = 0;
std::string stored_mode = get_str_attribute(attrs, RGW_ATTR_CRYPT_MODE);
return 0;
}
+ if (stored_mode == "SSE-C-AES256-GCM") {
+ if (s->cct->_conf->rgw_crypt_require_ssl &&
+ !rgw_transport_is_secure(s->cct, *s->info.env)) {
+ ldpp_dout(s, 5) << "ERROR: Insecure request, rgw_crypt_require_ssl is set" << dendl;
+ return -ERR_INVALID_REQUEST;
+ }
+
+ const char *sse_c_algo_hdr = copy_source ? "HTTP_X_AMZ_COPY_SOURCE_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM" :
+ "HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM";
+ const char *req_cust_alg = s->info.env->get(sse_c_algo_hdr, NULL);
+ if (nullptr == req_cust_alg) {
+ ldpp_dout(s, 5) << "ERROR: Request for SSE-C encrypted object missing "
+ << "x-amz-server-side-encryption-customer-algorithm"
+ << dendl;
+ s->err.message = "Requests specifying Server Side Encryption with Customer "
+ "provided keys must provide a valid encryption algorithm.";
+ return -EINVAL;
+ } else if (strcmp(req_cust_alg, "AES256") != 0) {
+ ldpp_dout(s, 5) << "ERROR: The requested encryption algorithm is not valid, must be AES256." << dendl;
+ s->err.message = "The requested encryption algorithm is not valid, must be AES256.";
+ return -ERR_INVALID_ENCRYPTION_ALGORITHM;
+ }
+
+ const char *sse_c_key_hdr = copy_source ? "HTTP_X_AMZ_COPY_SOURCE_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY" :
+ "HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY";
+ std::string key_bin;
+ try {
+ key_bin = from_base64(s->info.env->get(sse_c_key_hdr, ""));
+ } catch (...) {
+ ldpp_dout(s, 5) << "ERROR: rgw_s3_prepare_decrypt invalid encryption key "
+ << "which contains character that is not base64 encoded."
+ << dendl;
+ s->err.message = "Requests specifying Server Side Encryption with Customer "
+ "provided keys must provide an appropriate secret key.";
+ return -EINVAL;
+ }
+
+ if (key_bin.size() != AES_256_KEYSIZE) {
+ ldpp_dout(s, 5) << "ERROR: Invalid encryption key size" << dendl;
+ s->err.message = "Requests specifying Server Side Encryption with Customer "
+ "provided keys must provide an appropriate secret key.";
+ return -EINVAL;
+ }
+
+ const char *sse_c_key_md5_hdr = copy_source ? "HTTP_X_AMZ_COPY_SOURCE_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5" :
+ "HTTP_X_AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5";
+ std::string keymd5 = s->info.env->get(sse_c_key_md5_hdr, "");
+ std::string keymd5_bin;
+ try {
+ keymd5_bin = from_base64(keymd5);
+ } catch (...) {
+ ldpp_dout(s, 5) << "ERROR: rgw_s3_prepare_decrypt invalid encryption key md5 "
+ << "which contains character that is not base64 encoded."
+ << dendl;
+ s->err.message = "Requests specifying Server Side Encryption with Customer "
+ "provided keys must provide an appropriate secret key md5.";
+ return -EINVAL;
+ }
+
+ if (keymd5_bin.size() != CEPH_CRYPTO_MD5_DIGESTSIZE) {
+ ldpp_dout(s, 5) << "ERROR: Invalid key md5 size " << dendl;
+ s->err.message = "Requests specifying Server Side Encryption with Customer "
+ "provided keys must provide an appropriate secret key md5.";
+ return -EINVAL;
+ }
+
+ MD5 key_hash;
+ // Allow use of MD5 digest in FIPS mode for non-cryptographic purposes
+ key_hash.SetFlags(EVP_MD_CTX_FLAG_NON_FIPS_ALLOW);
+ uint8_t key_hash_res[CEPH_CRYPTO_MD5_DIGESTSIZE];
+ key_hash.Update(reinterpret_cast<const unsigned char*>(key_bin.c_str()), key_bin.size());
+ key_hash.Final(key_hash_res);
+
+ if ((memcmp(key_hash_res, keymd5_bin.c_str(), CEPH_CRYPTO_MD5_DIGESTSIZE) != 0) ||
+ (get_str_attribute(attrs, RGW_ATTR_CRYPT_KEYMD5) != keymd5_bin)) {
+ s->err.message = "The calculated MD5 hash of the key did not match the hash that was provided.";
+ return -EINVAL;
+ }
+
+ std::string stored_nonce = get_gcm_nonce(s, s, attrs, "SSE-C-AES256-GCM");
+ if (stored_nonce.empty()) return -EIO;
+
+ auto gcm = std::make_unique<AES_256_GCM>(s, s->cct);
+ gcm->set_nonce(reinterpret_cast<const uint8_t*>(stored_nonce.c_str()),
+ stored_nonce.size());
+ // Re-derive encryption key from user key + object identity
+ // For CopyObject, use the SOURCE object's identity (not destination)
+ std::string bucket_name;
+ std::string object_name;
+ pick_gcm_identity(s, copy_source, src_identity, bucket_name, object_name);
+ if (!gcm->derive_object_key(
+ reinterpret_cast<const uint8_t*>(key_bin.c_str()),
+ AES_256_KEYSIZE,
+ bucket_name,
+ object_name,
+ part_number)) {
+ ldpp_dout(s, 5) << "ERROR: SSE-C-AES256-GCM key derivation failed for "
+ << bucket_name << "/" << object_name << dendl;
+ s->err.message = "Failed to derive decryption key.";
+ return -EIO;
+ }
+ if (block_crypt) *block_crypt = std::move(gcm);
+
+ if (crypt_http_responses) {
+ crypt_http_responses->emplace("x-amz-server-side-encryption-customer-algorithm", "AES256");
+ crypt_http_responses->emplace("x-amz-server-side-encryption-customer-key-MD5", keymd5);
+ }
+
+ return 0;
+ }
+
if (stored_mode == "SSE-KMS") {
if (s->cct->_conf->rgw_crypt_require_ssl &&
!rgw_transport_is_secure(s->cct, *s->info.env)) {
auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s, s->cct));
aes->set_key(reinterpret_cast<const uint8_t*>(actual_key.c_str()), AES_256_KEYSIZE);
- actual_key.replace(0, actual_key.length(), actual_key.length(), '\000');
+ ::ceph::crypto::zeroize_for_security(actual_key.data(), actual_key.length());
+ if (block_crypt) *block_crypt = std::move(aes);
+
+ if (crypt_http_responses) {
+ crypt_http_responses->emplace("x-amz-server-side-encryption", "aws:kms");
+ crypt_http_responses->emplace("x-amz-server-side-encryption-aws-kms-key-id", key_id);
+ }
+
+ return 0;
+ }
+
+ if (stored_mode == "SSE-KMS-GCM") {
+ if (s->cct->_conf->rgw_crypt_require_ssl &&
+ !rgw_transport_is_secure(s->cct, *s->info.env)) {
+ ldpp_dout(s, 5) << "ERROR: Insecure request, rgw_crypt_require_ssl is set" << dendl;
+ return -ERR_INVALID_REQUEST;
+ }
+ /* try to retrieve actual key */
+ std::string key_id = get_str_attribute(attrs, RGW_ATTR_CRYPT_KEYID);
+ std::string actual_key;
+ res = reconstitute_actual_key_from_kms(s, attrs, y, actual_key);
+ if (res != 0) {
+ ldpp_dout(s, 10) << "ERROR: failed to retrieve actual key from key_id: " << key_id << dendl;
+ s->err.message = "Failed to retrieve the actual key, kms-keyid: " + key_id;
+ return res;
+ }
+ if (actual_key.size() != AES_256_KEYSIZE) {
+ ldpp_dout(s, 0) << "ERROR: key obtained from key_id:" <<
+ key_id << " is not 256 bit size" << dendl;
+ s->err.message = "KMS provided an invalid key for the given kms-keyid.";
+ return -EINVAL;
+ }
+
+ std::string stored_nonce = get_gcm_nonce(s, s, attrs, "SSE-KMS-GCM");
+ if (stored_nonce.empty()) {
+ ::ceph::crypto::zeroize_for_security(actual_key.data(), actual_key.length());
+ return -EIO;
+ }
+
+ auto aes = AES_256_GCM_create(s, s->cct,
+ reinterpret_cast<const uint8_t*>(actual_key.c_str()),
+ AES_256_KEYSIZE,
+ reinterpret_cast<const uint8_t*>(stored_nonce.c_str()),
+ stored_nonce.size(),
+ part_number);
+ ::ceph::crypto::zeroize_for_security(actual_key.data(), actual_key.length());
+ if (!aes) {
+ ldpp_dout(s, 5) << "ERROR: Failed to create AES-256-GCM instance for decryption" << dendl;
+ return -EIO;
+ }
if (block_crypt) *block_crypt = std::move(aes);
if (crypt_http_responses) {
return 0;
}
+ if (stored_mode == "RGW-AUTO-GCM") {
+ std::string master_encryption_key;
+ try {
+ master_encryption_key = from_base64(std::string(s->cct->_conf->rgw_crypt_default_encryption_key));
+ } catch (...) {
+ ldpp_dout(s, 5) << "ERROR: rgw_s3_prepare_decrypt invalid default encryption key "
+ << "which contains character that is not base64 encoded."
+ << dendl;
+ s->err.message = "The default encryption key is not valid base64.";
+ return -EINVAL;
+ }
+
+ if (master_encryption_key.size() != 256 / 8) {
+ ldpp_dout(s, 0) << "ERROR: failed to decode 'rgw crypt default encryption key' to 256 bit string" << dendl;
+ return -EIO;
+ }
+
+ std::string stored_nonce = get_gcm_nonce(s, s, attrs, "RGW-AUTO-GCM");
+ if (stored_nonce.empty()) return -EIO;
+
+ auto gcm = std::make_unique<AES_256_GCM>(s, s->cct);
+ gcm->set_nonce(reinterpret_cast<const uint8_t*>(stored_nonce.c_str()),
+ stored_nonce.size());
+
+ // Re-derive encryption key using HMAC-SHA256 with context binding
+ // For CopyObject, use the SOURCE object's identity (not destination)
+ std::string bucket_name;
+ std::string object_name;
+ pick_gcm_identity(s, copy_source, src_identity, bucket_name, object_name);
+
+ if (!gcm->derive_object_key(
+ reinterpret_cast<const uint8_t*>(master_encryption_key.c_str()),
+ AES_256_KEYSIZE,
+ bucket_name,
+ object_name,
+ part_number,
+ "RGW-AUTO-GCM")) {
+ ldpp_dout(s, 5) << "ERROR: RGW-AUTO-GCM key derivation failed for "
+ << bucket_name << "/" << object_name << dendl;
+ s->err.message = "Failed to derive decryption key.";
+ return -EIO;
+ }
+
+ if (block_crypt) *block_crypt = std::move(gcm);
+ return 0;
+ }
+
/* SSE-S3 */
if (stored_mode == "AES256") {
/* try to retrieve actual key */
auto aes = std::unique_ptr<AES_256_CBC>(new AES_256_CBC(s, s->cct));
aes->set_key(reinterpret_cast<const uint8_t*>(actual_key.c_str()), AES_256_KEYSIZE);
- actual_key.replace(0, actual_key.length(), actual_key.length(), '\000');
+ ::ceph::crypto::zeroize_for_security(actual_key.data(), actual_key.length());
+ if (block_crypt) *block_crypt = std::move(aes);
+
+ if (crypt_http_responses) {
+ crypt_http_responses->emplace("x-amz-server-side-encryption", "AES256");
+ }
+
+ return 0;
+ }
+
+ /* SSE-S3 with GCM */
+ if (stored_mode == "AES256-GCM") {
+ /* try to retrieve actual key */
+ std::string key_id = get_str_attribute(attrs, RGW_ATTR_CRYPT_KEYID);
+ std::string actual_key;
+ res = reconstitute_actual_key_from_sse_s3(s, attrs, y, actual_key);
+ if (res != 0) {
+ ldpp_dout(s, 10) << "ERROR: failed to retrieve actual key" << dendl;
+ s->err.message = "Failed to retrieve the actual key";
+ return res;
+ }
+ if (actual_key.size() != AES_256_KEYSIZE) {
+ ldpp_dout(s, 0) << "ERROR: key obtained " <<
+ "is not 256 bit size" << dendl;
+ s->err.message = "SSE-S3 provided an invalid key for the given keyid.";
+ return -EINVAL;
+ }
+
+ std::string stored_nonce = get_gcm_nonce(s, s, attrs, "AES256-GCM");
+ if (stored_nonce.empty()) {
+ ::ceph::crypto::zeroize_for_security(actual_key.data(), actual_key.length());
+ return -EIO;
+ }
+
+ auto aes = AES_256_GCM_create(s, s->cct,
+ reinterpret_cast<const uint8_t*>(actual_key.c_str()),
+ AES_256_KEYSIZE,
+ reinterpret_cast<const uint8_t*>(stored_nonce.c_str()),
+ stored_nonce.size(),
+ part_number);
+ ::ceph::crypto::zeroize_for_security(actual_key.data(), actual_key.length());
+ if (!aes) {
+ ldpp_dout(s, 5) << "ERROR: Failed to create AES-256-GCM instance for decryption" << dendl;
+ return -EIO;
+ }
if (block_crypt) *block_crypt = std::move(aes);
if (crypt_http_responses) {
#include "rgw_putobj.h"
#include "common/async/yield_context.h"
+struct rgw_crypt_src_identity {
+ std::string_view bucket;
+ std::string_view object;
+
+ bool valid() const {
+ return !bucket.empty() && !object.empty();
+ }
+};
+
/**
* \brief Interface for block encryption methods
*
*/
virtual size_t get_block_size() = 0;
+ /**
+ * Returns size of encrypted block (ciphertext + metadata like auth tags).
+ * For most ciphers this equals get_block_size(), but for AEAD modes like GCM
+ * it includes the authentication tag.
+ */
+ virtual size_t get_encrypted_block_size() {
+ return get_block_size();
+ }
+
/**
* Encrypts data.
* Argument \ref stream_offset shows where in generalized stream chunk is located.
bufferlist& output,
off_t stream_offset,
optional_yield y) = 0;
+
+ /**
+ * Set the part number for multipart object decryption.
+ * AEAD modes use this for per-part IV derivation.
+ * Default is no-op; CBC derives IVs from block offsets instead.
+ */
+ virtual void set_part_number(uint32_t part_number) {}
};
static const size_t AES_256_KEYSIZE = 256 / 8;
+static const size_t AES_256_GCM_NONCE_SIZE = 96 / 8; // 12 bytes, GCM standard
+
+/**
+ * AEAD chunk size constants used for size calculations across RGW.
+ * All supported AEAD ciphers use 128-bit (16-byte) authentication tags.
+ */
+static constexpr size_t AEAD_CHUNK_SIZE = 4096;
+static constexpr size_t AEAD_TAG_SIZE = 16;
+static constexpr size_t AEAD_ENCRYPTED_CHUNK_SIZE = AEAD_CHUNK_SIZE + AEAD_TAG_SIZE; // 4112
+
+/**
+ * Check if encryption mode is AEAD.
+ * AEAD modes have ciphertext expansion from auth tags and need special
+ * handling for size calculations and multipart part numbers.
+ *
+ * All AEAD modes currently end in "-GCM". When adding non-GCM AEAD modes
+ * (e.g., ChaCha20-Poly1305), update this function to match them.
+ */
+inline bool is_aead_mode(const std::string& mode) {
+ return mode.size() >= 4 && mode.compare(mode.size() - 4, 4, "-GCM") == 0;
+}
+
+/**
+ * Check if encryption mode string indicates CBC mode.
+ * CBC modes: SSE-C-AES256, SSE-KMS, RGW-AUTO, AES256
+ */
+inline bool is_cbc_mode(const std::string& mode) {
+ return mode == "SSE-C-AES256" || mode == "SSE-KMS" ||
+ mode == "RGW-AUTO" || mode == "AES256";
+}
+
+/**
+ * Convert encrypted size to plaintext size for AEAD modes.
+ * Each AEAD_CHUNK_SIZE-byte plaintext chunk becomes AEAD_ENCRYPTED_CHUNK_SIZE bytes
+ * (plaintext + AEAD_TAG_SIZE-byte auth tag).
+ * Pass dpp to enable warning logging for malformed data.
+ */
+inline uint64_t aead_encrypted_to_plaintext_size(uint64_t encrypted_size,
+ const DoutPrefixProvider* dpp = nullptr) {
+ uint64_t full_chunks = encrypted_size / AEAD_ENCRYPTED_CHUNK_SIZE;
+ uint64_t remainder = encrypted_size % AEAD_ENCRYPTED_CHUNK_SIZE;
+
+ if (remainder > 0 && remainder <= AEAD_TAG_SIZE) {
+ // Malformed: partial chunk has no ciphertext, only tag bytes
+ if (dpp) {
+ ldpp_dout(dpp, 1) << "WARNING: aead_encrypted_to_plaintext_size: "
+ << "partial chunk size " << remainder
+ << " is <= tag size " << AEAD_TAG_SIZE
+ << " - data may be corrupted" << dendl;
+ }
+ return full_chunks * AEAD_CHUNK_SIZE;
+ }
+
+ uint64_t partial = (remainder > AEAD_TAG_SIZE) ? (remainder - AEAD_TAG_SIZE) : 0;
+ return full_chunks * AEAD_CHUNK_SIZE + partial;
+}
+
+/**
+ * Convert plaintext size to encrypted size for AEAD modes.
+ * Each AEAD_CHUNK_SIZE-byte plaintext chunk becomes AEAD_ENCRYPTED_CHUNK_SIZE bytes.
+ */
+inline uint64_t aead_plaintext_to_encrypted_size(uint64_t plaintext_size) {
+ if (plaintext_size == 0) return 0;
+ uint64_t num_chunks = (plaintext_size + AEAD_CHUNK_SIZE - 1) / AEAD_CHUNK_SIZE;
+ return plaintext_size + (num_chunks * AEAD_TAG_SIZE);
+}
+
+/**
+ * Convert plaintext offset to encrypted offset for AEAD modes.
+ * Accounts for AEAD_TAG_SIZE-byte auth tag per chunk.
+ */
+inline uint64_t aead_plaintext_to_encrypted_offset(uint64_t plaintext_ofs) {
+ uint64_t chunk_idx = plaintext_ofs / AEAD_CHUNK_SIZE;
+ uint64_t offset_in_chunk = plaintext_ofs % AEAD_CHUNK_SIZE;
+ return chunk_idx * AEAD_ENCRYPTED_CHUNK_SIZE + offset_in_chunk;
+}
+
bool AES_256_ECB_encrypt(const DoutPrefixProvider* dpp,
CephContext* cct,
const uint8_t* key,
uint8_t* data_out,
size_t data_size);
+/**
+ * Create an AES-256-GCM BlockCrypt instance.
+ *
+ * For encryption: Pass nonce=nullptr to generate a random nonce.
+ * After creation, call AES_256_GCM_get_nonce() to retrieve it for storage.
+ *
+ * For decryption: Pass the stored nonce from RGW_ATTR_CRYPT_NONCE.
+ */
+std::unique_ptr<BlockCrypt> AES_256_GCM_create(const DoutPrefixProvider* dpp,
+ CephContext* cct,
+ const uint8_t* key,
+ size_t key_len,
+ const uint8_t* nonce = nullptr,
+ size_t nonce_len = 0,
+ uint32_t part_number = 0);
+
+/**
+ * Retrieve the nonce from a BlockCrypt instance for storage in RGW_ATTR_CRYPT_NONCE.
+ * Returns empty string if the BlockCrypt is not an AES_256_GCM instance.
+ */
+std::string AES_256_GCM_get_nonce(BlockCrypt* block_crypt);
+
class RGWGetObj_BlockDecrypt : public RGWGetObj_Filter {
+ friend class TestableBlockDecrypt; // For unit testing private members
const DoutPrefixProvider *dpp;
CephContext* cct;
std::unique_ptr<BlockCrypt> crypt; /**< already configured stateless BlockCrypt
for operations when enough data is accumulated */
off_t enc_begin_skip; /**< amount of data to skip from beginning of received data */
- off_t ofs; /**< stream offset of data we expect to show up next through \ref handle_data */
+ off_t ofs; /**< plaintext stream offset of data we expect to show up next through \ref handle_data */
+ off_t enc_ofs; /**< encrypted stream offset, for comparing against parts_len which contains encrypted sizes */
off_t end; /**< stream offset of last byte that is requested */
+ off_t encrypted_total_size; /**< total encrypted object size (for clamping ranges in fixup_range) */
+ bool has_compression{false}; /**< true if a decompression filter is present downstream */
bufferlist cache; /**< stores extra data that could not (yet) be processed by BlockCrypt */
- size_t block_size; /**< snapshot of \ref BlockCrypt.get_block_size() */
+ size_t block_size; /**< snapshot of \ref BlockCrypt.get_block_size() (plaintext block size) */
+ size_t encrypted_block_size; /**< snapshot of \ref BlockCrypt.get_encrypted_block_size() (includes auth tag for GCM) */
optional_yield y;
std::vector<size_t> parts_len; /**< size of parts of multipart object, parsed from manifest */
+ std::vector<uint32_t> part_nums; /**< actual S3 part numbers for multipart (e.g., [1,3,5]) */
+ uint32_t current_part_num = 0; /**< current part number (1-based, 0 means single-part object) */
int process(bufferlist& cipher, size_t part_ofs, size_t size);
+ /**
+ * Process cached data across part boundaries.
+ * Updates current_part_num and calls crypt->set_part_number() as needed.
+ * Returns 0 on success, negative error code on failure.
+ * On success, plain_part_ofs_out contains the plaintext offset for remaining data.
+ */
+ int process_part_boundaries(size_t& plain_part_ofs_out);
+
+ /**
+ * Result of finding which part contains a plaintext offset.
+ */
+ struct PartLocation {
+ size_t part_idx; /**< Index into parts_len */
+ off_t offset_in_part; /**< Plaintext offset within the part */
+ off_t cumulative_encrypted; /**< Total encrypted bytes before this part */
+ };
+
+ /**
+ * Find which part contains a given plaintext offset.
+ * @param plaintext_ofs The plaintext offset to locate
+ * @param clamp_to_last If true, stop at second-to-last part (for end offsets)
+ */
+ PartLocation find_part_for_plaintext_offset(off_t plaintext_ofs, bool clamp_to_last) const;
+
+ /**
+ * Align an encrypted offset up to the end of an encrypted block.
+ * For GCM: ensures we read complete blocks including auth tags.
+ */
+ off_t align_to_encrypted_block_end(off_t enc_ofs) const {
+ if (block_size == encrypted_block_size) {
+ return enc_ofs; // CBC - no alignment needed
+ }
+ return (enc_ofs / encrypted_block_size) * encrypted_block_size + (encrypted_block_size - 1);
+ }
+
+ /**
+ * Convert a logical (plaintext) offset to encrypted (storage) offset.
+ * For AEAD modes: accounts for auth tag overhead per chunk.
+ */
+ off_t logical_to_encrypted_offset(off_t logical_ofs) const {
+ if (block_size == encrypted_block_size) {
+ return logical_ofs; // Non-AEAD (CBC) - no conversion needed
+ }
+ return aead_plaintext_to_encrypted_offset(logical_ofs);
+ }
+
+ /**
+ * Convert an encrypted size to plaintext size.
+ * For AEAD modes: removes the auth tag overhead per chunk.
+ */
+ size_t encrypted_to_plaintext_size(size_t encrypted_size) const {
+ if (block_size == encrypted_block_size) {
+ return encrypted_size; // Non-AEAD (CBC) - no conversion needed
+ }
+ return aead_encrypted_to_plaintext_size(encrypted_size, dpp);
+ }
+
public:
RGWGetObj_BlockDecrypt(const DoutPrefixProvider *dpp,
CephContext* cct,
RGWGetObj_Filter* next,
std::unique_ptr<BlockCrypt> crypt,
std::vector<size_t> parts_len,
+ std::vector<uint32_t> part_nums,
+ off_t encrypted_total_size,
+ bool has_compression,
optional_yield y);
+ // Backward-compatible constructor for CBC mode (no size expansion)
+ RGWGetObj_BlockDecrypt(const DoutPrefixProvider *dpp,
+ CephContext* cct,
+ RGWGetObj_Filter* next,
+ std::unique_ptr<BlockCrypt> crypt,
+ std::vector<size_t> parts_len,
+ optional_yield y)
+ : RGWGetObj_BlockDecrypt(dpp, cct, next, std::move(crypt),
+ std::move(parts_len), {}, 0, false, y) {}
virtual ~RGWGetObj_BlockDecrypt();
virtual int fixup_range(off_t& bl_ofs,
static int read_manifest_parts(const DoutPrefixProvider *dpp,
const bufferlist& manifest_bl,
std::vector<size_t>& parts_len);
+
+ /**
+ * Returns true if this cipher expands the data size (e.g., AEAD adds auth tags).
+ * Used to determine if obj_size needs adjustment for Content-Length.
+ */
+ bool has_size_expansion() const {
+ return block_size != encrypted_block_size;
+ }
+
+ /**
+ * Calculate the plaintext size from encrypted size.
+ * For AEAD: removes the 16-byte auth tag overhead per chunk.
+ * Public wrapper for use by callers that need to adjust Content-Length.
+ */
+ uint64_t get_plaintext_size(uint64_t encrypted_size) const {
+ return encrypted_to_plaintext_size(encrypted_size);
+ }
}; /* RGWGetObj_BlockDecrypt */
std::unique_ptr<BlockCrypt> crypt; /**< already configured stateless BlockCrypt
for operations when enough data is accumulated */
bufferlist cache; /**< stores extra data that could not (yet) be processed by BlockCrypt */
- const size_t block_size; /**< snapshot of \ref BlockCrypt.get_block_size() */
+ const size_t block_size; /**< snapshot of \ref BlockCrypt.get_block_size() (plaintext block size) */
+ uint64_t encrypted_offset = 0; /**< tracks write position in encrypted stream (differs from plaintext for GCM) */
optional_yield y;
public:
RGWPutObj_BlockEncrypt(const DoutPrefixProvider *dpp,
std::map<std::string, ceph::bufferlist>& attrs,
std::unique_ptr<BlockCrypt>* block_crypt,
std::map<std::string,
- std::string>& crypt_http_responses);
+ std::string>& crypt_http_responses,
+ uint32_t part_number = 0);
int rgw_s3_prepare_decrypt(req_state* s, optional_yield y,
std::map<std::string, ceph::bufferlist>& attrs,
std::unique_ptr<BlockCrypt>* block_crypt,
std::map<std::string, std::string>* crypt_http_responses,
- bool copy_source);
+ bool copy_source,
+ uint32_t part_number = 0,
+ const rgw_crypt_src_identity* src_identity = nullptr);
+
+/**
+ * Get the original (uncompressed, unencrypted) size from ORIGINAL_SIZE attr.
+ * Use for: Content-Length, bucket index, quota.
+ * Returns false if attr missing, not AEAD mode, or parse failure.
+ */
+bool rgw_get_aead_original_size(const DoutPrefixProvider* dpp,
+ const std::map<std::string, ceph::bufferlist>& attrs,
+ uint64_t* original_size);
+
+/**
+ * Get the decrypted (but possibly still compressed) size.
+ * Uses CRYPT_PARTS if available, otherwise calculates from encrypted size.
+ * Use for: Copy path where data stays in compressed domain.
+ * WARNING: Never use when caller expects original (uncompressed) size AND
+ * compression may be present. This yields compressed-domain sizes.
+ */
+bool rgw_get_aead_decrypted_size(const DoutPrefixProvider* dpp,
+ const std::map<std::string, ceph::bufferlist>& attrs,
+ uint64_t encrypted_size,
+ uint64_t* decrypted_size);
static inline void set_attr(std::map<std::string, bufferlist>& attrs,
const char* key,
}
else
{
- if (part->get_size() != ent.meta.size) {
+ /**
+ * For AEAD encryption, the on-disk size includes authentication tags,
+ * but the bucket index stores plaintext size. Convert encrypted size
+ * to plaintext for comparison against the index entry.
+ */
+ uint64_t obj_size = part->get_size();
+ uint64_t decrypted_size = 0;
+ if (rgw_get_aead_decrypted_size(this, part->get_attrs(), obj_size, &decrypted_size)) {
+ obj_size = decrypted_size;
+ }
+ if (obj_size != ent.meta.size) {
// hmm.. something wrong, object not as expected, abort!
- ldpp_dout(this, 0) << "ERROR: expected obj_size=" << part->get_size()
+ ldpp_dout(this, 0) << "ERROR: expected obj_size=" << obj_size
<< ", actual read size=" << ent.meta.size << dendl;
return -EIO;
- }
+ }
}
op_ret = rgw_policy_from_attrset(s, s->cct, part->get_attrs(), &obj_policy);
}
}
+/**
+ * Calculate the correct object size for AEAD modes.
+ *
+ * Used for range request and Content-Length handling. When compression is
+ * present, stays in the compressed domain and avoids using ORIGINAL_SIZE
+ * since the compressed->encrypted pipeline differs from plaintext->encrypted.
+ */
+static bool rgw_calc_aead_obj_size(const DoutPrefixProvider* dpp,
+ const std::map<std::string, bufferlist>& attrs,
+ uint64_t encrypted_size,
+ bool has_compression,
+ uint64_t* out_size)
+{
+ if (!out_size) {
+ return false;
+ }
+ if (!has_compression &&
+ rgw_get_aead_original_size(dpp, attrs, out_size)) {
+ return true;
+ }
+ return rgw_get_aead_decrypted_size(dpp, attrs, encrypted_size, out_size);
+}
+
void RGWGetObj::execute(optional_yield y)
{
bufferlist bl;
op_ret = read_op->prepare(s->yield, this);
version_id = s->object->get_instance();
s->obj_size = s->object->get_size();
+ // Preserve encrypted size before compression/decompression modifies s->obj_size
+ // (needed for AEAD decrypt filter range clamping)
+ encrypted_obj_size = s->obj_size;
attrs = s->object->get_attrs();
multipart_parts_count = read_op->params.parts_count;
if (op_ret < 0)
/* start gettorrent */
if (get_torrent) {
attr_iter = attrs.find(RGW_ATTR_CRYPT_MODE);
- if (attr_iter != attrs.end() && attr_iter->second.to_str() == "SSE-C-AES256") {
- ldpp_dout(this, 0) << "ERROR: torrents are not supported for objects "
- "encrypted with SSE-C" << dendl;
- op_ret = -EINVAL;
- goto done_err;
+ if (attr_iter != attrs.end()) {
+ std::string crypt_mode = attr_iter->second.to_str();
+ // Block torrents for any SSE-C mode (SSE-C-AES256, SSE-C-AES256-GCM, etc.)
+ if (crypt_mode.compare(0, 5, "SSE-C") == 0) {
+ ldpp_dout(this, 0) << "ERROR: torrents are not supported for objects "
+ "encrypted with SSE-C" << dendl;
+ op_ret = -EINVAL;
+ goto done_err;
+ }
}
// read torrent info from attr
bufferlist torrentbl;
return;
}
+ /**
+ * For AEAD encryption: use original size for Content-Length/ranges.
+ * Key rule: compression active => never use AEAD decrypted fallback.
+ * Use encrypted_obj_size (saved earlier) as the raw encrypted input.
+ */
+ if (encrypted && !skip_decrypt) {
+ if (need_decompress) {
+ // compression active: cs_info.orig_size already set obj_size
+ } else {
+ // no compression: try ORIGINAL_SIZE, then decrypted fallback
+ uint64_t size = 0;
+ if (rgw_calc_aead_obj_size(this, attrs, encrypted_obj_size, false, &size)) {
+ s->obj_size = size;
+ s->object->set_obj_size(s->obj_size);
+ }
+ }
+ }
+
// for range requests with obj size 0
if (range_str && !(s->obj_size)) {
total_len = 0;
return ret;
obj_size = obj->get_size();
+ uint64_t encrypted_size = obj_size; // capture before any mutation
bool need_decompress;
op_ret = rgw_compression_info_from_attrset(obj->get_attrs(), need_decompress, cs_info);
return op_ret;
}
+ /**
+ * For AEAD encryption: adjust obj_size for range validation.
+ * Key rule: compression active => never use AEAD decrypted fallback.
+ */
+ if (decrypt != nullptr) {
+ if (need_decompress) {
+ // compression active: cs_info.orig_size already set obj_size
+ } else {
+ // no compression: try ORIGINAL_SIZE, then decrypted fallback (safe)
+ uint64_t size = 0;
+ if (rgw_calc_aead_obj_size(this, obj->get_attrs(), encrypted_size,
+ obj->get_attrs().count(RGW_ATTR_COMPRESSION), &size)) {
+ obj_size = size;
+ }
+ }
+ }
+
ret = obj->range_to_ofs(obj_size, new_ofs, new_end);
if (ret < 0)
return ret;
s->obj_size = ofs;
s->object->set_obj_size(ofs);
+ /* For AEAD modes, ensure ORIGINAL_SIZE is set now that final size is known.
+ * This handles cases where size was unknown at encryption setup:
+ * - Chunked uploads without x-amz-decoded-content-length
+ * - Copy-source-range operations
+ * Without this, bucket index would get size=0 for chunked AEAD uploads. */
+ {
+ const auto mode = get_str_attribute(attrs, RGW_ATTR_CRYPT_MODE);
+ if (is_aead_mode(mode)) {
+ if (attrs.find(RGW_ATTR_CRYPT_ORIGINAL_SIZE) == attrs.end()) {
+ set_attr(attrs, RGW_ATTR_CRYPT_ORIGINAL_SIZE, std::to_string(s->obj_size));
+ }
+ }
+ }
+
rgw::op_counters::inc(counters, l_rgw_op_put_obj_b, s->obj_size);
op_ret = do_aws4_auth_completion();
s->object->set_obj_size(ofs);
obj->set_obj_size(ofs);
+ /* For AEAD modes, always overwrite ORIGINAL_SIZE with the actual file
+ * payload size. set_gcm_plaintext_size() stored s->content_length which,
+ * for POST form uploads, is the entire HTTP body (form fields + boundaries
+ * + file data) — not just the file payload. */
+ {
+ const auto mode = get_str_attribute(attrs, RGW_ATTR_CRYPT_MODE);
+ if (is_aead_mode(mode)) {
+ set_attr(attrs, RGW_ATTR_CRYPT_ORIGINAL_SIZE, std::to_string(s->obj_size));
+ }
+ }
op_ret = s->bucket->check_quota(this, quota, s->obj_size, y);
if (op_ret < 0) {
}
bool src_encrypted = s->src_object->get_attrs().count(RGW_ATTR_CRYPT_MODE);
+ // Preserve encrypted size for decrypt range clamping before any plaintext conversion.
+ off_t encrypted_total_size = obj_size;
+
+ // Create decompress filter if source is compressed.
+ // Must be created BEFORE decrypt so the chain is: decrypt → decompress → cb
if (need_decompress) {
- obj_size = decompress_info.orig_size;
- s->src_object->set_obj_size(obj_size);
static constexpr bool partial_content = false;
decompress.emplace(s->cct, &decompress_info, partial_content, filter);
filter = &*decompress;
- end_x = obj_size;
}
// decrypt
if (src_encrypted) {
auto attr_iter = s->src_object->get_attrs().find(RGW_ATTR_MANIFEST);
static constexpr bool copy_source = true;
+
+ // part_num=0 for copy source (full object read)
ret = get_decrypt_filter(&decrypt, filter, s, s->src_object->get_attrs(),
attr_iter != s->src_object->get_attrs().end() ? &attr_iter->second : nullptr,
- nullptr, copy_source);
+ nullptr, copy_source, 0, encrypted_total_size);
if (ret < 0) {
return ret;
}
}
}
+ // Set obj_size to the final output size (plaintext) for range handling.
+ if (need_decompress) {
+ obj_size = decompress_info.orig_size;
+ s->src_object->set_obj_size(obj_size);
+ end_x = obj_size;
+ } else if (src_encrypted) {
+ uint64_t decrypted_size = 0;
+ const auto& src_attrs = s->src_object->get_attrs();
+ if (rgw_calc_aead_obj_size(dpp, src_attrs, encrypted_total_size,
+ src_attrs.count(RGW_ATTR_COMPRESSION), &decrypted_size)) {
+ obj_size = decrypted_size;
+ s->src_object->set_obj_size(obj_size);
+ end_x = obj_size;
+ }
+ }
+
filter->fixup_range(ofs_x, end_x);
/* rgw::sal::DataProcessor */
<< ", compressor_message=" << cs_info.compressor_message
<< ", blocks=" << cs_info.blocks.size() << dendl;
}
+
+ // Copy path: ensure AEAD ORIGINAL_SIZE matches copied payload size.
+ // If it doesn't, stale CRYPT_PARTS and CRYPT_PART_NUMS must be dropped.
+ const auto mode = get_str_attribute(attrs, RGW_ATTR_CRYPT_MODE);
+ if (is_aead_mode(mode)) {
+ uint64_t existing = 0;
+ if (!rgw_get_aead_original_size(s, attrs, &existing) ||
+ existing != obj_size) {
+ set_attr(attrs, RGW_ATTR_CRYPT_ORIGINAL_SIZE, std::to_string(obj_size));
+ attrs.erase(RGW_ATTR_CRYPT_PARTS);
+ attrs.erase(RGW_ATTR_CRYPT_PART_NUMS);
+ }
+ }
}
};
std::map<std::string, bufferlist>& attrs,
bufferlist* manifest_bl,
std::map<std::string, std::string>* crypt_http_responses,
- bool copy_source)
+ bool copy_source,
+ uint32_t part_num,
+ off_t encrypted_total_size,
+ const rgw_crypt_src_identity* src_identity)
{
std::unique_ptr<BlockCrypt> block_crypt;
int res = rgw_s3_prepare_decrypt(s, s->yield, attrs, &block_crypt,
- crypt_http_responses, copy_source);
+ crypt_http_responses, copy_source, part_num,
+ src_identity);
if (res < 0) {
return res;
}
// correctly decrypt across part boundaries
std::vector<size_t> parts_len;
+ // Read actual S3 part numbers from attribute (set by CompleteMultipartUpload)
+ std::vector<uint32_t> part_nums;
+ if (auto it = attrs.find(RGW_ATTR_CRYPT_PART_NUMS); it != attrs.end()) {
+ try {
+ auto p = it->second.cbegin();
+ using ceph::decode;
+ decode(part_nums, p);
+ } catch (const buffer::error&) {
+ ldpp_dout(s, 1) << "failed to decode RGW_ATTR_CRYPT_PART_NUMS" << dendl;
+ // Continue with empty part_nums - will fail for multipart, ok for single-part
+ }
+ }
+
+ /**
+ * Fallback for GET ?partNumber=N (single part read).
+ * When reading an individual part, the CRYPT_PART_NUMS attribute is skipped
+ * (see rgw_rados.cc skip list), so we use the requested part_num to ensure
+ * correct key derivation and IV generation.
+ */
+ if (part_nums.empty() && part_num > 0) {
+ part_nums.push_back(part_num);
+ }
+
// for replicated objects, the original part lengths are preserved in an xattr
if (auto i = attrs.find(RGW_ATTR_CRYPT_PARTS); i != attrs.end()) {
try {
}
}
+ /**
+ * For AEAD ciphers (GCM), we need encrypted_total_size to properly clamp
+ * range requests to the actual on-disk object size. AEAD ciphers expand
+ * data (block_size != encrypted_block_size due to auth tags).
+ *
+ * Derivation priority:
+ * 1. parts_len sum - most accurate, already in encrypted domain
+ * 2. CRYPT_ORIGINAL_SIZE - convert plaintext to encrypted size
+ *
+ * Skip derivation for compressed objects: compression changes the input
+ * to encryption, so plaintext_to_encrypted(ORIGINAL_SIZE) would be wrong.
+ * Compressed objects rely on the decompression filter for size handling.
+ */
+ if (encrypted_total_size == 0 &&
+ block_crypt->get_block_size() != block_crypt->get_encrypted_block_size()) {
+ if (!parts_len.empty()) {
+ for (size_t part_len : parts_len) {
+ encrypted_total_size += part_len;
+ }
+ } else if (!attrs.count(RGW_ATTR_COMPRESSION)) {
+ uint64_t orig_size = 0;
+ if (rgw_get_aead_original_size(s, attrs, &orig_size)) {
+ encrypted_total_size = aead_plaintext_to_encrypted_size(orig_size);
+ }
+ }
+ }
+
+ const bool has_compression = attrs.count(RGW_ATTR_COMPRESSION);
*filter = std::make_unique<RGWGetObj_BlockDecrypt>(
s, s->cct, cb, std::move(block_crypt),
- std::move(parts_len), s->yield);
+ std::move(parts_len), std::move(part_nums), encrypted_total_size,
+ has_compression, s->yield);
return 0;
}
#include "rgw_cksum.h"
#include "rgw_common.h"
#include "rgw_dmclock.h"
+
+struct rgw_crypt_src_identity;
#include "rgw_sal.h"
#include "driver/rados/rgw_user.h"
#include "rgw_bucket.h"
// DataProcessor requires ownership of the entire bufferlist.
// RGWGetObj_Filter, however, may reuse the original bufferlist after this call.
// To avoid unintended side effects, we create a copy of the relevant portion.
+ // Note: bl_ofs is the offset into this bufferlist, not an object offset.
bufferlist copy_bl;
- bl.begin().copy(bl_len, copy_bl);
+ bl.begin(bl_ofs).copy(bl_len, copy_bl);
int ret = processor->process(std::move(copy_bl), ofs);
if (ret < 0) return ret;
// PartsCount response when partNumber is specified
std::optional<int> multipart_parts_count;
+ // For AEAD encryption ciphers: preserve original encrypted size before plaintext conversion.
+ // Used by decrypt filter for range clamping. For non-AEAD modes, equals s->obj_size.
+ off_t encrypted_obj_size{0};
+
int init_common();
public:
RGWGetObj() {
std::map<std::string, bufferlist>& attrs,
bufferlist* manifest_bl,
std::map<std::string, std::string>* crypt_http_responses,
- bool copy_source);
+ bool copy_source,
+ uint32_t part_num = 0,
+ off_t encrypted_total_size = 0,
+ const rgw_crypt_src_identity* src_identity = nullptr);
}
static constexpr bool copy_source = false;
- return ::get_decrypt_filter(filter, cb, s, attrs, manifest_bl, &crypt_http_responses, copy_source);
+ // Only use part_num for actual multipart objects (parts_count is set)
+ uint32_t part_num = (multipart_part_num && multipart_parts_count)
+ ? static_cast<uint32_t>(*multipart_part_num)
+ : 0;
+ return ::get_decrypt_filter(filter, cb, s, attrs, manifest_bl, &crypt_http_responses, copy_source,
+ part_num, encrypted_obj_size);
}
int RGWGetObj_ObjStore_S3::verify_requester(const rgw::auth::StrategyRegistry& auth_registry, optional_yield y)
bufferlist* manifest_bl)
{
static constexpr bool copy_source = true;
- return ::get_decrypt_filter(filter, cb, s, attrs, manifest_bl, nullptr, copy_source);
+ rgw_crypt_src_identity src_identity{copy_source_bucket_name, copy_source_object_name};
+ // part_num=0 for copy source (full object read)
+ return ::get_decrypt_filter(filter, cb, s, attrs, manifest_bl, nullptr, copy_source,
+ 0, 0, &src_identity);
}
int RGWPutObj_ObjStore_S3::get_encrypt_filter(
/* We are adding to existing object.
* We use crypto mode that configured as if we were decrypting. */
static constexpr bool copy_source = false;
+ // Pass part_number for AEAD IV derivation - ensures unique IVs across parts
res = rgw_s3_prepare_decrypt(s, s->yield, obj->get_attrs(),
- &block_crypt, &crypt_http_responses, copy_source);
+ &block_crypt, &crypt_http_responses, copy_source,
+ multipart_part_num);
if (res == 0 && block_crypt != nullptr)
filter->reset(new RGWPutObj_BlockEncrypt(s, s->cct, cb, std::move(block_crypt), s->yield));
}
else
{
std::unique_ptr<BlockCrypt> block_crypt;
+ // Pass part_number for AEAD IV derivation - ensures unique IVs across parts
res = rgw_s3_prepare_encrypt(s, s->yield, attrs, &block_crypt,
- crypt_http_responses);
+ crypt_http_responses, multipart_part_num);
if (res == 0 && block_crypt != nullptr) {
filter->reset(new RGWPutObj_BlockEncrypt(s, s->cct, cb, std::move(block_crypt), s->yield));
}
std::unique_ptr<BlockCrypt> AES_256_CBC_create(const DoutPrefixProvider *dpp, CephContext* cct, const uint8_t* key, size_t len);
+bool AES_256_GCM_derive_object_key(BlockCrypt* block_crypt,
+ const uint8_t* user_key,
+ size_t key_len,
+ const std::string& bucket,
+ const std::string& object,
+ uint32_t part_number,
+ const std::string& domain = "SSE-C-GCM");
class ut_get_sink : public RGWGetObj_Filter {
std::stringstream sink;
}
};
+// Mock that simulates AEAD size expansion (encrypted_block_size > block_size)
+class BlockCryptNoneAEAD: public BlockCryptNone {
+public:
+ BlockCryptNoneAEAD() : BlockCryptNone(AEAD_CHUNK_SIZE) {}
+ size_t get_encrypted_block_size() override
+ {
+ return AEAD_ENCRYPTED_CHUNK_SIZE; // 4096 + 16 = 4112
+ }
+};
+
TEST(TestRGWCrypto, verify_AES_256_CBC_identity)
{
const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
}
+// AEAD Tests
+
+TEST(TestRGWCrypto, verify_AES_256_GCM_identity)
+{
+ const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
+ // Create some input for encryption
+ const off_t test_range = 1024*1024;
+ buffer::ptr buf(test_range);
+ char* p = buf.c_str();
+ for(size_t i = 0; i < buf.length(); i++)
+ p[i] = i + i*i + (i >> 2);
+
+ bufferlist input;
+ input.append(buf);
+
+ for (unsigned int step : {1, 2, 3, 5, 7, 11, 13, 17})
+ {
+ // Make some random key
+ uint8_t key[32];
+ for(size_t i=0;i<sizeof(key);i++)
+ key[i]=i*step;
+
+ auto aes(AES_256_GCM_create(&no_dpp, g_ceph_context, &key[0], 32));
+ ASSERT_NE(aes.get(), nullptr);
+
+ size_t block_size = aes->get_block_size();
+ ASSERT_EQ(block_size, 4096u);
+
+ for (size_t r = 97; r < 123 ; r++)
+ {
+ off_t begin = (r*r*r*r*r % test_range);
+ begin = begin - begin % block_size;
+ off_t end = begin + r*r*r*r*r*r*r % (test_range - begin);
+ if (r % 3)
+ end = end - end % block_size;
+ off_t offset = r*r*r*r*r*r*r*r % (1000*1000*1000);
+ offset = offset - offset % block_size;
+
+ ASSERT_EQ(begin % block_size, 0u);
+ ASSERT_LE(end, test_range);
+ ASSERT_EQ(offset % block_size, 0u);
+
+ bufferlist encrypted;
+ ASSERT_TRUE(aes->encrypt(input, begin, end - begin, encrypted, offset, null_yield));
+
+ size_t expected_encrypted_size = aead_plaintext_to_encrypted_size(end - begin);
+ ASSERT_EQ(encrypted.length(), expected_encrypted_size);
+
+ bufferlist decrypted;
+ ASSERT_TRUE(aes->decrypt(encrypted, 0, encrypted.length(), decrypted, offset, null_yield));
+
+ ASSERT_EQ(decrypted.length(), end - begin);
+ ASSERT_EQ(std::string_view(input.c_str() + begin, end - begin),
+ std::string_view(decrypted.c_str(), end - begin) );
+ }
+ }
+}
+
+
+TEST(TestRGWCrypto, verify_AES_256_GCM_chunk_sizes)
+{
+ const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
+ uint8_t key[32];
+ for(size_t i=0;i<sizeof(key);i++)
+ key[i]=i*3;
+
+ auto aes(AES_256_GCM_create(&no_dpp, g_ceph_context, &key[0], 32));
+ ASSERT_NE(aes.get(), nullptr);
+
+ // Test various sizes to verify chunk + tag handling
+ for (size_t size : {0, 1, 16, 4095, 4096, 4097, 8192, 10000})
+ {
+ buffer::ptr buf(size);
+ char* p = buf.c_str();
+ for(size_t i = 0; i < size; i++)
+ p[i] = i & 0xFF;
+
+ bufferlist input;
+ input.append(buf);
+
+ bufferlist encrypted;
+ ASSERT_TRUE(aes->encrypt(input, 0, size, encrypted, 0, null_yield));
+
+ size_t expected_size = aead_plaintext_to_encrypted_size(size);
+ ASSERT_EQ(encrypted.length(), expected_size);
+
+ bufferlist decrypted;
+ ASSERT_TRUE(aes->decrypt(encrypted, 0, encrypted.length(), decrypted, 0, null_yield));
+ ASSERT_EQ(decrypted.length(), size);
+
+ if (size > 0) {
+ ASSERT_EQ(std::string_view(input.c_str(), size),
+ std::string_view(decrypted.c_str(), size));
+ }
+ }
+}
+
+
+TEST(TestRGWCrypto, verify_AEAD_size_conversion)
+{
+ // Test is_aead_mode() - AEAD modes have ciphertext expansion
+ ASSERT_TRUE(is_aead_mode("SSE-C-AES256-GCM"));
+ ASSERT_TRUE(is_aead_mode("SSE-KMS-GCM"));
+ ASSERT_TRUE(is_aead_mode("RGW-AUTO-GCM"));
+ ASSERT_TRUE(is_aead_mode("AES256-GCM"));
+ ASSERT_FALSE(is_aead_mode("SSE-C-AES256"));
+ ASSERT_FALSE(is_aead_mode("SSE-KMS"));
+ ASSERT_FALSE(is_aead_mode(""));
+ ASSERT_FALSE(is_aead_mode("GCM"));
+
+ // Test is_cbc_mode() - exact match for known CBC modes
+ ASSERT_TRUE(is_cbc_mode("SSE-C-AES256"));
+ ASSERT_TRUE(is_cbc_mode("SSE-KMS"));
+ ASSERT_TRUE(is_cbc_mode("RGW-AUTO"));
+ ASSERT_TRUE(is_cbc_mode("AES256"));
+ ASSERT_FALSE(is_cbc_mode("SSE-C-AES256-GCM"));
+ ASSERT_FALSE(is_cbc_mode("SSE-KMS-GCM"));
+ ASSERT_FALSE(is_cbc_mode(""));
+ ASSERT_FALSE(is_cbc_mode("invalid"));
+
+ // Test aead_plaintext_to_encrypted_size()
+ ASSERT_EQ(aead_plaintext_to_encrypted_size(0), 0UL); // Zero bytes
+ ASSERT_EQ(aead_plaintext_to_encrypted_size(1), 17UL); // 1 + 16 tag
+ ASSERT_EQ(aead_plaintext_to_encrypted_size(100), 116UL); // 100 + 16 tag
+ ASSERT_EQ(aead_plaintext_to_encrypted_size(4095), 4111UL); // 4095 + 16 tag
+ ASSERT_EQ(aead_plaintext_to_encrypted_size(4096), 4112UL); // Full chunk
+ ASSERT_EQ(aead_plaintext_to_encrypted_size(4097), 4129UL); // 4097 + 32 tags (2 chunks)
+ ASSERT_EQ(aead_plaintext_to_encrypted_size(8192), 8224UL); // Two full chunks
+
+ // Test aead_encrypted_to_plaintext_size()
+ ASSERT_EQ(aead_encrypted_to_plaintext_size(0), 0UL); // Zero bytes
+ ASSERT_EQ(aead_encrypted_to_plaintext_size(16), 0UL); // Tag only -> 0 plaintext
+ ASSERT_EQ(aead_encrypted_to_plaintext_size(17), 1UL); // 1 byte + tag
+ ASSERT_EQ(aead_encrypted_to_plaintext_size(100), 84UL); // 100 - 16 tag
+ ASSERT_EQ(aead_encrypted_to_plaintext_size(4112), 4096UL); // Full chunk
+ ASSERT_EQ(aead_encrypted_to_plaintext_size(4212), 4180UL); // 4096 + 84 (one chunk + partial)
+ ASSERT_EQ(aead_encrypted_to_plaintext_size(8224), 8192UL); // Two full chunks
+
+ // Test roundtrip: plaintext -> encrypted -> plaintext
+ for (uint64_t plain : {0UL, 1UL, 16UL, 100UL, 4095UL, 4096UL, 4097UL,
+ 8192UL, 10000UL, 1000000UL}) {
+ uint64_t enc = aead_plaintext_to_encrypted_size(plain);
+ uint64_t recovered = aead_encrypted_to_plaintext_size(enc);
+ ASSERT_EQ(plain, recovered)
+ << "Roundtrip failed for plaintext=" << plain
+ << " encrypted=" << enc << " recovered=" << recovered;
+ }
+
+ // Test aead_plaintext_to_encrypted_offset()
+ ASSERT_EQ(aead_plaintext_to_encrypted_offset(0), 0UL); // Start of file
+ ASSERT_EQ(aead_plaintext_to_encrypted_offset(100), 100UL); // Within first chunk
+ ASSERT_EQ(aead_plaintext_to_encrypted_offset(4095), 4095UL); // End of first chunk
+ ASSERT_EQ(aead_plaintext_to_encrypted_offset(4096), 4112UL); // Start of second chunk
+ ASSERT_EQ(aead_plaintext_to_encrypted_offset(4196), 4212UL); // 100 bytes into second chunk
+ ASSERT_EQ(aead_plaintext_to_encrypted_offset(8192), 8224UL); // Start of third chunk
+
+ // Test has_size_expansion() via RGWGetObj_BlockDecrypt
+ // GCM expands data (adds 16-byte auth tag per chunk), CBC does not
+ {
+ const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
+ ut_get_sink get_sink;
+
+ auto cbc = std::make_unique<BlockCryptNone>(4096);
+ RGWGetObj_BlockDecrypt decrypt_cbc(&no_dpp, g_ceph_context, &get_sink,
+ std::move(cbc), {}, null_yield);
+ ASSERT_FALSE(decrypt_cbc.has_size_expansion());
+
+ auto gcm = std::make_unique<BlockCryptNoneAEAD>();
+ RGWGetObj_BlockDecrypt decrypt_gcm(&no_dpp, g_ceph_context, &get_sink,
+ std::move(gcm), {}, null_yield);
+ ASSERT_TRUE(decrypt_gcm.has_size_expansion());
+ }
+}
+
+
+TEST(TestRGWCrypto, verify_AES_256_GCM_tag_verification)
+{
+ const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
+ uint8_t key[32];
+ for(size_t i=0;i<sizeof(key);i++)
+ key[i]=i*7;
+
+ auto aes(AES_256_GCM_create(&no_dpp, g_ceph_context, &key[0], 32));
+ ASSERT_NE(aes.get(), nullptr);
+
+ const size_t size = 8192; // 2 chunks
+ buffer::ptr buf(size);
+ char* p = buf.c_str();
+ for(size_t i = 0; i < size; i++)
+ p[i] = i & 0xFF;
+
+ bufferlist input;
+ input.append(buf);
+
+ bufferlist encrypted;
+ ASSERT_TRUE(aes->encrypt(input, 0, size, encrypted, 0, null_yield));
+
+ // Corrupt the ciphertext
+ char* enc_p = encrypted.c_str();
+ enc_p[100] ^= 0xFF;
+
+ bufferlist decrypted;
+ // Decryption should fail due to tag mismatch
+ ASSERT_FALSE(aes->decrypt(encrypted, 0, encrypted.length(), decrypted, 0, null_yield));
+}
+
+
+TEST(TestRGWCrypto, verify_AES_256_GCM_nonce_uniqueness)
+{
+ // This test verifies the MinIO-style per-object random nonce mechanism:
+ // 1. Each GCM instance gets a unique random nonce
+ // 2. Decryption with wrong nonce fails
+ // 3. Decryption with correct nonce succeeds
+
+ const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
+ uint8_t key[32];
+ for(size_t i=0;i<sizeof(key);i++)
+ key[i]=i*11;
+
+ const size_t size = 4096;
+ buffer::ptr buf(size);
+ char* p = buf.c_str();
+ for(size_t i = 0; i < size; i++)
+ p[i] = i & 0xFF;
+
+ bufferlist input;
+ input.append(buf);
+
+ // Create first instance - auto-generates random nonce
+ auto aes1(AES_256_GCM_create(&no_dpp, g_ceph_context, &key[0], 32));
+ ASSERT_NE(aes1.get(), nullptr);
+
+ bufferlist encrypted1;
+ ASSERT_TRUE(aes1->encrypt(input, 0, size, encrypted1, 0, null_yield));
+
+ // Create second instance with different random nonce
+ auto aes2(AES_256_GCM_create(&no_dpp, g_ceph_context, &key[0], 32));
+ ASSERT_NE(aes2.get(), nullptr);
+
+ // Try to decrypt data from aes1 with aes2 - should FAIL
+ // (different nonces, even with same key)
+ bufferlist decrypted_wrong;
+ ASSERT_FALSE(aes2->decrypt(encrypted1, 0, encrypted1.length(), decrypted_wrong, 0, null_yield));
+
+ // Decrypt with original instance - should succeed
+ bufferlist decrypted_correct;
+ ASSERT_TRUE(aes1->decrypt(encrypted1, 0, encrypted1.length(), decrypted_correct, 0, null_yield));
+ ASSERT_EQ(decrypted_correct.length(), size);
+ ASSERT_EQ(std::string_view(input.c_str(), size),
+ std::string_view(decrypted_correct.c_str(), size));
+}
+
+
+TEST(TestRGWCrypto, verify_AES_256_GCM_nonce_restore)
+{
+ // This test simulates the encrypt/decrypt flow with stored nonce:
+ // 1. Encrypt with auto-generated nonce
+ // 2. Extract nonce (would be stored in RGW_ATTR_CRYPT_NONCE)
+ // 3. Create new instance with restored nonce
+ // 4. Decrypt successfully
+
+ const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
+ uint8_t key[32];
+ for(size_t i=0;i<sizeof(key);i++)
+ key[i]=i*13;
+
+ const size_t size = 8192;
+ buffer::ptr buf(size);
+ char* p = buf.c_str();
+ for(size_t i = 0; i < size; i++)
+ p[i] = (i * 7) & 0xFF;
+
+ bufferlist input;
+ input.append(buf);
+
+ // Simulate encryption path
+ auto aes_encrypt(AES_256_GCM_create(&no_dpp, g_ceph_context, &key[0], 32));
+ ASSERT_NE(aes_encrypt.get(), nullptr);
+
+ bufferlist encrypted;
+ ASSERT_TRUE(aes_encrypt->encrypt(input, 0, size, encrypted, 0, null_yield));
+
+ // Extract nonce (simulates storing to RGW_ATTR_CRYPT_NONCE)
+ std::string stored_nonce = AES_256_GCM_get_nonce(aes_encrypt.get());
+ ASSERT_EQ(stored_nonce.size(), AES_256_GCM_NONCE_SIZE);
+
+ // Release encryption instance (simulates different request)
+ aes_encrypt.reset();
+
+ // Simulate decryption path with restored nonce
+ auto aes_decrypt(AES_256_GCM_create(&no_dpp, g_ceph_context, &key[0], 32,
+ reinterpret_cast<const uint8_t*>(stored_nonce.c_str()),
+ stored_nonce.size()));
+ ASSERT_NE(aes_decrypt.get(), nullptr);
+
+ bufferlist decrypted;
+ ASSERT_TRUE(aes_decrypt->decrypt(encrypted, 0, encrypted.length(), decrypted, 0, null_yield));
+
+ ASSERT_EQ(decrypted.length(), size);
+ ASSERT_EQ(std::string_view(input.c_str(), size),
+ std::string_view(decrypted.c_str(), size));
+}
+
+
+TEST(TestRGWCrypto, verify_AES_256_GCM_key_derivation)
+{
+ NoDoutPrefix no_dpp(g_ceph_context, ceph_subsys_rgw);
+ uint8_t user_key[32];
+ for (size_t i = 0; i < sizeof(user_key); i++) user_key[i] = i;
+ const std::string plaintext = "test data for key derivation";
+
+ // Test 1: Same identity produces same derived key (encrypt/decrypt roundtrip)
+ {
+ auto aes1(AES_256_GCM_create(&no_dpp, g_ceph_context, &user_key[0], 32));
+ ASSERT_NE(aes1.get(), nullptr);
+ std::string nonce = AES_256_GCM_get_nonce(aes1.get());
+ ASSERT_TRUE(AES_256_GCM_derive_object_key(aes1.get(), user_key, 32,
+ "mybucket", "myobject", 0));
+
+ bufferlist input;
+ input.append(plaintext);
+ bufferlist encrypted;
+ ASSERT_TRUE(aes1->encrypt(input, 0, input.length(), encrypted, 0, null_yield));
+
+ auto aes2(AES_256_GCM_create(&no_dpp, g_ceph_context, &user_key[0], 32,
+ reinterpret_cast<const uint8_t*>(nonce.c_str()),
+ nonce.size()));
+ ASSERT_TRUE(AES_256_GCM_derive_object_key(aes2.get(), user_key, 32,
+ "mybucket", "myobject", 0));
+
+ bufferlist decrypted;
+ ASSERT_TRUE(aes2->decrypt(encrypted, 0, encrypted.length(), decrypted, 0, null_yield));
+ ASSERT_EQ(std::string_view(input.c_str(), input.length()),
+ std::string_view(decrypted.c_str(), decrypted.length()));
+ }
+
+ // Test 2: Different bucket/object produces different derived key
+ {
+ auto aes1(AES_256_GCM_create(&no_dpp, g_ceph_context, &user_key[0], 32));
+ ASSERT_TRUE(AES_256_GCM_derive_object_key(aes1.get(), user_key, 32,
+ "bucket1", "object1", 0));
+ std::string nonce = AES_256_GCM_get_nonce(aes1.get());
+
+ auto aes2(AES_256_GCM_create(&no_dpp, g_ceph_context, &user_key[0], 32,
+ reinterpret_cast<const uint8_t*>(nonce.c_str()),
+ nonce.size()));
+ ASSERT_TRUE(AES_256_GCM_derive_object_key(aes2.get(), user_key, 32,
+ "bucket2", "object2", 0));
+
+ bufferlist input;
+ input.append(plaintext);
+ bufferlist enc1, enc2;
+ ASSERT_TRUE(aes1->encrypt(input, 0, input.length(), enc1, 0, null_yield));
+ ASSERT_TRUE(aes2->encrypt(input, 0, input.length(), enc2, 0, null_yield));
+ ASSERT_NE(std::string_view(enc1.c_str(), enc1.length()),
+ std::string_view(enc2.c_str(), enc2.length()));
+ }
+
+ // Test 3: Different part numbers produce different derived keys
+ {
+ auto aes1(AES_256_GCM_create(&no_dpp, g_ceph_context, &user_key[0], 32));
+ std::string nonce = AES_256_GCM_get_nonce(aes1.get());
+ ASSERT_TRUE(AES_256_GCM_derive_object_key(aes1.get(), user_key, 32,
+ "bucket", "object", 1));
+
+ auto aes2(AES_256_GCM_create(&no_dpp, g_ceph_context, &user_key[0], 32,
+ reinterpret_cast<const uint8_t*>(nonce.c_str()),
+ nonce.size()));
+ ASSERT_TRUE(AES_256_GCM_derive_object_key(aes2.get(), user_key, 32,
+ "bucket", "object", 2));
+
+ bufferlist input;
+ input.append(plaintext);
+ bufferlist enc1, enc2;
+ ASSERT_TRUE(aes1->encrypt(input, 0, input.length(), enc1, 0, null_yield));
+ ASSERT_TRUE(aes2->encrypt(input, 0, input.length(), enc2, 0, null_yield));
+ ASSERT_NE(std::string_view(enc1.c_str(), enc1.length()),
+ std::string_view(enc2.c_str(), enc2.length()));
+ }
+
+ // Test 4: Wrong identity fails decryption (auth tag mismatch)
+ {
+ auto aes_enc(AES_256_GCM_create(&no_dpp, g_ceph_context, &user_key[0], 32));
+ std::string nonce = AES_256_GCM_get_nonce(aes_enc.get());
+ ASSERT_TRUE(AES_256_GCM_derive_object_key(aes_enc.get(), user_key, 32,
+ "bucket1", "object1", 0));
+
+ bufferlist input;
+ input.append(plaintext);
+ bufferlist encrypted;
+ ASSERT_TRUE(aes_enc->encrypt(input, 0, input.length(), encrypted, 0, null_yield));
+
+ auto aes_dec(AES_256_GCM_create(&no_dpp, g_ceph_context, &user_key[0], 32,
+ reinterpret_cast<const uint8_t*>(nonce.c_str()),
+ nonce.size()));
+ ASSERT_TRUE(AES_256_GCM_derive_object_key(aes_dec.get(), user_key, 32,
+ "bucket2", "object2", 0));
+
+ bufferlist decrypted;
+ ASSERT_FALSE(aes_dec->decrypt(encrypted, 0, encrypted.length(), decrypted, 0, null_yield));
+ }
+}
+
+TEST(TestRGWCrypto, verify_AES_256_GCM_chunk_reorder_detection)
+{
+ // Verify that swapping chunk positions is detected via AAD
+ NoDoutPrefix no_dpp(g_ceph_context, ceph_subsys_rgw);
+ uint8_t key[32];
+ for (size_t i = 0; i < sizeof(key); i++) key[i] = i * 7;
+
+ auto aes(AES_256_GCM_create(&no_dpp, g_ceph_context, &key[0], 32));
+ ASSERT_NE(aes.get(), nullptr);
+
+ // Create 2 chunks of data (8192 bytes = 2 x 4096)
+ const size_t size = 8192;
+ buffer::ptr buf(size);
+ char* p = buf.c_str();
+ for (size_t i = 0; i < size; i++) p[i] = i & 0xFF;
+
+ bufferlist input;
+ input.append(buf);
+
+ bufferlist encrypted;
+ ASSERT_TRUE(aes->encrypt(input, 0, size, encrypted, 0, null_yield));
+
+ // Encrypted layout: [chunk0_cipher(4096)][tag0(16)][chunk1_cipher(4096)][tag1(16)]
+ // Total: 8192 + 32 = 8224 bytes
+ ASSERT_EQ(encrypted.length(), 8224u);
+
+ // Swap the two encrypted chunks (including their tags)
+ buffer::ptr enc_copy(encrypted.length());
+ encrypted.begin().copy(encrypted.length(), enc_copy.c_str());
+ char* enc_data = enc_copy.c_str();
+ const size_t enc_chunk_size = 4096 + 16; // ciphertext + tag
+ char temp[4112];
+ memcpy(temp, enc_data, enc_chunk_size); // save chunk0
+ memcpy(enc_data, enc_data + enc_chunk_size, enc_chunk_size); // chunk1 -> chunk0 position
+ memcpy(enc_data + enc_chunk_size, temp, enc_chunk_size); // chunk0 -> chunk1 position
+
+ bufferlist swapped;
+ swapped.append(enc_copy);
+
+ // Decryption should fail because AAD (chunk_index) won't match
+ bufferlist decrypted;
+ ASSERT_FALSE(aes->decrypt(swapped, 0, swapped.length(), decrypted, 0, null_yield));
+}
+
+TEST(TestRGWCrypto, verify_AES_256_GCM_aad_roundtrip)
+{
+ // Verify basic encrypt/decrypt still works correctly with AAD
+ NoDoutPrefix no_dpp(g_ceph_context, ceph_subsys_rgw);
+ uint8_t key[32];
+ for (size_t i = 0; i < sizeof(key); i++) key[i] = i * 11;
+
+ // Test multiple chunk scenarios
+ for (size_t size : {4096u, 8192u, 12288u, 10000u}) {
+ auto aes(AES_256_GCM_create(&no_dpp, g_ceph_context, &key[0], 32));
+ ASSERT_NE(aes.get(), nullptr);
+
+ buffer::ptr buf(size);
+ char* p = buf.c_str();
+ for (size_t i = 0; i < size; i++) p[i] = (i * 17) & 0xFF;
+
+ bufferlist input;
+ input.append(buf);
+
+ bufferlist encrypted;
+ ASSERT_TRUE(aes->encrypt(input, 0, size, encrypted, 0, null_yield));
+
+ bufferlist decrypted;
+ ASSERT_TRUE(aes->decrypt(encrypted, 0, encrypted.length(), decrypted, 0, null_yield));
+
+ ASSERT_EQ(decrypted.length(), size);
+ ASSERT_EQ(std::string_view(input.c_str(), size),
+ std::string_view(decrypted.c_str(), size));
+ }
+}
+
+TEST(TestRGWCrypto, verify_AES_256_GCM_aad_offset_mismatch)
+{
+ // Verify AAD detects wrong stream offset during decryption
+ NoDoutPrefix no_dpp(g_ceph_context, ceph_subsys_rgw);
+ uint8_t key[32];
+ for (size_t i = 0; i < sizeof(key); i++) key[i] = i * 13;
+
+ auto aes(AES_256_GCM_create(&no_dpp, g_ceph_context, &key[0], 32));
+ ASSERT_NE(aes.get(), nullptr);
+
+ const size_t size = 4096;
+ buffer::ptr buf(size);
+ char* p = buf.c_str();
+ for (size_t i = 0; i < size; i++) p[i] = i & 0xFF;
+
+ bufferlist input;
+ input.append(buf);
+
+ // Encrypt at offset 8192 (chunk index 2)
+ off_t stream_offset = 8192;
+ bufferlist encrypted;
+ ASSERT_TRUE(aes->encrypt(input, 0, size, encrypted, stream_offset, null_yield));
+
+ // Decrypt with same offset - should succeed
+ bufferlist decrypted;
+ ASSERT_TRUE(aes->decrypt(encrypted, 0, encrypted.length(), decrypted, stream_offset, null_yield));
+ ASSERT_EQ(std::string_view(input.c_str(), size),
+ std::string_view(decrypted.c_str(), size));
+
+ // Decrypt with wrong offset (0 instead of 8192) - should FAIL (AAD mismatch)
+ bufferlist decrypted_wrong;
+ ASSERT_FALSE(aes->decrypt(encrypted, 0, encrypted.length(), decrypted_wrong, 0, null_yield));
+}
+
+
+// Test helper class to expose private methods for unit testing
+// (declared as friend in RGWGetObj_BlockDecrypt)
+class TestableBlockDecrypt : public RGWGetObj_BlockDecrypt {
+public:
+ // Re-export PartLocation struct for test code
+ using PartLocation = RGWGetObj_BlockDecrypt::PartLocation;
+
+ TestableBlockDecrypt(const DoutPrefixProvider* dpp,
+ CephContext* cct,
+ RGWGetObj_Filter* next,
+ std::unique_ptr<BlockCrypt> crypt,
+ std::vector<size_t> parts_len)
+ : RGWGetObj_BlockDecrypt(dpp, cct, next, std::move(crypt),
+ std::move(parts_len), null_yield) {}
+
+ // Wrapper to expose private method for testing
+ PartLocation find_part_for_plaintext_offset(off_t plaintext_ofs, bool clamp_to_last) const {
+ return RGWGetObj_BlockDecrypt::find_part_for_plaintext_offset(plaintext_ofs, clamp_to_last);
+ }
+};
+
+TEST(TestRGWCrypto, verify_PartLocation_single_part)
+{
+ const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
+ ut_get_sink get_sink;
+
+ std::vector<size_t> parts = {10 * 1024 * 1024};
+ auto crypt = std::make_unique<BlockCryptNone>(4096);
+ TestableBlockDecrypt decrypt(&no_dpp, g_ceph_context, &get_sink,
+ std::move(crypt), parts);
+
+ auto loc = decrypt.find_part_for_plaintext_offset(0, false);
+ ASSERT_EQ(loc.part_idx, 0u);
+ ASSERT_EQ(loc.offset_in_part, 0);
+ ASSERT_EQ(loc.cumulative_encrypted, 0);
+
+ loc = decrypt.find_part_for_plaintext_offset(1000, false);
+ ASSERT_EQ(loc.part_idx, 0u);
+ ASSERT_EQ(loc.offset_in_part, 1000);
+ ASSERT_EQ(loc.cumulative_encrypted, 0);
+
+ loc = decrypt.find_part_for_plaintext_offset(10 * 1024 * 1024 - 1, false);
+ ASSERT_EQ(loc.part_idx, 0u);
+ ASSERT_EQ(loc.offset_in_part, 10 * 1024 * 1024 - 1);
+ ASSERT_EQ(loc.cumulative_encrypted, 0);
+}
+
+TEST(TestRGWCrypto, verify_PartLocation_multipart_cbc)
+{
+ const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
+ ut_get_sink get_sink;
+
+ const size_t part_size = 5 * 1024 * 1024;
+ std::vector<size_t> parts = {part_size, part_size, part_size};
+ auto crypt = std::make_unique<BlockCryptNone>(4096);
+ TestableBlockDecrypt decrypt(&no_dpp, g_ceph_context, &get_sink,
+ std::move(crypt), parts);
+
+ auto loc = decrypt.find_part_for_plaintext_offset(0, false);
+ ASSERT_EQ(loc.part_idx, 0u);
+ ASSERT_EQ(loc.offset_in_part, 0);
+ ASSERT_EQ(loc.cumulative_encrypted, 0);
+
+ loc = decrypt.find_part_for_plaintext_offset(part_size - 1, false);
+ ASSERT_EQ(loc.part_idx, 0u);
+ ASSERT_EQ(loc.offset_in_part, (off_t)(part_size - 1));
+ ASSERT_EQ(loc.cumulative_encrypted, 0);
+
+ loc = decrypt.find_part_for_plaintext_offset(part_size, false);
+ ASSERT_EQ(loc.part_idx, 1u);
+ ASSERT_EQ(loc.offset_in_part, 0);
+ ASSERT_EQ(loc.cumulative_encrypted, (off_t)part_size);
+
+ loc = decrypt.find_part_for_plaintext_offset(part_size + 1000, false);
+ ASSERT_EQ(loc.part_idx, 1u);
+ ASSERT_EQ(loc.offset_in_part, 1000);
+ ASSERT_EQ(loc.cumulative_encrypted, (off_t)part_size);
+
+ loc = decrypt.find_part_for_plaintext_offset(2 * part_size, false);
+ ASSERT_EQ(loc.part_idx, 2u);
+ ASSERT_EQ(loc.offset_in_part, 0);
+ ASSERT_EQ(loc.cumulative_encrypted, (off_t)(2 * part_size));
+
+ loc = decrypt.find_part_for_plaintext_offset(3 * part_size - 100, false);
+ ASSERT_EQ(loc.part_idx, 2u);
+ ASSERT_EQ(loc.offset_in_part, (off_t)(part_size - 100));
+ ASSERT_EQ(loc.cumulative_encrypted, (off_t)(2 * part_size));
+}
+
+TEST(TestRGWCrypto, verify_PartLocation_multipart_aead)
+{
+ const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
+ ut_get_sink get_sink;
+
+ // For AEAD, parts_len contains ENCRYPTED sizes (with auth tag overhead)
+ const size_t plaintext_per_part = 1024 * AEAD_CHUNK_SIZE; // 4MB
+ const size_t encrypted_per_part = aead_plaintext_to_encrypted_size(plaintext_per_part);
+ std::vector<size_t> parts = {encrypted_per_part, encrypted_per_part, encrypted_per_part};
+
+ auto crypt = std::make_unique<BlockCryptNoneAEAD>();
+ TestableBlockDecrypt decrypt(&no_dpp, g_ceph_context, &get_sink,
+ std::move(crypt), parts);
+
+ auto loc = decrypt.find_part_for_plaintext_offset(0, false);
+ ASSERT_EQ(loc.part_idx, 0u);
+ ASSERT_EQ(loc.offset_in_part, 0);
+ ASSERT_EQ(loc.cumulative_encrypted, 0);
+
+ loc = decrypt.find_part_for_plaintext_offset(plaintext_per_part - 1, false);
+ ASSERT_EQ(loc.part_idx, 0u);
+ ASSERT_EQ(loc.offset_in_part, (off_t)(plaintext_per_part - 1));
+ ASSERT_EQ(loc.cumulative_encrypted, 0);
+
+ loc = decrypt.find_part_for_plaintext_offset(plaintext_per_part, false);
+ ASSERT_EQ(loc.part_idx, 1u);
+ ASSERT_EQ(loc.offset_in_part, 0);
+ ASSERT_EQ(loc.cumulative_encrypted, (off_t)encrypted_per_part);
+
+ loc = decrypt.find_part_for_plaintext_offset(plaintext_per_part + 8192, false);
+ ASSERT_EQ(loc.part_idx, 1u);
+ ASSERT_EQ(loc.offset_in_part, 8192);
+ ASSERT_EQ(loc.cumulative_encrypted, (off_t)encrypted_per_part);
+
+ loc = decrypt.find_part_for_plaintext_offset(2 * plaintext_per_part, false);
+ ASSERT_EQ(loc.part_idx, 2u);
+ ASSERT_EQ(loc.offset_in_part, 0);
+ ASSERT_EQ(loc.cumulative_encrypted, (off_t)(2 * encrypted_per_part));
+}
+
+TEST(TestRGWCrypto, verify_PartLocation_clamp_to_last)
+{
+ const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
+ ut_get_sink get_sink;
+
+ const size_t part_size = 5 * 1024 * 1024;
+ std::vector<size_t> parts = {part_size, part_size, part_size};
+ auto crypt = std::make_unique<BlockCryptNone>(4096);
+ TestableBlockDecrypt decrypt(&no_dpp, g_ceph_context, &get_sink,
+ std::move(crypt), parts);
+
+ // Offset within object - both modes return the same
+ auto loc = decrypt.find_part_for_plaintext_offset(2 * part_size + 1000, false);
+ ASSERT_EQ(loc.part_idx, 2u);
+ ASSERT_EQ(loc.offset_in_part, 1000);
+
+ loc = decrypt.find_part_for_plaintext_offset(2 * part_size + 1000, true);
+ ASSERT_EQ(loc.part_idx, 2u);
+ ASSERT_EQ(loc.offset_in_part, 1000);
+
+ // Offset beyond all parts - without clamping returns invalid index
+ loc = decrypt.find_part_for_plaintext_offset(3 * part_size + 5000, false);
+ ASSERT_EQ(loc.part_idx, 3u);
+ ASSERT_EQ(loc.offset_in_part, 5000);
+
+ // With clamping - stays at last valid part
+ loc = decrypt.find_part_for_plaintext_offset(3 * part_size + 5000, true);
+ ASSERT_EQ(loc.part_idx, 2u);
+ ASSERT_EQ(loc.offset_in_part, (off_t)(part_size + 5000));
+}
+
+TEST(TestRGWCrypto, verify_PartLocation_unequal_parts)
+{
+ const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
+ ut_get_sink get_sink;
+
+ // Parts: 1MB, 3MB, 2MB
+ std::vector<size_t> parts = {1024 * 1024, 3 * 1024 * 1024, 2 * 1024 * 1024};
+ auto crypt = std::make_unique<BlockCryptNone>(4096);
+ TestableBlockDecrypt decrypt(&no_dpp, g_ceph_context, &get_sink,
+ std::move(crypt), parts);
+
+ auto loc = decrypt.find_part_for_plaintext_offset(500 * 1024, false);
+ ASSERT_EQ(loc.part_idx, 0u);
+ ASSERT_EQ(loc.offset_in_part, 500 * 1024);
+ ASSERT_EQ(loc.cumulative_encrypted, 0);
+
+ loc = decrypt.find_part_for_plaintext_offset(1536 * 1024, false); // 1.5MB
+ ASSERT_EQ(loc.part_idx, 1u);
+ ASSERT_EQ(loc.offset_in_part, 512 * 1024);
+ ASSERT_EQ(loc.cumulative_encrypted, 1024 * 1024);
+
+ loc = decrypt.find_part_for_plaintext_offset(4608 * 1024, false); // 4.5MB
+ ASSERT_EQ(loc.part_idx, 2u);
+ ASSERT_EQ(loc.offset_in_part, 512 * 1024);
+ ASSERT_EQ(loc.cumulative_encrypted, 4 * 1024 * 1024);
+}
+
+TEST(TestRGWCrypto, verify_PartLocation_no_manifest)
+{
+ // Single-part objects don't have a multipart manifest, so parts_len is empty.
+ // The code treats this as one logical part starting at offset 0.
+ const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys);
+ ut_get_sink get_sink;
+
+ std::vector<size_t> parts = {};
+ auto crypt = std::make_unique<BlockCryptNone>(4096);
+ TestableBlockDecrypt decrypt(&no_dpp, g_ceph_context, &get_sink,
+ std::move(crypt), parts);
+
+ auto loc = decrypt.find_part_for_plaintext_offset(0, false);
+ ASSERT_EQ(loc.part_idx, 0u);
+ ASSERT_EQ(loc.offset_in_part, 0);
+ ASSERT_EQ(loc.cumulative_encrypted, 0);
+
+ loc = decrypt.find_part_for_plaintext_offset(1000, false);
+ ASSERT_EQ(loc.part_idx, 0u);
+ ASSERT_EQ(loc.offset_in_part, 1000);
+ ASSERT_EQ(loc.cumulative_encrypted, 0);
+
+ loc = decrypt.find_part_for_plaintext_offset(10 * 1024 * 1024, false);
+ ASSERT_EQ(loc.part_idx, 0u);
+ ASSERT_EQ(loc.offset_in_part, 10 * 1024 * 1024);
+ ASSERT_EQ(loc.cumulative_encrypted, 0);
+}
+
+
int main(int argc, char **argv) {
auto args = argv_to_vec(argc, argv);
auto cct = global_init(NULL, args, CEPH_ENTITY_TYPE_CLIENT,