From 4f69b78218e6165f7c753958e4147d0c33dca12e Mon Sep 17 00:00:00 2001 From: WillChilds-Klein Date: Fri, 6 Sep 2024 22:39:51 +0000 Subject: [PATCH] Add PKCS7-internal cipher BIO --- crypto/CMakeLists.txt | 2 + crypto/pkcs7/internal.h | 9 + crypto/pkcs7/pkcs7_internal_bio_cipher.c | 383 +++++++++++++++++++++++ crypto/pkcs7/pkcs7_internal_bio_test.cc | 253 +++++++++++++++ crypto/test/test_util.cc | 1 - 5 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 crypto/pkcs7/pkcs7_internal_bio_cipher.c create mode 100644 crypto/pkcs7/pkcs7_internal_bio_test.cc diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index 8887df791e9..d9b736ea011 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -464,6 +464,7 @@ add_library( pem/pem_xaux.c pkcs7/pkcs7.c pkcs7/pkcs7_asn1.c + pkcs7/pkcs7_internal_bio_cipher.c pkcs7/pkcs7_x509.c pkcs8/pkcs8.c pkcs8/pkcs8_x509.c @@ -814,6 +815,7 @@ if(BUILD_TESTING) obj/obj_test.cc ocsp/ocsp_test.cc pem/pem_test.cc + pkcs7/pkcs7_internal_bio_test.cc pkcs7/pkcs7_test.cc pkcs8/pkcs8_test.cc pkcs8/pkcs12_test.cc diff --git a/crypto/pkcs7/internal.h b/crypto/pkcs7/internal.h index 4cdda60d001..25d31a0d415 100644 --- a/crypto/pkcs7/internal.h +++ b/crypto/pkcs7/internal.h @@ -199,6 +199,15 @@ int pkcs7_add_signed_data(CBB *out, int (*signer_infos_cb)(CBB *out, const void *arg), const void *arg); +// TODO [childw] +const BIO_METHOD *BIO_f_cipher(void); + +#define BIO_get_cipher_ctx(bio, contents) \ + BIO_ctrl(bio, BIO_C_GET_CIPHER_CTX, 0, (char *)(contents)) + +#define BIO_get_cipher_status(bio) \ + BIO_ctrl(bio, BIO_C_GET_CIPHER_STATUS, 0, NULL) + #if defined(__cplusplus) } // extern C diff --git a/crypto/pkcs7/pkcs7_internal_bio_cipher.c b/crypto/pkcs7/pkcs7_internal_bio_cipher.c new file mode 100644 index 00000000000..36cda0faa77 --- /dev/null +++ b/crypto/pkcs7/pkcs7_internal_bio_cipher.c @@ -0,0 +1,383 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#include +#include +#include +#include +#include +#include +#include "../crypto/bio/internal.h" +#include "../internal.h" +#include "./internal.h" + +static int enc_write(BIO *h, const char *buf, int num); +static int enc_read(BIO *h, char *buf, int size); +static long enc_ctrl(BIO *h, int cmd, long arg1, void *arg2); +static int enc_new(BIO *h); +static int enc_free(BIO *data); +#define ENC_BLOCK_SIZE (1024 * 4) +#define ENC_MIN_CHUNK (256) +#define BUF_OFFSET (ENC_MIN_CHUNK + EVP_MAX_BLOCK_LENGTH) + +typedef struct enc_struct { + int buf_len; + int buf_off; + int cont; // <= 0 when finished + int finished; + int ok; // bad decrypt + EVP_CIPHER_CTX *cipher; + unsigned char *read_start, *read_end; + // buf is larger than ENC_BLOCK_SIZE because EVP_DecryptUpdate can return + // up to a block more data than is presented to it + unsigned char buf[BUF_OFFSET + ENC_BLOCK_SIZE]; +} BIO_ENC_CTX; + +static const BIO_METHOD methods_enc = { + BIO_TYPE_CIPHER, + "cipher", + enc_write, + enc_read, + NULL, // enc_puts + NULL, // enc_gets + enc_ctrl, + enc_new, + enc_free, + NULL, // enc_callback_ctrl +}; + +const BIO_METHOD *BIO_f_cipher(void) { return &methods_enc; } + +static int enc_new(BIO *bi) { + BIO_ENC_CTX *ctx; + + if ((ctx = OPENSSL_zalloc(sizeof(*ctx))) == NULL) { + return 0; + } + + ctx->cipher = EVP_CIPHER_CTX_new(); + if (ctx->cipher == NULL) { + OPENSSL_free(ctx); + return 0; + } + ctx->cont = 1; + ctx->ok = 1; + ctx->read_end = ctx->read_start = &(ctx->buf[BUF_OFFSET]); + BIO_set_data(bi, ctx); + BIO_set_init(bi, 1); + + return 1; +} + +static int enc_free(BIO *a) { + BIO_ENC_CTX *b; + + if (a == NULL) { + return 0; + } + + b = BIO_get_data(a); + if (b == NULL) { + return 0; + } + + EVP_CIPHER_CTX_free(b->cipher); + OPENSSL_cleanse(b, sizeof(BIO_ENC_CTX)); + OPENSSL_free(b); + BIO_set_data(a, NULL); + BIO_set_init(a, 0); + + return 1; +} + +static int enc_read(BIO *b, char *out, int outl) { + int ret = 0, i, blocksize; + BIO_ENC_CTX *ctx; + BIO *next; + + if (out == NULL) { + return 0; + } + ctx = BIO_get_data(b); + + next = BIO_next(b); + if ((ctx == NULL) || (next == NULL)) { + return 0; + } + + // First check if there are bytes decoded/encoded + if (ctx->buf_len > 0) { + i = ctx->buf_len - ctx->buf_off; + if (i > outl) { + i = outl; + } + OPENSSL_memcpy(out, &(ctx->buf[ctx->buf_off]), i); + ret = i; + out += i; + outl -= i; + ctx->buf_off += i; + if (ctx->buf_len == ctx->buf_off) { + ctx->buf_len = 0; + ctx->buf_off = 0; + } + } + + blocksize = EVP_CIPHER_CTX_block_size(ctx->cipher); + + if (blocksize == 0) { + return 0; + } + + if (blocksize == 1) { + blocksize = 0; + } + + // At this point, we have room of outl bytes and an empty buffer, so we + // should read in some more. + while (outl > 0) { + if (ctx->cont <= 0) { + break; + } + + if (ctx->read_start == ctx->read_end) { // time to read more data + ctx->read_end = ctx->read_start = &(ctx->buf[BUF_OFFSET]); + i = BIO_read(next, ctx->read_start, ENC_BLOCK_SIZE); + if (i > 0) { + /*printf("READ %d BYTES!!\n", i);*/ + ctx->read_end += i; + } else { + /*printf("FAILED!!\n");*/ + } + } else { + i = ctx->read_end - ctx->read_start; + } + + if (i <= 0) { + // Should be continue next time we are called? + if (!BIO_should_retry(next)) { + ctx->cont = i; + i = EVP_CipherFinal_ex(ctx->cipher, ctx->buf, &(ctx->buf_len)); + ctx->ok = i; + ctx->buf_off = 0; + } else { + ret = (ret == 0) ? i : ret; + break; + } + } else { + if (outl > ENC_MIN_CHUNK) { + // Depending on flags block cipher decrypt can write + // one extra block and then back off, i.e. output buffer + // has to accommodate extra block... + int j = outl - blocksize, buf_len; + + if (!EVP_CipherUpdate(ctx->cipher, (unsigned char *)out, &buf_len, + ctx->read_start, i > j ? j : i)) { + BIO_clear_retry_flags(b); + return 0; + } + ret += buf_len; + out += buf_len; + outl -= buf_len; + + if ((i -= j) <= 0) { + ctx->read_start = ctx->read_end; + continue; + } + ctx->read_start += j; + } + if (i > ENC_MIN_CHUNK) { + i = ENC_MIN_CHUNK; + } + if (!EVP_CipherUpdate(ctx->cipher, ctx->buf, &ctx->buf_len, + ctx->read_start, i)) { + BIO_clear_retry_flags(b); + ctx->ok = 0; + return 0; + } + ctx->read_start += i; + ctx->cont = 1; + // Note: it is possible for EVP_CipherUpdate to decrypt zero + // bytes because this is or looks like the final block: if this + // happens we should retry and either read more data or decrypt + // the final block + if (ctx->buf_len == 0) { + continue; + } + } + + if (ctx->buf_len <= outl) { + i = ctx->buf_len; + } else { + i = outl; + } + if (i <= 0) { + break; + } + OPENSSL_memcpy(out, ctx->buf, i); + ret += i; + ctx->buf_off = i; + outl -= i; + out += i; + } + + BIO_clear_retry_flags(b); + BIO_copy_next_retry(b); + // TODO [childw] note that ossl was incorrect here + // return ((ret == 0) ? ctx->cont : ret); + return ret; +} + +static int enc_write(BIO *b, const char *in, int inl) { + int ret = 0; + BIO_ENC_CTX *ctx; + BIO *next; + + ctx = BIO_get_data(b); + next = BIO_next(b); + // We need a context and a |next| BIO to write to + if ((ctx == NULL) || (next == NULL)) { + return 0; + } + + ret = inl; + + BIO_clear_retry_flags(b); + int bytes_remaining = ctx->buf_len - ctx->buf_off; + while (bytes_remaining > 0) { + int bytes_written = + BIO_write(next, &(ctx->buf[ctx->buf_off]), bytes_remaining); + if (bytes_written <= 0) { + BIO_copy_next_retry(b); + return bytes_written; + } + ctx->buf_off += bytes_written; + bytes_remaining -= bytes_written; + } + // at this point all pending data has been written + if ((in == NULL) || (inl <= 0)) { + return 0; + } + + ctx->buf_off = 0; + while (inl > 0) { + int n = (inl > ENC_BLOCK_SIZE) ? ENC_BLOCK_SIZE : inl; + if (!EVP_CipherUpdate(ctx->cipher, ctx->buf, &ctx->buf_len, + (const unsigned char *)in, n)) { + BIO_clear_retry_flags(b); + ctx->ok = 0; + return 0; + } + inl -= n; + in += n; + + ctx->buf_off = 0; + n = ctx->buf_len; + while (n > 0) { + int i = BIO_write(next, &(ctx->buf[ctx->buf_off]), n); + if (i <= 0) { + BIO_copy_next_retry(b); + return (ret == inl) ? i : ret - inl; + } + n -= i; + ctx->buf_off += i; + } + ctx->buf_len = 0; + ctx->buf_off = 0; + } + BIO_copy_next_retry(b); + return ret; +} + +static long enc_ctrl(BIO *b, int cmd, long num, void *ptr) { + BIO_ENC_CTX *ctx; + long ret = 1; + int i; + EVP_CIPHER_CTX **c_ctx; + BIO *next; + int pend; + + ctx = BIO_get_data(b); + next = BIO_next(b); + if (ctx == NULL) { + return 0; + } + + switch (cmd) { + case BIO_CTRL_RESET: + ctx->ok = 1; + ctx->finished = 0; + if (!EVP_CipherInit_ex(ctx->cipher, NULL, NULL, NULL, NULL, + EVP_CIPHER_CTX_encrypting(ctx->cipher))) + return 0; + ret = BIO_ctrl(next, cmd, num, ptr); + break; + case BIO_CTRL_EOF: // More to read + if (ctx->cont <= 0) { + ret = 1; + } else { + ret = BIO_ctrl(next, cmd, num, ptr); + } + break; + case BIO_CTRL_WPENDING: + case BIO_CTRL_PENDING: + // Calculate number of bytes left to process if we have anything buffered, + // else consult underlying BIO. + ret = ctx->buf_len - ctx->buf_off; + if (ret <= 0) { + ret = BIO_ctrl(next, cmd, num, ptr); + } + break; + case BIO_CTRL_FLUSH: + // do a final write + again: + while (ctx->buf_len != ctx->buf_off) { + pend = ctx->buf_len - ctx->buf_off; + i = enc_write(b, NULL, 0); + // i should never be > 0 here because we didn't ask to write any + // new data. We stop if we get an error or we failed to make any + // progress writing pending data. + if (i < 0 || (ctx->buf_len - ctx->buf_off) == pend) { + return i; + } + } + + if (!ctx->finished) { + ctx->finished = 1; + ctx->buf_off = 0; + ret = EVP_CipherFinal_ex(ctx->cipher, (unsigned char *)ctx->buf, + &(ctx->buf_len)); + ctx->ok = (int)ret; + if (ret <= 0) { + break; + } + + // push out the bytes + goto again; + } + + // Finally flush the underlying BIO + ret = BIO_ctrl(next, cmd, num, ptr); + BIO_copy_next_retry(b); + break; + case BIO_C_GET_CIPHER_STATUS: + ret = (long)ctx->ok; + break; + case BIO_C_GET_CIPHER_CTX: + c_ctx = (EVP_CIPHER_CTX **)ptr; + *c_ctx = ctx->cipher; + BIO_set_init(b, 1); + break; + // OpenSSL implements these, but because we don't need them and cipher BIO + // is internal, we can fail loudly if they're called. If this case is hit, + // it likely means you're making a change that will require implementing + // these. + case BIO_C_DO_STATE_MACHINE: + case BIO_CTRL_DUP: + OPENSSL_PUT_ERROR(PKCS7, ERR_R_BIO_LIB); + return 0; + default: + ret = BIO_ctrl(next, cmd, num, ptr); + break; + } + return ret; +} diff --git a/crypto/pkcs7/pkcs7_internal_bio_test.cc b/crypto/pkcs7/pkcs7_internal_bio_test.cc new file mode 100644 index 00000000000..38aca28d452 --- /dev/null +++ b/crypto/pkcs7/pkcs7_internal_bio_test.cc @@ -0,0 +1,253 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../bytestring/internal.h" +#include "../internal.h" +#include "../test/test_util.h" +#include "./internal.h" + +// NOTE: need to keep these in sync with cipher BIO source file +#define ENC_MIN_CHUNK_SIZE 256 +#define ENC_BLOCK_SIZE 1024 * 4 + +// TODO [childw] +// 1. parameterize over a meaningful set of ciphers +// 2. deduplicate round-trip code to the extent possible +// 3. seed RNG with fixed (random? need to print stdout?) value +// 4. clang-format all the things +// 5. parameterize over plaintext sizes that break cipher block size boundaries +// 6. test case with variable-sized writes + +TEST(PKCS7Test, CipherBIO) { + uint8_t key[EVP_MAX_KEY_LENGTH]; + uint8_t iv[EVP_MAX_IV_LENGTH]; + uint8_t pt[1024 * 64 + 7]; // TODO [childw] need both block-aligned, block + // misaligned, small, large, etc. + uint8_t pt_decrypted[sizeof(pt)]; + uint8_t ct[sizeof(pt)]; + EVP_CIPHER_CTX *ctx; + bssl::UniquePtr bio_cipher; + bssl::UniquePtr bio_mem; + + int io_sizes[] = {1, + 3, + 7, + 8, + 9, + 64, + 31, + 923, + 2 * ENC_BLOCK_SIZE, + 16, + 32, + 512, + ENC_MIN_CHUNK_SIZE - 1, + ENC_MIN_CHUNK_SIZE, + ENC_MIN_CHUNK_SIZE + 1, + ENC_BLOCK_SIZE - 1, + ENC_BLOCK_SIZE, + ENC_BLOCK_SIZE + 1}; + + ASSERT_TRUE(RAND_bytes(pt, sizeof(pt))); + ASSERT_TRUE(RAND_bytes(key, sizeof(key))); + ASSERT_TRUE(RAND_bytes(iv, sizeof(iv))); + + // Round-trip using only |BIO_read|, backing mem buffer with pt/ct + bio_cipher.reset(BIO_new(BIO_f_cipher())); + ASSERT_TRUE(bio_cipher); + // Unsupported CTRL flags + EXPECT_FALSE(BIO_ctrl(bio_cipher.get(), BIO_C_DO_STATE_MACHINE, 0, NULL)); + EXPECT_FALSE(BIO_ctrl(bio_cipher.get(), BIO_CTRL_DUP, 0, NULL)); + EXPECT_FALSE(BIO_ctrl(bio_cipher.get(), BIO_CTRL_GET_CLOSE, 0, NULL)); + // Get underlying cipher context so we can initialize it + EXPECT_TRUE(BIO_get_cipher_ctx(bio_cipher.get(), &ctx)); + ASSERT_TRUE( + EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv, /*enc*/ 1)); + bio_mem.reset(BIO_new_mem_buf(pt, sizeof(pt))); + ASSERT_TRUE(bio_mem); + ASSERT_TRUE(BIO_push(bio_cipher.get(), bio_mem.get())); + bio_mem.release(); // |bio_cipher| will take ownership + // Copy |pt| contents to |ct| so we can detect that |ct| gets overwritten + OPENSSL_memcpy(ct, pt, sizeof(pt)); + OPENSSL_cleanse(pt_decrypted, sizeof(pt_decrypted)); + EXPECT_FALSE(BIO_eof(bio_cipher.get())); + EXPECT_LT(0UL, BIO_pending(bio_cipher.get())); + EXPECT_TRUE(BIO_read(bio_cipher.get(), ct, sizeof(ct))); + EXPECT_TRUE(BIO_eof(bio_cipher.get())); + EXPECT_EQ(0UL, BIO_pending(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + // only consider first |sizeof(pt)| bytes of |ct|, exclude tag + EXPECT_NE(Bytes(pt, sizeof(pt)), Bytes(ct, sizeof(pt))); + // Reset both BIOs and decrypt + bio_cipher.reset(BIO_new(BIO_f_cipher())); + ASSERT_TRUE(bio_cipher); + EXPECT_TRUE(BIO_get_cipher_ctx(bio_cipher.get(), &ctx)); + ASSERT_TRUE( + EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv, /*enc*/ 0)); + bio_mem.reset(BIO_new_mem_buf((const uint8_t *)ct, sizeof(ct))); + ASSERT_TRUE(bio_mem); + ASSERT_TRUE(BIO_push(bio_cipher.get(), bio_mem.get())); + bio_mem.release(); // |bio_cipher| will take ownership + EXPECT_TRUE(BIO_read(bio_cipher.get(), pt_decrypted, sizeof(pt_decrypted))); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + EXPECT_EQ(Bytes(pt, sizeof(pt)), Bytes(pt_decrypted, sizeof(pt_decrypted))); + + // Round-trip using |BIO_write| for encryption with same BIOs, reset between + // encryption/decryption using |BIO_reset|. + bio_cipher.reset(BIO_new(BIO_f_cipher())); + ASSERT_TRUE(bio_cipher); + EXPECT_TRUE(BIO_get_cipher_ctx(bio_cipher.get(), &ctx)); + ASSERT_TRUE( + EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv, /*enc*/ 1)); + bio_mem.reset(BIO_new(BIO_s_mem())); + ASSERT_TRUE(bio_mem); + ASSERT_TRUE(BIO_push(bio_cipher.get(), bio_mem.get())); + // Copy |pt| contents to |ct| so we can detect that |ct| gets overwritten + OPENSSL_memcpy(ct, pt, sizeof(pt)); + OPENSSL_cleanse(pt_decrypted, sizeof(pt_decrypted)); + EXPECT_TRUE(BIO_eof(bio_cipher.get())); + EXPECT_EQ(0UL, BIO_wpending(bio_cipher.get())); + EXPECT_TRUE(BIO_write(bio_cipher.get(), pt, sizeof(pt))); + EXPECT_FALSE(BIO_eof(bio_cipher.get())); + EXPECT_EQ(0UL, BIO_wpending(bio_cipher.get())); + EXPECT_TRUE(BIO_flush(bio_cipher.get())); + EXPECT_FALSE(BIO_eof(bio_cipher.get())); + EXPECT_EQ(0UL, BIO_wpending(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + EXPECT_TRUE(BIO_read(bio_mem.get(), ct, sizeof(ct))); + // only consider first |sizeof(pt)| bytes of |ct|, exclude tag + EXPECT_NE(Bytes(pt, sizeof(pt)), Bytes(ct, sizeof(pt))); + // Reset both BIOs and decrypt + EXPECT_TRUE(BIO_reset(bio_cipher.get())); // also resets owned |bio_mem| + EXPECT_TRUE(BIO_write(bio_mem.get(), ct, sizeof(ct))); + bio_mem.release(); // |bio_cipher| took ownership + EXPECT_TRUE(BIO_get_cipher_ctx(bio_cipher.get(), &ctx)); + ASSERT_TRUE( + EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv, /*enc*/ 0)); + EXPECT_TRUE(BIO_read(bio_cipher.get(), pt_decrypted, sizeof(pt_decrypted))); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + EXPECT_EQ(Bytes(pt, sizeof(pt)), Bytes(pt_decrypted, sizeof(pt_decrypted))); + + // TODO [childw] variable read/write sizes + bio_cipher.reset(BIO_new(BIO_f_cipher())); + ASSERT_TRUE(bio_cipher); + EXPECT_TRUE(BIO_get_cipher_ctx(bio_cipher.get(), &ctx)); + ASSERT_TRUE( + EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv, /*enc*/ 1)); + bio_mem.reset(BIO_new(BIO_s_mem())); + ASSERT_TRUE(bio_mem); + ASSERT_TRUE(BIO_push(bio_cipher.get(), bio_mem.get())); + std::vector pt_vec, ct_vec, decrypted_pt_vec; + uint8_t buff[2 * ENC_BLOCK_SIZE]; + ASSERT_TRUE(RAND_bytes(buff, sizeof(buff))); + for (size_t wsize : io_sizes) { + pt_vec.insert(pt_vec.end(), buff, buff + wsize); + EXPECT_TRUE(BIO_write(bio_cipher.get(), buff, wsize)); + } + EXPECT_TRUE(BIO_flush(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + while (!BIO_eof(bio_mem.get())) { + size_t bytes_read = BIO_read(bio_mem.get(), buff, sizeof(buff)); + ct_vec.insert(ct_vec.end(), buff, buff + bytes_read); + } + EXPECT_TRUE(BIO_reset(bio_cipher.get())); // also resets owned |bio_mem| + EXPECT_TRUE( + BIO_write(bio_mem.get(), ct_vec.data(), ct_vec.size())); // replace ct + bio_mem.release(); // |bio_cipher| took ownership + EXPECT_TRUE(BIO_get_cipher_ctx(bio_cipher.get(), &ctx)); + ASSERT_TRUE( + EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv, /*enc*/ 0)); + for (size_t rsize : io_sizes) { + EXPECT_TRUE(BIO_read(bio_cipher.get(), buff, rsize)); + decrypted_pt_vec.insert(decrypted_pt_vec.end(), buff, buff + rsize); + } + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + EXPECT_EQ(pt_vec.size(), decrypted_pt_vec.size()); + EXPECT_EQ(Bytes(pt_vec.data(), pt_vec.size()), + Bytes(decrypted_pt_vec.data(), decrypted_pt_vec.size())); + + for (int io_size : io_sizes) { + // TODO [childw] explain induce write failure + pt_vec.clear(); + ct_vec.clear(); + decrypted_pt_vec.clear(); + bio_cipher.reset(BIO_new(BIO_f_cipher())); + ASSERT_TRUE(bio_cipher); + EXPECT_TRUE(BIO_get_cipher_ctx(bio_cipher.get(), &ctx)); + ASSERT_TRUE( + EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv, /*enc*/ 1)); + bio_mem.reset(BIO_new(BIO_s_mem())); + ASSERT_TRUE(bio_mem); + ASSERT_TRUE(BIO_push(bio_cipher.get(), bio_mem.get())); + pt_vec.insert(pt_vec.end(), buff, buff + io_size); + EXPECT_EQ(io_size, BIO_write(bio_cipher.get(), buff, io_size)); + // |bio_mem| is writeable, so shouldn't have any buffered data + EXPECT_EQ(0UL, BIO_wpending(bio_cipher.get())); + // Set underlying BIO to r/o to induce buffering in |bio_cipher| + BIO_set_flags(bio_mem.get(), BIO_FLAGS_MEM_RDONLY); + pt_vec.insert(pt_vec.end(), buff, buff + io_size); + // Write to |bio_cipher| should still succeed in writing up to + // ENC_BLOCK_SIZE bytes by buffering them + int wsize = io_size > ENC_BLOCK_SIZE ? ENC_BLOCK_SIZE : io_size; + EXPECT_EQ(wsize, BIO_write(bio_cipher.get(), buff, io_size)); + BIO_clear_flags(bio_mem.get(), BIO_FLAGS_MEM_RDONLY); + // Now that there's buffered data, |BIO_wpending| should match + EXPECT_EQ((size_t)wsize, BIO_wpending(bio_cipher.get())); + const int remaining = io_size - ENC_BLOCK_SIZE; + if (remaining > 0) { + EXPECT_EQ(remaining, + BIO_write(bio_cipher.get(), buff + ENC_BLOCK_SIZE, remaining)); + } + // Flush should empty the buffered data + EXPECT_TRUE(BIO_flush(bio_cipher.get())); + EXPECT_EQ(0UL, BIO_wpending(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_ctx(bio_cipher.get(), &ctx)); + // Reset BIOs, hydrate ciphertext for decryption + while (!BIO_eof(bio_mem.get())) { + size_t bytes_read = BIO_read(bio_mem.get(), buff, sizeof(buff)); + ct_vec.insert(ct_vec.end(), buff, buff + bytes_read); + } + EXPECT_TRUE(BIO_reset(bio_cipher.get())); // also resets owned |bio_mem| + EXPECT_TRUE( + BIO_write(bio_mem.get(), ct_vec.data(), ct_vec.size())); // replace ct + ASSERT_TRUE( + EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv, /*enc*/ 0)); + decrypted_pt_vec.resize(pt_vec.size()); + EXPECT_EQ(decrypted_pt_vec.size(), BIO_pending(bio_cipher.get())); + // First read should fully succeed + EXPECT_EQ(io_size, + BIO_read(bio_cipher.get(), decrypted_pt_vec.data(), io_size)); + // Disable reads from underlying BIO + auto disable_reads = [](BIO *bio, int oper, const char *argp, size_t len, + int argi, long argl, int bio_ret, + size_t *processed) -> long { + return (oper & BIO_CB_RETURN) || !(oper & BIO_CB_READ); + }; + BIO_set_callback_ex(bio_mem.get(), disable_reads); + // Set retry flags so |cipher_bio| doesn't give up when the read fails + BIO_set_retry_read(bio_mem.get()); + int rsize = + BIO_read(bio_cipher.get(), decrypted_pt_vec.data() + io_size, io_size); + EXPECT_EQ(0UL, BIO_pending(bio_cipher.get())); + // Re-enable reads from underlying BIO + BIO_set_callback_ex(bio_mem.get(), nullptr); + BIO_clear_retry_flags(bio_mem.get()); + rsize = BIO_read(bio_cipher.get(), + decrypted_pt_vec.data() + io_size + rsize, io_size); + EXPECT_EQ(0UL, BIO_pending(bio_cipher.get())); + EXPECT_TRUE(BIO_get_cipher_status(bio_cipher.get())); + EXPECT_EQ(pt_vec.size(), decrypted_pt_vec.size()); + EXPECT_EQ(Bytes(pt_vec.data(), pt_vec.size()), + Bytes(decrypted_pt_vec.data(), decrypted_pt_vec.size())); + bio_mem.release(); // |bio_cipher| took ownership + } +} diff --git a/crypto/test/test_util.cc b/crypto/test/test_util.cc index a0e923eb923..d8fdf3601f0 100644 --- a/crypto/test/test_util.cc +++ b/crypto/test/test_util.cc @@ -155,4 +155,3 @@ void CustomDataFree(void *parent, void *ptr, CRYPTO_EX_DATA *ad, int index, long argl, void *argp) { free(ptr); } -