ECDH encryption using sr25519 keys

Elliptic-curve Diffie-Hellman (ECDH) is a key agreement protocol that allows two parties, each having an elliptic public-private key pair to establish a shared secret over an insecure channel. The shared secret can then be used to encrypt subsequent communications using a symmetric-key cipher. SR25519 is a Schnorr Signature protocol on top of the Ristretto compressed point format of the Ed25519 curve. This tutorial will explain in detail how the ECDH works and has also some code examples in golang.


You can find the full source code and examples at

How ECDH encryption works

ECDH is based on the following property of EC points:

(a * G) * b = (b * G) * a

If we have two secret numbers a and b (two private keys, belonging to Alice and Bob) and an ECC elliptic curve with generator point G, we can exchange over an insecure channel the values (a * G) and (b * G) (the public keys of Alice and Bob) and then we can derive a shared secret: secret = (a * G) * b = (b * G) * a. Pretty simple. The above equation takes the following form:

alicePubKey * bobPrivKey = bobPubKey * alicePrivKey = secret

The ECDH algorithm (Elliptic Curve Diffie-Hellman Key Exchange) goes like this:

  • Alice generates a random ECC key pair: alicePrivKey, alicePubKey
  • Bob generates a random ECC key pair: bobPrivKey, bobPubKey
  • Alice and Bob exchange their public keys through the insecure channel (e.g. over Internet)
  • Alice calculates sharedKey = bobPubKey * alicePrivKey
  • Bob calculates sharedKey = alicePubKey * bobPrivKey
  • Now both Alice and Bob have the same sharedKey == bobPubKey * alicePrivKey == alicePubKey * bobPrivKey

How to get shared secret using sr25519 keys

In order to generate the shared secret we will use the package created by George Tankersley

import (

	r255 ""

func SharedSecret(pub, priv []byte) ([]byte, error) {

	pre := &r255.Element{}
	err := pre.Decode(pub)
	if err != nil {
		return nil, err

	sc := &r255.Scalar{}
	err = sc.Decode(expandKey(priv))
	if err != nil {
		return nil, err

	e := r255.Element{}
	secret := e.ScalarMult(sc, pre).Encode([]byte{})
	return secret, nil

This function is doing a simple scalar multiplication of the public key of Alice with the private key of Bob.

In order to be able to do the multiplication we need fist to expand the private key using the following function:

// expandKey expands a secret key using ed25519-style bit clamping
func expandKey(key []byte) []byte {
	newKey := [32]byte{}
	h := sha512.Sum512(key)
	copy(newKey[:], h[:32])
	newKey[0] &= 248
	newKey[31] &= 63
	newKey[31] |= 64
	l := len(newKey) - 1
	low := byte(0)
	for i := range newKey {
		r := newKey[l-i] & 0x07
		newKey[l-i] >>= 3
		newKey[l-i] += low
		low = r << 5
	return newKey[:]

Next we should test if our implementation works as expected.

First let’s generate some key pairs and for this we can use a tool from Parity called subkey.

docker run -it --rm parity/subkey generate --scheme Sr25519

amd the output should be like this

Secret phrase:       seed cream flash destroy pistol twin fabric trend decrease hint leg describe
  Secret seed:       0x4371abacb33d4819e69ae66cb120a10a2403645926f9def758577278b39b43d3
  Public key (hex):  0x32bc4936c7925bfc8f7305b1672235254cdf9f0589474e06351365e461558d31
  Account ID:        0x32bc4936c7925bfc8f7305b1672235254cdf9f0589474e06351365e461558d31
  Public key (SS58): 4iGvtfaq4zF9ZvNRBpsPN3YE2KrVz8bidgZyR9GBVVBFenkn
  SS58 Address:      4iGvtfaq4zF9ZvNRBpsPN3YE2KrVz8bidgZyR9GBVVBFenkn

and another one for Bob

Secret phrase:       clarify battle shoot shy matrix erupt note often expect pull increase scale
  Secret seed:       0xb38d9ec4180312196f474fd36cc2919b121bafe9bb5185d86a3f97021e665177
  Public key (hex):  0xde372a0e94cc1f2c3e5ec27742c9a6011f3823c2e98f091d04c2899586d00d47
  Account ID:        0xde372a0e94cc1f2c3e5ec27742c9a6011f3823c2e98f091d04c2899586d00d47
  Public key (SS58): 4n9majJnhYE6tr8jkQ9csrdbdop7KnbBMxUJxSNDiKwyxGN3
  SS58 Address:      4n9majJnhYE6tr8jkQ9csrdbdop7KnbBMxUJxSNDiKwyxGN3

Nex we need to create our test function

import (


func TestSharedSecret(t *testing.T) {

	alicePrivKey,_ := hex.DecodeString("4371abacb33d4819e69ae66cb120a10a2403645926f9def758577278b39b43d3")
	alicePubKey,_ := hex.DecodeString("32bc4936c7925bfc8f7305b1672235254cdf9f0589474e06351365e461558d31")

	bobPrivKey,_ := hex.DecodeString("b38d9ec4180312196f474fd36cc2919b121bafe9bb5185d86a3f97021e665177")
	bobPubKey,_ := hex.DecodeString("de372a0e94cc1f2c3e5ec27742c9a6011f3823c2e98f091d04c2899586d00d47")

	aliceSecret, err := SharedSecret(bobPubKey, alicePrivKey)
	require.Nil(t, err)

	bobSecret, err := SharedSecret(alicePubKey, bobPrivKey)
	require.Nil(t, err)

	assert.Equal(t, encryptSecret, decryptSecret)

This is it. Now this shared secret can be used as a key to encrypt any message using a symmetric-key cipher.

In reality when you need to encrypt a message for Bob you don’t need to use the sender’s public key and it is more practical to use an ephemeral key pair and include the ephemeral public key into the final message.

  • Bob provides a public key: bobPubKey
  • A new ephemeral key is generated: ephemeralPubKey, ephemeralPrivKey
  • A shared secret is calculated: bobPubKey * ephemeralPrivKey
  • A message is encrypted with the shared secret: encryptedMessage
  • Bob will receive a message containing both ephemeralPubKey + encryptedMessage
  • When Bob wants to decrypt the message read the ephemeralPubKey and the encryptedMessage
  • Calculates the shared secret: ephemeralPubKey * bobPrivKey
  • Decrypts the message using the shared secret

For a complete example of how to do ECDHE with AES-128-CBC-HMAC-SHA1 check the example in this repository


I’m not an expert in cryptography. I’m not the actual author of all this. All I did is to copy pieces of code from the following source and put them together in a way that makes sense to me and I hope it will make sense for someone else too.

ECDH Key Exchange by Svetlin Nakov

Ristretto by The Ristretto Group