Elliptic Curve Integrated Encryption Scheme (ECIES): Encrypting Using Elliptic Curves

With ECC (Elliptic Curve Cryptography), we have an opportunity to use both the power of public-key encryption, with the speed and security…

https://unsplash.com/photos/DlnK1KOREds

Elliptic Curve Integrated Encryption Scheme (ECIES): Encrypting Using Elliptic Curves

With ECC (Elliptic Curve Cryptography), we have an opportunity to use both the power of public-key encryption, with the speed and security of symmetric key encryption. And, so we slowly move to the best practice for encryption, where there’s an increasing consensus around:

  • Public key encryption key exchange: ECDH (P256), ECDH (P384), ECDH (P521), X25519 and X448.
  • Public key encryption: RSA and ECIES.
  • Hashing method for key derivation (HKDF): SHA256, SHA384 and SHA512.
  • Symmetric key: 128-bit AES GCM and 256-bit AES GCM.

All of the above methods are compatible with most systems. For this, Bob and Alice could pick an elliptic curve to define their key pair, and then use a given hashing methods to derive an encryption key. This is normally achieved with HKDF (HMAC Key Derivation Function). For the actual encryption, we can use symmetric-key encryption, as this is the most efficient and much faster than public key encryption. Overall, with this, there is a general move towards using AEAD (Authenticated Encryption with Additional Data). A typical mode for this is GCM.

So let’s build a hybrid encryption method with Golang.

The basics

Now, let’s say that Bob will send an encrypted message to Alice. Alice will then generate a key pair (a public key and a private key). She then sends her public key to Bob, and he then uses this to derive a symmetric key for the encryption (S). He then encrypts the message using K and with AES GCM. Bob receives the cipher (C) and a value of R. From R, she can then derive S from her private key. With this key, she can decrypt the cipher text to derive the plaintext message.

To achieve this in RSA, Alice would create a random symmetric key (KA), and then encrypt it with the public key of Bob, and then encrypt a file with KA. She would then send the encrypted key (Epk(KA)) to Bob, and the encrypted file, and then he would decrypt the encrypted key to reveal KA, and can then decrypt the file:

But, elliptic curve methods do not naturally encrypt data, so let’s see how this is done with these methods.

Theory

In this method, Alice generates a random private key (dA) and then takes a point on an elliptic curve (G) and determines her public key (QA):

QA=dA×G

G and QA are thus points on an elliptic curve. Alice then sends QA to Bob. Next, Bob will generate:

R=r×G

S=r×QA

and where r is a random number (nonce) generated by Bob. The symmetric key (S) is then used to encrypt a message.

Alice will then receive the encrypted message - along with R. She is then able to determine the same encryption key with:

S=dA×R

which is:

S=dA×(r×G)

S=r×(dA×G)

S=r×QA

and which is the same as the key that Bob generated. Once she has S, she now has the symmetric key that has encrypted the file, and can then decrypt it. The method is illustrated here:

Sample run

A sample run is:

Public key type:	HPKE_KEM_P256_HKDF_SHA256
Params kem_id: 16 kdf_id: 1 aead_id: 1
Key exchange parameters:
Ciphersize: 65
EncapsulationSeedSize: 32
PrivateKeySize: 32
PublicKeySize: 65
SeedSize: 32
SharedKeySize: 32
Cipher parameters:
Key Length: 16
Key derivation function:
Extract size: 32
Message: Testing 123
Cipher: 343363373461353461343437666463613435653437383435666537653264666334306331623239653439303333646236646334323663
Decipher: Testing 123

The code is based on [here]:

package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"strconv"
"github.com/cloudflare/circl/hpke"
)
func main() {
kemID := int(hpke.KEM_P256_HKDF_SHA256)
kdfID := int(hpke.KDF_HKDF_SHA256)
aeadID := int(hpke.AEAD_AES128GCM)
msg := "Hello"
argCount := len(os.Args[1:])
if argCount > 0 {
msg = os.Args[1]
}
if argCount > 1 {
kemID, _ = strconv.Atoi(os.Args[2])
}
if argCount > 2 {
kdfID, _ = strconv.Atoi(os.Args[3])
}
if argCount > 3 {
aeadID, _ = strconv.Atoi(os.Args[4])
}
suite := hpke.NewSuite(hpke.KEM(kemID), hpke.KDF(kdfID), hpke.AEAD(aeadID))
info := []byte("Test")
Bob_pub, Bob_private, _ := hpke.KEM(kemID).Scheme().GenerateKeyPair()
Bob, _ := suite.NewReceiver(Bob_private, info)
Alice, _ := suite.NewSender(Bob_pub, info)
enc, sealer, _ := Alice.Setup(rand.Reader)
Alice_msg := []byte(msg)
aad := []byte("Additional data")
ct, _ := sealer.Seal(Alice_msg, aad)
opener, _ := Bob.Setup(enc)
Bob_msg, _ := opener.Open(ct, aad)
fmt.Printf("Public key type:\t%s\n", Bob_pub.Scheme().Name())
fmt.Printf(" Params\t%s\n", suite.String())
fmt.Printf("Key exchange parameters:\n")
fmt.Printf(" Ciphersize:\t%d\n", hpke.KEM(kemID).Scheme().CiphertextSize())
fmt.Printf(" EncapsulationSeedSize:\t%d\n", hpke.KEM(kemID).Scheme().EncapsulationSeedSize())
fmt.Printf(" PrivateKeySize:\t%d\n", hpke.KEM(kemID).Scheme().PrivateKeySize())
fmt.Printf(" PublicKeySize:\t%d\n", hpke.KEM(kemID).Scheme().PublicKeySize())
fmt.Printf(" SeedSize:\t%d\n", hpke.KEM(kemID).Scheme().SeedSize())
fmt.Printf(" SharedKeySize:\t%d\n", hpke.KEM(kemID).Scheme().SharedKeySize())
fmt.Printf("Cipher parameters:\n")
fmt.Printf(" Key Length:\t%d\n", hpke.AEAD(aeadID).KeySize())
fmt.Printf("Key derivation function:\n")
fmt.Printf(" Extract size:\t%d\n", hpke.KDF(kdfID).ExtractSize())
fmt.Printf("\nMessage:\t%s\n", Alice_msg)
fmt.Printf("Cipher:\t%x\n", hex.EncodeToString(ct))
fmt.Printf("Decipher:\t%s\n", Bob_msg)
}

Conclusion

While RSA is still the most common form of public key encryption, ECIES does provide the power of public key and the speed of symmetric key. But, of course, RSA can be used with symmetric key encryption, and where we encrypt the symmetric key to be used with the public key. And, so, RSA is typically used for encrypt our encryption keys.