This EIP proposes a new way to encrypt and decrypt using Ethereum keys. This EIP uses only the secp256k1
curve, and proposes two new RPC methods: eth_getEncryptionPublicKey
and eth_performECDH
. These two methods, in conjunction, allow users to receive encryptions and perform decryptions (respectively). We require that the wallet only perform the core ECDH operation, leaving the ECIES operations up to implementers (we do suggest a standardized version of ECIES, however). In contrast, a previous EIPs used the same secret key, in both signing and encryption, on two different curves (namely, secp256k1
and ec25519
), and hardcoded a particular version of ECIES.
We discuss a few motivating examples. One key motivation is direct-to-address encryption on Ethereum. Using our EIP, one can directly send encrypted messages to some desired recipient on-chain, without having a prior direct channel to that recipient. (Note that in this EIP, we standardize only the encryption procedure—that is, the generation of the ciphertext—and not how exactly the on-chain message should be sent. In practice, ideally, smart-contract infrastructure will be set up for this purpose; barring this, encryptors could make use of the raw data
field available in each standard transfer.)
We discuss a second sort of example. In a certain common design pattern, a dApp generates a fresh secret on behalf of a user. It is of interest if, instead of forcing this user to independently store, safeguard, and back up this latter secret, the dApp may instead encrypt this secret to a public key which the user controls—and whose secret key, crucially, resides within the user's HD wallet hierarchy—and then post the resulting ciphertext to secure storage (e.g., on-chain). This design pattern allows the dApp/user to bootstrap the security of the fresh secret onto the security of the user's existing HD wallet seed phrase, which the user has already gone through the trouble of safeguarding and storing. This represents a far lower UX burden than forcing the user to store and manage fresh keys directly (which can, and often does, lead to loss of funds). We note that this design pattern described above is used today by, various dApps (e.g., Tornado Cash).
We describe our approach here; we compare our approach to prior EIPs in the Rationale section below. Throughout, we make reference to SEC 1: Elliptic Curve Cryptography, by Daniel R. L. Brown.
We use the secp256k1
curve for both signing and encryption.
For encryption, we use ECIES. We specify that the wallet only perform the sensitive ECDH operation. This lets implementers select their own ECIES variants at will.
We propose that all binary data be serialized to and from 0x
-prefixed hex strings. We moreover use 0x
-prefixed hex strings to specify private keys and public keys, and represent public keys in compressed form. We represent Ethereum accounts in the usual way (0x
-prefixed, 20-byte hex strings). Specifically, to serialize and deserialize elliptic curve points, implementers MUST use the following standard:
to deserialize a point: use [SEC 1, §2.3.3], while requiring point compression; that is:
the input byte string MUST have length ⌈log₂q / 8⌉ + 1 = 33
.
0x02
or 0x03
.For application-level implementers actually implementing ECIES, we propose the following variant. Unless they have a reason to do otherwise, implementers SHOULD use the following standardized choices:
ANSI-X9.63-KDF
, where the hash function SHA-512
is used,HMAC–SHA-256–256 with 32 octet or 256 bit keys
,AES–256 in CBC mode
.We propose that the binary, concatenated serialization mode for ECIES ciphertexts be used, both for encryption and decryption, where moreover elliptic curve points are compressed.
Thus, on the request:
request({
method: 'eth_getEncryptionPublicKey',
params: [account]
})
where account
is a standard 20-byte, 0x
-prefixed, hex-encoded Ethereum account, the client should operate as follows:
sk
corresponding to the Ethereum account account
, or else return an error if none exists.secp256k1
public key corresponding to sk
.0x
-prefixed, hex-encoded form, following [SEC 1, §2.3.3].On the request
request({
method: 'eth_performECDH',
params: [account, ephemeralKey]
})
where account
is as above, and ephemeralKey
is an elliptic curve point encoded as above:
sk
corresponding to the Ethereum account account
, or else return an error if none exists.ephemeralKey
to an elliptic curve point using [SEC 1, §2.3.3] (where compression is required), throwing an error if deserialization fails.Test vectors are given below.
In light of account abstraction, EIP-4337, and the advent of smart-contract wallets, we moreover specify a way to encrypt to a contract. More precisely, we specify a way for a contract to advertise how it would like encryptions to it to be constructed. This should be viewed as an analogue of EIP-1271, but for encryption, as opposed to signing.
Our specification is as follows.
pragma solidity ^0.8.0;
contract ERC5630 {
/**
* @dev Should return an encryption of the provided plaintext, using the provided randomness.
* @param plaintext Plaintext to be encrypted
* @param randomness Entropy to be used during encryption
*/
function encryptTo(bytes memory plaintext, bytes32 randomness)
public
view
returns (bytes memory ciphertext);
}
Each contract MAY implement encryptTo
as it desires. Unless it has a good reason to do otherwise, it SHOULD use the ECIES variant we propose above.
There is no security proof for a scheme which simultaneously invokes signing on the secp256k1
curve and encryption on the ec25519
curve, and where the same secret key is moreover used in both cases. Though no attacks are known, it is not desirable to use a scheme which lacks a proof in this way.
We, instead, propose the reuse of the same key in signing and encryption, but where the same curve is used in both. This very setting has been studied in prior work; see, e.g., Degabriele, Lehmann, Paterson, Smart and Strefler, On the Joint Security of Encryption and Signature in EMV, 2011. That work found this joint scheme to be secure in the generic group model.
We note that this very joint scheme (i.e., using ECDSA and ECIES on the same curve) is used live in production in EMV payments.
We now discuss a few further aspects of our approach.
On-chain public key discovery. Our proposal has an important feature whereby an encryption to some account can be constructed whenever that account has signed at least one transaction.
Indeed, it is possible to recover an account's secp256k1
public key directly from any signature on behalf of that account.
ECDH vs. ECIES. We specify that the wallet only perform the sensitive ECDH operation, and let application-level implementers perform the remaining steps of ECIES. This has two distinct advantages:
Twist attacks. A certain GitHub post by Christian Lundkvist warns against "twist attacks" on the secp256k1
curve. These attacks are not applicable to this EIP, for multiple distinct reasons, which we itemize:
Our eth_performECDH
method is new, and so doesn't raise any backwards compatibility issues.
A previous proposal proposed an eth_getEncryptionPublicKey
method (together with an eth_decrypt
method unrelated to this EIP). Our proposal overwrites the previous behavior of eth_getEncryptionPublicKey
.
It is unlikely that this will be an issue, since encryption keys need be newly retrieved only upon the time of encryption; on the other hand, new ciphertexts will be generated using our new approach.
(In particular, our modification will not affect the ability of ciphertexts generated using the old EIP to be eth_decrypt
ed.)
In any case, the previous EIP was never standardized, and is not (to our knowledge) implemented in a non-deprecated manner in any production code today.
The secret signing key
0x439047a312c8502d7dd276540e89fe6639d39da1d8466f79be390579d7eaa3b2
with Ethereum address 0x72682F2A3c160947696ac3c9CC48d290aa89549c
, has secp256k1
public key
0x03ff5763a2d3113229f2eda8305fae5cc1729e89037532a42df357437532770010
Thus, the request:
request({
method: 'eth_getEncryptionPublicKey',
params: ["0x72682F2A3c160947696ac3c9CC48d290aa89549c"]
})
should return:
"0x03ff5763a2d3113229f2eda8305fae5cc1729e89037532a42df357437532770010"
If an encryptor were to encrypt a message—say, I use Firn Protocol to gain privacy on Ethereum.
—under the above public key, using the above ECIES variant, he could obtain, for example:
"0x036f06f9355b0e3f7d2971da61834513d5870413d28a16d7d68ce05dc78744daf850e6c2af8fb38e3e31d679deac82bd12148332fa0e34aecb31981bd4fe8f7ac1b74866ce65cbe848ee7a9d39093e0de0bd8523a615af8d6a83bbd8541bf174f47b1ea2bd57396b4a950a0a2eb77af09e36bd5832b8841848a8b302bd816c41ce"
Upon obtaining this ciphertext, the decryptor would extract the relevant ephemeral public key, namely:
"0x036f06f9355b0e3f7d2971da61834513d5870413d28a16d7d68ce05dc78744daf8"
And submit the request:
request({
method: 'eth_performECDH',
params: [
"0x72682F2A3c160947696ac3c9CC48d290aa89549c",
"0x036f06f9355b0e3f7d2971da61834513d5870413d28a16d7d68ce05dc78744daf8"
]
})
which in turn would return the Diffie–Hellman secret:
"0x4ad782e7409702101abe6d0279f242a2c545c46dd50a6704a4b9e3ae2730522e"
Upon proceeding with the above ECIES variant, the decryptor would then obtain the string I use Firn Protocol to gain privacy on Ethereum.
.
Our proposal uses heavily standardized algorithms and follows all best practices.
Copyright and related rights waived via CC0.