Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-5630: New approach for encryption / decryption

defines a specification for encryption and decryption using Ethereum wallets.

Authors Firn Protocol (@firnprotocol), Fried L. Trout, Weiji Guo (@weijiguo)
Created 2022-09-07
Discussion Link https://ethereum-magicians.org/t/eip-5630-encryption-and-decryption/10761

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 or 0x03.
    • 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).

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 function SHA-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 account account, or else return an error if none exists.
  • compute the secp256k1 public key corresponding to sk.
  • 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 account account, 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_decrypted.)

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 and related rights waived via CC0.

Citation

Please cite this document as:

Firn Protocol (@firnprotocol), Fried L. Trout, Weiji Guo (@weijiguo), "EIP-5630: New approach for encryption / decryption [DRAFT]," Ethereum Improvement Proposals, no. 5630, September 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5630.