Skip to content

framing/frame_cipher.cpp

Namespaces

Name
rlpx
rlpx::framing

Classes

Name
struct rlpx::framing::FrameCipher::FrameCipherImpl

Source code

// Copyright 2025 GeniusVentures
// SPDX-License-Identifier: Apache-2.0
//
// go-ethereum rlpx frame cipher — exact port of sessionState / hashMAC.
// Reference: go-ethereum/p2p/rlpx/rlpx.go

#include <rlpx/framing/frame_cipher.hpp>
#include <base/rlp-logger.hpp>
#include <nil/crypto3/hash/algorithm/hash.hpp>
#include <nil/crypto3/hash/keccak.hpp>
#include <nil/crypto3/hash/accumulators/hash.hpp>
#include <openssl/evp.h>
#include <openssl/crypto.h>
#include <cstring>

namespace rlpx::framing {

namespace {

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

    std::array<uint8_t, 32> keccak256(const uint8_t* data, size_t len) noexcept
    {
        using Hasher = nil::crypto3::hashes::keccak_1600<256>;
        nil::crypto3::accumulator_set<Hasher> acc;
        nil::crypto3::hash<Hasher>(data, data + len, acc);
        auto digest = nil::crypto3::accumulators::extract::hash<Hasher>(acc);
        std::array<uint8_t, 32> result{};
        std::copy(digest.begin(), digest.end(), result.begin());
        return result;
    }

    void aes_ecb_encrypt_block(const uint8_t* key32, const uint8_t* in16,
                                uint8_t* out16) noexcept
    {
        EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
        EVP_EncryptInit_ex(ctx, EVP_aes_256_ecb(), nullptr, key32, nullptr);
        EVP_CIPHER_CTX_set_padding(ctx, 0);
        int outl = 0;
        EVP_EncryptUpdate(ctx, out16, &outl, in16, 16);
        EVP_CIPHER_CTX_free(ctx);
    }

    struct HashMAC
    {
        std::array<uint8_t, 32> mac_key{};  
        ByteBuffer              written{};  

        void write(const uint8_t* data, size_t len)
        {
            written.insert(written.end(), data, data + len);
        }

        std::array<uint8_t, 32> sum() const noexcept
        {
            return keccak256(written.data(), written.size());
        }

        std::array<uint8_t, 16> compute_header(const uint8_t* header_ct16) noexcept
        {
            auto sum1 = sum();
            return compute(sum1, header_ct16);
        }

        std::array<uint8_t, 16> compute_frame(const uint8_t* framedata,
                                               size_t len) noexcept
        {
            write(framedata, len);
            auto seed = sum();
            return compute(seed, seed.data());
        }

        std::array<uint8_t, 16> compute(const std::array<uint8_t, 32>& sum1,
                                         const uint8_t* seed16) noexcept
        {
            std::array<uint8_t, 16> aes_buf{};
            aes_ecb_encrypt_block(mac_key.data(), sum1.data(), aes_buf.data());
            for (size_t i = 0; i < 16; ++i)
            {
                aes_buf[i] ^= seed16[i];
            }
            write(aes_buf.data(), 16);
            auto sum2 = sum();
            std::array<uint8_t, 16> result{};
            std::memcpy(result.data(), sum2.data(), 16);
            return result;
        }
    };

    struct AesCtrState
    {
        EVP_CIPHER_CTX* ctx = nullptr;

        void init(const uint8_t* key32) noexcept
        {
            ctx = EVP_CIPHER_CTX_new();
            uint8_t iv[16]{};
            EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, key32, iv);
        }

        ~AesCtrState()
        {
            if (ctx) { EVP_CIPHER_CTX_free(ctx); }
        }

        void process(const uint8_t* in, uint8_t* out, size_t len) noexcept
        {
            int outl = 0;
            EVP_EncryptUpdate(ctx, out, &outl, in,
                              static_cast<int>(len));
        }
    };

} // anonymous namespace

// ── FrameCipher pimpl definition ─────────────────────────────────────────────

struct FrameCipher::FrameCipherImpl
{
    AesCtrState enc;
    AesCtrState dec;
    HashMAC     egress_mac;
    HashMAC     ingress_mac;
};

// ── FrameCipher constructor / destructor ──────────────────────────────────────

FrameCipher::FrameCipher(const auth::FrameSecrets& secrets) noexcept
    : secrets_(secrets)
    , impl_(std::make_unique<FrameCipherImpl>())
{
    impl_->enc.init(secrets.aes_secret.data());
    impl_->dec.init(secrets.aes_secret.data());

    impl_->egress_mac.mac_key  = secrets.mac_secret;
    impl_->ingress_mac.mac_key = secrets.mac_secret;
    // Seed written buffers: go-ethereum writes xor(MAC,nonce) then auth/ack wire.
    // derive_frame_secrets stores those exact raw bytes in egress/ingress_mac_seed.
    impl_->egress_mac.write(secrets.egress_mac_seed.data(),
                             secrets.egress_mac_seed.size());
    impl_->ingress_mac.write(secrets.ingress_mac_seed.data(),
                              secrets.ingress_mac_seed.size());
    fc_log()->debug("FrameCipher: egress_seed_len={} ingress_seed_len={}",
                    secrets.egress_mac_seed.size(), secrets.ingress_mac_seed.size());
}

FrameCipher::~FrameCipher() = default;

// ── encrypt_frame ─────────────────────────────────────────────────────────────

FramingResult<ByteBuffer> FrameCipher::encrypt_frame(
    const FrameEncryptParams& params) noexcept
{
    const size_t frame_data_size = static_cast<size_t>(params.frame_data.size());
    if (params.frame_data.empty() || frame_data_size > kMaxFrameSize)
    {
        return FramingError::kInvalidFrameSize;
    }

    const size_t fsize   = frame_data_size;
    const size_t padding = (fsize % kFramePaddingAlignment != 0)
                         ? (kFramePaddingAlignment - (fsize % kFramePaddingAlignment))
                         : 0;
    const size_t rsize   = fsize + padding;

    // Header: 3-byte frame length + fixed RLP header bytes + trailing zeros.
    std::array<uint8_t, kFrameHeaderSize> header{};
    header[kFrameLengthMsbOffset] = static_cast<uint8_t>((fsize >> kFrameLengthMsbShift) & 0xFFU);
    header[kFrameLengthMiddleOffset] = static_cast<uint8_t>((fsize >> kFrameLengthMiddleShift) & 0xFFU);
    header[kFrameLengthLsbOffset] = static_cast<uint8_t>((fsize >> kFrameLengthLsbShift) & 0xFFU);
    std::memcpy(
        header.data() + kFrameHeaderDataOffset,
        kFrameHeaderStaticRlpBytes.data(),
        kFrameHeaderStaticRlpBytes.size());

    std::array<uint8_t, kFrameHeaderSize> header_ct{};
    impl_->enc.process(header.data(), header_ct.data(), kFrameHeaderSize);

    auto header_mac = impl_->egress_mac.compute_header(header_ct.data());

    ByteBuffer frame_padded(rsize, 0);
    std::memcpy(frame_padded.data(), params.frame_data.data(), fsize);

    ByteBuffer frame_ct(rsize);
    impl_->enc.process(frame_padded.data(), frame_ct.data(), rsize);

    auto frame_mac = impl_->egress_mac.compute_frame(frame_ct.data(), rsize);

    ByteBuffer out;
    out.reserve(kFrameHeaderWithMacSize + rsize + kMacSize);
    out.insert(out.end(), header_ct.begin(),  header_ct.end());
    out.insert(out.end(), header_mac.begin(), header_mac.end());
    out.insert(out.end(), frame_ct.begin(),   frame_ct.end());
    out.insert(out.end(), frame_mac.begin(),  frame_mac.end());
    return out;
}

// ── decrypt_header ────────────────────────────────────────────────────────────

FramingResult<size_t> FrameCipher::decrypt_header(
    gsl::span<const uint8_t, kFrameHeaderSize> header_ct,
    gsl::span<const uint8_t, kMacSize>         header_mac_wire) noexcept
{
    auto expected_mac = impl_->ingress_mac.compute_header(header_ct.data());
    if (CRYPTO_memcmp(header_mac_wire.data(), expected_mac.data(), kMacSize) != 0)
    {
        fc_log()->debug("decrypt_header: MAC mismatch — expected={:02x}{:02x}{:02x}{:02x} got={:02x}{:02x}{:02x}{:02x} seed_len={}",
                        expected_mac[0], expected_mac[1], expected_mac[2], expected_mac[3],
                        header_mac_wire[0], header_mac_wire[1], header_mac_wire[2], header_mac_wire[3],
                        impl_->ingress_mac.written.size());
        return FramingError::kMacMismatch;
    }

    std::array<uint8_t, kFrameHeaderSize> header_pt{};
    impl_->dec.process(header_ct.data(), header_pt.data(), kFrameHeaderSize);

    const size_t fsize = (static_cast<size_t>(header_pt[kFrameLengthMsbOffset]) << kFrameLengthMsbShift)
                       | (static_cast<size_t>(header_pt[kFrameLengthMiddleOffset]) << kFrameLengthMiddleShift)
                       | (static_cast<size_t>(header_pt[kFrameLengthLsbOffset]) << kFrameLengthLsbShift);
    if (fsize == 0 || fsize > kMaxFrameSize)
    {
        return FramingError::kInvalidFrameSize;
    }
    return fsize;
}

// ── decrypt_frame ─────────────────────────────────────────────────────────────

FramingResult<ByteBuffer> FrameCipher::decrypt_frame(
    const FrameDecryptParams& params) noexcept
{
    const size_t header_ciphertext_size = static_cast<size_t>(params.header_ciphertext.size());
    const size_t header_mac_size        = static_cast<size_t>(params.header_mac.size());
    const size_t frame_ciphertext_size  = static_cast<size_t>(params.frame_ciphertext.size());
    const size_t frame_mac_size         = static_cast<size_t>(params.frame_mac.size());

    if (header_ciphertext_size < kFrameHeaderSize
        || header_mac_size < kMacSize
        || frame_mac_size < kMacSize)
    {
        return FramingError::kInvalidFrameSize;
    }

    gsl::span<const uint8_t, kFrameHeaderSize> hct(
        params.header_ciphertext.data(), kFrameHeaderSize);
    gsl::span<const uint8_t, kMacSize> hm(
        params.header_mac.data(), kMacSize);

    auto fsize_result = decrypt_header(hct, hm);
    if (!fsize_result) { return fsize_result.error(); }
    const size_t fsize = fsize_result.value();

    if (frame_ciphertext_size < fsize)
    {
        return FramingError::kInvalidFrameSize;
    }

    const size_t rsize = frame_ciphertext_size;
    auto frame_mac_expected = impl_->ingress_mac.compute_frame(
        params.frame_ciphertext.data(), rsize);
    if (CRYPTO_memcmp(params.frame_mac.data(), frame_mac_expected.data(),
                      kMacSize) != 0)
    {
        fc_log()->debug("decrypt_frame: frame MAC mismatch");
        return FramingError::kMacMismatch;
    }

    ByteBuffer frame_pt(rsize);
    impl_->dec.process(params.frame_ciphertext.data(), frame_pt.data(), rsize);
    frame_pt.resize(fsize);
    return frame_pt;
}

// ── decrypt_frame_body ────────────────────────────────────────────────────────

FramingResult<ByteBuffer> FrameCipher::decrypt_frame_body(
    size_t   fsize,
    ByteView frame_ct_padded,
    ByteView frame_mac) noexcept
{
    const size_t rsize          = static_cast<size_t>(frame_ct_padded.size());
    const size_t frame_mac_size = static_cast<size_t>(frame_mac.size());
    if (rsize < fsize || frame_mac_size < kMacSize)
    {
        return FramingError::kInvalidFrameSize;
    }

    auto frame_mac_expected = impl_->ingress_mac.compute_frame(
        frame_ct_padded.data(), rsize);
    if (CRYPTO_memcmp(frame_mac.data(), frame_mac_expected.data(), kMacSize) != 0)
    {
        fc_log()->debug("decrypt_frame_body: frame MAC mismatch");
        return FramingError::kMacMismatch;
    }

    ByteBuffer frame_pt(rsize);
    impl_->dec.process(frame_ct_padded.data(), frame_pt.data(), rsize);
    frame_pt.resize(fsize);
    return frame_pt;
}

// ── legacy stubs ─────────────────────────────────────────────────────────────

void FrameCipher::update_egress_mac(ByteView /*data*/) noexcept {}
void FrameCipher::update_ingress_mac(ByteView /*data*/) noexcept {}
MacDigest FrameCipher::compute_header_mac(ByteView /*hct*/) noexcept { return {}; }
MacDigest FrameCipher::compute_frame_mac(ByteView /*fct*/) noexcept  { return {}; }

} // namespace rlpx::framing

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