diff --git a/.gitignore b/.gitignore index 81c95f5..1d96a7d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ build *.exe *.out *.app +.claude/ diff --git a/include/sframe/sframe.h b/include/sframe/sframe.h index dcceb72..3c00bef 100644 --- a/include/sframe/sframe.h +++ b/include/sframe/sframe.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -84,20 +85,74 @@ enum struct KeyUsage unprotect, }; +/// +/// CipherState - pre-warmed cipher state for efficient repeated operations +/// +/// Holds a pre-initialized cipher context so that expensive key schedule +/// computation happens once at construction, not on every seal/open call. +/// + +// Opaque handles - defined by each crypto backend +struct CipherHandle; +struct HmacHandle; + +struct CipherState +{ + static CipherState create_seal(CipherSuite suite, input_bytes key); + static CipherState create_open(CipherSuite suite, input_bytes key); + + output_bytes seal(input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt); + + output_bytes open(input_bytes nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct); + +private: + struct Deleter + { + void operator()(CipherHandle* h) const; + void operator()(HmacHandle* h) const; + }; + + std::unique_ptr cipher_handle; + std::unique_ptr hmac_handle; // null for GCM + CipherSuite suite; + + CipherState(CipherHandle* cipher, HmacHandle* hmac, CipherSuite suite); +}; + struct KeyRecord { + static constexpr size_t max_key_size = 48; + static constexpr size_t max_salt_size = 12; + static KeyRecord from_base_key(CipherSuite suite, KeyID key_id, KeyUsage usage, input_bytes base_key); - static constexpr size_t max_key_size = 48; - static constexpr size_t max_salt_size = 12; + KeyRecord(owned_bytes key, + owned_bytes salt, + KeyUsage usage, + Counter counter, + CipherState cipher); + ~KeyRecord(); + + KeyRecord(KeyRecord&&) noexcept; + KeyRecord& operator=(KeyRecord&&) noexcept; + + KeyRecord(const KeyRecord&) = delete; + KeyRecord& operator=(const KeyRecord&) = delete; owned_bytes key; owned_bytes salt; KeyUsage usage; Counter counter; + CipherState cipher; // Pre-warmed cipher state }; // Context applies the full SFrame transform. It tracks a counter for each key diff --git a/src/crypto_boringssl.cpp b/src/crypto_boringssl.cpp index 4bb6ba3..1752fb0 100644 --- a/src/crypto_boringssl.cpp +++ b/src/crypto_boringssl.cpp @@ -11,6 +11,14 @@ namespace SFRAME_NAMESPACE { +/// +/// Scoped pointers for OpenSSL objects +/// + +using scoped_evp_cipher_ctx = + std::unique_ptr; +using scoped_hmac_ctx = std::unique_ptr; + /// /// Convert between native identifiers / errors and OpenSSL ones /// @@ -58,181 +66,330 @@ openssl_cipher(CipherSuite suite) } } +static bool +is_ctr_hmac_suite(CipherSuite suite) +{ + switch (suite) { + case CipherSuite::AES_128_CTR_HMAC_SHA256_80: + case CipherSuite::AES_128_CTR_HMAC_SHA256_64: + case CipherSuite::AES_128_CTR_HMAC_SHA256_32: + return true; + default: + return false; + } +} + /// -/// HKDF +/// CipherHandle and HmacHandle definitions /// -owned_bytes -hkdf_extract(CipherSuite suite, input_bytes salt, input_bytes ikm) +struct CipherHandle { - const auto* md = openssl_digest_type(suite); - auto out = owned_bytes(EVP_MD_size(md)); - auto out_len = size_t(out.size()); - if (1 != HKDF_extract(out.data(), - &out_len, - md, - ikm.data(), - ikm.size(), - salt.data(), - salt.size())) { + scoped_evp_cipher_ctx ctx; + CipherHandle() + : ctx(nullptr, EVP_CIPHER_CTX_free) + { + } +}; + +struct HmacHandle +{ + scoped_hmac_ctx ctx; + HmacHandle() + : ctx(nullptr, HMAC_CTX_free) + { + } +}; + +void +CipherState::Deleter::operator()(CipherHandle* h) const +{ + delete h; +} + +void +CipherState::Deleter::operator()(HmacHandle* h) const +{ + delete h; +} + +CipherState::CipherState(CipherHandle* cipher, + HmacHandle* hmac, + CipherSuite suite_in) + : cipher_handle(cipher) + , hmac_handle(hmac) + , suite(suite_in) +{ +} + +CipherState +CipherState::create_seal(CipherSuite suite, input_bytes key) +{ + auto cipher_h = std::make_unique(); + cipher_h->ctx.reset(EVP_CIPHER_CTX_new()); + if (cipher_h->ctx == nullptr) { throw crypto_error(); } - return out; + auto cipher = openssl_cipher(suite); + std::unique_ptr hmac_h; + + if (is_ctr_hmac_suite(suite)) { + // CTR+HMAC: key is split into enc_key and auth_key + auto enc_key_size = cipher_enc_key_size(suite); + auto enc_key = key.first(enc_key_size); + auto auth_key = key.subspan(enc_key_size); + + // Initialize AES-CTR context (always encrypt for CTR mode) + if (1 != EVP_EncryptInit_ex( + cipher_h->ctx.get(), cipher, nullptr, enc_key.data(), nullptr)) { + throw crypto_error(); + } + + // Initialize HMAC + hmac_h = std::make_unique(); + hmac_h->ctx.reset(HMAC_CTX_new()); + if (hmac_h->ctx == nullptr) { + throw crypto_error(); + } + + const auto* md = openssl_digest_type(suite); + if (1 != + HMAC_Init_ex( + hmac_h->ctx.get(), auth_key.data(), auth_key.size(), md, nullptr)) { + throw crypto_error(); + } + } else { + // GCM: use full key + if (1 != EVP_EncryptInit_ex( + cipher_h->ctx.get(), cipher, nullptr, key.data(), nullptr)) { + throw crypto_error(); + } + } + + return CipherState(cipher_h.release(), hmac_h.release(), suite); } -owned_bytes -hkdf_expand(CipherSuite suite, input_bytes prk, input_bytes info, size_t size) +CipherState +CipherState::create_open(CipherSuite suite, input_bytes key) { - const auto* md = openssl_digest_type(suite); - auto out = owned_bytes(size); - if (1 != HKDF_expand(out.data(), - out.size(), - md, - prk.data(), - prk.size(), - info.data(), - info.size())) { + auto cipher_h = std::make_unique(); + cipher_h->ctx.reset(EVP_CIPHER_CTX_new()); + if (cipher_h->ctx == nullptr) { throw crypto_error(); } - return out; + auto cipher = openssl_cipher(suite); + std::unique_ptr hmac_h; + + if (is_ctr_hmac_suite(suite)) { + // CTR+HMAC: key is split into enc_key and auth_key + auto enc_key_size = cipher_enc_key_size(suite); + auto enc_key = key.first(enc_key_size); + auto auth_key = key.subspan(enc_key_size); + + // Initialize AES-CTR context (always encrypt for CTR mode - CTR is + // symmetric) + if (1 != EVP_EncryptInit_ex( + cipher_h->ctx.get(), cipher, nullptr, enc_key.data(), nullptr)) { + throw crypto_error(); + } + + // Initialize HMAC + hmac_h = std::make_unique(); + hmac_h->ctx.reset(HMAC_CTX_new()); + if (hmac_h->ctx == nullptr) { + throw crypto_error(); + } + + const auto* md = openssl_digest_type(suite); + if (1 != + HMAC_Init_ex( + hmac_h->ctx.get(), auth_key.data(), auth_key.size(), md, nullptr)) { + throw crypto_error(); + } + } else { + // GCM: use full key + if (1 != EVP_DecryptInit_ex( + cipher_h->ctx.get(), cipher, nullptr, key.data(), nullptr)) { + throw crypto_error(); + } + } + + return CipherState(cipher_h.release(), hmac_h.release(), suite); } /// -/// AEAD Algorithms +/// AEAD Algorithms - CTR+HMAC /// -static owned_bytes<64> -compute_tag(CipherSuite suite, - input_bytes auth_key, - input_bytes nonce, - input_bytes aad, - input_bytes ct, - size_t tag_size) +static output_bytes +seal_ctr(EVP_CIPHER_CTX* ctx, + HMAC_CTX* hmac, + CipherSuite suite, + input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) { - using scoped_hmac_ctx = std::unique_ptr; + auto tag_size = cipher_overhead(suite); + if (ct.size() < pt.size() + tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + // Pad nonce to 16 bytes for AES-CTR + auto padded_nonce = owned_bytes<16>(0); + padded_nonce.append(nonce); + padded_nonce.resize(16); - auto ctx = scoped_hmac_ctx(HMAC_CTX_new(), HMAC_CTX_free); - const auto md = openssl_digest_type(suite); + // Reset AES-CTR context with new nonce (key is preserved) + if (1 != + EVP_EncryptInit_ex(ctx, nullptr, nullptr, nullptr, padded_nonce.data())) { + throw crypto_error(); + } - // Guard against sending nullptr to HMAC_Init_ex - const auto* key_data = auth_key.data(); - auto key_size = static_cast(auth_key.size()); - const auto non_null_zero_length_key = uint8_t(0); - if (key_data == nullptr) { - key_data = &non_null_zero_length_key; + // Encrypt with AES-CTR + auto inner_ct = ct.subspan(0, pt.size()); + int outlen = 0; + auto pt_size_int = static_cast(pt.size()); + if (1 != EVP_EncryptUpdate( + ctx, inner_ct.data(), &outlen, pt.data(), pt_size_int)) { + throw crypto_error(); } - if (1 != HMAC_Init_ex(ctx.get(), key_data, key_size, md, nullptr)) { + if (1 != EVP_EncryptFinal(ctx, nullptr, &outlen)) { throw crypto_error(); } + // Compute HMAC tag + // Reset HMAC context (key is preserved from init) + if (1 != HMAC_Init_ex(hmac, nullptr, 0, nullptr, nullptr)) { + throw crypto_error(); + } + + // Build length block auto len_block = owned_bytes<24>(); auto len_view = output_bytes(len_block); encode_uint(aad.size(), len_view.first(8)); - encode_uint(ct.size(), len_view.first(16).last(8)); + encode_uint(inner_ct.size(), len_view.first(16).last(8)); encode_uint(tag_size, len_view.last(8)); - if (1 != HMAC_Update(ctx.get(), len_block.data(), len_block.size())) { + + if (1 != HMAC_Update(hmac, len_block.data(), len_block.size())) { throw crypto_error(); } - - if (1 != HMAC_Update(ctx.get(), nonce.data(), nonce.size())) { + if (1 != HMAC_Update(hmac, nonce.data(), nonce.size())) { throw crypto_error(); } - - if (1 != HMAC_Update(ctx.get(), aad.data(), aad.size())) { + if (1 != HMAC_Update(hmac, aad.data(), aad.size())) { throw crypto_error(); } - - if (1 != HMAC_Update(ctx.get(), ct.data(), ct.size())) { + if (1 != HMAC_Update(hmac, inner_ct.data(), inner_ct.size())) { throw crypto_error(); } - auto tag = owned_bytes<64>(); - auto size = static_cast(EVP_MD_size(md)); - if (1 != HMAC_Final(ctx.get(), tag.data(), &size)) { + auto mac_buf = owned_bytes<64>(); + unsigned int mac_size = mac_buf.size(); + if (1 != HMAC_Final(hmac, mac_buf.data(), &mac_size)) { throw crypto_error(); } - tag.resize(tag_size); - return tag; -} + auto tag = ct.subspan(pt.size(), tag_size); + std::copy(mac_buf.begin(), mac_buf.begin() + tag_size, tag.begin()); -using scoped_evp_cipher_ctx = - std::unique_ptr; + return ct.subspan(0, pt.size() + tag_size); +} -static void -ctr_crypt(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes out, - input_bytes in) +static output_bytes +open_ctr(EVP_CIPHER_CTX* ctx, + HMAC_CTX* hmac, + CipherSuite suite, + input_bytes nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) { - if (out.size() != in.size()) { - throw buffer_too_small_error("CTR size mismatch"); + auto tag_size = cipher_overhead(suite); + if (ct.size() < tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + auto inner_ct_size = ct.size() - tag_size; + if (pt.size() < inner_ct_size) { + throw buffer_too_small_error("Plaintext buffer too small"); } - auto ctx = scoped_evp_cipher_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); - if (ctx.get() == nullptr) { + auto inner_ct = ct.subspan(0, inner_ct_size); + auto tag = ct.subspan(inner_ct_size, tag_size); + + // Verify HMAC tag + // Reset HMAC context (key is preserved from init) + if (1 != HMAC_Init_ex(hmac, nullptr, 0, nullptr, nullptr)) { + throw crypto_error(); + } + + // Build length block + auto len_block = owned_bytes<24>(); + auto len_view = output_bytes(len_block); + encode_uint(aad.size(), len_view.first(8)); + encode_uint(inner_ct.size(), len_view.first(16).last(8)); + encode_uint(tag_size, len_view.last(8)); + + if (1 != HMAC_Update(hmac, len_block.data(), len_block.size())) { + throw crypto_error(); + } + if (1 != HMAC_Update(hmac, nonce.data(), nonce.size())) { + throw crypto_error(); + } + if (1 != HMAC_Update(hmac, aad.data(), aad.size())) { + throw crypto_error(); + } + if (1 != HMAC_Update(hmac, inner_ct.data(), inner_ct.size())) { throw crypto_error(); } + auto mac_buf = owned_bytes<64>(); + unsigned int mac_size = mac_buf.size(); + if (1 != HMAC_Final(hmac, mac_buf.data(), &mac_size)) { + throw crypto_error(); + } + + if (CRYPTO_memcmp(mac_buf.data(), tag.data(), tag_size) != 0) { + throw authentication_error(); + } + + // Decrypt with AES-CTR + // Pad nonce to 16 bytes for AES-CTR auto padded_nonce = owned_bytes<16>(0); padded_nonce.append(nonce); padded_nonce.resize(16); - auto cipher = openssl_cipher(suite); + // Reset AES-CTR context with new nonce (key is preserved) if (1 != - EVP_EncryptInit(ctx.get(), cipher, key.data(), padded_nonce.data())) { + EVP_EncryptInit_ex(ctx, nullptr, nullptr, nullptr, padded_nonce.data())) { throw crypto_error(); } int outlen = 0; - auto in_size_int = static_cast(in.size()); + auto inner_ct_size_int = static_cast(inner_ct_size); if (1 != EVP_EncryptUpdate( - ctx.get(), out.data(), &outlen, in.data(), in_size_int)) { + ctx, pt.data(), &outlen, inner_ct.data(), inner_ct_size_int)) { throw crypto_error(); } - if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + if (1 != EVP_EncryptFinal(ctx, nullptr, &outlen)) { throw crypto_error(); } -} - -static output_bytes -seal_ctr(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes ct, - input_bytes aad, - input_bytes pt) -{ - auto tag_size = cipher_overhead(suite); - if (ct.size() < pt.size() + tag_size) { - throw buffer_too_small_error("Ciphertext buffer too small"); - } - - // Split the key into enc and auth subkeys - auto enc_key_size = cipher_enc_key_size(suite); - auto enc_key = key.first(enc_key_size); - auto auth_key = key.subspan(enc_key_size); - - // Encrypt with AES-CM - auto inner_ct = ct.subspan(0, pt.size()); - ctr_crypt(suite, enc_key, nonce, inner_ct, pt); - - // Authenticate with truncated HMAC - auto mac = compute_tag(suite, auth_key, nonce, aad, inner_ct, tag_size); - auto tag = ct.subspan(pt.size(), tag_size); - std::copy(mac.begin(), mac.begin() + tag_size, tag.begin()); - return ct.subspan(0, pt.size() + tag_size); + return pt.subspan(0, inner_ct_size); } +/// +/// AEAD Algorithms - GCM +/// + static output_bytes -seal_aead(CipherSuite suite, - input_bytes key, +seal_aead(EVP_CIPHER_CTX* ctx, + CipherSuite suite, input_bytes nonce, output_bytes ct, input_bytes aad, @@ -243,34 +400,26 @@ seal_aead(CipherSuite suite, throw buffer_too_small_error("Ciphertext buffer too small"); } - auto ctx = scoped_evp_cipher_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); - if (ctx.get() == nullptr) { - throw crypto_error(); - } - - auto cipher = openssl_cipher(suite); - if (1 != EVP_EncryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + // Reset context and set new nonce (key is preserved) + if (1 != EVP_EncryptInit_ex(ctx, nullptr, nullptr, nullptr, nonce.data())) { throw crypto_error(); } int outlen = 0; auto aad_size_int = static_cast(aad.size()); if (aad.size() > 0) { - if (1 != EVP_EncryptUpdate( - ctx.get(), nullptr, &outlen, aad.data(), aad_size_int)) { + if (1 != + EVP_EncryptUpdate(ctx, nullptr, &outlen, aad.data(), aad_size_int)) { throw crypto_error(); } } auto pt_size_int = static_cast(pt.size()); - if (1 != EVP_EncryptUpdate( - ctx.get(), ct.data(), &outlen, pt.data(), pt_size_int)) { + if (1 != EVP_EncryptUpdate(ctx, ct.data(), &outlen, pt.data(), pt_size_int)) { throw crypto_error(); } - // Providing nullptr as an argument is safe here because this - // function never writes with GCM; it only computes the tag - if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + if (1 != EVP_EncryptFinal(ctx, nullptr, &outlen)) { throw crypto_error(); } @@ -278,75 +427,16 @@ seal_aead(CipherSuite suite, auto tag_ptr = const_cast(static_cast(tag.data())); auto tag_size_downcast = static_cast(tag.size()); if (1 != EVP_CIPHER_CTX_ctrl( - ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size_downcast, tag_ptr)) { + ctx, EVP_CTRL_GCM_GET_TAG, tag_size_downcast, tag_ptr)) { throw crypto_error(); } return ct.subspan(0, pt.size() + tag_size); } -output_bytes -seal(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes ct, - input_bytes aad, - input_bytes pt) -{ - switch (suite) { - case CipherSuite::AES_128_CTR_HMAC_SHA256_80: - case CipherSuite::AES_128_CTR_HMAC_SHA256_64: - case CipherSuite::AES_128_CTR_HMAC_SHA256_32: { - return seal_ctr(suite, key, nonce, ct, aad, pt); - } - - case CipherSuite::AES_GCM_128_SHA256: - case CipherSuite::AES_GCM_256_SHA512: { - return seal_aead(suite, key, nonce, ct, aad, pt); - } - } - - throw unsupported_ciphersuite_error(); -} - static output_bytes -open_ctr(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes pt, - input_bytes aad, - input_bytes ct) -{ - auto tag_size = cipher_overhead(suite); - if (ct.size() < tag_size) { - throw buffer_too_small_error("Ciphertext buffer too small"); - } - - auto inner_ct_size = ct.size() - tag_size; - auto inner_ct = ct.subspan(0, inner_ct_size); - auto tag = ct.subspan(inner_ct_size, tag_size); - - // Split the key into enc and auth subkeys - auto enc_key_size = cipher_enc_key_size(suite); - auto enc_key = key.first(enc_key_size); - auto auth_key = key.subspan(enc_key_size); - - // Authenticate with truncated HMAC - auto mac = compute_tag(suite, auth_key, nonce, aad, inner_ct, tag_size); - if (CRYPTO_memcmp(mac.data(), tag.data(), tag.size()) != 0) { - throw authentication_error(); - } - - // Decrypt with AES-CTR - const auto pt_out = pt.first(inner_ct_size); - ctr_crypt(suite, enc_key, nonce, pt_out, ct.first(inner_ct_size)); - - return pt_out; -} - -static output_bytes -open_aead(CipherSuite suite, - input_bytes key, +open_aead(EVP_CIPHER_CTX* ctx, + CipherSuite suite, input_bytes nonce, output_bytes pt, input_bytes aad, @@ -362,13 +452,8 @@ open_aead(CipherSuite suite, throw buffer_too_small_error("Plaintext buffer too small"); } - auto ctx = scoped_evp_cipher_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); - if (ctx.get() == nullptr) { - throw crypto_error(); - } - - auto cipher = openssl_cipher(suite); - if (1 != EVP_DecryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + // Reset context and set new nonce (key is preserved) + if (1 != EVP_DecryptInit_ex(ctx, nullptr, nullptr, nullptr, nonce.data())) { throw crypto_error(); } @@ -376,34 +461,88 @@ open_aead(CipherSuite suite, auto tag_ptr = const_cast(static_cast(tag.data())); auto tag_size_downcast = static_cast(tag.size()); if (1 != EVP_CIPHER_CTX_ctrl( - ctx.get(), EVP_CTRL_GCM_SET_TAG, tag_size_downcast, tag_ptr)) { + ctx, EVP_CTRL_GCM_SET_TAG, tag_size_downcast, tag_ptr)) { throw crypto_error(); } int out_size; auto aad_size_int = static_cast(aad.size()); if (aad.size() > 0) { - if (1 != EVP_DecryptUpdate( - ctx.get(), nullptr, &out_size, aad.data(), aad_size_int)) { + if (1 != + EVP_DecryptUpdate(ctx, nullptr, &out_size, aad.data(), aad_size_int)) { throw crypto_error(); } } auto inner_ct_size_int = static_cast(inner_ct_size); if (1 != EVP_DecryptUpdate( - ctx.get(), pt.data(), &out_size, ct.data(), inner_ct_size_int)) { + ctx, pt.data(), &out_size, ct.data(), inner_ct_size_int)) { throw crypto_error(); } - // Providing nullptr as an argument is safe here because this - // function never writes with GCM; it only verifies the tag - if (1 != EVP_DecryptFinal(ctx.get(), nullptr, &out_size)) { + if (1 != EVP_DecryptFinal(ctx, nullptr, &out_size)) { throw authentication_error(); } return pt.subspan(0, inner_ct_size); } +/// +/// CipherState seal/open methods +/// + +output_bytes +CipherState::seal(input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + if (is_ctr_hmac_suite(suite)) { + return seal_ctr(cipher_handle->ctx.get(), + hmac_handle->ctx.get(), + suite, + nonce, + ct, + aad, + pt); + } + return seal_aead(cipher_handle->ctx.get(), suite, nonce, ct, aad, pt); +} + +output_bytes +CipherState::open(input_bytes nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + if (is_ctr_hmac_suite(suite)) { + return open_ctr(cipher_handle->ctx.get(), + hmac_handle->ctx.get(), + suite, + nonce, + pt, + aad, + ct); + } + return open_aead(cipher_handle->ctx.get(), suite, nonce, pt, aad, ct); +} + +/// +/// Stateless seal/open (used by test vectors) +/// + +output_bytes +seal(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + auto state = CipherState::create_seal(suite, key); + return state.seal(nonce, ct, aad, pt); +} + output_bytes open(CipherSuite suite, input_bytes key, @@ -412,22 +551,51 @@ open(CipherSuite suite, input_bytes aad, input_bytes ct) { - switch (suite) { - case CipherSuite::AES_128_CTR_HMAC_SHA256_80: - case CipherSuite::AES_128_CTR_HMAC_SHA256_64: - case CipherSuite::AES_128_CTR_HMAC_SHA256_32: { - return open_ctr(suite, key, nonce, pt, aad, ct); - } + auto state = CipherState::create_open(suite, key); + return state.open(nonce, pt, aad, ct); +} - case CipherSuite::AES_GCM_128_SHA256: - case CipherSuite::AES_GCM_256_SHA512: { - return open_aead(suite, key, nonce, pt, aad, ct); - } +/// +/// HKDF +/// + +owned_bytes +hkdf_extract(CipherSuite suite, input_bytes salt, input_bytes ikm) +{ + const auto* md = openssl_digest_type(suite); + auto out = owned_bytes(EVP_MD_size(md)); + auto out_len = size_t(out.size()); + if (1 != HKDF_extract(out.data(), + &out_len, + md, + ikm.data(), + ikm.size(), + salt.data(), + salt.size())) { + throw crypto_error(); } - throw unsupported_ciphersuite_error(); + return out; +} + +owned_bytes +hkdf_expand(CipherSuite suite, input_bytes prk, input_bytes info, size_t size) +{ + const auto* md = openssl_digest_type(suite); + auto out = owned_bytes(size); + if (1 != HKDF_expand(out.data(), + out.size(), + md, + prk.data(), + prk.size(), + info.data(), + info.size())) { + throw crypto_error(); + } + + return out; } } // namespace SFRAME_NAMESPACE -#endif // defined(OPENSSL_3) +#endif // defined(BORINGSSL) diff --git a/src/crypto_openssl11.cpp b/src/crypto_openssl11.cpp index d68e841..4a9cb0b 100644 --- a/src/crypto_openssl11.cpp +++ b/src/crypto_openssl11.cpp @@ -64,213 +64,330 @@ openssl_cipher(CipherSuite suite) } } +static bool +is_ctr_hmac_suite(CipherSuite suite) +{ + switch (suite) { + case CipherSuite::AES_128_CTR_HMAC_SHA256_80: + case CipherSuite::AES_128_CTR_HMAC_SHA256_64: + case CipherSuite::AES_128_CTR_HMAC_SHA256_32: + return true; + default: + return false; + } +} + /// -/// HMAC +/// CipherHandle and HmacHandle definitions /// -struct HMAC +struct CipherHandle { -private: - scoped_hmac_ctx ctx; + scoped_evp_ctx ctx; + CipherHandle() + : ctx(nullptr, EVP_CIPHER_CTX_free) + { + } +}; -public: - HMAC(CipherSuite suite, input_bytes key) - : ctx(HMAC_CTX_new(), HMAC_CTX_free) +struct HmacHandle +{ + scoped_hmac_ctx ctx; + HmacHandle() + : ctx(nullptr, HMAC_CTX_free) { - const auto type = openssl_digest_type(suite); + } +}; - // Some FIPS-enabled libraries are overly conservative in their - // interpretation of NIST SP 800-131A, which requires HMAC keys to be at - // least 112 bits long. That document does not impose that requirement on - // HKDF, so we disable FIPS enforcement for purposes of HKDF. - // - // https://doi.org/10.6028/NIST.SP.800-131Ar2 - static const auto fips_min_hmac_key_len = 14; - auto key_size = static_cast(key.size()); - if (FIPS_mode() != 0 && key_size < fips_min_hmac_key_len) { - HMAC_CTX_set_flags(ctx.get(), EVP_MD_CTX_FLAG_NON_FIPS_ALLOW); +void +CipherState::Deleter::operator()(CipherHandle* h) const +{ + delete h; +} + +void +CipherState::Deleter::operator()(HmacHandle* h) const +{ + delete h; +} + +CipherState::CipherState(CipherHandle* cipher, + HmacHandle* hmac, + CipherSuite suite_in) + : cipher_handle(cipher) + , hmac_handle(hmac) + , suite(suite_in) +{ +} + +CipherState +CipherState::create_seal(CipherSuite suite, input_bytes key) +{ + auto cipher_h = std::make_unique(); + cipher_h->ctx.reset(EVP_CIPHER_CTX_new()); + if (cipher_h->ctx == nullptr) { + throw crypto_error(); + } + + auto cipher = openssl_cipher(suite); + std::unique_ptr hmac_h; + + if (is_ctr_hmac_suite(suite)) { + // CTR+HMAC: key is split into enc_key and auth_key + auto enc_key_size = cipher_enc_key_size(suite); + auto enc_key = key.first(enc_key_size); + auto auth_key = key.subspan(enc_key_size); + + // Initialize AES-CTR context (always encrypt for CTR mode) + if (1 != EVP_EncryptInit_ex( + cipher_h->ctx.get(), cipher, nullptr, enc_key.data(), nullptr)) { + throw crypto_error(); } - // Guard against sending nullptr to HMAC_Init_ex - const auto* key_data = key.data(); - const auto non_null_zero_length_key = uint8_t(0); - if (key_data == nullptr) { - key_data = &non_null_zero_length_key; + // Initialize HMAC + hmac_h = std::make_unique(); + hmac_h->ctx.reset(HMAC_CTX_new()); + if (hmac_h->ctx == nullptr) { + throw crypto_error(); } - if (1 != HMAC_Init_ex(ctx.get(), key_data, key_size, type, nullptr)) { + const auto* md = openssl_digest_type(suite); + auto key_size = static_cast(auth_key.size()); + if (1 != HMAC_Init_ex( + hmac_h->ctx.get(), auth_key.data(), key_size, md, nullptr)) { + throw crypto_error(); + } + } else { + // GCM: use full key + if (1 != EVP_EncryptInit_ex( + cipher_h->ctx.get(), cipher, nullptr, key.data(), nullptr)) { throw crypto_error(); } } - void write(input_bytes data) - { - if (1 != HMAC_Update(ctx.get(), data.data(), data.size())) { + return CipherState(cipher_h.release(), hmac_h.release(), suite); +} + +CipherState +CipherState::create_open(CipherSuite suite, input_bytes key) +{ + auto cipher_h = std::make_unique(); + cipher_h->ctx.reset(EVP_CIPHER_CTX_new()); + if (cipher_h->ctx == nullptr) { + throw crypto_error(); + } + + auto cipher = openssl_cipher(suite); + std::unique_ptr hmac_h; + + if (is_ctr_hmac_suite(suite)) { + // CTR+HMAC: key is split into enc_key and auth_key + auto enc_key_size = cipher_enc_key_size(suite); + auto enc_key = key.first(enc_key_size); + auto auth_key = key.subspan(enc_key_size); + + // Initialize AES-CTR context (always encrypt for CTR mode - CTR is + // symmetric) + if (1 != EVP_EncryptInit_ex( + cipher_h->ctx.get(), cipher, nullptr, enc_key.data(), nullptr)) { throw crypto_error(); } - } - output_bytes digest(output_bytes md) - { - unsigned int size = md.size(); - if (1 != HMAC_Final(ctx.get(), md.data(), &size)) { + // Initialize HMAC + hmac_h = std::make_unique(); + hmac_h->ctx.reset(HMAC_CTX_new()); + if (hmac_h->ctx == nullptr) { throw crypto_error(); } - return md.first(size); + const auto* md = openssl_digest_type(suite); + auto key_size = static_cast(auth_key.size()); + if (1 != HMAC_Init_ex( + hmac_h->ctx.get(), auth_key.data(), key_size, md, nullptr)) { + throw crypto_error(); + } + } else { + // GCM: use full key + if (1 != EVP_DecryptInit_ex( + cipher_h->ctx.get(), cipher, nullptr, key.data(), nullptr)) { + throw crypto_error(); + } } -}; + + return CipherState(cipher_h.release(), hmac_h.release(), suite); +} /// -/// HKDF +/// AEAD Algorithms - CTR+HMAC /// -owned_bytes -hkdf_extract(CipherSuite suite, input_bytes salt, input_bytes ikm) +static output_bytes +seal_ctr(EVP_CIPHER_CTX* ctx, + HMAC_CTX* hmac, + CipherSuite suite, + input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) { - auto h = HMAC(suite, salt); - h.write(ikm); + auto tag_size = cipher_overhead(suite); + if (ct.size() < pt.size() + tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } - auto out = owned_bytes(); - const auto md = h.digest(out); - out.resize(md.size()); - return out; -} + // Pad nonce to 16 bytes for AES-CTR + auto padded_nonce = owned_bytes<16>(0); + padded_nonce.append(nonce); + padded_nonce.resize(16); -owned_bytes -hkdf_expand(CipherSuite suite, input_bytes prk, input_bytes info, size_t size) -{ - // Ensure that we need only one hash invocation - if (size > max_hkdf_extract_size) { - throw invalid_parameter_error("Size too big for hkdf_expand"); + // Reset AES-CTR context with new nonce (key is preserved) + if (1 != + EVP_EncryptInit_ex(ctx, nullptr, nullptr, nullptr, padded_nonce.data())) { + throw crypto_error(); } - auto out = owned_bytes(0); + // Encrypt with AES-CTR + auto inner_ct = ct.subspan(0, pt.size()); + int outlen = 0; + auto pt_size_int = static_cast(pt.size()); + if (1 != EVP_EncryptUpdate( + ctx, inner_ct.data(), &outlen, pt.data(), pt_size_int)) { + throw crypto_error(); + } - auto block = owned_bytes(0); - const auto block_size = cipher_digest_size(suite); - auto counter = owned_bytes<1>(); - counter[0] = 0x01; - while (out.size() < size) { - auto h = HMAC(suite, prk); - h.write(block); - h.write(info); - h.write(counter); + if (1 != EVP_EncryptFinal(ctx, nullptr, &outlen)) { + throw crypto_error(); + } - block.resize(block_size); - h.digest(block); + // Compute HMAC tag + // Reset HMAC context (key is preserved from init) + if (1 != HMAC_Init_ex(hmac, nullptr, 0, nullptr, nullptr)) { + throw crypto_error(); + } - const auto remaining = size - out.size(); - const auto to_write = (remaining < block_size) ? remaining : block_size; - out.append(input_bytes(block).first(to_write)); + // Build length block + auto len_block = owned_bytes<24>(); + auto len_view = output_bytes(len_block); + encode_uint(aad.size(), len_view.first(8)); + encode_uint(inner_ct.size(), len_view.first(16).last(8)); + encode_uint(tag_size, len_view.last(8)); - counter[0] += 1; + if (1 != HMAC_Update(hmac, len_block.data(), len_block.size())) { + throw crypto_error(); + } + if (1 != HMAC_Update(hmac, nonce.data(), nonce.size())) { + throw crypto_error(); + } + if (1 != HMAC_Update(hmac, aad.data(), aad.size())) { + throw crypto_error(); + } + if (1 != HMAC_Update(hmac, inner_ct.data(), inner_ct.size())) { + throw crypto_error(); } - return out; -} + auto mac_buf = owned_bytes<64>(); + unsigned int mac_size = mac_buf.size(); + if (1 != HMAC_Final(hmac, mac_buf.data(), &mac_size)) { + throw crypto_error(); + } -/// -/// AEAD Algorithms -/// + auto tag = ct.subspan(pt.size(), tag_size); + std::copy(mac_buf.begin(), mac_buf.begin() + tag_size, tag.begin()); -static owned_bytes<64> -compute_tag(CipherSuite suite, - input_bytes auth_key, - input_bytes nonce, - input_bytes aad, - input_bytes ct, - size_t tag_size) + return ct.subspan(0, pt.size() + tag_size); +} + +static output_bytes +open_ctr(EVP_CIPHER_CTX* ctx, + HMAC_CTX* hmac, + CipherSuite suite, + input_bytes nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) { + auto tag_size = cipher_overhead(suite); + if (ct.size() < tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + auto inner_ct_size = ct.size() - tag_size; + if (pt.size() < inner_ct_size) { + throw buffer_too_small_error("Plaintext buffer too small"); + } + + auto inner_ct = ct.subspan(0, inner_ct_size); + auto tag = ct.subspan(inner_ct_size, tag_size); + + // Verify HMAC tag + // Reset HMAC context (key is preserved from init) + if (1 != HMAC_Init_ex(hmac, nullptr, 0, nullptr, nullptr)) { + throw crypto_error(); + } + + // Build length block auto len_block = owned_bytes<24>(); auto len_view = output_bytes(len_block); encode_uint(aad.size(), len_view.first(8)); - encode_uint(ct.size(), len_view.first(16).last(8)); + encode_uint(inner_ct.size(), len_view.first(16).last(8)); encode_uint(tag_size, len_view.last(8)); - auto h = HMAC(suite, auth_key); - h.write(len_block); - h.write(nonce); - h.write(aad); - h.write(ct); - - auto tag = owned_bytes<64>(); - h.digest(tag); - tag.resize(tag_size); - return tag; -} - -static void -ctr_crypt(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes out, - input_bytes in) -{ - if (out.size() != in.size()) { - throw buffer_too_small_error("CTR size mismatch"); + if (1 != HMAC_Update(hmac, len_block.data(), len_block.size())) { + throw crypto_error(); + } + if (1 != HMAC_Update(hmac, nonce.data(), nonce.size())) { + throw crypto_error(); + } + if (1 != HMAC_Update(hmac, aad.data(), aad.size())) { + throw crypto_error(); + } + if (1 != HMAC_Update(hmac, inner_ct.data(), inner_ct.size())) { + throw crypto_error(); } - auto ctx = scoped_evp_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); - if (ctx.get() == nullptr) { + auto mac_buf = owned_bytes<64>(); + unsigned int mac_size = mac_buf.size(); + if (1 != HMAC_Final(hmac, mac_buf.data(), &mac_size)) { throw crypto_error(); } + if (CRYPTO_memcmp(mac_buf.data(), tag.data(), tag_size) != 0) { + throw authentication_error(); + } + + // Decrypt with AES-CTR + // Pad nonce to 16 bytes for AES-CTR auto padded_nonce = owned_bytes<16>(0); padded_nonce.append(nonce); padded_nonce.resize(16); - auto cipher = openssl_cipher(suite); + // Reset AES-CTR context with new nonce (key is preserved) if (1 != - EVP_EncryptInit(ctx.get(), cipher, key.data(), padded_nonce.data())) { + EVP_EncryptInit_ex(ctx, nullptr, nullptr, nullptr, padded_nonce.data())) { throw crypto_error(); } int outlen = 0; - auto in_size_int = static_cast(in.size()); + auto inner_ct_size_int = static_cast(inner_ct_size); if (1 != EVP_EncryptUpdate( - ctx.get(), out.data(), &outlen, in.data(), in_size_int)) { + ctx, pt.data(), &outlen, inner_ct.data(), inner_ct_size_int)) { throw crypto_error(); } - if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + if (1 != EVP_EncryptFinal(ctx, nullptr, &outlen)) { throw crypto_error(); } -} -static output_bytes -seal_ctr(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes ct, - input_bytes aad, - input_bytes pt) -{ - auto tag_size = cipher_overhead(suite); - if (ct.size() < pt.size() + tag_size) { - throw buffer_too_small_error("Ciphertext buffer too small"); - } - - // Split the key into enc and auth subkeys - auto enc_key_size = cipher_enc_key_size(suite); - auto enc_key = key.first(enc_key_size); - auto auth_key = key.subspan(enc_key_size); - - // Encrypt with AES-CM - auto inner_ct = ct.subspan(0, pt.size()); - ctr_crypt(suite, enc_key, nonce, inner_ct, pt); - - // Authenticate with truncated HMAC - auto mac = compute_tag(suite, auth_key, nonce, aad, inner_ct, tag_size); - auto tag = ct.subspan(pt.size(), tag_size); - std::copy(mac.begin(), mac.begin() + tag_size, tag.begin()); - - return ct.subspan(0, pt.size() + tag_size); + return pt.subspan(0, inner_ct_size); } +/// +/// AEAD Algorithms - GCM +/// + static output_bytes -seal_aead(CipherSuite suite, - input_bytes key, +seal_aead(EVP_CIPHER_CTX* ctx, + CipherSuite suite, input_bytes nonce, output_bytes ct, input_bytes aad, @@ -281,34 +398,26 @@ seal_aead(CipherSuite suite, throw buffer_too_small_error("Ciphertext buffer too small"); } - auto ctx = scoped_evp_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); - if (ctx.get() == nullptr) { - throw crypto_error(); - } - - auto cipher = openssl_cipher(suite); - if (1 != EVP_EncryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + // Reset context and set new nonce (key is preserved) + if (1 != EVP_EncryptInit_ex(ctx, nullptr, nullptr, nullptr, nonce.data())) { throw crypto_error(); } int outlen = 0; auto aad_size_int = static_cast(aad.size()); if (aad.size() > 0) { - if (1 != EVP_EncryptUpdate( - ctx.get(), nullptr, &outlen, aad.data(), aad_size_int)) { + if (1 != + EVP_EncryptUpdate(ctx, nullptr, &outlen, aad.data(), aad_size_int)) { throw crypto_error(); } } auto pt_size_int = static_cast(pt.size()); - if (1 != EVP_EncryptUpdate( - ctx.get(), ct.data(), &outlen, pt.data(), pt_size_int)) { + if (1 != EVP_EncryptUpdate(ctx, ct.data(), &outlen, pt.data(), pt_size_int)) { throw crypto_error(); } - // Providing nullptr as an argument is safe here because this - // function never writes with GCM; it only computes the tag - if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + if (1 != EVP_EncryptFinal(ctx, nullptr, &outlen)) { throw crypto_error(); } @@ -316,75 +425,16 @@ seal_aead(CipherSuite suite, auto tag_ptr = const_cast(static_cast(tag.data())); auto tag_size_downcast = static_cast(tag.size()); if (1 != EVP_CIPHER_CTX_ctrl( - ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size_downcast, tag_ptr)) { + ctx, EVP_CTRL_GCM_GET_TAG, tag_size_downcast, tag_ptr)) { throw crypto_error(); } return ct.subspan(0, pt.size() + tag_size); } -output_bytes -seal(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes ct, - input_bytes aad, - input_bytes pt) -{ - switch (suite) { - case CipherSuite::AES_128_CTR_HMAC_SHA256_80: - case CipherSuite::AES_128_CTR_HMAC_SHA256_64: - case CipherSuite::AES_128_CTR_HMAC_SHA256_32: { - return seal_ctr(suite, key, nonce, ct, aad, pt); - } - - case CipherSuite::AES_GCM_128_SHA256: - case CipherSuite::AES_GCM_256_SHA512: { - return seal_aead(suite, key, nonce, ct, aad, pt); - } - } - - throw unsupported_ciphersuite_error(); -} - -static output_bytes -open_ctr(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes pt, - input_bytes aad, - input_bytes ct) -{ - auto tag_size = cipher_overhead(suite); - if (ct.size() < tag_size) { - throw buffer_too_small_error("Ciphertext buffer too small"); - } - - auto inner_ct_size = ct.size() - tag_size; - auto inner_ct = ct.subspan(0, inner_ct_size); - auto tag = ct.subspan(inner_ct_size, tag_size); - - // Split the key into enc and auth subkeys - auto enc_key_size = cipher_enc_key_size(suite); - auto enc_key = key.first(enc_key_size); - auto auth_key = key.subspan(enc_key_size); - - // Authenticate with truncated HMAC - auto mac = compute_tag(suite, auth_key, nonce, aad, inner_ct, tag_size); - if (CRYPTO_memcmp(mac.data(), tag.data(), tag.size()) != 0) { - throw authentication_error(); - } - - // Decrypt with AES-CTR - const auto pt_out = pt.first(inner_ct_size); - ctr_crypt(suite, enc_key, nonce, pt_out, ct.first(inner_ct_size)); - - return pt_out; -} - static output_bytes -open_aead(CipherSuite suite, - input_bytes key, +open_aead(EVP_CIPHER_CTX* ctx, + CipherSuite suite, input_bytes nonce, output_bytes pt, input_bytes aad, @@ -400,13 +450,8 @@ open_aead(CipherSuite suite, throw buffer_too_small_error("Plaintext buffer too small"); } - auto ctx = scoped_evp_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); - if (ctx.get() == nullptr) { - throw crypto_error(); - } - - auto cipher = openssl_cipher(suite); - if (1 != EVP_DecryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + // Reset context and set new nonce (key is preserved) + if (1 != EVP_DecryptInit_ex(ctx, nullptr, nullptr, nullptr, nonce.data())) { throw crypto_error(); } @@ -414,34 +459,88 @@ open_aead(CipherSuite suite, auto tag_ptr = const_cast(static_cast(tag.data())); auto tag_size_downcast = static_cast(tag.size()); if (1 != EVP_CIPHER_CTX_ctrl( - ctx.get(), EVP_CTRL_GCM_SET_TAG, tag_size_downcast, tag_ptr)) { + ctx, EVP_CTRL_GCM_SET_TAG, tag_size_downcast, tag_ptr)) { throw crypto_error(); } int out_size; auto aad_size_int = static_cast(aad.size()); if (aad.size() > 0) { - if (1 != EVP_DecryptUpdate( - ctx.get(), nullptr, &out_size, aad.data(), aad_size_int)) { + if (1 != + EVP_DecryptUpdate(ctx, nullptr, &out_size, aad.data(), aad_size_int)) { throw crypto_error(); } } auto inner_ct_size_int = static_cast(inner_ct_size); if (1 != EVP_DecryptUpdate( - ctx.get(), pt.data(), &out_size, ct.data(), inner_ct_size_int)) { + ctx, pt.data(), &out_size, ct.data(), inner_ct_size_int)) { throw crypto_error(); } - // Providing nullptr as an argument is safe here because this - // function never writes with GCM; it only verifies the tag - if (1 != EVP_DecryptFinal(ctx.get(), nullptr, &out_size)) { + if (1 != EVP_DecryptFinal(ctx, nullptr, &out_size)) { throw authentication_error(); } return pt.subspan(0, inner_ct_size); } +/// +/// CipherState seal/open methods +/// + +output_bytes +CipherState::seal(input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + if (is_ctr_hmac_suite(suite)) { + return seal_ctr(cipher_handle->ctx.get(), + hmac_handle->ctx.get(), + suite, + nonce, + ct, + aad, + pt); + } + return seal_aead(cipher_handle->ctx.get(), suite, nonce, ct, aad, pt); +} + +output_bytes +CipherState::open(input_bytes nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + if (is_ctr_hmac_suite(suite)) { + return open_ctr(cipher_handle->ctx.get(), + hmac_handle->ctx.get(), + suite, + nonce, + pt, + aad, + ct); + } + return open_aead(cipher_handle->ctx.get(), suite, nonce, pt, aad, ct); +} + +/// +/// Stateless seal/open (used by test vectors) +/// + +output_bytes +seal(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + auto state = CipherState::create_seal(suite, key); + return state.seal(nonce, ct, aad, pt); +} + output_bytes open(CipherSuite suite, input_bytes key, @@ -450,20 +549,114 @@ open(CipherSuite suite, input_bytes aad, input_bytes ct) { - switch (suite) { - case CipherSuite::AES_128_CTR_HMAC_SHA256_80: - case CipherSuite::AES_128_CTR_HMAC_SHA256_64: - case CipherSuite::AES_128_CTR_HMAC_SHA256_32: { - return open_ctr(suite, key, nonce, pt, aad, ct); + auto state = CipherState::create_open(suite, key); + return state.open(nonce, pt, aad, ct); +} + +/// +/// HMAC wrapper class for HKDF +/// + +struct HMACForHKDF +{ +private: + scoped_hmac_ctx ctx; + +public: + HMACForHKDF(CipherSuite suite, input_bytes key) + : ctx(HMAC_CTX_new(), HMAC_CTX_free) + { + const auto type = openssl_digest_type(suite); + + // Some FIPS-enabled libraries are overly conservative in their + // interpretation of NIST SP 800-131A, which requires HMAC keys to be at + // least 112 bits long. That document does not impose that requirement on + // HKDF, so we disable FIPS enforcement for purposes of HKDF. + // + // https://doi.org/10.6028/NIST.SP.800-131Ar2 + static const auto fips_min_hmac_key_len = 14; + auto key_size = static_cast(key.size()); + if (FIPS_mode() != 0 && key_size < fips_min_hmac_key_len) { + HMAC_CTX_set_flags(ctx.get(), EVP_MD_CTX_FLAG_NON_FIPS_ALLOW); } - case CipherSuite::AES_GCM_128_SHA256: - case CipherSuite::AES_GCM_256_SHA512: { - return open_aead(suite, key, nonce, pt, aad, ct); + // Guard against sending nullptr to HMAC_Init_ex + const auto* key_data = key.data(); + const auto non_null_zero_length_key = uint8_t(0); + if (key_data == nullptr) { + key_data = &non_null_zero_length_key; + } + + if (1 != HMAC_Init_ex(ctx.get(), key_data, key_size, type, nullptr)) { + throw crypto_error(); + } + } + + void write(input_bytes data) + { + if (1 != HMAC_Update(ctx.get(), data.data(), data.size())) { + throw crypto_error(); } } - throw unsupported_ciphersuite_error(); + output_bytes digest(output_bytes md) + { + unsigned int size = md.size(); + if (1 != HMAC_Final(ctx.get(), md.data(), &size)) { + throw crypto_error(); + } + + return md.first(size); + } +}; + +/// +/// HKDF +/// + +owned_bytes +hkdf_extract(CipherSuite suite, input_bytes salt, input_bytes ikm) +{ + auto h = HMACForHKDF(suite, salt); + h.write(ikm); + + auto out = owned_bytes(); + const auto md = h.digest(out); + out.resize(md.size()); + return out; +} + +owned_bytes +hkdf_expand(CipherSuite suite, input_bytes prk, input_bytes info, size_t size) +{ + // Ensure that we need only one hash invocation + if (size > max_hkdf_extract_size) { + throw invalid_parameter_error("Size too big for hkdf_expand"); + } + + auto out = owned_bytes(0); + + auto block = owned_bytes(0); + const auto block_size = cipher_digest_size(suite); + auto counter = owned_bytes<1>(); + counter[0] = 0x01; + while (out.size() < size) { + auto h = HMACForHKDF(suite, prk); + h.write(block); + h.write(info); + h.write(counter); + + block.resize(block_size); + h.digest(block); + + const auto remaining = size - out.size(); + const auto to_write = (remaining < block_size) ? remaining : block_size; + out.append(input_bytes(block).first(to_write)); + + counter[0] += 1; + } + + return out; } } // namespace SFRAME_NAMESPACE diff --git a/src/crypto_openssl3.cpp b/src/crypto_openssl3.cpp index d1bda30..460a4dd 100644 --- a/src/crypto_openssl3.cpp +++ b/src/crypto_openssl3.cpp @@ -11,6 +11,13 @@ namespace SFRAME_NAMESPACE { +/// +/// Scoped pointers for OpenSSL objects +/// + +using scoped_evp_cipher_ctx = + std::unique_ptr; + /// /// Convert between native identifiers / errors and OpenSSL ones /// @@ -58,221 +65,356 @@ openssl_digest_name(CipherSuite suite) } } +static bool +is_ctr_hmac_suite(CipherSuite suite) +{ + switch (suite) { + case CipherSuite::AES_128_CTR_HMAC_SHA256_80: + case CipherSuite::AES_128_CTR_HMAC_SHA256_64: + case CipherSuite::AES_128_CTR_HMAC_SHA256_32: + return true; + default: + return false; + } +} + /// -/// HKDF +/// CipherHandle and HmacHandle definitions /// -using scoped_evp_kdf = std::unique_ptr; -using scoped_evp_kdf_ctx = - std::unique_ptr; +struct CipherHandle +{ + scoped_evp_cipher_ctx ctx; + CipherHandle() + : ctx(nullptr, EVP_CIPHER_CTX_free) + { + } +}; -owned_bytes -hkdf_extract(CipherSuite suite, input_bytes salt, input_bytes ikm) +// HmacHandle for OpenSSL 3.x holds both the MAC algorithm and context +struct HmacHandle { - auto mode = EVP_KDF_HKDF_MODE_EXTRACT_ONLY; - auto digest_name = openssl_digest_name(suite); - auto* salt_ptr = - const_cast(reinterpret_cast(salt.data())); - auto* ikm_ptr = const_cast(reinterpret_cast(ikm.data())); + std::unique_ptr mac; + std::unique_ptr ctx; - const auto params = std::array{ - OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &mode), - OSSL_PARAM_construct_utf8_string( - OSSL_KDF_PARAM_DIGEST, digest_name.data(), digest_name.size()), - OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, ikm_ptr, ikm.size()), - OSSL_PARAM_construct_octet_string( - OSSL_KDF_PARAM_SALT, salt_ptr, salt.size()), - OSSL_PARAM_construct_end(), - }; + HmacHandle() + : mac(nullptr, EVP_MAC_free) + , ctx(nullptr, EVP_MAC_CTX_free) + { + } +}; - const auto kdf = - scoped_evp_kdf(EVP_KDF_fetch(NULL, "HKDF", NULL), EVP_KDF_free); - const auto ctx = - scoped_evp_kdf_ctx(EVP_KDF_CTX_new(kdf.get()), EVP_KDF_CTX_free); - if (1 != EVP_KDF_CTX_set_params(ctx.get(), params.data())) { +void +CipherState::Deleter::operator()(CipherHandle* h) const +{ + delete h; +} + +void +CipherState::Deleter::operator()(HmacHandle* h) const +{ + delete h; +} + +CipherState::CipherState(CipherHandle* cipher, + HmacHandle* hmac, + CipherSuite suite_in) + : cipher_handle(cipher) + , hmac_handle(hmac) + , suite(suite_in) +{ +} + +CipherState +CipherState::create_seal(CipherSuite suite, input_bytes key) +{ + auto cipher_h = std::make_unique(); + cipher_h->ctx.reset(EVP_CIPHER_CTX_new()); + if (cipher_h->ctx == nullptr) { throw crypto_error(); } - const auto digest_size = EVP_KDF_CTX_get_kdf_size(ctx.get()); - auto out = owned_bytes(digest_size); - if (1 != EVP_KDF_derive(ctx.get(), out.data(), out.size(), nullptr)) { - throw crypto_error(); + auto cipher = openssl_cipher(suite); + std::unique_ptr hmac_h; + + if (is_ctr_hmac_suite(suite)) { + // CTR+HMAC: key is split into enc_key and auth_key + auto enc_key_size = cipher_enc_key_size(suite); + auto enc_key = key.first(enc_key_size); + auto auth_key = key.subspan(enc_key_size); + + // Initialize AES-CTR context (always encrypt for CTR mode) + if (1 != EVP_EncryptInit_ex( + cipher_h->ctx.get(), cipher, nullptr, enc_key.data(), nullptr)) { + throw crypto_error(); + } + + // Initialize HMAC + hmac_h = std::make_unique(); + hmac_h->mac.reset(EVP_MAC_fetch(nullptr, OSSL_MAC_NAME_HMAC, nullptr)); + if (hmac_h->mac == nullptr) { + throw crypto_error(); + } + + hmac_h->ctx.reset(EVP_MAC_CTX_new(hmac_h->mac.get())); + if (hmac_h->ctx == nullptr) { + throw crypto_error(); + } + + auto digest_name = openssl_digest_name(suite); + std::array params = { + OSSL_PARAM_construct_utf8_string( + OSSL_ALG_PARAM_DIGEST, digest_name.data(), 0), + OSSL_PARAM_construct_end() + }; + + if (1 != + EVP_MAC_init( + hmac_h->ctx.get(), auth_key.data(), auth_key.size(), params.data())) { + throw crypto_error(); + } + } else { + // GCM: use full key + if (1 != EVP_EncryptInit_ex( + cipher_h->ctx.get(), cipher, nullptr, key.data(), nullptr)) { + throw crypto_error(); + } } - return out; + return CipherState(cipher_h.release(), hmac_h.release(), suite); } -owned_bytes -hkdf_expand(CipherSuite suite, input_bytes prk, input_bytes info, size_t size) +CipherState +CipherState::create_open(CipherSuite suite, input_bytes key) { - auto mode = EVP_KDF_HKDF_MODE_EXPAND_ONLY; - auto digest_name = openssl_digest_name(suite); - auto* prk_ptr = const_cast(reinterpret_cast(prk.data())); - auto* info_ptr = - const_cast(reinterpret_cast(info.data())); + auto cipher_h = std::make_unique(); + cipher_h->ctx.reset(EVP_CIPHER_CTX_new()); + if (cipher_h->ctx == nullptr) { + throw crypto_error(); + } - const auto params = std::array{ - OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &mode), - OSSL_PARAM_construct_utf8_string( - OSSL_KDF_PARAM_DIGEST, digest_name.data(), digest_name.size()), - OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, prk_ptr, prk.size()), - OSSL_PARAM_construct_octet_string( - OSSL_KDF_PARAM_INFO, info_ptr, info.size()), - OSSL_PARAM_construct_end(), - }; + auto cipher = openssl_cipher(suite); + std::unique_ptr hmac_h; + + if (is_ctr_hmac_suite(suite)) { + // CTR+HMAC: key is split into enc_key and auth_key + auto enc_key_size = cipher_enc_key_size(suite); + auto enc_key = key.first(enc_key_size); + auto auth_key = key.subspan(enc_key_size); + + // Initialize AES-CTR context (always encrypt for CTR mode - CTR is + // symmetric) + if (1 != EVP_EncryptInit_ex( + cipher_h->ctx.get(), cipher, nullptr, enc_key.data(), nullptr)) { + throw crypto_error(); + } - const auto kdf = - scoped_evp_kdf(EVP_KDF_fetch(NULL, "HKDF", NULL), EVP_KDF_free); - const auto ctx = - scoped_evp_kdf_ctx(EVP_KDF_CTX_new(kdf.get()), EVP_KDF_CTX_free); + // Initialize HMAC + hmac_h = std::make_unique(); + hmac_h->mac.reset(EVP_MAC_fetch(nullptr, OSSL_MAC_NAME_HMAC, nullptr)); + if (hmac_h->mac == nullptr) { + throw crypto_error(); + } - auto out = owned_bytes(size); - if (1 != EVP_KDF_derive(ctx.get(), out.data(), out.size(), params.data())) { - throw crypto_error(); + hmac_h->ctx.reset(EVP_MAC_CTX_new(hmac_h->mac.get())); + if (hmac_h->ctx == nullptr) { + throw crypto_error(); + } + + auto digest_name = openssl_digest_name(suite); + std::array params = { + OSSL_PARAM_construct_utf8_string( + OSSL_ALG_PARAM_DIGEST, digest_name.data(), 0), + OSSL_PARAM_construct_end() + }; + + if (1 != + EVP_MAC_init( + hmac_h->ctx.get(), auth_key.data(), auth_key.size(), params.data())) { + throw crypto_error(); + } + } else { + // GCM: use full key + if (1 != EVP_DecryptInit_ex( + cipher_h->ctx.get(), cipher, nullptr, key.data(), nullptr)) { + throw crypto_error(); + } } - return out; + return CipherState(cipher_h.release(), hmac_h.release(), suite); } /// -/// AEAD Algorithms +/// AEAD Algorithms - CTR+HMAC /// -static owned_bytes<64> -compute_tag(CipherSuite suite, - input_bytes auth_key, - input_bytes nonce, - input_bytes aad, - input_bytes ct, - size_t tag_size) +static output_bytes +seal_ctr(EVP_CIPHER_CTX* ctx, + EVP_MAC_CTX* hmac, + CipherSuite suite, + input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) { - using scoped_evp_mac = std::unique_ptr; - using scoped_evp_mac_ctx = - std::unique_ptr; - - auto len_block = owned_bytes<24>(); - auto len_view = output_bytes(len_block); - encode_uint(aad.size(), len_view.first(8)); - encode_uint(ct.size(), len_view.first(16).last(8)); - encode_uint(tag_size, len_view.last(8)); - - auto digest_name = openssl_digest_name(suite); - std::array params = { - OSSL_PARAM_construct_utf8_string( - OSSL_ALG_PARAM_DIGEST, digest_name.data(), 0), - OSSL_PARAM_construct_end() - }; + auto tag_size = cipher_overhead(suite); + if (ct.size() < pt.size() + tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } - const auto mac = scoped_evp_mac( - EVP_MAC_fetch(nullptr, OSSL_MAC_NAME_HMAC, nullptr), EVP_MAC_free); - const auto ctx = - scoped_evp_mac_ctx(EVP_MAC_CTX_new(mac.get()), EVP_MAC_CTX_free); + // Pad nonce to 16 bytes for AES-CTR + auto padded_nonce = owned_bytes<16>(0); + padded_nonce.append(nonce); + padded_nonce.resize(16); - if (1 != EVP_MAC_init( - ctx.get(), auth_key.data(), auth_key.size(), params.data())) { + // Reset AES-CTR context with new nonce (key is preserved) + if (1 != + EVP_EncryptInit_ex(ctx, nullptr, nullptr, nullptr, padded_nonce.data())) { throw crypto_error(); } - if (1 != EVP_MAC_update(ctx.get(), len_block.data(), len_block.size())) { + // Encrypt with AES-CTR + auto inner_ct = ct.subspan(0, pt.size()); + int outlen = 0; + auto pt_size_int = static_cast(pt.size()); + if (1 != EVP_EncryptUpdate( + ctx, inner_ct.data(), &outlen, pt.data(), pt_size_int)) { throw crypto_error(); } - if (1 != EVP_MAC_update(ctx.get(), nonce.data(), nonce.size())) { + if (1 != EVP_EncryptFinal(ctx, nullptr, &outlen)) { throw crypto_error(); } - if (1 != EVP_MAC_update(ctx.get(), aad.data(), aad.size())) { + // Compute HMAC tag + // Reset HMAC context (key is preserved from init) + if (1 != EVP_MAC_init(hmac, nullptr, 0, nullptr)) { throw crypto_error(); } - if (1 != EVP_MAC_update(ctx.get(), ct.data(), ct.size())) { + // Build length block + auto len_block = owned_bytes<24>(); + auto len_view = output_bytes(len_block); + encode_uint(aad.size(), len_view.first(8)); + encode_uint(inner_ct.size(), len_view.first(16).last(8)); + encode_uint(tag_size, len_view.last(8)); + + if (1 != EVP_MAC_update(hmac, len_block.data(), len_block.size())) { + throw crypto_error(); + } + if (1 != EVP_MAC_update(hmac, nonce.data(), nonce.size())) { + throw crypto_error(); + } + if (1 != EVP_MAC_update(hmac, aad.data(), aad.size())) { + throw crypto_error(); + } + if (1 != EVP_MAC_update(hmac, inner_ct.data(), inner_ct.size())) { throw crypto_error(); } - size_t size = 0; - auto tag = owned_bytes<64>(); - if (1 != EVP_MAC_final(ctx.get(), tag.data(), &size, tag.size())) { + size_t mac_size = 0; + auto mac_buf = owned_bytes<64>(); + if (1 != EVP_MAC_final(hmac, mac_buf.data(), &mac_size, mac_buf.size())) { throw crypto_error(); } - tag.resize(tag_size); - return tag; -} + auto tag = ct.subspan(pt.size(), tag_size); + std::copy(mac_buf.begin(), mac_buf.begin() + tag_size, tag.begin()); -using scoped_evp_cipher_ctx = - std::unique_ptr; + return ct.subspan(0, pt.size() + tag_size); +} -static void -ctr_crypt(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes out, - input_bytes in) +static output_bytes +open_ctr(EVP_CIPHER_CTX* ctx, + EVP_MAC_CTX* hmac, + CipherSuite suite, + input_bytes nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) { - if (out.size() != in.size()) { - throw buffer_too_small_error("CTR size mismatch"); + auto tag_size = cipher_overhead(suite); + if (ct.size() < tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + auto inner_ct_size = ct.size() - tag_size; + if (pt.size() < inner_ct_size) { + throw buffer_too_small_error("Plaintext buffer too small"); + } + + auto inner_ct = ct.subspan(0, inner_ct_size); + auto tag = ct.subspan(inner_ct_size, tag_size); + + // Verify HMAC tag + // Reset HMAC context (key is preserved from init) + if (1 != EVP_MAC_init(hmac, nullptr, 0, nullptr)) { + throw crypto_error(); + } + + // Build length block + auto len_block = owned_bytes<24>(); + auto len_view = output_bytes(len_block); + encode_uint(aad.size(), len_view.first(8)); + encode_uint(inner_ct.size(), len_view.first(16).last(8)); + encode_uint(tag_size, len_view.last(8)); + + if (1 != EVP_MAC_update(hmac, len_block.data(), len_block.size())) { + throw crypto_error(); + } + if (1 != EVP_MAC_update(hmac, nonce.data(), nonce.size())) { + throw crypto_error(); + } + if (1 != EVP_MAC_update(hmac, aad.data(), aad.size())) { + throw crypto_error(); + } + if (1 != EVP_MAC_update(hmac, inner_ct.data(), inner_ct.size())) { + throw crypto_error(); } - auto ctx = scoped_evp_cipher_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); - if (ctx.get() == nullptr) { + size_t mac_size = 0; + auto mac_buf = owned_bytes<64>(); + if (1 != EVP_MAC_final(hmac, mac_buf.data(), &mac_size, mac_buf.size())) { throw crypto_error(); } + if (CRYPTO_memcmp(mac_buf.data(), tag.data(), tag_size) != 0) { + throw authentication_error(); + } + + // Decrypt with AES-CTR + // Pad nonce to 16 bytes for AES-CTR auto padded_nonce = owned_bytes<16>(0); padded_nonce.append(nonce); padded_nonce.resize(16); - auto cipher = openssl_cipher(suite); + // Reset AES-CTR context with new nonce (key is preserved) if (1 != - EVP_EncryptInit(ctx.get(), cipher, key.data(), padded_nonce.data())) { + EVP_EncryptInit_ex(ctx, nullptr, nullptr, nullptr, padded_nonce.data())) { throw crypto_error(); } int outlen = 0; - auto in_size_int = static_cast(in.size()); + auto inner_ct_size_int = static_cast(inner_ct_size); if (1 != EVP_EncryptUpdate( - ctx.get(), out.data(), &outlen, in.data(), in_size_int)) { + ctx, pt.data(), &outlen, inner_ct.data(), inner_ct_size_int)) { throw crypto_error(); } - if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + if (1 != EVP_EncryptFinal(ctx, nullptr, &outlen)) { throw crypto_error(); } -} - -static output_bytes -seal_ctr(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes ct, - input_bytes aad, - input_bytes pt) -{ - auto tag_size = cipher_overhead(suite); - if (ct.size() < pt.size() + tag_size) { - throw buffer_too_small_error("Ciphertext buffer too small"); - } - - // Split the key into enc and auth subkeys - auto enc_key_size = cipher_enc_key_size(suite); - auto enc_key = key.first(enc_key_size); - auto auth_key = key.subspan(enc_key_size); - // Encrypt with AES-CM - auto inner_ct = ct.subspan(0, pt.size()); - ctr_crypt(suite, enc_key, nonce, inner_ct, pt); - - // Authenticate with truncated HMAC - auto mac = compute_tag(suite, auth_key, nonce, aad, inner_ct, tag_size); - auto tag = ct.subspan(pt.size(), tag_size); - std::copy(mac.begin(), mac.begin() + tag_size, tag.begin()); - - return ct.subspan(0, pt.size() + tag_size); + return pt.subspan(0, inner_ct_size); } +/// +/// AEAD Algorithms - GCM +/// + static output_bytes -seal_aead(CipherSuite suite, - input_bytes key, +seal_aead(EVP_CIPHER_CTX* ctx, + CipherSuite suite, input_bytes nonce, output_bytes ct, input_bytes aad, @@ -283,34 +425,26 @@ seal_aead(CipherSuite suite, throw buffer_too_small_error("Ciphertext buffer too small"); } - auto ctx = scoped_evp_cipher_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); - if (ctx.get() == nullptr) { - throw crypto_error(); - } - - auto cipher = openssl_cipher(suite); - if (1 != EVP_EncryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + // Reset context and set new nonce (key is preserved) + if (1 != EVP_EncryptInit_ex(ctx, nullptr, nullptr, nullptr, nonce.data())) { throw crypto_error(); } int outlen = 0; auto aad_size_int = static_cast(aad.size()); if (aad.size() > 0) { - if (1 != EVP_EncryptUpdate( - ctx.get(), nullptr, &outlen, aad.data(), aad_size_int)) { + if (1 != + EVP_EncryptUpdate(ctx, nullptr, &outlen, aad.data(), aad_size_int)) { throw crypto_error(); } } auto pt_size_int = static_cast(pt.size()); - if (1 != EVP_EncryptUpdate( - ctx.get(), ct.data(), &outlen, pt.data(), pt_size_int)) { + if (1 != EVP_EncryptUpdate(ctx, ct.data(), &outlen, pt.data(), pt_size_int)) { throw crypto_error(); } - // Providing nullptr as an argument is safe here because this - // function never writes with GCM; it only computes the tag - if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + if (1 != EVP_EncryptFinal(ctx, nullptr, &outlen)) { throw crypto_error(); } @@ -318,75 +452,16 @@ seal_aead(CipherSuite suite, auto tag_ptr = const_cast(static_cast(tag.data())); auto tag_size_downcast = static_cast(tag.size()); if (1 != EVP_CIPHER_CTX_ctrl( - ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size_downcast, tag_ptr)) { + ctx, EVP_CTRL_GCM_GET_TAG, tag_size_downcast, tag_ptr)) { throw crypto_error(); } return ct.subspan(0, pt.size() + tag_size); } -output_bytes -seal(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes ct, - input_bytes aad, - input_bytes pt) -{ - switch (suite) { - case CipherSuite::AES_128_CTR_HMAC_SHA256_80: - case CipherSuite::AES_128_CTR_HMAC_SHA256_64: - case CipherSuite::AES_128_CTR_HMAC_SHA256_32: { - return seal_ctr(suite, key, nonce, ct, aad, pt); - } - - case CipherSuite::AES_GCM_128_SHA256: - case CipherSuite::AES_GCM_256_SHA512: { - return seal_aead(suite, key, nonce, ct, aad, pt); - } - } - - throw unsupported_ciphersuite_error(); -} - static output_bytes -open_ctr(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes pt, - input_bytes aad, - input_bytes ct) -{ - auto tag_size = cipher_overhead(suite); - if (ct.size() < tag_size) { - throw buffer_too_small_error("Ciphertext buffer too small"); - } - - auto inner_ct_size = ct.size() - tag_size; - auto inner_ct = ct.subspan(0, inner_ct_size); - auto tag = ct.subspan(inner_ct_size, tag_size); - - // Split the key into enc and auth subkeys - auto enc_key_size = cipher_enc_key_size(suite); - auto enc_key = key.first(enc_key_size); - auto auth_key = key.subspan(enc_key_size); - - // Authenticate with truncated HMAC - auto mac = compute_tag(suite, auth_key, nonce, aad, inner_ct, tag_size); - if (CRYPTO_memcmp(mac.data(), tag.data(), tag.size()) != 0) { - throw authentication_error(); - } - - // Decrypt with AES-CTR - const auto pt_out = pt.first(inner_ct_size); - ctr_crypt(suite, enc_key, nonce, pt_out, ct.first(inner_ct_size)); - - return pt_out; -} - -static output_bytes -open_aead(CipherSuite suite, - input_bytes key, +open_aead(EVP_CIPHER_CTX* ctx, + CipherSuite suite, input_bytes nonce, output_bytes pt, input_bytes aad, @@ -402,13 +477,8 @@ open_aead(CipherSuite suite, throw buffer_too_small_error("Plaintext buffer too small"); } - auto ctx = scoped_evp_cipher_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); - if (ctx.get() == nullptr) { - throw crypto_error(); - } - - auto cipher = openssl_cipher(suite); - if (1 != EVP_DecryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + // Reset context and set new nonce (key is preserved) + if (1 != EVP_DecryptInit_ex(ctx, nullptr, nullptr, nullptr, nonce.data())) { throw crypto_error(); } @@ -416,34 +486,88 @@ open_aead(CipherSuite suite, auto tag_ptr = const_cast(static_cast(tag.data())); auto tag_size_downcast = static_cast(tag.size()); if (1 != EVP_CIPHER_CTX_ctrl( - ctx.get(), EVP_CTRL_GCM_SET_TAG, tag_size_downcast, tag_ptr)) { + ctx, EVP_CTRL_GCM_SET_TAG, tag_size_downcast, tag_ptr)) { throw crypto_error(); } int out_size; auto aad_size_int = static_cast(aad.size()); if (aad.size() > 0) { - if (1 != EVP_DecryptUpdate( - ctx.get(), nullptr, &out_size, aad.data(), aad_size_int)) { + if (1 != + EVP_DecryptUpdate(ctx, nullptr, &out_size, aad.data(), aad_size_int)) { throw crypto_error(); } } auto inner_ct_size_int = static_cast(inner_ct_size); if (1 != EVP_DecryptUpdate( - ctx.get(), pt.data(), &out_size, ct.data(), inner_ct_size_int)) { + ctx, pt.data(), &out_size, ct.data(), inner_ct_size_int)) { throw crypto_error(); } - // Providing nullptr as an argument is safe here because this - // function never writes with GCM; it only verifies the tag - if (1 != EVP_DecryptFinal(ctx.get(), nullptr, &out_size)) { + if (1 != EVP_DecryptFinal(ctx, nullptr, &out_size)) { throw authentication_error(); } return pt.subspan(0, inner_ct_size); } +/// +/// CipherState seal/open methods +/// + +output_bytes +CipherState::seal(input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + if (is_ctr_hmac_suite(suite)) { + return seal_ctr(cipher_handle->ctx.get(), + hmac_handle->ctx.get(), + suite, + nonce, + ct, + aad, + pt); + } + return seal_aead(cipher_handle->ctx.get(), suite, nonce, ct, aad, pt); +} + +output_bytes +CipherState::open(input_bytes nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + if (is_ctr_hmac_suite(suite)) { + return open_ctr(cipher_handle->ctx.get(), + hmac_handle->ctx.get(), + suite, + nonce, + pt, + aad, + ct); + } + return open_aead(cipher_handle->ctx.get(), suite, nonce, pt, aad, ct); +} + +/// +/// Stateless seal/open (used by test vectors) +/// + +output_bytes +seal(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + auto state = CipherState::create_seal(suite, key); + return state.seal(nonce, ct, aad, pt); +} + output_bytes open(CipherSuite suite, input_bytes key, @@ -452,20 +576,84 @@ open(CipherSuite suite, input_bytes aad, input_bytes ct) { - switch (suite) { - case CipherSuite::AES_128_CTR_HMAC_SHA256_80: - case CipherSuite::AES_128_CTR_HMAC_SHA256_64: - case CipherSuite::AES_128_CTR_HMAC_SHA256_32: { - return open_ctr(suite, key, nonce, pt, aad, ct); - } + auto state = CipherState::create_open(suite, key); + return state.open(nonce, pt, aad, ct); +} - case CipherSuite::AES_GCM_128_SHA256: - case CipherSuite::AES_GCM_256_SHA512: { - return open_aead(suite, key, nonce, pt, aad, ct); - } +/// +/// HKDF +/// + +using scoped_evp_kdf = std::unique_ptr; +using scoped_evp_kdf_ctx = + std::unique_ptr; + +owned_bytes +hkdf_extract(CipherSuite suite, input_bytes salt, input_bytes ikm) +{ + auto mode = EVP_KDF_HKDF_MODE_EXTRACT_ONLY; + auto digest_name = openssl_digest_name(suite); + auto* salt_ptr = + const_cast(reinterpret_cast(salt.data())); + auto* ikm_ptr = const_cast(reinterpret_cast(ikm.data())); + + const auto params = std::array{ + OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &mode), + OSSL_PARAM_construct_utf8_string( + OSSL_KDF_PARAM_DIGEST, digest_name.data(), digest_name.size()), + OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, ikm_ptr, ikm.size()), + OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_SALT, salt_ptr, salt.size()), + OSSL_PARAM_construct_end(), + }; + + const auto kdf = + scoped_evp_kdf(EVP_KDF_fetch(NULL, "HKDF", NULL), EVP_KDF_free); + const auto ctx = + scoped_evp_kdf_ctx(EVP_KDF_CTX_new(kdf.get()), EVP_KDF_CTX_free); + if (1 != EVP_KDF_CTX_set_params(ctx.get(), params.data())) { + throw crypto_error(); + } + + const auto digest_size = EVP_KDF_CTX_get_kdf_size(ctx.get()); + auto out = owned_bytes(digest_size); + if (1 != EVP_KDF_derive(ctx.get(), out.data(), out.size(), nullptr)) { + throw crypto_error(); + } + + return out; +} + +owned_bytes +hkdf_expand(CipherSuite suite, input_bytes prk, input_bytes info, size_t size) +{ + auto mode = EVP_KDF_HKDF_MODE_EXPAND_ONLY; + auto digest_name = openssl_digest_name(suite); + auto* prk_ptr = const_cast(reinterpret_cast(prk.data())); + auto* info_ptr = + const_cast(reinterpret_cast(info.data())); + + const auto params = std::array{ + OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &mode), + OSSL_PARAM_construct_utf8_string( + OSSL_KDF_PARAM_DIGEST, digest_name.data(), digest_name.size()), + OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, prk_ptr, prk.size()), + OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_INFO, info_ptr, info.size()), + OSSL_PARAM_construct_end(), + }; + + const auto kdf = + scoped_evp_kdf(EVP_KDF_fetch(NULL, "HKDF", NULL), EVP_KDF_free); + const auto ctx = + scoped_evp_kdf_ctx(EVP_KDF_CTX_new(kdf.get()), EVP_KDF_CTX_free); + + auto out = owned_bytes(size); + if (1 != EVP_KDF_derive(ctx.get(), out.data(), out.size(), params.data())) { + throw crypto_error(); } - throw unsupported_ciphersuite_error(); + return out; } } // namespace SFRAME_NAMESPACE diff --git a/src/sframe.cpp b/src/sframe.cpp index c4510d4..12acc08 100644 --- a/src/sframe.cpp +++ b/src/sframe.cpp @@ -23,6 +23,25 @@ authentication_error::authentication_error() /// KeyRecord /// +KeyRecord::KeyRecord(owned_bytes key_in, + owned_bytes salt_in, + KeyUsage usage_in, + Counter counter_in, + CipherState cipher_in) + : key(std::move(key_in)) + , salt(std::move(salt_in)) + , usage(usage_in) + , counter(counter_in) + , cipher(std::move(cipher_in)) +{ +} + +KeyRecord::~KeyRecord() = default; + +KeyRecord::KeyRecord(KeyRecord&&) noexcept = default; +KeyRecord& +KeyRecord::operator=(KeyRecord&&) noexcept = default; + static auto from_ascii(const char* str, size_t len) { @@ -79,7 +98,13 @@ KeyRecord::from_base_key(CipherSuite suite, auto key = hkdf_expand(suite, secret, key_label, key_size); auto salt = hkdf_expand(suite, secret, salt_label, nonce_size); - return KeyRecord{ key, salt, usage, 0 }; + // Create pre-warmed cipher state + auto cipher_state = (usage == KeyUsage::protect) + ? CipherState::create_seal(suite, key) + : CipherState::create_open(suite, key); + + return KeyRecord( + std::move(key), std::move(salt), usage, 0, std::move(cipher_state)); } /// @@ -171,11 +196,12 @@ Context::protect_inner(const Header& header, throw buffer_too_small_error("Ciphertext too small for cipher overhead"); } - const auto& key_and_salt = keys.at(header.key_id); + auto& key_and_salt = keys.at(header.key_id); const auto aad = form_aad(header, metadata); const auto nonce = form_nonce(header.counter, key_and_salt.salt); - return seal(suite, key_and_salt.key, nonce, ciphertext, aad, plaintext); + + return key_and_salt.cipher.seal(nonce, ciphertext, aad, plaintext); } output_bytes @@ -192,11 +218,12 @@ Context::unprotect_inner(const Header& header, throw buffer_too_small_error("Plaintext too small for decrypted value"); } - const auto& key_and_salt = keys.at(header.key_id); + auto& key_and_salt = keys.at(header.key_id); const auto aad = form_aad(header, metadata); const auto nonce = form_nonce(header.counter, key_and_salt.salt); - return open(suite, key_and_salt.key, nonce, plaintext, aad, ciphertext); + + return key_and_salt.cipher.open(nonce, plaintext, aad, ciphertext); } /// diff --git a/test/vectors.cpp b/test/vectors.cpp index a3e1c45..470ab2d 100644 --- a/test/vectors.cpp +++ b/test/vectors.cpp @@ -48,7 +48,7 @@ from_json(const json& j, HexBytes& b) } } -static void +[[maybe_unused]] static void to_json(json& /* j */, const HexBytes& /* p */) { // Included just so that macros work