New approach for encryption / decryption
The ERC-5630 proposal outlines a new approach to encryption and decryption using Ethereum wallets. One key motivation is direct-to-address encryption on Ethereum, allowing encrypted messages to be sent to a desired recipient on-chain without a prior direct channel. Another use case is encrypting a fresh secret to a public key controlled by the user, which can be stored securely on-chain. The proposal standardizes only the encryption procedure, not how the on-chain message should be sent. The proposal uses heavily standardized algorithms and follows best practices for security. The proposal can be cited as Firn Protocol, Fried L. Trout, Weiji Guo, "ERC-5630: New approach for encryption / decryption [DRAFT]," Ethereum Improvement Proposals, no. 5630, September 2022.
Video
Original
Abstract
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.
Motivation
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).
Specification
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 serialize a point: use [SEC 1, §2.3.3], with point compression.
-
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
. - the first byte MUST be
0x02
or0x03
. - the integer represented by the remaining 32 bytes (as in [SEC 1, §2.3.8]) MUST reside in {0, ..., p - 1}, and moreover MUST yield a quadratic residue modulo p under the Weierstrass expression X^3 + 7 (modulo p).
- the input byte string MUST have length ⌈log₂q / 8⌉ + 1 =
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:
- the KDF
ANSI-X9.63-KDF
, where the hash functionSHA-512
is used, - the HMAC
HMAC–SHA-256–256 with 32 octet or 256 bit keys
, - the symmetric encryption scheme
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:
- find the secret signing key
sk
corresponding to the Ethereum accountaccount
, or else return an error if none exists. - compute the
secp256k1
public key corresponding tosk
. - return this public key in compressed,
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:
- find the secret key
sk
corresponding to the Ethereum accountaccount
, or else return an error if none exists. - deserialize
ephemeralKey
to an elliptic curve point using [SEC 1, §2.3.3] (where compression is required), throwing an error if deserialization fails. - compute the elliptic curve Diffie–Hellman secret, following [SEC 1, §3.3.1].
- return the resulting field element as an 0x-prefixed, hex-encoded, 32-byte string, using [SEC 1, §2.3.5].
Test vectors are given below.
Encrypting to a smart contract
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.
Rationale
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:
- Flexibility. It allows implementers to select arbitrary variants of ECIES, without having to update what the wallet does.
- Bandwidth. Our approach requires that only small messages (on the order of 32 bytes) be exchanged between the client and the wallet. This could be material in settings in which the plaintexts and ciphertexts at play are large, and when the client and the wallet are separated by an internet connection.
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:
- Only applies to classical ECDH, not ECIES. This attack only applies to classical ECDH (i.e., in which both parties use persistent, authenticated public keys), and not to ECIES (in which one party, the encryptor, uses an ephemeral key). Indeed, it only applies to a scenario in which an attacker can induce a victim to exponentiate an attacker-supplied point by a sensitive scalar, and then moreover send the result back to the attacker. But this pattern only happens in classical Diffie–Hellman, and never in ECIES. Indeed, in ECIES, we recall that the only sensitive Diffie–Hellman operation happens during decryption, but in this case, the victim (who would be the decryptor) never sends the resulting DH point back to the attacker (rather, the victim merely uses it locally to attempt an AES decryption). During encryption, the exponentiation is done by the encryptor, who has no secret at all (sure enough, the exponentiation is by an ephemeral scalar), so here there would be nothing for the attacker to learn.
- Only applies to uncompressed points. Indeed, we use compressed points in this EIP. When compressed points are used, each 33-byte string necessarily either resolves to a point on the correct curve, or else has no reasonable interpretation. There is no such thing as "a point not on the curve" (which, in particular, can pass undetectedly as such).
- Only applies when you fail to check a point is on the curve. But this is inapplicable for us anyway, since we use compressed points (see above). We also require that all validations be performed.
Backwards Compatibility
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.
Test Cases
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.
.
Security Considerations
Our proposal uses heavily standardized algorithms and follows all best practices.
Copyright
Copyright and related rights waived via CC0.
Adopted by projects
Not miss a beat of EIPs' update?
Subscribe EIPs Fun to receive the latest updates of EIPs Good for Buidlers to follow up.
View all