Skip to content

auth/ecies_cipher.cpp

Namespaces

Name
rlpx
rlpx::auth

Source code

// Copyright 2025 GeniusVentures
// SPDX-License-Identifier: Apache-2.0

#include <rlpx/auth/ecies_cipher.hpp>
#include <rlpx/crypto/ecdh.hpp>
#include <base/rlp-logger.hpp>
#include <secp256k1.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>
#include <cstring>

namespace rlpx::auth {

namespace {

static rlp::base::Logger& ecies_log() {
    static auto log = rlp::base::createLogger("rlpx.ecies");
    return log;
}

struct EciesKeys {
    std::array<uint8_t, 16> enc_key{};  
    std::array<uint8_t, 32> mac_key{};  
};

EciesKeys derive_keys(const SharedSecret& shared_secret) noexcept {
    // Step 1: concatKDF — SHA256(0x00000001 || z) → 32 bytes
    uint8_t kdf_input[4 + kSharedSecretSize];
    kdf_input[0] = 0x00;
    kdf_input[1] = 0x00;
    kdf_input[2] = 0x00;
    kdf_input[3] = 0x01;
    std::memcpy(kdf_input + 4, shared_secret.data(), kSharedSecretSize);

    uint8_t kdf_out[32];
    EVP_Digest(kdf_input, sizeof(kdf_input), kdf_out, nullptr, EVP_sha256(), nullptr);

    EciesKeys keys;
    // Ke = first 16 bytes
    std::memcpy(keys.enc_key.data(), kdf_out, 16);
    // Km = SHA256(kdf_out[16:32])  — go-ethereum deriveKeys hashes Km before use
    EVP_Digest(kdf_out + 16, 16, keys.mac_key.data(), nullptr, EVP_sha256(), nullptr);
    return keys;
}

ByteBuffer aes128_ctr(ByteView data, const std::array<uint8_t, 16>& key, ByteView iv) noexcept {
    ByteBuffer out(data.size());
    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    if (!ctx) { return {}; }
    EVP_EncryptInit_ex(ctx, EVP_aes_128_ctr(), nullptr, key.data(), iv.data());
    int len = 0;
    EVP_EncryptUpdate(ctx, out.data(), &len, data.data(), static_cast<int>(data.size()));
    EVP_CIPHER_CTX_free(ctx);
    out.resize(static_cast<size_t>(len));
    return out;
}

std::array<uint8_t, 32> hmac_sha256(ByteView key, ByteView data) noexcept {
    std::array<uint8_t, 32> mac{};
    unsigned int mac_len = 0;
    HMAC(EVP_sha256(), key.data(), static_cast<int>(key.size()),
         data.data(), data.size(), mac.data(), &mac_len);
    return mac;
}

} // anonymous namespace

// ---------------------------------------------------------------------------
// Public interface
// ---------------------------------------------------------------------------

AuthResult<ByteBuffer> EciesCipher::encrypt(const EciesEncryptParams& params) noexcept {
    // Generate ephemeral keypair
    secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
    if (!ctx) { return AuthError::kEciesEncryptFailed; }

    PrivateKey ephemeral_priv{};
    for (;;) {
        RAND_bytes(ephemeral_priv.data(), static_cast<int>(ephemeral_priv.size()));
        if (secp256k1_ec_seckey_verify(ctx, ephemeral_priv.data())) { break; }
    }

    secp256k1_pubkey ephemeral_secp_pub;
    if (!secp256k1_ec_pubkey_create(ctx, &ephemeral_secp_pub, ephemeral_priv.data())) {
        ecies_log()->debug("encrypt: pubkey_create failed");
        secp256k1_context_destroy(ctx);
        return AuthError::kInvalidPublicKey;
    }

    // Serialize ephemeral public key (65 bytes uncompressed)
    std::array<uint8_t, kUncompressedPubKeySize> ephemeral_pub_bytes{};
    size_t pub_len = kUncompressedPubKeySize;
    secp256k1_ec_pubkey_serialize(ctx, ephemeral_pub_bytes.data(), &pub_len,
                                  &ephemeral_secp_pub, SECP256K1_EC_UNCOMPRESSED);
    secp256k1_context_destroy(ctx);

    // ECDH: shared secret = x-coordinate of (ephemeral_priv * recipient_pub)
    auto shared_result = compute_shared_secret(params.recipient_public_key, ephemeral_priv);
    if (!shared_result) {
        ecies_log()->debug("encrypt: compute_shared_secret failed (code {})",
                           static_cast<int>(shared_result.error()));
        return shared_result.error();
    }

    // Derive AES-128 + MAC-16 keys
    const auto keys = derive_keys(shared_result.value());

    // Generate random 16-byte IV
    std::array<uint8_t, kAesBlockSize> iv{};
    RAND_bytes(iv.data(), static_cast<int>(iv.size()));

    // AES-128-CTR encrypt
    const ByteBuffer ciphertext = aes128_ctr(params.plaintext, keys.enc_key, iv);

    // MAC: HMAC-SHA256(mac_key, iv || ciphertext || shared_mac_data)
    ByteBuffer mac_input;
    mac_input.reserve(iv.size() + ciphertext.size() + params.shared_mac_data.size());
    mac_input.insert(mac_input.end(), iv.begin(), iv.end());
    mac_input.insert(mac_input.end(), ciphertext.begin(), ciphertext.end());
    mac_input.insert(mac_input.end(), params.shared_mac_data.begin(), params.shared_mac_data.end());
    const auto mac = hmac_sha256(ByteView(keys.mac_key.data(), keys.mac_key.size()), mac_input);

    // Wire format: ephemeral_pub(65) || iv(16) || ciphertext(N) || mac(32)
    ByteBuffer result;
    result.reserve(ephemeral_pub_bytes.size() + iv.size() + ciphertext.size() + mac.size());
    result.insert(result.end(), ephemeral_pub_bytes.begin(), ephemeral_pub_bytes.end());
    result.insert(result.end(), iv.begin(), iv.end());
    result.insert(result.end(), ciphertext.begin(), ciphertext.end());
    result.insert(result.end(), mac.begin(), mac.end());
    return result;
}

AuthResult<ByteBuffer> EciesCipher::decrypt(const EciesDecryptParams& params) noexcept {
    // Wire: ephemeral_pub(65) || iv(16) || ciphertext(N) || mac(32)
    constexpr size_t kMinSize = kEciesOverheadSize;
    const size_t ciphertext_size = static_cast<size_t>(params.ciphertext.size());
    if (ciphertext_size < kMinSize) {
        return AuthError::kEciesDecryptFailed;
    }

    const ByteView ephemeral_pub_bytes = params.ciphertext.subspan(0, kUncompressedPubKeySize);
    const ByteView iv                  = params.ciphertext.subspan(kUncompressedPubKeySize, kAesBlockSize);
    const size_t   ct_offset           = kUncompressedPubKeySize + kAesBlockSize;
    const size_t   ct_len              = ciphertext_size - kMinSize;
    const ByteView ciphertext          = params.ciphertext.subspan(ct_offset, ct_len);
    const ByteView mac                 = params.ciphertext.subspan(ct_offset + ct_len, kEciesMacSize);

    // Parse ephemeral public key (skip 0x04 prefix)
    PublicKey ephemeral_pub{};
    std::memcpy(ephemeral_pub.data(),
                ephemeral_pub_bytes.data() + kUncompressedPubKeyPrefixSize,
                kPublicKeySize);

    // ECDH
    auto shared_result = compute_shared_secret(ephemeral_pub, params.recipient_private_key);
    if (!shared_result) { return shared_result.error(); }

    const auto keys = derive_keys(shared_result.value());

    // Verify MAC
    ByteBuffer mac_input;
    mac_input.reserve(iv.size() + ciphertext.size() + params.shared_mac_data.size());
    mac_input.insert(mac_input.end(), iv.begin(), iv.end());
    mac_input.insert(mac_input.end(), ciphertext.begin(), ciphertext.end());
    mac_input.insert(mac_input.end(), params.shared_mac_data.begin(), params.shared_mac_data.end());
    const auto expected_mac = hmac_sha256(ByteView(keys.mac_key.data(), keys.mac_key.size()), mac_input);

    if (std::memcmp(mac.data(), expected_mac.data(), kEciesMacSize) != 0) {
        ecies_log()->debug("decrypt: MAC mismatch");
        return AuthError::kEciesDecryptFailed;
    }

    return aes128_ctr(ciphertext, keys.enc_key, iv);
}

size_t EciesCipher::estimate_encrypted_size(size_t plaintext_size) noexcept {
    return kUncompressedPubKeySize + kAesBlockSize + plaintext_size + kEciesMacSize;
}

AuthResult<SharedSecret> EciesCipher::compute_shared_secret(
    gsl::span<const uint8_t, kPublicKeySize> public_key,
    gsl::span<const uint8_t, kPrivateKeySize> private_key
) noexcept {
    auto result = rlpx::crypto::Ecdh::compute_shared_secret(public_key, private_key);
    if (!result) { return AuthError::kSharedSecretFailed; }
    return result.value();
}

AesKey EciesCipher::derive_aes_key(ByteView /*shared_secret*/) noexcept {
    return AesKey{};  // Not used — derive_keys() handles both keys together
}

MacKey EciesCipher::derive_mac_key(ByteView /*shared_secret*/) noexcept {
    return MacKey{};  // Not used — derive_keys() handles both keys together
}

} // namespace rlpx::auth

Updated on 2026-04-13 at 23:22:46 -0700