Search code examples
c#encryptionblockchainecdsaecdh

ECDSA KeyPair to Perform ECDH Shared Secret Encryption


I needed to be able to take an ECDSA keypair (private and public key) to then perform an ECDH to get a shared secret between two parties.

In this case we are talking about Alice and Bob. I could not find any relevant C# examples anywhere and where people did talk about it all they said was it was possible, but provided no real answer.

So given the two key pairs below:

string alicePrivateKeyHex = "6105a3237a98d2843a35ac35fb63ba2cfbde4deabc97faa9664f42762103e0de";
string alicePublicKeyHex = "04cadf4b345e6f62a858fb2c25509bace24d81fd600dfb3aa40dcf021902bfc012ba1a8c2364b6323b71ab659891ef856cba546b58c881311fafc75103111a5bdb";

string bobPrivateKeyHex = "7034a8fe220fa73704be34e0fdf82d5671598a19c49f71cec3bce5cee5b30e59";
string bobPublicKeyHex = "04e7526b124f22c8549a2b91783b34a8df067a74df9b601447bc7561a4e7eaea7a652582d1510b59365a7684195f99e4464a6b612441e5a8c2b63664d957384ba9";

I needed to be able to produce a shared secret. The issue I ran into was that these keys produces from ECDSA don't follow the ANS.1 standard and so most of Microsoft and Bouncy Castle libs did not work with these despite converting them to Cgn, PEM, Der, etc.

I also can't do other things like Diffie-Hellman Key Exchange (DHKE) as I have no way to securely exchange keys as EVERYTHING will be over public channels and there is no way to communicate outside these channel without it being public, so my only option is to do a shared secret through ECDH.

Also I know that there is an obvious concern of using 1 key for permanence as if that key is ever revealed then someone could decrypt all messages. That is not the goal of this. I simply wanted to get one working example. IN the future each message will come from a new key to better protect.

After many failed attempts and even trying to get chatGPT to help I finally created my own solution and decided to post here for those who many need this in the future.

I put answer below.


Solution

  • The code below is how to take an ECDSA (Curve - secp256k1) derived keypair and produce a shared secret following the ECDH standard.

    Note this is applicable to things like Bitcoin address, Ethereum Address, ReserveBlock Network, and anything else that might be using ECDSA to create keypairs for digital signing.

    void Main()
    {
        string alicePrivateKeyHex = "6105a3237a98d2843a35ac35fb63ba2cfbde4deabc97faa9664f42762103e0de";
        string alicePublicKeyHex = "04cadf4b345e6f62a858fb2c25509bace24d81fd600dfb3aa40dcf021902bfc012ba1a8c2364b6323b71ab659891ef856cba546b58c881311fafc75103111a5bdb";
    
        string bobPrivateKeyHex = "7034a8fe220fa73704be34e0fdf82d5671598a19c49f71cec3bce5cee5b30e59";
        string bobPublicKeyHex = "04e7526b124f22c8549a2b91783b34a8df067a74df9b601447bc7561a4e7eaea7a652582d1510b59365a7684195f99e4464a6b612441e5a8c2b63664d957384ba9";
    
        BigInteger a1 = BigInteger.Parse(alicePrivateKeyHex, NumberStyles.AllowHexSpecifier);//converts hex private key into big int.
        BigInteger a2 = BigInteger.Parse(alicePublicKeyHex, NumberStyles.AllowHexSpecifier);//converts hex private key into big int.
        
        BigInteger b1 = BigInteger.Parse(bobPrivateKeyHex, NumberStyles.AllowHexSpecifier);//converts hex private key into big int.
        BigInteger b2 = BigInteger.Parse(bobPublicKeyHex, NumberStyles.AllowHexSpecifier);//converts hex private key into big int.
    
        BigInteger P, G, a, b, x, y, ka, kb;
    
        // Both the persons will be agreed upon the
        // public keys G and P
    
        P = a2; // A prime number P is taken
        Console.WriteLine("The value of P:" + P);
    
        G = b2; // A primitive root for P, G is taken
        Console.WriteLine("The value of G:" + G);
    
        // Alice will choose the private key a
        a = a1; // a is the chosen private key
        Console.WriteLine("\nThe private key a for Alice:"
                          + a);
        x = power(G, a, P); // gets the generated key
    
        // Bob will choose the private key b
        b = b1; // b is the chosen private key
        Console.WriteLine("The private key b for Bob:" + b);
        y = power(G, b, P); // gets the generated key
    
        // Generating the secret key after the exchange
        // of keys
        ka = power(y, a, P); // Secret key for Alice
        kb = power(x, b, P); // Secret key for Bob
    
        Console.WriteLine("\nSecret key for the Alice is:"
                          + ka.ToString("X"));
        Console.WriteLine("Secret key for the Bob is:"
                          + kb.ToString("X"));
                          
        if(ka == kb)
            Console.WriteLine("Shared Secrets Match!");
    
        string sharedSecretHex = ka.ToString("X").Substring(0,64).Dump(); // Replace with your shared secret in hexadecimal format
        byte[] sharedSecret = HexStringToByteArray(sharedSecretHex);
    
        string plainText = @"Hello, world!";
        byte[] encryptedData = Encrypt(plainText, sharedSecret);
        string decryptedText = Decrypt(encryptedData, sharedSecret);
    
        Console.WriteLine("Plain Text: " + plainText);
        Console.WriteLine("Encrypted Data (Hex): " + ByteArrayToHexString(encryptedData));
        Console.WriteLine("Decrypted Text: " + decryptedText);
    }
    static byte[] Encrypt(string plainText, byte[] key)
    {
        using (Aes aes = Aes.Create())
        {
            aes.KeySize = 256;
            aes.Key = key;
            aes.GenerateIV();
    
            ICryptoTransform encryptor = aes.CreateEncryptor();
    
            byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
            byte[] encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
    
            byte[] encryptedData = new byte[aes.IV.Length + encryptedBytes.Length];
            Array.Copy(aes.IV, 0, encryptedData, 0, aes.IV.Length);
            Array.Copy(encryptedBytes, 0, encryptedData, aes.IV.Length, encryptedBytes.Length);
    
            return encryptedData;
        }
    }
    
    static string Decrypt(byte[] encryptedData, byte[] key)
    {
        using (Aes aes = Aes.Create())
        {
            aes.KeySize = 256;
            aes.Key = key;
    
            byte[] iv = new byte[aes.BlockSize / 8];
            byte[] encryptedBytes = new byte[encryptedData.Length - iv.Length];
    
            Array.Copy(encryptedData, 0, iv, 0, iv.Length);
            Array.Copy(encryptedData, iv.Length, encryptedBytes, 0, encryptedBytes.Length);
    
            aes.IV = iv;
    
            ICryptoTransform decryptor = aes.CreateDecryptor();
    
            byte[] decryptedBytes = decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
    
            string decryptedText = Encoding.UTF8.GetString(decryptedBytes);
            return decryptedText;
        }
    }
    static byte[] HexStringToByteArray(string hexString)
    {
        int byteCount = hexString.Length / 2;
        byte[] bytes = new byte[byteCount];
        for (int i = 0; i < byteCount; i++)
        {
            bytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
        }
        return bytes;
    }
    
    static string ByteArrayToHexString(byte[] bytes)
    {
        StringBuilder sb = new StringBuilder(bytes.Length * 2);
        foreach (byte b in bytes)
        {
            sb.AppendFormat("{0:x2}", b);
        }
        return sb.ToString();
    }
    
    private static BigInteger power(BigInteger a, BigInteger b, BigInteger P)
    {
        return BigInteger.ModPow(a, b, P);
    }
    

    Again this is not so much about the safety or security of this and obviously should you use this do so in a way that matches your requirements like only ever use a key once, so if someone did get a key they could only decrypt 1 message, not an entire thread and obviously don't store things like the shared secret, but all in all this is what I was looking for to be able to modify and adapt to my code so I simplified this so others who are working with ECDSA can adapt to use ECDH to produce shared secrets and encrypt messages.

    Also, just as a side note, this uses just standard Microsoft libraries so no need for any extra.

    I made this into a project/nuget. Feel free to use and leave any comments on things: https://github.com/ReserveBlockIO/ecdsatoecdh

    https://www.nuget.org/packages/ECDSAToECDH/