How to Encrypt Data to a Public Key with ECIES
Symmetric encryption needs both sides to hold the same secret before any message is sent. Public-key encryption removes that step. A sender who knows only the recipient's public key can produce a ciphertext that the recipient's private key alone can open, which suits messages to a wallet address, a device identity or any party whose public key is already published.
Sphere10 Framework provides this through the Elliptic Curve Integrated Encryption Scheme (ECIES) in the Sphere10.Framework.CryptoEx package. The same key pair that signs with ECDSA or Schnorr can receive encrypted messages. This article covers the API, the layout of the ciphertext, and the signed envelope that should surround it.
1. Encrypt and decrypt
The recipient creates a key pair and publishes the public key.
var ecdsa = new ECDSA(ECDSAKeyType.SECP256K1);
var recipientKey = ecdsa.GeneratePrivateKey();
var recipientPublicKey = ecdsa.DerivePublicKey(recipientKey);
Anyone holding the public key can encrypt, and only the private key decrypts.
var ciphertext = ecdsa.IES.Encrypt(plaintext, recipientPublicKey);
var decrypted = ecdsa.IES.Decrypt(ciphertext, recipientKey);
Require(decrypted.SequenceEqual(plaintext), "Decryption should return the plaintext.");
Decrypt is an extension method that throws when decryption fails. TryDecrypt is the underlying member and returns false instead, which is the form to use for input that may be damaged or meant for someone else.
Require(!ecdsa.IES.TryDecrypt(ciphertext, out _, ecdsa.GeneratePrivateKey()), "Another key should not decrypt.");
var corrupted = (byte[])ciphertext.Clone();
corrupted[^1] ^= 1;
Require(!ecdsa.IES.TryDecrypt(corrupted, out _, recipientKey), "A changed ciphertext should not decrypt.");
Because every encryption uses a new temporary key, encrypting the same message twice gives two different ciphertexts. An observer cannot tell that the plaintexts were equal.
Keys from a Schnorr scheme work the same way through schnorr.IES, and the four ECDSAKeyType curves are all supported.
2. How ECIES is exposed
Every signature scheme in the Framework has an IES property of type IIESAlgorithm. The interface has two members. Encrypt takes a message and a public key, and TryDecrypt takes a ciphertext and a private key. ECDSA.IES and Schnorr.IES both return the CryptoEx ECIES implementation.
Encryption generates a temporary key pair for each message. The temporary private key and the recipient's public key produce a shared secret by Diffie-Hellman agreement, the secret is expanded into an encryption key and a MAC key, and the temporary public key travels with the ciphertext so that the recipient can derive the same secret.
Diagram source
---
config:
sequence:
actorMargin: 8
width: 96
mirrorActors: false
messageMargin: 28
noteMargin: 8
---
sequenceDiagram
participant S as Sender
participant R as Recipient
R->>S: public key
Note over S: new ephemeral key
Note over S: Encrypt(message, key)
S->>R: ciphertext
Note over R: TryDecrypt(private key)
3. The ciphertext layout
ECIES writes the format used by PascalCoin's OpenSSL-based implementation, so its output is interoperable with that network. The body is encrypted with AES-256 in CBC mode, and an HMAC-MD5 tag is computed over the encrypted body.
A 23-byte message encrypted to a secp256k1 key produces 87 bytes.
The six-byte header holds the length of the temporary public key, the length of the tag, the original message length and the body length. The body is the message padded with zeros to a whole number of 16-byte blocks, and decryption returns exactly the original length recorded in the header.
Diagram source
---
config:
packet:
bitsPerRow: 16
bitWidth: 22
showBits: false
---
packet-beta
0-5: "Header, 6 bytes"
6-15: "Ephemeral key, 33 bytes"
16-31: "HMAC-MD5 tag, 16 bytes"
32-47: "AES-256-CBC body, 16-byte blocks"
The fixed cost is therefore 55 bytes on secp256k1, plus up to 15 bytes of padding. Larger curves have longer temporary keys.
4. Wrap the ciphertext in a signed envelope
Two properties of the format mean the ciphertext should not be trusted on its own. The tag covers the encrypted body but not the header, and nothing in ECIES identifies the sender, since anyone with the public key can encrypt. A signature over the whole ciphertext addresses both.
var senderKey = ecdsa.GeneratePrivateKey();
var senderPublicKey = ecdsa.DerivePublicKey(senderKey);
var envelopeSignature = ecdsa.Sign(senderKey, ciphertext);
Require(ecdsa.Verify(envelopeSignature, ciphertext, senderPublicKey), "The envelope signature should verify.");
Require(!ecdsa.Verify(envelopeSignature, corrupted, senderPublicKey), "A changed envelope should fail verification.");
The sender signs the ciphertext with its own private key and sends both. The recipient verifies the signature against the sender's known public key first, and decrypts only when it holds. This gives the recipient the two facts ECIES alone does not, namely who sent the message and that no byte of it changed.
5. Limits to plan for
-
Messages are limited to 32,000 bytes.
Encryptthrows anArgumentExceptionabove that. For larger data, encrypt a random symmetric key with ECIES and encrypt the data with that key. -
Only ECDSA and Schnorr keys are accepted. The hash-based schemes do not support
IES. -
The format is fixed for compatibility. The cipher, the zero initialisation vector and the MD5-based tag follow the PascalCoin format and cannot be configured.
-
There is no forward secrecy for the recipient. Anyone who later obtains the recipient's private key can decrypt every message sent to it.
Encrypted<TObject>, which derives its key with PBKDF2 and checks an authentication tag before deserializing.
6. Get the package and run the sample
IIESAlgorithm is in Sphere10.Framework, and ECDSA, Schnorr and ECIES are in Sphere10.Framework.CryptoEx. The sample targets .NET 10.
dotnet new console --framework net10.0 --name ArticleSample
cd ArticleSample
dotnet add package Sphere10.Framework --version 3.1.3
dotnet add package Sphere10.Framework.CryptoEx --version 3.1.3
The complete MIT-licensed sample contains the program shown above. Extract the download and run it.
dotnet run -c Release
Each line is a check that throws on failure.
Plaintext 23 bytes, ciphertext 87 bytes, decrypted: passed
Second encryption of the same message differs: passed
Wrong key and changed ciphertext rejected: passed
Message over 32,000 bytes rejected: passed
Schnorr key round trip: passed
Signed envelope verified before decryption: passed
The CryptoEx guide documents the format and its compatibility rules, and the ECIES source shows the cipher configuration.
The Sphere10 Framework overview describes the rest of the toolkit.
Author: Herman Schoenfeld