Skip to content

security/node_identity.cpp

secp256k1 keypair implementation More...

Namespaces

Name
sgns
sgns::neoswarm
sgns::neoswarm::security

Classes

Name
struct sgns::neoswarm::security::NodeIdentity::Impl

Detailed Description

secp256k1 keypair implementation

Date: 2026-05-08

Source code

#include "node_identity.hpp"
#include "common/logging.hpp"

#include <fstream>
#include <iomanip>
#include <sstream>

#include <secp256k1.h>

#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <cstring>

namespace sgns::neoswarm::security
{
    namespace
    {
        auto IdentityLogger()
        {
            return neoswarm::CreateLogger( "NodeIdentity" );
        }

        std::string ToHex( const uint8_t* data, size_t len )
        {
            std::ostringstream oss;
            for ( size_t i = 0; i < len; ++i )
            {
                oss << std::hex << std::setw( 2 ) << std::setfill( '0' ) << static_cast<int>( data[i] );
            }
            return oss.str();
        }

        std::vector<uint8_t> FromHex( const std::string& hex )
        {
            std::vector<uint8_t> bytes;
            for ( size_t i = 0; i + 1 < hex.size(); i += 2 )
            {
                bytes.push_back( static_cast<uint8_t>( std::stoul( hex.substr( i, 2 ), nullptr, 16 ) ) );
            }
            return bytes;
        }
    } // namespace

    // -----------------------------------------------------------------------
    // Impl
    // -----------------------------------------------------------------------
    struct NodeIdentity::Impl
    {
        PrivKey m_privKey{};
        secp256k1_context* m_ctx = nullptr;
    };

    NodeIdentity::NodeIdentity()
        : m_impl( std::make_unique<Impl>() )
    {
        m_impl->m_ctx = secp256k1_context_create( SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY );
    }

    NodeIdentity::~NodeIdentity()
    {
        if ( m_impl && m_impl->m_ctx )
        {
            secp256k1_context_destroy( m_impl->m_ctx );
        }
    }

    // -----------------------------------------------------------------------
    // Generate
    // -----------------------------------------------------------------------
    outcome::result<void> NodeIdentity::Generate()
    {
        for ( int attempt = 0; attempt < 100; ++attempt )
        {
            if ( !RAND_bytes( m_impl->m_privKey.data(),
                              static_cast<int>( m_impl->m_privKey.size() ) ) )
            {
                return outcome::failure( Error::IdentityError );
            }
            if ( secp256k1_ec_seckey_verify( m_impl->m_ctx, m_impl->m_privKey.data() ) )
            {
                secp256k1_pubkey pubkey;
                (void)secp256k1_ec_pubkey_create( m_impl->m_ctx, &pubkey, m_impl->m_privKey.data() );
                size_t pub_len = kPubKeySize;
                secp256k1_ec_pubkey_serialize( m_impl->m_ctx, m_pubKey.data(), &pub_len, &pubkey,
                                               SECP256K1_EC_COMPRESSED );
                m_privKey = m_impl->m_privKey;
                m_loaded = true;
                IdentityLogger()->info( "NodeIdentity generated: peerId={}", GetPeerId() );
                return outcome::success();
            }
        }
        return outcome::failure( Error::IdentityError );

    }

    // -----------------------------------------------------------------------
    // PeerId
    // -----------------------------------------------------------------------
    std::string NodeIdentity::GetPeerId() const
    {
        if ( !m_loaded )
        {
            return "";
        }
        uint8_t hash[SHA256_DIGEST_LENGTH];
        SHA256( m_pubKey.data(), m_pubKey.size(), hash );
        return ToHex( hash, SHA256_DIGEST_LENGTH );

    }

    // -----------------------------------------------------------------------
    // LoadFromFile
    // -----------------------------------------------------------------------
    outcome::result<void> NodeIdentity::LoadFromFile( const std::string& path )
    {
        std::ifstream f( path );
        if ( !f )
        {
            return outcome::failure( Error::IdentityError );
        }
        std::string hex_priv;
        f >> hex_priv;
        auto bytes = FromHex( hex_priv );
        if ( bytes.size() != kPrivKeySize )
        {
            return outcome::failure( Error::IdentityError );
        }
        std::copy( bytes.begin(), bytes.end(), m_impl->m_privKey.begin() );
        secp256k1_pubkey pubkey;
        (void)secp256k1_ec_pubkey_create( m_impl->m_ctx, &pubkey, m_impl->m_privKey.data() );
        size_t pub_len = kPubKeySize;
        secp256k1_ec_pubkey_serialize( m_impl->m_ctx, m_pubKey.data(), &pub_len, &pubkey, SECP256K1_EC_COMPRESSED );
        m_privKey = m_impl->m_privKey;
        m_loaded = true;
        return outcome::success();
    }

    // -----------------------------------------------------------------------
    // SaveToFile
    // -----------------------------------------------------------------------
    outcome::result<void> NodeIdentity::SaveToFile( const std::string& path ) const
    {
        if ( !m_loaded )
        {
            return outcome::failure( Error::IdentityError );
        }
        std::ofstream f( path );
        if ( !f )
        {
            return outcome::failure( Error::IdentityError );
        }
        f << ToHex( m_impl->m_privKey.data(), kPrivKeySize ) << '\n';
        return outcome::success();
    }

    // -----------------------------------------------------------------------
    // SaveEncrypted
    // -----------------------------------------------------------------------
    outcome::result<void> NodeIdentity::SaveEncrypted( const std::string& path, const std::string& passphrase ) const
    {
        if ( !m_loaded )
        {
            return outcome::failure( Error::IdentityError );
        }

        // 1. Generate 32-byte random salt
        uint8_t salt[32];
        if ( !RAND_bytes( salt, sizeof( salt ) ) )
        {
            return outcome::failure( Error::IdentityError );
        }

        // 2. Derive 256-bit encryption key via PBKDF2
        uint8_t key[32]; // AES-256
        if ( !PKCS5_PBKDF2_HMAC( passphrase.c_str(), static_cast<int>( passphrase.size() ), salt, sizeof( salt ),
                                 600000, // iterations
                                 EVP_sha256(), sizeof( key ), key ) )
        {
            return outcome::failure( Error::IdentityError );
        }

        // 3. Generate 12-byte random IV for GCM
        uint8_t iv[12];
        if ( !RAND_bytes( iv, sizeof( iv ) ) )
        {
            return outcome::failure( Error::IdentityError );
        }

        // 4. Encrypt with AES-256-GCM
        EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
        if ( !ctx )
        {
            return outcome::failure( Error::IdentityError );
        }

        if ( !EVP_EncryptInit_ex( ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr ) )
        {
            EVP_CIPHER_CTX_free( ctx );
            return outcome::failure( Error::IdentityError );
        }

        if ( !EVP_CIPHER_CTX_ctrl( ctx, EVP_CTRL_GCM_SET_IVLEN, sizeof( iv ), nullptr ) )
        {
            EVP_CIPHER_CTX_free( ctx );
            return outcome::failure( Error::IdentityError );
        }

        if ( !EVP_EncryptInit_ex( ctx, nullptr, nullptr, key, iv ) )
        {
            EVP_CIPHER_CTX_free( ctx );
            return outcome::failure( Error::IdentityError );
        }

        // Encrypt the private key
        std::vector<uint8_t> ciphertext( kPrivKeySize + 16 ); // room for block padding
        int outLen = 0;
        if ( !EVP_EncryptUpdate( ctx, ciphertext.data(), &outLen, m_impl->m_privKey.data(),
                                 static_cast<int>( kPrivKeySize ) ) )
        {
            EVP_CIPHER_CTX_free( ctx );
            return outcome::failure( Error::IdentityError );
        }
        int totalLen = outLen;

        if ( !EVP_EncryptFinal_ex( ctx, ciphertext.data() + totalLen, &outLen ) )
        {
            EVP_CIPHER_CTX_free( ctx );
            return outcome::failure( Error::IdentityError );
        }
        totalLen += outLen;
        ciphertext.resize( totalLen );

        // 5. Get GCM tag
        uint8_t tag[16];
        if ( !EVP_CIPHER_CTX_ctrl( ctx, EVP_CTRL_GCM_GET_TAG, sizeof( tag ), tag ) )
        {
            EVP_CIPHER_CTX_free( ctx );
            return outcome::failure( Error::IdentityError );
        }

        EVP_CIPHER_CTX_free( ctx );

        // 6. Write binary file: [4B salt_len][32B salt][12B IV][ciphertext][16B tag]
        std::ofstream f( path, std::ios::binary );
        if ( !f )
        {
            return outcome::failure( Error::IdentityError );
        }

        uint32_t saltLen = static_cast<uint32_t>( sizeof( salt ) );
        f.write( reinterpret_cast<const char*>( &saltLen ), sizeof( saltLen ) );
        f.write( reinterpret_cast<const char*>( salt ), sizeof( salt ) );
        f.write( reinterpret_cast<const char*>( iv ), sizeof( iv ) );
        f.write( reinterpret_cast<const char*>( ciphertext.data() ),
                 static_cast<std::streamsize>( ciphertext.size() ) );
        f.write( reinterpret_cast<const char*>( tag ), sizeof( tag ) );

        if ( !f.good() )
        {
            return outcome::failure( Error::IdentityError );
        }

        IdentityLogger()->info( "NodeIdentity saved encrypted to {}", path );
        return outcome::success();

    }

    // -----------------------------------------------------------------------
    // LoadEncrypted
    // -----------------------------------------------------------------------
    outcome::result<void> NodeIdentity::LoadEncrypted( const std::string& path, const std::string& passphrase )
    {
        std::ifstream f( path, std::ios::binary );
        if ( !f )
        {
            return outcome::failure( Error::IdentityError );
        }

        // 1. Read salt length
        uint32_t saltLen = 0;
        f.read( reinterpret_cast<char*>( &saltLen ), sizeof( saltLen ) );
        if ( !f.good() || saltLen == 0 || saltLen > 1024 )
        {
            return outcome::failure( Error::IdentityError );
        }

        // 2. Read salt
        std::vector<uint8_t> salt( saltLen );
        f.read( reinterpret_cast<char*>( salt.data() ), static_cast<std::streamsize>( saltLen ) );
        if ( !f.good() )
        {
            return outcome::failure( Error::IdentityError );
        }

        // 3. Read IV
        uint8_t iv[12];
        f.read( reinterpret_cast<char*>( iv ), sizeof( iv ) );
        if ( !f.good() )
        {
            return outcome::failure( Error::IdentityError );
        }

        // 4. Read ciphertext (remaining bytes minus 16-byte tag)
        f.seekg( 0, std::ios::end );
        auto fileSize = f.tellg();
        f.seekg( static_cast<std::streamoff>( sizeof( saltLen ) + saltLen + sizeof( iv ) ), std::ios::beg );
        auto ciphertextSize = static_cast<size_t>( fileSize - f.tellg() ) - 16; // minus tag
        if ( ciphertextSize > 1024 || ciphertextSize < kPrivKeySize )
        {
            return outcome::failure( Error::IdentityError );
        }
        std::vector<uint8_t> ciphertext( ciphertextSize );
        f.read( reinterpret_cast<char*>( ciphertext.data() ), static_cast<std::streamsize>( ciphertextSize ) );
        if ( !f.good() )
        {
            return outcome::failure( Error::IdentityError );
        }

        // 5. Read GCM tag
        uint8_t tag[16];
        f.read( reinterpret_cast<char*>( tag ), sizeof( tag ) );
        if ( !f.good() )
        {
            return outcome::failure( Error::IdentityError );
        }

        // 6. Derive key via PBKDF2
        uint8_t key[32];
        if ( !PKCS5_PBKDF2_HMAC( passphrase.c_str(), static_cast<int>( passphrase.size() ), salt.data(),
                                 static_cast<int>( salt.size() ), 600000, EVP_sha256(), sizeof( key ), key ) )
        {
            return outcome::failure( Error::IdentityError );
        }

        // 7. Decrypt with AES-256-GCM
        EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
        if ( !ctx )
        {
            return outcome::failure( Error::IdentityError );
        }

        if ( !EVP_DecryptInit_ex( ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr ) )
        {
            EVP_CIPHER_CTX_free( ctx );
            return outcome::failure( Error::IdentityError );
        }

        if ( !EVP_CIPHER_CTX_ctrl( ctx, EVP_CTRL_GCM_SET_IVLEN, sizeof( iv ), nullptr ) )
        {
            EVP_CIPHER_CTX_free( ctx );
            return outcome::failure( Error::IdentityError );
        }

        if ( !EVP_DecryptInit_ex( ctx, nullptr, nullptr, key, iv ) )
        {
            EVP_CIPHER_CTX_free( ctx );
            return outcome::failure( Error::IdentityError );
        }

        std::vector<uint8_t> plaintext( ciphertextSize );
        int outLen = 0;
        if ( !EVP_DecryptUpdate( ctx, plaintext.data(), &outLen, ciphertext.data(),
                                 static_cast<int>( ciphertextSize ) ) )
        {
            EVP_CIPHER_CTX_free( ctx );
            return outcome::failure( Error::IdentityError );
        }
        int totalLen = outLen;

        // 8. Set expected GCM tag BEFORE Final
        if ( !EVP_CIPHER_CTX_ctrl( ctx, EVP_CTRL_GCM_SET_TAG, sizeof( tag ), const_cast<uint8_t*>( tag ) ) )
        {
            EVP_CIPHER_CTX_free( ctx );
            return outcome::failure( Error::IdentityError );
        }

        // 9. Finalize — this validates the GCM tag
        int ret = EVP_DecryptFinal_ex( ctx, plaintext.data() + totalLen, &outLen );
        EVP_CIPHER_CTX_free( ctx );

        if ( ret <= 0 )
        {
            // Tag verification failed — wrong passphrase or tampered file
            IdentityLogger()->error( "LoadEncrypted: decryption failed — wrong passphrase or corrupt file" );
            return outcome::failure( Error::IdentityError );
        }
        totalLen += outLen;
        plaintext.resize( totalLen );

        // 10. Copy decrypted key
        if ( plaintext.size() != kPrivKeySize )
        {
            return outcome::failure( Error::IdentityError );
        }
        std::memcpy( m_impl->m_privKey.data(), plaintext.data(), kPrivKeySize );

        // 11. Derive public key from private key
        secp256k1_pubkey pubkey;
        (void)secp256k1_ec_pubkey_create( m_impl->m_ctx, &pubkey, m_impl->m_privKey.data() );
        size_t pubLen = kPubKeySize;
        secp256k1_ec_pubkey_serialize( m_impl->m_ctx, m_pubKey.data(), &pubLen, &pubkey, SECP256K1_EC_COMPRESSED );

        m_privKey = m_impl->m_privKey;
        m_loaded = true;
        IdentityLogger()->info( "NodeIdentity loaded encrypted from {}", path );
        return outcome::success();

    }

    // -----------------------------------------------------------------------
    // Sign
    // -----------------------------------------------------------------------
    outcome::result<std::vector<uint8_t>> NodeIdentity::Sign( const std::vector<uint8_t>& message ) const
    {
        if ( !m_loaded )
        {
            return outcome::failure( Error::IdentityError );
        }
        uint8_t hash[32];
        SHA256( message.data(), message.size(), hash );

        secp256k1_ecdsa_signature sig;
        if ( !secp256k1_ecdsa_sign( m_impl->m_ctx, &sig, hash, m_impl->m_privKey.data(), secp256k1_nonce_function_rfc6979,
                                    nullptr ) )
        {
            return outcome::failure( Error::IdentityError );
        }
        std::vector<uint8_t> der( 72 );
        size_t der_len = 72;
        secp256k1_ecdsa_signature_serialize_der( m_impl->m_ctx, der.data(), &der_len, &sig );
        der.resize( der_len );
        return outcome::success( std::move( der ) );

    }

    // -----------------------------------------------------------------------
    // Verify
    // -----------------------------------------------------------------------
    bool NodeIdentity::Verify( const std::vector<uint8_t>& message, const std::vector<uint8_t>& signature ) const
    {
        if ( !m_loaded )
        {
            return false;
        }
        uint8_t hash[32];
        SHA256( message.data(), message.size(), hash );

        secp256k1_ecdsa_signature sig;
        if ( !secp256k1_ecdsa_signature_parse_der( m_impl->m_ctx, &sig, signature.data(), signature.size() ) )
        {
            return false;
        }
        secp256k1_pubkey pubkey;
        if ( !secp256k1_ec_pubkey_parse( m_impl->m_ctx, &pubkey, m_pubKey.data(), kPubKeySize ) )
        {
            return false;
        }
        return secp256k1_ecdsa_verify( m_impl->m_ctx, &sig, hash, &pubkey ) == 1;

    }

} // namespace sgns::neoswarm::security

Updated on 2026-07-25 at 22:56:57 +0000