diff --git a/include/ncrypto.h b/include/ncrypto.h index d69b45b..6a84162 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -12,6 +12,10 @@ #include #include +#ifdef OPENSSL_IS_BORINGSSL +#include +#endif + #include #include #include @@ -21,6 +25,7 @@ #include #include #include +#include #include #if NCRYPTO_DEVELOPMENT_CHECKS @@ -257,6 +262,8 @@ class ECKeyPointer; class Dsa; class Rsa; class Ec; +class Aead; +class AeadCtxPointer; struct StackOfXASN1Deleter { void operator()(STACK_OF(ASN1_OBJECT) * p) const { @@ -311,7 +318,25 @@ DataPointer xofHashDigest(const Buffer& data, const EVP_MD* md, size_t length); -class Cipher final { +template +class ModeMixin { + public: + std::string_view getModeLabel() const; + + bool isGcmMode() const { return self().getMode() == EVP_CIPH_GCM_MODE; } + bool isWrapMode() const { return self().getMode() == EVP_CIPH_WRAP_MODE; } + bool isCtrMode() const { return self().getMode() == EVP_CIPH_CTR_MODE; } + bool isCcmMode() const { return self().getMode() == EVP_CIPH_CCM_MODE; } + bool isOcbMode() const { return self().getMode() == EVP_CIPH_OCB_MODE; } + bool isStreamMode() const { + return self().getMode() == EVP_CIPH_STREAM_CIPHER; + } + + private: + const T& self() const { return static_cast(*this); } +}; + +class Cipher final : public ModeMixin { public: static constexpr size_t MAX_KEY_LENGTH = EVP_MAX_KEY_LENGTH; static constexpr size_t MAX_IV_LENGTH = EVP_MAX_IV_LENGTH; @@ -344,15 +369,9 @@ class Cipher final { int getIvLength() const; int getKeyLength() const; int getBlockSize() const; - std::string_view getModeLabel() const; + const char* getName() const; - bool isGcmMode() const; - bool isWrapMode() const; - bool isCtrMode() const; - bool isCcmMode() const; - bool isOcbMode() const; - bool isStreamMode() const; bool isChaCha20Poly1305() const; bool isSupportedAuthenticatedMode() const; @@ -1734,6 +1753,137 @@ class KEM final { #endif // OPENSSL_VERSION_MAJOR >= 3 +// ============================================================================ +// AEAD (Authenticated Encryption with Associated Data) +// Note that the underlying EVP_AEAD interface is specific to BoringSSL. AEAD +// primitives are accessed through the Cipher class instead, if using OpenSSL. + +#ifdef OPENSSL_IS_BORINGSSL +class Aead final : public ModeMixin { + private: + // BoringSSL does not keep a list of AEADs, so we need to maintain our own. + struct AeadInfo { + std::string name; + int mode; + int nid = 0; // Note: BoringSSL only defines NIDs for some AEADs + }; + + public: + Aead() = default; + Aead(const AeadInfo* info, const EVP_AEAD* aead) : info_(info), aead_(aead) {} + Aead(const Aead&) = default; + Aead& operator=(const Aead&) = default; + NCRYPTO_DISALLOW_MOVE(Aead) + + inline const EVP_AEAD* get() const { return aead_; } + inline operator const EVP_AEAD*() const { return aead_; } + inline operator bool() const { return aead_ != nullptr; } + + int getMode() const; + int getNonceLength() const; + int getKeyLength() const; + int getBlockSize() const; + int getMaxOverhead() const; + int getMaxTagLength() const; + std::string_view getName() const; + + static const Aead FromName(std::string_view name); + + // TODO(npaun): BoringSSL does not define NIDs for all AEADs. + // This method is included only for implementing getCipherInfo and can't be + // used to construct an Aead instance. + int getNid() const; + // static const AEAD FromNid(int nid); + + static const Aead FromCtx(std::string_view name, const AeadCtxPointer& ctx); + + using AeadNameCallback = std::function; + + // Iterates the known ciphers if the underlying implementation + // is able to do so. + static void ForEach(AeadNameCallback callback); + + // Utilities to get various AEADs by type. + + static const Aead EMPTY; + static const Aead AES_128_GCM; + static const Aead AES_192_GCM; + static const Aead AES_256_GCM; + static const Aead CHACHA20_POLY1305; + static const Aead XCHACHA20_POLY1305; + static const Aead AES_128_CTR_HMAC_SHA256; + static const Aead AES_256_CTR_HMAC_SHA256; + static const Aead AES_128_GCM_SIV; + static const Aead AES_256_GCM_SIV; + static const Aead AES_128_GCM_RANDNONCE; + static const Aead AES_256_GCM_RANDNONCE; + static const Aead AES_128_CCM_BLUETOOTH; + static const Aead AES_128_CCM_BLUETOOTH_8; + static const Aead AES_128_CCM_MATTER; + static const Aead AES_128_EAX; + static const Aead AES_256_EAX; + + private: + const EVP_AEAD* aead_ = nullptr; + const AeadInfo* info_ = nullptr; + + using AeadConstructor = const EVP_AEAD* (*)(); + static const std::unordered_map aeadIndex; + static const Aead FromConstructor(AeadConstructor construct); +}; + +class AeadCtxPointer final { + public: + static AeadCtxPointer New( + const Aead& aead, + bool encrypt, + const unsigned char* key = nullptr, + size_t keyLen = 0, + size_t tagLen = EVP_AEAD_DEFAULT_TAG_LENGTH /* = 0 */); + + AeadCtxPointer() = default; + explicit AeadCtxPointer(EVP_AEAD_CTX* ctx); + AeadCtxPointer(AeadCtxPointer&& other) noexcept; + AeadCtxPointer& operator=(AeadCtxPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(AeadCtxPointer) + ~AeadCtxPointer(); + + inline bool operator==(std::nullptr_t) const noexcept { + return ctx_ == nullptr; + } + inline operator bool() const { return ctx_ != nullptr; } + inline EVP_AEAD_CTX* get() const { return ctx_.get(); } + inline operator EVP_AEAD_CTX*() const { return ctx_.get(); } + void reset(EVP_AEAD_CTX* ctx = nullptr); + EVP_AEAD_CTX* release(); + + bool init(const Aead& aead, + bool encrypt, + const unsigned char* key = nullptr, + size_t keyLen = 0, + size_t tagLen = EVP_AEAD_DEFAULT_TAG_LENGTH /* = 0 */); + + // TODO(npaun): BoringSSL does not define NIDs for all AEADs. + // Decide if we will even implement this method. + // int getNid() const; + + bool encrypt(const Buffer& in, + Buffer& out, + Buffer& tag, + const Buffer& nonce, + const Buffer& aad); + + bool decrypt(const Buffer& in, + Buffer& out, + const Buffer& tag, + const Buffer& nonce, + const Buffer& aad); + + private: + DeleteFnPtr ctx_; +}; +#endif + // ============================================================================ // Version metadata #define NCRYPTO_VERSION "0.0.1" diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 120bb0b..f482c53 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -8,6 +8,7 @@ #include #include #include +#include "openssl/cipher.h" #ifndef NCRYPTO_NO_KDF_H #include @@ -3226,6 +3227,36 @@ bool SSLCtxPointer::setCipherSuites(const char* ciphers) { return true; #endif } +// ============================================================================ + +template +std::string_view ModeMixin::getModeLabel() const { + switch (self().getMode()) { + case EVP_CIPH_CCM_MODE: + return "ccm"; + case EVP_CIPH_CFB_MODE: + return "cfb"; + case EVP_CIPH_CBC_MODE: + return "cbc"; + case EVP_CIPH_CTR_MODE: + return "ctr"; + case EVP_CIPH_ECB_MODE: + return "ecb"; + case EVP_CIPH_GCM_MODE: + return "gcm"; + case EVP_CIPH_OCB_MODE: + return "ocb"; + case EVP_CIPH_OFB_MODE: + return "ofb"; + case EVP_CIPH_WRAP_MODE: + return "wrap"; + case EVP_CIPH_XTS_MODE: + return "xts"; + case EVP_CIPH_STREAM_CIPHER: + return "stream"; + } + return "{unknown}"; +} // ============================================================================ @@ -3263,43 +3294,13 @@ const Cipher Cipher::AES_256_OCB = Cipher::FromNid(NID_aes_256_ocb); const Cipher Cipher::CHACHA20_POLY1305 = Cipher::FromNid(NID_chacha20_poly1305); -bool Cipher::isGcmMode() const { - if (!cipher_) return false; - return getMode() == EVP_CIPH_GCM_MODE; -} - -bool Cipher::isWrapMode() const { - if (!cipher_) return false; - return getMode() == EVP_CIPH_WRAP_MODE; -} - -bool Cipher::isCtrMode() const { - if (!cipher_) return false; - return getMode() == EVP_CIPH_CTR_MODE; -} - -bool Cipher::isCcmMode() const { - if (!cipher_) return false; - return getMode() == EVP_CIPH_CCM_MODE; -} - -bool Cipher::isOcbMode() const { - if (!cipher_) return false; - return getMode() == EVP_CIPH_OCB_MODE; -} - -bool Cipher::isStreamMode() const { - if (!cipher_) return false; - return getMode() == EVP_CIPH_STREAM_CIPHER; -} - bool Cipher::isChaCha20Poly1305() const { if (!cipher_) return false; return getNid() == NID_chacha20_poly1305; } int Cipher::getMode() const { - if (!cipher_) return 0; + if (!cipher_) return -1; return EVP_CIPHER_mode(cipher_); } @@ -3323,35 +3324,6 @@ int Cipher::getNid() const { return EVP_CIPHER_nid(cipher_); } -std::string_view Cipher::getModeLabel() const { - if (!cipher_) return {}; - switch (getMode()) { - case EVP_CIPH_CCM_MODE: - return "ccm"; - case EVP_CIPH_CFB_MODE: - return "cfb"; - case EVP_CIPH_CBC_MODE: - return "cbc"; - case EVP_CIPH_CTR_MODE: - return "ctr"; - case EVP_CIPH_ECB_MODE: - return "ecb"; - case EVP_CIPH_GCM_MODE: - return "gcm"; - case EVP_CIPH_OCB_MODE: - return "ocb"; - case EVP_CIPH_OFB_MODE: - return "ofb"; - case EVP_CIPH_WRAP_MODE: - return "wrap"; - case EVP_CIPH_XTS_MODE: - return "xts"; - case EVP_CIPH_STREAM_CIPHER: - return "stream"; - } - return "{unknown}"; -} - const char* Cipher::getName() const { if (!cipher_) return {}; // OBJ_nid2sn(EVP_CIPHER_nid(cipher)) is used here instead of @@ -3382,6 +3354,8 @@ int Cipher::bytesToKey(const Digest& digest, *this, Digest::MD5, nullptr, input.data, input.len, 1, key, iv); } +template class ModeMixin; + namespace { struct CipherCallbackContext { Cipher::CipherNameCallback cb; @@ -5040,6 +5014,289 @@ DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key, #endif // OPENSSL_VERSION_MAJOR >= 3 +// ============================================================================ +// AEAD (Authenticated Encryption with Associated Data) +#ifdef OPENSSL_IS_BORINGSSL + +const Aead Aead::FromName(std::string_view name) { + for (const auto& [construct, info] : aeadIndex) { + if (EqualNoCase(info.name, name)) { + return Aead(&info, construct()); + } + } + + return Aead(); +} + +const Aead Aead::FromCtx(std::string_view name, const AeadCtxPointer& ctx) { + for (const auto& [_, info] : aeadIndex) { + if (info.name == name) { + return Aead(&info, EVP_AEAD_CTX_aead(ctx.get())); + } + } + + return Aead(); +} + +int Aead::getMode() const { + if (!aead_) return -1; + + return info_->mode; +} + +int Aead::getNonceLength() const { + if (!aead_) return 0; + return EVP_AEAD_nonce_length(aead_); +} + +int Aead::getKeyLength() const { + if (!aead_) return 0; + return EVP_AEAD_key_length(aead_); +} + +int Aead::getMaxOverhead() const { + if (!aead_) return 0; + return EVP_AEAD_max_overhead(aead_); +} + +int Aead::getMaxTagLength() const { + if (!aead_) return 0; + return EVP_AEAD_max_tag_len(aead_); +} + +int Aead::getBlockSize() const { + if (!aead_) return 0; + + // EVP_CIPHER_CTX_block_size returns the block size, in bytes, of the cipher + // underlying |ctx|, or one if the cipher is a stream cipher. + return 1; +} + +std::string_view Aead::getName() const { + if (!aead_) return ""; + + return info_->name; +} + +int Aead::getNid() const { + if (!aead_) return 0; + + return info_->nid; +} + +const Aead Aead::FromConstructor(Aead::AeadConstructor construct) { + return Aead(&aeadIndex.at(construct), construct()); +} + +const std::unordered_map + Aead::aeadIndex = { + {EVP_aead_aes_128_gcm, + {.name = LN_aes_128_gcm, + .mode = EVP_CIPH_GCM_MODE, + .nid = NID_aes_128_gcm}}, + {EVP_aead_aes_192_gcm, + {.name = LN_aes_192_gcm, + .mode = EVP_CIPH_GCM_MODE, + .nid = NID_aes_192_gcm}}, + {EVP_aead_aes_256_gcm, + {.name = LN_aes_256_gcm, + .mode = EVP_CIPH_GCM_MODE, + .nid = NID_aes_256_gcm}}, + {EVP_aead_chacha20_poly1305, + {.name = LN_chacha20_poly1305, + .mode = EVP_CIPH_STREAM_CIPHER, + .nid = NID_chacha20_poly1305}}, + {EVP_aead_xchacha20_poly1305, + { + .name = "xchacha20-poly1305", + .mode = EVP_CIPH_STREAM_CIPHER, + }}, + {EVP_aead_aes_128_ctr_hmac_sha256, + { + .name = "aes-128-ctr-hmac-sha256", + .mode = EVP_CIPH_CTR_MODE, + }}, + {EVP_aead_aes_256_ctr_hmac_sha256, + { + .name = "aes-256-ctr-hmac-sha256", + .mode = EVP_CIPH_CTR_MODE, + }}, + {EVP_aead_aes_128_gcm_siv, + { + .name = "aes-128-gcm-siv", + .mode = EVP_CIPH_GCM_MODE, + }}, + {EVP_aead_aes_256_gcm_siv, + { + .name = "aes-256-gcm-siv", + .mode = EVP_CIPH_GCM_MODE, + }}, + {EVP_aead_aes_128_gcm_randnonce, + { + .name = "aes-128-gcm-randnonce", + .mode = EVP_CIPH_GCM_MODE, + }}, + {EVP_aead_aes_256_gcm_randnonce, + { + .name = "aes-256-gcm-randnonce", + .mode = EVP_CIPH_GCM_MODE, + }}, + {EVP_aead_aes_128_ccm_bluetooth, + { + .name = "aes-128-ccm-bluetooth", + .mode = EVP_CIPH_CCM_MODE, + }}, + {EVP_aead_aes_128_ccm_bluetooth_8, + { + .name = "aes-128-ccm-bluetooth-8", + .mode = EVP_CIPH_CCM_MODE, + }}, + {EVP_aead_aes_128_ccm_matter, + { + .name = "aes-128-ccm-matter", + .mode = EVP_CIPH_CCM_MODE, + }}, + {EVP_aead_aes_128_eax, + {.name = "aes-128-eax", + // BoringSSL does not define a mode constant for EAX. Using STREAM + // arbitrarily + .mode = EVP_CIPH_STREAM_CIPHER}}, + {EVP_aead_aes_256_eax, + {.name = "aes-256-eax", + // BoringSSL does not define a mode constant for EAX. Using STREAM + // arbitrarily + .mode = EVP_CIPH_STREAM_CIPHER}}, + }; + +void Aead::ForEach(AeadNameCallback callback) { + for (const auto& [_, info] : aeadIndex) { + callback(info.name); + } +} + +const Aead Aead::EMPTY = Aead(); +const Aead Aead::AES_128_GCM = Aead::FromConstructor(EVP_aead_aes_128_gcm); +const Aead Aead::AES_192_GCM = Aead::FromConstructor(EVP_aead_aes_192_gcm); +const Aead Aead::AES_256_GCM = Aead::FromConstructor(EVP_aead_aes_256_gcm); +const Aead Aead::CHACHA20_POLY1305 = + Aead::FromConstructor(EVP_aead_chacha20_poly1305); +const Aead Aead::XCHACHA20_POLY1305 = + Aead::FromConstructor(EVP_aead_xchacha20_poly1305); +const Aead Aead::AES_128_CTR_HMAC_SHA256 = + Aead::FromConstructor(EVP_aead_aes_128_ctr_hmac_sha256); +const Aead Aead::AES_256_CTR_HMAC_SHA256 = + Aead::FromConstructor(EVP_aead_aes_256_ctr_hmac_sha256); +const Aead Aead::AES_128_GCM_SIV = + Aead::FromConstructor(EVP_aead_aes_128_gcm_siv); +const Aead Aead::AES_256_GCM_SIV = + Aead::FromConstructor(EVP_aead_aes_256_gcm_siv); +const Aead Aead::AES_128_GCM_RANDNONCE = + Aead::FromConstructor(EVP_aead_aes_128_gcm_randnonce); +const Aead Aead::AES_256_GCM_RANDNONCE = + Aead::FromConstructor(EVP_aead_aes_256_gcm_randnonce); +const Aead Aead::AES_128_CCM_BLUETOOTH = + Aead::FromConstructor(EVP_aead_aes_128_ccm_bluetooth); +const Aead Aead::AES_128_CCM_BLUETOOTH_8 = + Aead::FromConstructor(EVP_aead_aes_128_ccm_bluetooth_8); +const Aead Aead::AES_128_CCM_MATTER = + Aead::FromConstructor(EVP_aead_aes_128_ccm_matter); +const Aead Aead::AES_128_EAX = Aead::FromConstructor(EVP_aead_aes_128_eax); +const Aead Aead::AES_256_EAX = Aead::FromConstructor(EVP_aead_aes_256_eax); + +template class ModeMixin; + +AeadCtxPointer AeadCtxPointer::New(const Aead& aead, + bool encrypt, + const unsigned char* key, + size_t keyLen, + size_t tagLen) { + // Note: In the EVP_AEAD API new always calls init + auto ret = AeadCtxPointer(EVP_AEAD_CTX_new(aead.get(), key, keyLen, tagLen)); + + if (!ret) { + return {}; + } + + return ret; +} + +AeadCtxPointer::AeadCtxPointer(EVP_AEAD_CTX* ctx) : ctx_(ctx) {} + +AeadCtxPointer::AeadCtxPointer(AeadCtxPointer&& other) noexcept + : ctx_(other.release()) {} + +AeadCtxPointer& AeadCtxPointer::operator=(AeadCtxPointer&& other) noexcept { + if (this == &other) return *this; + this->~AeadCtxPointer(); + return *new (this) AeadCtxPointer(std::move(other)); +} + +AeadCtxPointer::~AeadCtxPointer() { + reset(); +} + +void AeadCtxPointer::reset(EVP_AEAD_CTX* ctx) { + ctx_.reset(ctx); +} + +EVP_AEAD_CTX* AeadCtxPointer::release() { + return ctx_.release(); +} + +bool AeadCtxPointer::init(const Aead& aead, + bool encrypt, + const unsigned char* key, + size_t keyLen, + size_t tagLen) { + return EVP_AEAD_CTX_init_with_direction( + ctx_.get(), + aead, + key, + keyLen, + tagLen, + encrypt ? evp_aead_seal : evp_aead_open); +} + +bool AeadCtxPointer::encrypt(const Buffer& in, + Buffer& out, + Buffer& tag, + const Buffer& nonce, + const Buffer& aad) { + if (!ctx_) return false; + return EVP_AEAD_CTX_seal_scatter(ctx_.get(), + out.data, + tag.data, + &tag.len, + tag.len, + nonce.data, + nonce.len, + in.data, + in.len, + nullptr /* extra_in */, + 0 /* extra_in_len */, + aad.data, + aad.len) == 1; +} + +bool AeadCtxPointer::decrypt(const Buffer& in, + Buffer& out, + const Buffer& tag, + const Buffer& nonce, + const Buffer& aad) { + if (!ctx_) return false; + + return EVP_AEAD_CTX_open_gather(ctx_.get(), + out.data, + nonce.data, + nonce.len, + in.data, + in.len, + tag.data, + tag.len, + aad.data, + aad.len) == 1; +} +#endif } // namespace ncrypto // =========================================================================== diff --git a/tests/basic.cpp b/tests/basic.cpp index 29e34bf..05744cd 100644 --- a/tests/basic.cpp +++ b/tests/basic.cpp @@ -1,6 +1,27 @@ #include #include +#include + +using namespace ncrypto; + +// Convenience class for creating buffers in tests +struct TestBuf : public std::string { + TestBuf(const std::string& constStr) + : std::string(constStr), + buf{reinterpret_cast(data()), size()} {} + TestBuf(size_t n) : TestBuf(std::string(n, 0)) {} + + operator Buffer&() { return buf; } + + Buffer asConst() const { + return Buffer{ + .data = reinterpret_cast(data()), .len = size()}; + } + + private: + Buffer buf; +}; #include #include @@ -19,3 +40,47 @@ TEST(basic, cipher_foreach) { ASSERT_TRUE(foundCiphers.count("AES-128-CTR")); ASSERT_TRUE(foundCiphers.count("AES-256-CBC")); } + +#ifdef OPENSSL_IS_BORINGSSL +TEST(basic, chacha20_poly1305) { + unsigned char key[] = {0xde, 0xad, 0xbe, 0xef, 0x00, 0x01, 0x02, 0x03, + 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, + 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, + 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7}; + + auto aead = Aead::CHACHA20_POLY1305; + auto encryptCtx = AeadCtxPointer::New(aead, true, key, aead.getKeyLength()); + + TestBuf input("Hello world"); + TestBuf tag(aead.getMaxTagLength()); + TestBuf nonce(aead.getNonceLength()); + TestBuf aad("I dunno man"); + TestBuf encryptOutput(input.size()); + + auto encryptOk = encryptCtx.encrypt( + input.asConst(), encryptOutput, tag, nonce.asConst(), aad.asConst()); + ASSERT_TRUE(encryptOk); + ASSERT_NE(input, encryptOutput); + + auto decryptCtx = AeadCtxPointer::New(aead, false, key, aead.getKeyLength()); + + TestBuf decryptOutput(encryptOutput.size()); + + auto decryptOk = decryptCtx.decrypt(encryptOutput.asConst(), + decryptOutput, + tag.asConst(), + nonce.asConst(), + aad.asConst()); + ASSERT_TRUE(decryptOk); + ASSERT_EQ(input, decryptOutput); +} + +TEST(basic, aead_info) { + auto aead = Aead::FromName("aEs-256-gcM"); // spongebob does encryption + ASSERT_EQ(aead.getName(), "aes-256-gcm"); + ASSERT_EQ(aead.getModeLabel(), "gcm"); + ASSERT_EQ(aead.getBlockSize(), 1); + ASSERT_EQ(aead.getNonceLength(), 12); + ASSERT_EQ(aead.getMaxTagLength(), 16); +} +#endif