Refactor OHTTP buffers for chunked client implementation.

It refactors `ObliviousHttpRequest` to expose `CreateHpkeSenderContext`, `EncryptChunk`, and `GetEncapsulatedKey`. Also exposes `DecryptChunk` in `ObliviousHttpResponse`. These will be used for client chunks implementation.

PiperOrigin-RevId: 834968917
diff --git a/quiche/oblivious_http/buffers/oblivious_http_request.cc b/quiche/oblivious_http/buffers/oblivious_http_request.cc
index 562833f..f6d3803 100644
--- a/quiche/oblivious_http/buffers/oblivious_http_request.cc
+++ b/quiche/oblivious_http/buffers/oblivious_http_request.cc
@@ -101,10 +101,31 @@
   if (plaintext_payload.empty() || hpke_public_key.empty()) {
     return absl::InvalidArgumentError("Invalid input.");
   }
-  // Initialize HPKE key and context.
-  bssl::UniquePtr<EVP_HPKE_KEY> client_key(EVP_HPKE_KEY_new());
-  if (client_key == nullptr) {
-    return SslErrorAsStatus("Failed to initialize HPKE Client Key.");
+  absl::StatusOr<Context> context = CreateHpkeSenderContext(
+      hpke_public_key, ohttp_key_config, seed, request_label);
+  if (!context.ok()) {
+    return context.status();
+  }
+  std::string encapsulated_key = context->encapsulated_key_;
+  // EncryptChunk with `is_final_chunk` set to false is the same implementation
+  // as encrypting the full request.
+  absl::StatusOr<std::string> ciphertext =
+      EncryptChunk(plaintext_payload, *context, /*is_final_chunk=*/false);
+  if (!ciphertext.ok()) {
+    return ciphertext.status();
+  }
+  return ObliviousHttpRequest(
+      std::move(context->hpke_context_), std::move(encapsulated_key),
+      ohttp_key_config, std::move(*ciphertext), std::move(plaintext_payload));
+}
+
+absl::StatusOr<ObliviousHttpRequest::Context>
+ObliviousHttpRequest::CreateHpkeSenderContext(
+    absl::string_view hpke_public_key,
+    const ObliviousHttpHeaderKeyConfig& ohttp_key_config,
+    absl::string_view seed, absl::string_view request_label) {
+  if (hpke_public_key.empty()) {
+    return absl::InvalidArgumentError("HPKE public key is empty.");
   }
   bssl::UniquePtr<EVP_HPKE_CTX> client_ctx(EVP_HPKE_CTX_new());
   if (client_ctx == nullptr) {
@@ -144,30 +165,45 @@
     }
   }
   encapsulated_key.resize(enc_len);
+
+  return Context(std::move(client_ctx), std::move(encapsulated_key));
+}
+
+absl::StatusOr<std::string> ObliviousHttpRequest::EncryptChunk(
+    absl::string_view plaintext_payload, const Context& context,
+    bool is_final_chunk) {
+  if (plaintext_payload.empty() && !is_final_chunk) {
+    return absl::InvalidArgumentError("Invalid input.");
+  }
+
+  uint8_t* ad = nullptr;
+  size_t ad_len = 0;
+  if (is_final_chunk) {
+    ad = const_cast<uint8_t*>(kFinalAdBytes);
+    ad_len = sizeof(kFinalAdBytes);
+  }
+
   std::string ciphertext(
-      plaintext_payload.size() + EVP_HPKE_CTX_max_overhead(client_ctx.get()),
+      plaintext_payload.size() +
+          EVP_HPKE_CTX_max_overhead(context.hpke_context_.get()),
       '\0');
   size_t ciphertext_len;
   if (!EVP_HPKE_CTX_seal(
-          client_ctx.get(), reinterpret_cast<uint8_t*>(ciphertext.data()),
-          &ciphertext_len, ciphertext.size(),
+          context.hpke_context_.get(),
+          reinterpret_cast<uint8_t*>(ciphertext.data()), &ciphertext_len,
+          ciphertext.size(),
           reinterpret_cast<const uint8_t*>(plaintext_payload.data()),
-          plaintext_payload.size(), nullptr, 0)) {
-    return SslErrorAsStatus(
-        "Failed to encrypt plaintext_payload with given public key param "
-        "hpke_public_key.");
+          plaintext_payload.size(), ad, ad_len)) {
+    return SslErrorAsStatus("Failed to encrypt plaintext_payload.");
   }
   ciphertext.resize(ciphertext_len);
-  if (encapsulated_key.empty() || ciphertext.empty()) {
+  if (context.encapsulated_key_.empty() || ciphertext.empty()) {
     return absl::InternalError(absl::StrCat(
-        "Failed to generate required data: ",
-        (encapsulated_key.empty() ? "encapsulated key is empty" : ""),
-        (ciphertext.empty() ? "encrypted data is empty" : ""), "."));
+        "Failed to generate required data:",
+        (context.encapsulated_key_.empty() ? " encapsulated key is empty" : ""),
+        (ciphertext.empty() ? " encrypted data is empty" : ""), "."));
   }
-
-  return ObliviousHttpRequest(
-      std::move(client_ctx), std::move(encapsulated_key), ohttp_key_config,
-      std::move(ciphertext), std::move(plaintext_payload));
+  return ciphertext;
 }
 
 // Request Serialize.
diff --git a/quiche/oblivious_http/buffers/oblivious_http_request.h b/quiche/oblivious_http/buffers/oblivious_http_request.h
index 3293e4a..cdaa69b 100644
--- a/quiche/oblivious_http/buffers/oblivious_http_request.h
+++ b/quiche/oblivious_http/buffers/oblivious_http_request.h
@@ -31,6 +31,8 @@
     Context(Context&& other) = default;
     Context& operator=(Context&& other) = default;
 
+    std::string GetEncapsulatedKey() const { return encapsulated_key_; }
+
    private:
     explicit Context(bssl::UniquePtr<EVP_HPKE_CTX> hpke_context,
                      std::string encapsulated_key);
@@ -112,7 +114,20 @@
       const ObliviousHttpHeaderKeyConfig& ohttp_key_config,
       absl::string_view request_label);
 
-  // Decrypts an encrypted chunk.
+  // Creates the client's HPKE sender context.
+  static absl::StatusOr<Context> CreateHpkeSenderContext(
+      absl::string_view hpke_public_key,
+      const ObliviousHttpHeaderKeyConfig& ohttp_key_config,
+      absl::string_view seed, absl::string_view request_label);
+
+  // Encrypts a chunk of plaintext. If `is_final_chunk` is true, the chunk will
+  // be encrypted with a final AAD.
+  static absl::StatusOr<std::string> EncryptChunk(
+      absl::string_view plaintext_payload, const Context& context,
+      bool is_final_chunk);
+
+  // Decrypts an encrypted chunk. If `is_final_chunk` is true, the chunk will
+  // be decrypted with a final AAD.
   static absl::StatusOr<std::string> DecryptChunk(
       Context& context, absl::string_view encrypted_chunk, bool is_final_chunk);
 
diff --git a/quiche/oblivious_http/buffers/oblivious_http_request_test.cc b/quiche/oblivious_http/buffers/oblivious_http_request_test.cc
index 8e5eb69..3b97655 100644
--- a/quiche/oblivious_http/buffers/oblivious_http_request_test.cc
+++ b/quiche/oblivious_http/buffers/oblivious_http_request_test.cc
@@ -66,12 +66,12 @@
 
 bssl::UniquePtr<EVP_HPKE_KEY> ConstructHpkeKey(
     absl::string_view hpke_key,
-    const ObliviousHttpHeaderKeyConfig &ohttp_key_config) {
+    const ObliviousHttpHeaderKeyConfig& ohttp_key_config) {
   bssl::UniquePtr<EVP_HPKE_KEY> bssl_hpke_key(EVP_HPKE_KEY_new());
   EXPECT_NE(bssl_hpke_key, nullptr);
   EXPECT_TRUE(EVP_HPKE_KEY_init(
       bssl_hpke_key.get(), ohttp_key_config.GetHpkeKem(),
-      reinterpret_cast<const uint8_t *>(hpke_key.data()), hpke_key.size()));
+      reinterpret_cast<const uint8_t*>(hpke_key.data()), hpke_key.size()));
   return bssl_hpke_key;
 }
 
@@ -474,4 +474,60 @@
       ohttp_key_config);
   EXPECT_EQ(decapsulate.status().code(), absl::StatusCode::kInvalidArgument);
 }
+
+TEST(ObliviousHttpRequest, CreateHpkeSenderContext) {
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM);
+  absl::StatusOr<ObliviousHttpRequest::Context> context =
+      ObliviousHttpRequest::CreateHpkeSenderContext(
+          GetHpkePublicKey(), ohttp_key_config, GetSeed(),
+          ObliviousHttpHeaderKeyConfig::kChunkedOhttpRequestLabel);
+  QUICHE_EXPECT_OK(context);
+}
+
+TEST(ObliviousHttpRequest, EncryptChunkAndDecryptChunkSuccess) {
+  ObliviousHttpHeaderKeyConfig ohttp_key_config =
+      GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM);
+  absl::StatusOr<ObliviousHttpRequest::Context> sender_ctx =
+      ObliviousHttpRequest::CreateHpkeSenderContext(
+          GetHpkePublicKey(), ohttp_key_config, GetSeed(),
+          ObliviousHttpHeaderKeyConfig::kOhttpRequestLabel);
+  QUICHE_EXPECT_OK(sender_ctx);
+  if (!sender_ctx.ok()) {
+    return;
+  }
+
+  std::string plaintext_chunk = "test_chunk";
+  absl::StatusOr<std::string> encrypted_chunk =
+      ObliviousHttpRequest::EncryptChunk(plaintext_chunk, *sender_ctx,
+                                         /*is_final_chunk=*/false);
+  QUICHE_EXPECT_OK(encrypted_chunk);
+  if (!encrypted_chunk.ok()) {
+    return;
+  }
+
+  bssl::UniquePtr<EVP_HPKE_KEY> hpke_key =
+      ConstructHpkeKey(GetHpkePrivateKey(), ohttp_key_config);
+  std::string request_header = ohttp_key_config.SerializeOhttpPayloadHeader();
+  std::string oblivious_http_request_header =
+      absl::StrCat(request_header, sender_ctx->GetEncapsulatedKey());
+  QuicheDataReader reader(oblivious_http_request_header);
+  auto receiver_ctx = ObliviousHttpRequest::DecodeEncapsulatedRequestHeader(
+      reader, *hpke_key, ohttp_key_config,
+      ObliviousHttpHeaderKeyConfig::kOhttpRequestLabel);
+  QUICHE_EXPECT_OK(receiver_ctx);
+  if (!receiver_ctx.ok()) {
+    return;
+  }
+
+  auto decrypted_chunk = ObliviousHttpRequest::DecryptChunk(
+      *receiver_ctx, *encrypted_chunk, /*is_final_chunk=*/false);
+  QUICHE_EXPECT_OK(decrypted_chunk);
+  if (!decrypted_chunk.ok()) {
+    return;
+  }
+  EXPECT_EQ(*decrypted_chunk, plaintext_chunk);
+}
 }  // namespace quiche
diff --git a/quiche/oblivious_http/buffers/oblivious_http_response.cc b/quiche/oblivious_http/buffers/oblivious_http_response.cc
index 321415b..ed668c3 100644
--- a/quiche/oblivious_http/buffers/oblivious_http_response.cc
+++ b/quiche/oblivious_http/buffers/oblivious_http_response.cc
@@ -78,37 +78,26 @@
   absl::string_view encrypted_response =
       absl::string_view(encrypted_data).substr(secret_len);
 
-  // Steps (1, 3 to 5) + AEAD context SetUp before 6th step is performed in
-  // CommonOperations.
-  auto common_ops_st = CommonOperationsToEncapDecap(
-      response_nonce, oblivious_http_request_context, resp_label,
-      aead_params_st.value().aead_key_len,
-      aead_params_st.value().aead_nonce_len, aead_params_st.value().secret_len);
-  if (!common_ops_st.ok()) {
-    return common_ops_st.status();
+  absl::StatusOr<AeadContextData> aead_context_data =
+      GetAeadContextData(oblivious_http_request_context, *aead_params_st,
+                         resp_label, response_nonce);
+  if (!aead_context_data.ok()) {
+    return aead_context_data.status();
   }
 
-  std::string decrypted(encrypted_response.size(), '\0');
-  size_t decrypted_len;
-
   // Decrypt with initialized AEAD context.
   // response, error = Open(aead_key, aead_nonce, "", ct)
   // https://www.rfc-editor.org/rfc/rfc9458.html#section-4.4-6
-  if (!EVP_AEAD_CTX_open(
-          common_ops_st.value().aead_ctx.get(),
-          reinterpret_cast<uint8_t*>(decrypted.data()), &decrypted_len,
-          decrypted.size(),
-          reinterpret_cast<const uint8_t*>(
-              common_ops_st.value().aead_nonce.data()),
-          aead_params_st.value().aead_nonce_len,
-          reinterpret_cast<const uint8_t*>(encrypted_response.data()),
-          encrypted_response.size(), nullptr, 0)) {
-    return SslErrorAsStatus(
-        "Failed to decrypt the response with derived AEAD key and nonce.");
+  // DecryptChunk with `is_final_chunk` as false is the same implementation as
+  // decrypting the full encrypted response.
+  absl::StatusOr<std::string> decrypted =
+      DecryptChunk(encrypted_response, *aead_context_data,
+                   aead_context_data->aead_nonce, /*is_final_chunk=*/false);
+  if (!decrypted.ok()) {
+    return decrypted.status();
   }
-  decrypted.resize(decrypted_len);
   ObliviousHttpResponse oblivious_response(std::move(encrypted_data),
-                                           std::move(decrypted));
+                                           std::move(*decrypted));
   return oblivious_response;
 }
 
@@ -407,6 +396,33 @@
   return encrypted_data;
 }
 
+absl::StatusOr<std::string> ObliviousHttpResponse::DecryptChunk(
+    absl::string_view encrypted_chunk, const AeadContextData& aead_context_data,
+    absl::string_view chunk_nonce, bool is_final_chunk) {
+  uint8_t* ad = nullptr;
+  size_t ad_len = 0;
+  if (is_final_chunk) {
+    ad = const_cast<uint8_t*>(kFinalAdBytes);
+    ad_len = sizeof(kFinalAdBytes);
+  }
+
+  std::string decrypted(encrypted_chunk.size(), '\0');
+  size_t decrypted_len;
+  if (!EVP_AEAD_CTX_open(
+          aead_context_data.aead_ctx.get(),
+          reinterpret_cast<uint8_t*>(decrypted.data()), &decrypted_len,
+          decrypted.size(),
+          reinterpret_cast<const uint8_t*>(chunk_nonce.data()),
+          aead_context_data.aead_nonce.size(),
+          reinterpret_cast<const uint8_t*>(encrypted_chunk.data()),
+          encrypted_chunk.size(), ad, ad_len)) {
+    return SslErrorAsStatus(
+        "Failed to decrypt the response with derived AEAD key.");
+  }
+  decrypted.resize(decrypted_len);
+  return decrypted;
+}
+
 absl::StatusOr<ObliviousHttpResponse::ChunkCounter>
 ObliviousHttpResponse::ChunkCounter::Create(std::string nonce) {
   if (nonce.empty()) {
diff --git a/quiche/oblivious_http/buffers/oblivious_http_response.h b/quiche/oblivious_http/buffers/oblivious_http_response.h
index 29b6763..508f2c0 100644
--- a/quiche/oblivious_http/buffers/oblivious_http_response.h
+++ b/quiche/oblivious_http/buffers/oblivious_http_response.h
@@ -106,6 +106,11 @@
       absl::string_view plaintext_payload, absl::string_view chunk_nonce,
       bool is_final_chunk);
 
+  static absl::StatusOr<std::string> DecryptChunk(
+      absl::string_view encrypted_chunk,
+      const AeadContextData& aead_context_data, absl::string_view chunk_nonce,
+      bool is_final_chunk);
+
   // Generic Usecase : server-side calls this method in the context of Response
   // to serialize OHTTP response that will be returned to client-side.
   // Returns serialized OHTTP response bytestring.
diff --git a/quiche/oblivious_http/buffers/oblivious_http_response_test.cc b/quiche/oblivious_http/buffers/oblivious_http_response_test.cc
index 8c0060b..5e13054 100644
--- a/quiche/oblivious_http/buffers/oblivious_http_response_test.cc
+++ b/quiche/oblivious_http/buffers/oblivious_http_response_test.cc
@@ -24,6 +24,19 @@
 namespace quiche {
 
 namespace {
+
+// Example from
+// https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-06.html#name-example
+constexpr absl::string_view kChunkNonce1Hex = "fead854635d2d5527d64f546";
+constexpr absl::string_view kEncryptedChunk1Hex =
+    "79bf1cc87fa0e2c02de4546945aa3d1e48";
+constexpr absl::string_view kChunkNonce2Hex = "fead854635d2d5527d64f547";
+constexpr absl::string_view kEncryptedChunk2Hex =
+    "b348b5bd4c594c16b6170b07b475845d1f32";
+constexpr absl::string_view kChunkNonce3Hex = "fead854635d2d5527d64f544";
+constexpr absl::string_view kEncryptedChunk3Hex =
+    "ed9d8a796617a5b27265f4d73247f639";
+
 std::string GetHpkePrivateKey() {
   absl::string_view hpke_key_hex =
       "b77431ecfa8f4cfc30d6e467aafa06944dffe28cb9dd1409e33a3045f5adc8a1";
@@ -77,12 +90,12 @@
                          .SerializeRecipientContextInfo();
 
   EXPECT_TRUE(EVP_HPKE_CTX_setup_sender_with_seed_for_testing(
-      client_ctx.get(), reinterpret_cast<uint8_t *>(encapsulated_key.data()),
+      client_ctx.get(), reinterpret_cast<uint8_t*>(encapsulated_key.data()),
       &enc_len, encapsulated_key.size(), EVP_hpke_x25519_hkdf_sha256(),
       EVP_hpke_hkdf_sha256(), EVP_hpke_aes_256_gcm(),
-      reinterpret_cast<const uint8_t *>(GetHpkePublicKey().data()),
-      GetHpkePublicKey().size(), reinterpret_cast<const uint8_t *>(info.data()),
-      info.size(), reinterpret_cast<const uint8_t *>(GetSeed().data()),
+      reinterpret_cast<const uint8_t*>(GetHpkePublicKey().data()),
+      GetHpkePublicKey().size(), reinterpret_cast<const uint8_t*>(info.data()),
+      info.size(), reinterpret_cast<const uint8_t*>(GetSeed().data()),
       GetSeed().size()));
   encapsulated_key.resize(enc_len);
   EXPECT_EQ(encapsulated_key, GetSeededEncapsulatedKey());
@@ -91,12 +104,12 @@
 
 bssl::UniquePtr<EVP_HPKE_KEY> ConstructHpkeKey(
     absl::string_view hpke_key,
-    const ObliviousHttpHeaderKeyConfig &ohttp_key_config) {
+    const ObliviousHttpHeaderKeyConfig& ohttp_key_config) {
   bssl::UniquePtr<EVP_HPKE_KEY> bssl_hpke_key(EVP_HPKE_KEY_new());
   EXPECT_NE(bssl_hpke_key, nullptr);
   EXPECT_TRUE(EVP_HPKE_KEY_init(
       bssl_hpke_key.get(), ohttp_key_config.GetHpkeKem(),
-      reinterpret_cast<const uint8_t *>(hpke_key.data()), hpke_key.size()));
+      reinterpret_cast<const uint8_t*>(hpke_key.data()), hpke_key.size()));
   return bssl_hpke_key;
 }
 
@@ -128,7 +141,7 @@
   TestQuicheRandom(char seed) : seed_(seed) {}
   ~TestQuicheRandom() override {}
 
-  void RandBytes(void *data, size_t len) override { memset(data, seed_, len); }
+  void RandBytes(void* data, size_t len) override { memset(data, seed_, len); }
 
   uint64_t RandUint64() override {
     uint64_t random_int;
@@ -136,7 +149,7 @@
     return random_int;
   }
 
-  void InsecureRandBytes(void *data, size_t len) override {
+  void InsecureRandBytes(void* data, size_t len) override {
     return RandBytes(data, len);
   }
   uint64_t InsecureRandUint64() override { return RandUint64(); }
@@ -145,9 +158,9 @@
   char seed_;
 };
 
-size_t GetResponseNonceLength(const EVP_HPKE_CTX &hpke_context) {
+size_t GetResponseNonceLength(const EVP_HPKE_CTX& hpke_context) {
   EXPECT_NE(&hpke_context, nullptr);
-  const EVP_AEAD *evp_hpke_aead =
+  const EVP_AEAD* evp_hpke_aead =
       EVP_HPKE_AEAD_aead(EVP_HPKE_CTX_aead(&hpke_context));
   EXPECT_NE(evp_hpke_aead, nullptr);
   // Nk = [AEAD key len], is determined by BSSL.
@@ -288,39 +301,98 @@
   std::string plaintext_payload_bytes;
   EXPECT_TRUE(
       absl::HexStringToBytes(plaintext_payload, &plaintext_payload_bytes));
-  std::string chunk_nonce = "fead854635d2d5527d64f546";
   std::string chunk_nonce_bytes;
-  EXPECT_TRUE(absl::HexStringToBytes(chunk_nonce, &chunk_nonce_bytes));
+  EXPECT_TRUE(absl::HexStringToBytes(kChunkNonce1Hex, &chunk_nonce_bytes));
 
   auto encrypted_chunk = ObliviousHttpResponse::EncryptChunk(
       context, aead_context_data, plaintext_payload_bytes, chunk_nonce_bytes,
       /*is_final_chunk=*/false);
   QUICHE_EXPECT_OK(encrypted_chunk);
   std::string encrypted_chunk_hex = absl::BytesToHexString(*encrypted_chunk);
-  EXPECT_EQ(encrypted_chunk_hex, "79bf1cc87fa0e2c02de4546945aa3d1e48");
+  EXPECT_EQ(encrypted_chunk_hex, kEncryptedChunk1Hex);
 
   plaintext_payload = "40c8";
   EXPECT_TRUE(
       absl::HexStringToBytes(plaintext_payload, &plaintext_payload_bytes));
-  chunk_nonce = "fead854635d2d5527d64f547";
-  EXPECT_TRUE(absl::HexStringToBytes(chunk_nonce, &chunk_nonce_bytes));
+  EXPECT_TRUE(absl::HexStringToBytes(kChunkNonce2Hex, &chunk_nonce_bytes));
 
   encrypted_chunk = ObliviousHttpResponse::EncryptChunk(
       context, aead_context_data, plaintext_payload_bytes, chunk_nonce_bytes,
       /*is_final_chunk=*/false);
   QUICHE_EXPECT_OK(encrypted_chunk);
   encrypted_chunk_hex = absl::BytesToHexString(*encrypted_chunk);
-  EXPECT_EQ(encrypted_chunk_hex, "b348b5bd4c594c16b6170b07b475845d1f32");
+  EXPECT_EQ(encrypted_chunk_hex, kEncryptedChunk2Hex);
 
-  chunk_nonce = "fead854635d2d5527d64f544";
-  EXPECT_TRUE(absl::HexStringToBytes(chunk_nonce, &chunk_nonce_bytes));
+  EXPECT_TRUE(absl::HexStringToBytes(kChunkNonce3Hex, &chunk_nonce_bytes));
 
   encrypted_chunk = ObliviousHttpResponse::EncryptChunk(
       context, aead_context_data, /*plaintext_payload=*/"", chunk_nonce_bytes,
       /*is_final_chunk=*/true);
   QUICHE_EXPECT_OK(encrypted_chunk);
   encrypted_chunk_hex = absl::BytesToHexString(*encrypted_chunk);
-  EXPECT_EQ(encrypted_chunk_hex, "ed9d8a796617a5b27265f4d73247f639");
+  EXPECT_EQ(encrypted_chunk_hex, kEncryptedChunk3Hex);
+}
+
+TEST(ObliviousHttpResponse, TestDecryptChunks) {
+  absl::StatusOr<EncryptChunkTestParams> test_params = SetUpEncryptChunkTest();
+  QUICHE_EXPECT_OK(test_params);
+  if (!test_params.ok()) {
+    return;
+  }
+  auto& [context, aead_params, aead_context_data] = *test_params;
+
+  // Chunk 1 decryption
+  std::string encrypted_chunk1_bytes;
+  EXPECT_TRUE(
+      absl::HexStringToBytes(kEncryptedChunk1Hex, &encrypted_chunk1_bytes));
+  std::string chunk_nonce1_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(kChunkNonce1Hex, &chunk_nonce1_bytes));
+  auto decrypted_chunk1 = ObliviousHttpResponse::DecryptChunk(
+      encrypted_chunk1_bytes, aead_context_data, chunk_nonce1_bytes,
+      /*is_final_chunk=*/false);
+  QUICHE_EXPECT_OK(decrypted_chunk1);
+  if (!decrypted_chunk1.ok()) {
+    return;
+  }
+  std::string expected_plaintext1_hex = "01";
+  std::string expected_plaintext1_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(expected_plaintext1_hex,
+                                     &expected_plaintext1_bytes));
+  EXPECT_EQ(*decrypted_chunk1, expected_plaintext1_bytes);
+
+  // Chunk 2 decryption
+  std::string encrypted_chunk2_bytes;
+  EXPECT_TRUE(
+      absl::HexStringToBytes(kEncryptedChunk2Hex, &encrypted_chunk2_bytes));
+  std::string chunk_nonce2_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(kChunkNonce2Hex, &chunk_nonce2_bytes));
+  auto decrypted_chunk2 = ObliviousHttpResponse::DecryptChunk(
+      encrypted_chunk2_bytes, aead_context_data, chunk_nonce2_bytes,
+      /*is_final_chunk=*/false);
+  QUICHE_EXPECT_OK(decrypted_chunk2);
+  if (!decrypted_chunk2.ok()) {
+    return;
+  }
+  std::string expected_plaintext2_hex = "40c8";
+  std::string expected_plaintext2_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(expected_plaintext2_hex,
+                                     &expected_plaintext2_bytes));
+  EXPECT_EQ(*decrypted_chunk2, expected_plaintext2_bytes);
+
+  // Chunk 3 decryption
+  std::string encrypted_chunk3_bytes;
+  EXPECT_TRUE(
+      absl::HexStringToBytes(kEncryptedChunk3Hex, &encrypted_chunk3_bytes));
+  std::string chunk_nonce3_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(kChunkNonce3Hex, &chunk_nonce3_bytes));
+  auto decrypted_chunk3 = ObliviousHttpResponse::DecryptChunk(
+      encrypted_chunk3_bytes, aead_context_data, chunk_nonce3_bytes,
+      /*is_final_chunk=*/true);
+  QUICHE_EXPECT_OK(decrypted_chunk3);
+  if (!decrypted_chunk3.ok()) {
+    return;
+  }
+  EXPECT_EQ(*decrypted_chunk3, "");
 }
 
 TEST(OblviousHttpResponse, EncryptNonFinalChunkWithEmptyPayloadError) {