@@ -77,7 +77,7 @@ use iggy_binary_protocol::{
7777 Command2 , GenericHeader , IGGY_PROTOCOL_VERSION , KIND_CONSUMER_GROUP , Operation , ReplyHeader ,
7878 RequestHeader , WireDecode , WireEncode , WireIdentifier , WireName , WirePartitioning ,
7979} ;
80- use iggy_common:: { Identifier , IggyError , IggyTimestamp , MaxTopicSize } ;
80+ use iggy_common:: { EncryptorKind , Identifier , IggyError , IggyTimestamp , MaxTopicSize } ;
8181use metadata:: impls:: metadata:: StreamsFrontend ;
8282use partitions:: PollFragments ;
8383use server_common:: Message ;
@@ -1307,6 +1307,7 @@ pub(crate) fn build_polled_messages_body(
13071307 partition_id : u32 ,
13081308 current_offset : u64 ,
13091309 fragments : PollFragments ,
1310+ encryptor : Option < & EncryptorKind > ,
13101311) -> Result < Bytes , IggyError > {
13111312 // Body head: [partition_id:4][current_offset:8][count:4]. `count` sits at
13121313 // COUNT_OFFSET and is backpatched once the walk below knows it.
@@ -1374,20 +1375,56 @@ pub(crate) fn build_polled_messages_body(
13741375 body. extend_from_slice ( & offset. to_le_bytes ( ) ) ;
13751376 body. extend_from_slice ( & timestamp. to_le_bytes ( ) ) ;
13761377 body. extend_from_slice ( & origin_timestamp. to_le_bytes ( ) ) ;
1377- body. extend_from_slice (
1378- & u32:: try_from ( user_headers_length)
1379- . expect ( "length came from u32" )
1380- . to_le_bytes ( ) ,
1381- ) ;
1382- body. extend_from_slice (
1383- & u32:: try_from ( payload_length)
1384- . expect ( "length came from u32" )
1385- . to_le_bytes ( ) ,
1386- ) ;
1387- body. extend_from_slice ( & 0u64 . to_le_bytes ( ) ) ; // reserved
1388- // Stored sections are already in legacy order
1389- // (`[payload][user_headers]`): copy through contiguously.
1390- body. extend_from_slice ( & stream[ sections_start..sections_end] ) ;
1378+ if let Some ( encryptor) = encryptor {
1379+ // At-rest encryption: stored sections are ciphertext (encrypted
1380+ // once at ingestion, replicated verbatim); this reply is the
1381+ // single decrypt point, so lengths are rewritten to the
1382+ // plaintext sizes. The stored per-message checksum still covers
1383+ // the ciphertext and is passed through untouched (the SDK does
1384+ // not re-validate it against the reply layout).
1385+ let payload_end = sections_start + payload_length;
1386+ let payload = encryptor
1387+ . decrypt ( & stream[ sections_start..payload_end] )
1388+ . map_err ( |_| IggyError :: CannotDecryptData ) ?;
1389+ let user_headers = if user_headers_length > 0 {
1390+ Some (
1391+ encryptor
1392+ . decrypt ( & stream[ payload_end..sections_end] )
1393+ . map_err ( |_| IggyError :: CannotDecryptData ) ?,
1394+ )
1395+ } else {
1396+ None
1397+ } ;
1398+ let user_headers_bytes: & [ u8 ] = user_headers. as_deref ( ) . unwrap_or_default ( ) ;
1399+ body. extend_from_slice (
1400+ & u32:: try_from ( user_headers_bytes. len ( ) )
1401+ . map_err ( |_| IggyError :: InvalidCommand ) ?
1402+ . to_le_bytes ( ) ,
1403+ ) ;
1404+ body. extend_from_slice (
1405+ & u32:: try_from ( payload. len ( ) )
1406+ . map_err ( |_| IggyError :: InvalidCommand ) ?
1407+ . to_le_bytes ( ) ,
1408+ ) ;
1409+ body. extend_from_slice ( & 0u64 . to_le_bytes ( ) ) ; // reserved
1410+ body. extend_from_slice ( & payload) ;
1411+ body. extend_from_slice ( user_headers_bytes) ;
1412+ } else {
1413+ body. extend_from_slice (
1414+ & u32:: try_from ( user_headers_length)
1415+ . expect ( "length came from u32" )
1416+ . to_le_bytes ( ) ,
1417+ ) ;
1418+ body. extend_from_slice (
1419+ & u32:: try_from ( payload_length)
1420+ . expect ( "length came from u32" )
1421+ . to_le_bytes ( ) ,
1422+ ) ;
1423+ body. extend_from_slice ( & 0u64 . to_le_bytes ( ) ) ; // reserved
1424+ // Stored sections are already in legacy order
1425+ // (`[payload][user_headers]`): copy through contiguously.
1426+ body. extend_from_slice ( & stream[ sections_start..sections_end] ) ;
1427+ }
13911428
13921429 count += 1 ;
13931430 cursor = sections_end;
0 commit comments