blob: 4a9cb0b0a6af3dc6c9d04c1169d02762c4695a14 [file] [edit]
#if defined(OPENSSL_1_1)
#include "crypto.h"
#include "header.h"
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
namespace SFRAME_NAMESPACE {
///
/// Scoped pointers for OpenSSL objects
///
using scoped_evp_ctx =
std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)>;
using scoped_hmac_ctx = std::unique_ptr<HMAC_CTX, decltype(&HMAC_CTX_free)>;
///
/// Convert between native identifiers / errors and OpenSSL ones
///
crypto_error::crypto_error()
: std::runtime_error(ERR_error_string(ERR_get_error(), nullptr))
{
}
static const EVP_MD*
openssl_digest_type(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:
case CipherSuite::AES_GCM_128_SHA256:
return EVP_sha256();
case CipherSuite::AES_GCM_256_SHA512:
return EVP_sha512();
default:
throw unsupported_ciphersuite_error();
}
}
static const EVP_CIPHER*
openssl_cipher(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 EVP_aes_128_ctr();
case CipherSuite::AES_GCM_128_SHA256:
return EVP_aes_128_gcm();
case CipherSuite::AES_GCM_256_SHA512:
return EVP_aes_256_gcm();
default:
throw unsupported_ciphersuite_error();
}
}
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;
}
}
///
/// CipherHandle and HmacHandle definitions
///
struct CipherHandle
{
scoped_evp_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<CipherHandle>();
cipher_h->ctx.reset(EVP_CIPHER_CTX_new());
if (cipher_h->ctx == nullptr) {
throw crypto_error();
}
auto cipher = openssl_cipher(suite);
std::unique_ptr<HmacHandle> 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<HmacHandle>();
hmac_h->ctx.reset(HMAC_CTX_new());
if (hmac_h->ctx == nullptr) {
throw crypto_error();
}
const auto* md = openssl_digest_type(suite);
auto key_size = static_cast<int>(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();
}
}
return CipherState(cipher_h.release(), hmac_h.release(), suite);
}
CipherState
CipherState::create_open(CipherSuite suite, input_bytes key)
{
auto cipher_h = std::make_unique<CipherHandle>();
cipher_h->ctx.reset(EVP_CIPHER_CTX_new());
if (cipher_h->ctx == nullptr) {
throw crypto_error();
}
auto cipher = openssl_cipher(suite);
std::unique_ptr<HmacHandle> 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<HmacHandle>();
hmac_h->ctx.reset(HMAC_CTX_new());
if (hmac_h->ctx == nullptr) {
throw crypto_error();
}
const auto* md = openssl_digest_type(suite);
auto key_size = static_cast<int>(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);
}
///
/// AEAD Algorithms - CTR+HMAC
///
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 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);
// 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();
}
// Encrypt with AES-CTR
auto inner_ct = ct.subspan(0, pt.size());
int outlen = 0;
auto pt_size_int = static_cast<int>(pt.size());
if (1 != EVP_EncryptUpdate(
ctx, inner_ct.data(), &outlen, pt.data(), pt_size_int)) {
throw crypto_error();
}
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(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();
}
auto tag = ct.subspan(pt.size(), tag_size);
std::copy(mac_buf.begin(), mac_buf.begin() + tag_size, tag.begin());
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(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);
// 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();
}
int outlen = 0;
auto inner_ct_size_int = static_cast<int>(inner_ct_size);
if (1 != EVP_EncryptUpdate(
ctx, pt.data(), &outlen, inner_ct.data(), inner_ct_size_int)) {
throw crypto_error();
}
if (1 != EVP_EncryptFinal(ctx, nullptr, &outlen)) {
throw crypto_error();
}
return pt.subspan(0, inner_ct_size);
}
///
/// AEAD Algorithms - GCM
///
static output_bytes
seal_aead(EVP_CIPHER_CTX* ctx,
CipherSuite suite,
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");
}
// 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<int>(aad.size());
if (aad.size() > 0) {
if (1 !=
EVP_EncryptUpdate(ctx, nullptr, &outlen, aad.data(), aad_size_int)) {
throw crypto_error();
}
}
auto pt_size_int = static_cast<int>(pt.size());
if (1 != EVP_EncryptUpdate(ctx, ct.data(), &outlen, pt.data(), pt_size_int)) {
throw crypto_error();
}
if (1 != EVP_EncryptFinal(ctx, nullptr, &outlen)) {
throw crypto_error();
}
auto tag = ct.subspan(pt.size(), tag_size);
auto tag_ptr = const_cast<void*>(static_cast<const void*>(tag.data()));
auto tag_size_downcast = static_cast<int>(tag.size());
if (1 != EVP_CIPHER_CTX_ctrl(
ctx, EVP_CTRL_GCM_GET_TAG, tag_size_downcast, tag_ptr)) {
throw crypto_error();
}
return ct.subspan(0, pt.size() + tag_size);
}
static output_bytes
open_aead(EVP_CIPHER_CTX* ctx,
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");
}
// Reset context and set new nonce (key is preserved)
if (1 != EVP_DecryptInit_ex(ctx, nullptr, nullptr, nullptr, nonce.data())) {
throw crypto_error();
}
auto tag = ct.subspan(inner_ct_size, tag_size);
auto tag_ptr = const_cast<void*>(static_cast<const void*>(tag.data()));
auto tag_size_downcast = static_cast<int>(tag.size());
if (1 != EVP_CIPHER_CTX_ctrl(
ctx, EVP_CTRL_GCM_SET_TAG, tag_size_downcast, tag_ptr)) {
throw crypto_error();
}
int out_size;
auto aad_size_int = static_cast<int>(aad.size());
if (aad.size() > 0) {
if (1 !=
EVP_DecryptUpdate(ctx, nullptr, &out_size, aad.data(), aad_size_int)) {
throw crypto_error();
}
}
auto inner_ct_size_int = static_cast<int>(inner_ct_size);
if (1 != EVP_DecryptUpdate(
ctx, pt.data(), &out_size, ct.data(), inner_ct_size_int)) {
throw crypto_error();
}
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,
input_bytes nonce,
output_bytes pt,
input_bytes aad,
input_bytes 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<int>(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);
}
// 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();
}
}
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<max_hkdf_expand_size>
hkdf_extract(CipherSuite suite, input_bytes salt, input_bytes ikm)
{
auto h = HMACForHKDF(suite, salt);
h.write(ikm);
auto out = owned_bytes<max_hkdf_expand_size>();
const auto md = h.digest(out);
out.resize(md.size());
return out;
}
owned_bytes<max_hkdf_extract_size>
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<max_hkdf_extract_size>(0);
auto block = owned_bytes<max_hkdf_extract_size>(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
#endif // defined(OPENSSL_1_1)