blob: ab135f12fae928d81a9512b78f3616b849851ce6 [file] [edit]
#include <sframe/sframe.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <array>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <tuple>
namespace sframe {
std::ostream&
operator<<(std::ostream& str, const input_bytes data)
{
str.flags(std::ios::hex);
for (const auto& byte : data) {
str << std::setw(2) << std::setfill('0') << int(byte);
}
return str;
}
static auto evp_cipher_ctx_free = [](EVP_CIPHER_CTX* ptr) {
EVP_CIPHER_CTX_free(ptr);
};
static auto hmac_ctx_free = [](HMAC_CTX* ptr) {
HMAC_CTX_free(ptr);
};
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)>;
static std::runtime_error
openssl_error()
{
auto code = ERR_get_error();
return std::runtime_error(ERR_error_string(code, nullptr));
}
static const EVP_CIPHER*
openssl_cipher(CipherSuite suite)
{
switch (suite) {
case CipherSuite::AES_CM_128_HMAC_SHA256_4:
case CipherSuite::AES_CM_128_HMAC_SHA256_8:
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 std::runtime_error("Unsupported ciphersuite");
}
}
static size_t
openssl_key_size(CipherSuite suite)
{
switch (suite) {
case CipherSuite::AES_CM_128_HMAC_SHA256_4:
case CipherSuite::AES_CM_128_HMAC_SHA256_8:
case CipherSuite::AES_GCM_128_SHA256:
return 16;
case CipherSuite::AES_GCM_256_SHA512:
return 32;
default:
throw std::runtime_error("Unsupported ciphersuite");
}
}
static size_t
openssl_tag_size(CipherSuite suite)
{
switch (suite) {
case CipherSuite::AES_CM_128_HMAC_SHA256_4:
return 4;
case CipherSuite::AES_CM_128_HMAC_SHA256_8:
return 8;
case CipherSuite::AES_GCM_128_SHA256:
case CipherSuite::AES_GCM_256_SHA512:
return 16;
default:
throw std::runtime_error("Unsupported ciphersuite");
}
}
static size_t
openssl_nonce_size(CipherSuite suite)
{
switch (suite) {
case CipherSuite::AES_CM_128_HMAC_SHA256_4:
case CipherSuite::AES_CM_128_HMAC_SHA256_8:
case CipherSuite::AES_GCM_128_SHA256:
case CipherSuite::AES_GCM_256_SHA512:
return 12;
default:
throw std::runtime_error("Unsupported ciphersuite");
}
}
static const EVP_MD*
openssl_digest_type(CipherSuite suite)
{
switch (suite) {
case CipherSuite::AES_CM_128_HMAC_SHA256_4:
case CipherSuite::AES_CM_128_HMAC_SHA256_8:
case CipherSuite::AES_GCM_128_SHA256:
return EVP_sha256();
case CipherSuite::AES_GCM_256_SHA512:
return EVP_sha512();
default:
throw std::runtime_error("Unsupported ciphersuite");
}
}
static size_t
openssl_digest_size(CipherSuite suite)
{
return EVP_MD_size(openssl_digest_type(suite));
}
struct HMAC {
HMAC(CipherSuite suite, input_bytes key)
: ctx(HMAC_CTX_new(), hmac_ctx_free)
{
auto type = openssl_digest_type(suite);
if (1 != HMAC_Init_ex(ctx.get(), key.data(), key.size(), type, nullptr)) {
throw openssl_error();
}
}
HMAC& write(input_bytes data) {
if (1 != HMAC_Update(ctx.get(), data.data(), data.size())) {
throw openssl_error();
}
return *this;
}
input_bytes digest() {
unsigned int size = 0;
if (1 != HMAC_Final(ctx.get(), md.data(), &size)) {
throw openssl_error();
}
return input_bytes(md.data(), size);
}
scoped_hmac_ctx ctx;
std::array<uint8_t, EVP_MAX_MD_SIZE> md;
};
static bytes
hkdf_extract(CipherSuite suite, const bytes& salt, const bytes& ikm)
{
auto mac = HMAC(suite, salt).write(ikm).digest();
return bytes(mac.begin(), mac.end());
}
// For simplicity, we enforce that size <= Hash.length, so that
// HKDF-Expand(Secret, Label) reduces to:
//
// HMAC(Secret, Label || 0x01)
static bytes
hkdf_expand(CipherSuite suite,
const bytes& secret,
const bytes& info,
size_t size)
{
// Ensure that we need only one hash invocation
if (size > openssl_digest_size(suite)) {
throw std::runtime_error("Size too big for hkdf_expand");
}
auto label = info;
label.push_back(0x01);
auto mac = HMAC(suite, secret).write(label).digest();
return bytes(mac.begin(), mac.begin() + size);
}
void
ctr_crypt(CipherSuite suite,
input_bytes key,
input_bytes nonce,
output_bytes out,
input_bytes in)
{
if (out.size() != in.size()) {
std::cout << out.size() << " <> " << in.size() << std::endl;
throw std::runtime_error("CTR size mismatch");
}
auto ctx = scoped_evp_ctx(EVP_CIPHER_CTX_new(), evp_cipher_ctx_free);
if (ctx.get() == nullptr) {
throw openssl_error();
}
static auto padded_nonce =
std::array<uint8_t, 16>{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
std::copy(nonce.begin(), nonce.end(), padded_nonce.begin());
auto cipher = openssl_cipher(suite);
if (1 !=
EVP_EncryptInit(ctx.get(), cipher, key.data(), padded_nonce.data())) {
throw openssl_error();
}
int outlen = 0;
auto in_size_int = static_cast<int>(in.size());
if (1 != EVP_EncryptUpdate(
ctx.get(), out.data(), &outlen, in.data(), in_size_int)) {
throw openssl_error();
}
if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) {
throw openssl_error();
}
}
static output_bytes
seal_ctr(CipherSuite suite,
const bytes& key,
const bytes& nonce,
output_bytes ct,
input_bytes aad,
input_bytes pt)
{
auto tag_size = openssl_tag_size(suite);
if (ct.size() < pt.size() + tag_size) {
throw std::runtime_error("Ciphertext buffer too small");
}
// Split the key into enc and auth subkeys
auto key_span = input_bytes(key);
auto enc_key_size = openssl_key_size(suite);
auto enc_key = key_span.subspan(0, enc_key_size);
auto auth_key = key_span.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 = HMAC(suite, auth_key).write(aad).write(inner_ct).digest();
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);
}
static output_bytes
seal_aead(CipherSuite suite,
const bytes& key,
const bytes& nonce,
output_bytes ct,
input_bytes aad,
input_bytes pt)
{
auto tag_size = openssl_tag_size(suite);
if (ct.size() < pt.size() + tag_size) {
throw std::runtime_error("Ciphertext buffer too small");
}
auto ctx = scoped_evp_ctx(EVP_CIPHER_CTX_new(), evp_cipher_ctx_free);
if (ctx.get() == nullptr) {
throw openssl_error();
}
auto cipher = openssl_cipher(suite);
if (1 != EVP_EncryptInit(ctx.get(), cipher, key.data(), nonce.data())) {
throw openssl_error();
}
int outlen = 0;
auto aad_size_int = static_cast<int>(aad.size());
if (aad.size() > 0) {
if (1 != EVP_EncryptUpdate(
ctx.get(), nullptr, &outlen, aad.data(), aad_size_int)) {
throw openssl_error();
}
}
auto pt_size_int = static_cast<int>(pt.size());
if (1 !=
EVP_EncryptUpdate(
ctx.get(), ct.data(), &outlen, pt.data(), pt_size_int)) {
throw openssl_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)) {
throw openssl_error();
}
auto tag = ct.subspan(pt.size(), tag_size);
auto tag_ptr = const_cast<void*>(static_cast<const void*>(tag.data()));
if (1 !=
EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_GET_TAG, tag.size(), tag_ptr)) {
throw openssl_error();
}
return ct.subspan(0, pt.size() + tag_size);
}
static output_bytes
seal(CipherSuite suite,
const bytes& key,
const bytes& nonce,
output_bytes ct,
input_bytes aad,
input_bytes pt)
{
switch (suite) {
case CipherSuite::AES_CM_128_HMAC_SHA256_4:
case CipherSuite::AES_CM_128_HMAC_SHA256_8: {
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 std::runtime_error("Unknown algorithm");
}
static output_bytes
open_ctr(CipherSuite suite,
const bytes& key,
const bytes& nonce,
output_bytes pt,
input_bytes aad,
input_bytes ct)
{
auto tag_size = openssl_tag_size(suite);
if (ct.size() < tag_size) {
throw std::runtime_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 key_span = input_bytes(key);
auto enc_key_size = openssl_key_size(suite);
auto enc_key = key_span.subspan(0, enc_key_size);
auto auth_key = key_span.subspan(enc_key_size);
// Authenticate with truncated HMAC
auto mac = HMAC(suite, auth_key).write(aad).write(inner_ct).digest();
if (CRYPTO_memcmp(mac.data(), tag.data(), tag.size()) != 0) {
throw std::runtime_error("AEAD authentication failure");
}
// Decrypt with AES-CM
ctr_crypt(suite, enc_key, nonce, pt, ct.subspan(0, inner_ct_size));
return pt.subspan(0, inner_ct_size);
}
static output_bytes
open_aead(CipherSuite suite,
const bytes& key,
const bytes& nonce,
output_bytes pt,
input_bytes aad,
input_bytes ct)
{
auto tag_size = openssl_tag_size(suite);
if (ct.size() < tag_size) {
throw std::runtime_error("Ciphertext buffer too small");
}
auto inner_ct_size = ct.size() - tag_size;
if (pt.size() < inner_ct_size) {
throw std::runtime_error("Plaintext buffer too small");
}
auto ctx = scoped_evp_ctx(EVP_CIPHER_CTX_new(), evp_cipher_ctx_free);
if (ctx.get() == nullptr) {
throw openssl_error();
}
auto cipher = openssl_cipher(suite);
if (1 != EVP_DecryptInit(ctx.get(), cipher, key.data(), nonce.data())) {
throw openssl_error();
}
auto tag = ct.subspan(inner_ct_size, tag_size);
auto tag_ptr = const_cast<void*>(static_cast<const void*>(tag.data()));
if (1 != EVP_CIPHER_CTX_ctrl(
ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), tag_ptr)) {
throw openssl_error();
}
int out_size;
auto aad_size_int = static_cast<int>(aad.size());
if (aad.size() > 0) {
if (1 != EVP_DecryptUpdate(
ctx.get(), nullptr, &out_size, aad.data(), aad_size_int)) {
throw openssl_error();
}
}
auto inner_ct_size_int = static_cast<int>(inner_ct_size);
if (1 != EVP_DecryptUpdate(
ctx.get(), pt.data(), &out_size, ct.data(), inner_ct_size_int)) {
throw openssl_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)) {
throw std::runtime_error("AEAD authentication failure");
}
return pt.subspan(0, inner_ct_size);
}
static output_bytes
open(CipherSuite suite,
const bytes& key,
const bytes& nonce,
output_bytes pt,
input_bytes aad,
input_bytes ct)
{
switch (suite) {
case CipherSuite::AES_CM_128_HMAC_SHA256_4:
case CipherSuite::AES_CM_128_HMAC_SHA256_8: {
return open_ctr(suite, key, 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);
}
}
throw std::runtime_error("Unknown algorithm");
}
Context::Context(CipherSuite suite_in)
: suite(suite_in)
{}
static const bytes sframe_label{
0x53, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x31, 0x30 // "SFrame10"
};
static const bytes sframe_key_label{ 0x6b, 0x65, 0x79 }; // "key"
static const bytes sframe_salt_label{ 0x73, 0x61, 0x6c, 0x74 }; // "salt"
static const bytes sframe_ctr_label{
// "SFrame10 AES CM AEAD"
0x53, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x31, 0x30, 0x20, 0x41,
0x45, 0x53, 0x20, 0x43, 0x4d, 0x20, 0x41, 0x45, 0x41, 0x44,
};
static const bytes sframe_enc_label{ 0x65, 0x6e, 0x63 }; // "enc"
static const bytes sframe_auth_label{ 0x61, 0x75, 0x74, 0x68 }; // "auth"
void
Context::add_key(KeyID key_id, const bytes& base_key)
{
auto key_size = openssl_key_size(suite);
auto nonce_size = openssl_nonce_size(suite);
auto hash_size = openssl_digest_size(suite);
auto secret = hkdf_extract(suite, sframe_label, base_key);
auto key = hkdf_expand(suite, secret, sframe_key_label, key_size);
auto salt = hkdf_expand(suite, secret, sframe_salt_label, nonce_size);
// If using CTR+HMAC, set key = enc_key || auth_key
if (suite == CipherSuite::AES_CM_128_HMAC_SHA256_4 ||
suite == CipherSuite::AES_CM_128_HMAC_SHA256_8) {
secret = hkdf_extract(suite, sframe_ctr_label, key);
auto main_key = key;
auto enc_key = hkdf_expand(suite, secret, sframe_enc_label, key_size);
auto auth_key = hkdf_expand(suite, secret, sframe_auth_label, hash_size);
key = enc_key;
key.insert(key.end(), auth_key.begin(), auth_key.end());
}
state.insert_or_assign(key_id,
KeyState{ std::move(key), std::move(salt), 0 });
}
static size_t
encode_uint(uint64_t val, output_bytes start)
{
size_t size = 1;
while (val >> (8 * size) > 0) {
size += 1;
}
for (size_t i = 0; i < size; i++) {
start[size - i - 1] = uint8_t(val >> (8 * i));
}
return size;
}
static uint64_t
decode_uint(input_bytes data)
{
uint64_t val = 0;
for (size_t i = 0; i < data.size(); i++) {
val = (val << 8) + static_cast<uint64_t>(data[i]);
}
return val;
}
static bytes
form_nonce(CipherSuite suite, Counter ctr, const bytes& salt)
{
auto nonce_size = openssl_nonce_size(suite);
auto nonce = bytes(nonce_size);
for (size_t i = 0; i < sizeof(ctr); i++) {
nonce[nonce_size - i - 1] = uint8_t(ctr >> (8 * i));
}
for (size_t i = 0; i < nonce.size(); i++) {
nonce[i] ^= salt[i];
}
return nonce;
}
static constexpr size_t min_header_size = 1;
static constexpr size_t max_header_size = 1 + 8 + 8;
static size_t
encode_header(KeyID kid, Counter ctr, output_bytes data)
{
size_t kid_size = 0;
if (kid > 0x07) {
kid_size = encode_uint(kid, data.subspan(1));
}
size_t ctr_size = encode_uint(ctr, data.subspan(1 + kid_size));
if ((ctr_size > 0x07) || (kid_size > 0x07)) {
throw std::runtime_error("Header overflow");
}
data[0] = uint8_t(ctr_size << 4);
if (kid <= 0x07) {
data[0] |= kid;
} else {
data[0] |= 0x08 | kid_size;
}
return 1 + kid_size + ctr_size;
}
static std::tuple<KeyID, Counter, input_bytes>
decode_header(input_bytes data)
{
if (data.size() < min_header_size) {
throw std::runtime_error("Ciphertext too small to decode header");
}
auto cfg = data[0];
auto ctr_size = size_t((cfg >> 4) & 0x07);
auto kid_long = (cfg & 0x08) > 0;
auto kid_size = size_t(cfg & 0x07);
auto kid = KeyID(kid_size);
if (kid_long) {
if (data.size() < 1 + kid_size) {
throw std::runtime_error("Ciphertext too small to decode KID");
}
kid = KeyID(decode_uint(data.subspan(1, kid_size)));
} else {
kid_size = 0;
}
if (data.size() < 1 + kid_size + ctr_size) {
throw std::runtime_error("Ciphertext too small to decode CTR");
}
auto ctr = Counter(decode_uint(data.subspan(1 + kid_size, ctr_size)));
return std::make_tuple(kid, ctr, data.subspan(0, 1 + kid_size + ctr_size));
}
output_bytes
Context::protect(KeyID key_id, output_bytes ciphertext, input_bytes plaintext)
{
auto it = state.find(key_id);
if (it == state.end()) {
throw std::runtime_error("Unknown key");
}
auto& st = it->second;
const auto ctr = st.counter;
st.counter += 1;
if (ciphertext.size() < max_header_size) {
throw std::runtime_error("Ciphertext to small to encod header");
}
auto hdr_size = encode_header(key_id, ctr, ciphertext);
auto header = ciphertext.subspan(0, hdr_size);
auto inner_ciphertext = ciphertext.subspan(hdr_size);
const auto nonce = form_nonce(suite, ctr, st.salt);
auto final_ciphertext = seal(suite, st.key, nonce, inner_ciphertext, header, plaintext);
return ciphertext.subspan(0, hdr_size + final_ciphertext.size());
}
output_bytes
Context::unprotect(output_bytes plaintext, input_bytes ciphertext)
{
auto [kid, ctr, header] = decode_header(ciphertext);
auto inner_ciphertext = ciphertext.subspan(header.size());
auto it = state.find(kid);
if (it == state.end()) {
throw std::runtime_error("Unknown key");
}
const auto& st = it->second;
const auto nonce = form_nonce(suite, ctr, st.salt);
return open(suite, st.key, nonce, plaintext, header, inner_ciphertext);
}
} // namespace sframe