Tutorial on how to use native MetaMask encryption-decryption functions
Every Ethereum account is associated with private/public key pair. A natural question is how to use these keys for encrypting data? Does MetaMask support encryption? This tutorial will show a user-friendly way to use MetaMask to exchange encrypted messages on a blockchain.
When developing our latest project FELToken — a tool for decentralized privacy-preserving machine learning — we needed to exchange data only between concrete accounts on the blockchain. Every account is associated with a key pair, which primary purpose is for signing transactions. However, it can be used for encryption as well. The only issue is that users’ wallets usually handle these keys, and asking users to copy their private keys is terrible. Using functions provided by the wallet is a much cleaner solution.
Here is just a quick review of public-key cryptography. The core idea is that you have public and private keys linked by some mathematical properties. If you encrypt a message with the public key, only the private key can decrypt it. This means that you can share the public key with other people. They can then use it for encrypting messages which only you can decrypt. Moreover, encryption works the other way round, commonly used for signing messages.
Essential for us is that if Alice wants to send a secret message to Bob, it requires the following steps:
- Bob sends Alice his public key
- Alice encrypts the message using Bob’s public key
- Alice sends the encrypted message to Bob
- Bob decrypts the message using his private key
Cryptography is a crucial part of the web, regardless of the number, and in my opinion, every developer should know at least the basics of cryptography. For more practical info, I recommend the following book:
Luckily, there is a native decryption function inside MetaMask. On the other hand, the encryption mechanism isn’t standardised, so this function most likely isn’t supported by other wallets. Regardless of that, let’s look at the MetaMask decryption in more detail.
Most information about this function is provided in the MetaMask documentation:
I will mention this later as well, but the documentation skipped the fact that the decryption works only if the plain text contains valid UTF-8. It won’t work for arbitrary bytes sequences!
1. Bob sends Alice his public key
The first step is to obtain Bob’s public key and send it to Alice. This key is necessary for encryption, and it can be publicly shared. In fact, you share this key with every blockchain transaction you send. You might already know that the account address isn’t the same as the public key. The address is calculated as a hash of the public key, making it impossible to easily calculate the public key for based on user address (untunate decision, in my opinion).
We first need to request a public key from MetaMask. We can use function
eth_getEncryptionPublicKey provided by MetaMask. Using this function is relatively straightforward; just pass the account address. The app must have access to a specified account; Passing a random account will fail.
Notice: MetaMask automatically injects the
window.ethereumobject to websites. If you use some library for communicating with MataMask, you might need to access this object differently (the request part will be the same).
Now that you have the public key, you can send it to Alice so she can use it for encryption. The good news is that the public key fits into
bytes32 variable; hence you can easily pass it to smart contracts. Or you can use any other way for exchanging the public key.
2. Alice encrypts the message using Bob’s public key
Once we have the public key, we can proceed with encrypting the message. You will need to install a package from MetaMask:
@metamask/eth-sig-util that provides the encryption function. Then for encrypting data provided as
Bufferwe use the following function (public key is same as obtained in the previous step):
Important: MetaMask decrypts only valid UTF-8 sequences, so if you want to send arbitrary bytes, you must first encode it using something like base64. I am using base85 provided by
ascii85 package. I tried to save as much space as possible since we are storing the encrypted data in a smart contract.
3. Alice sends the encrypted message to Bob
The exchange of encrypted messages is up to you. We are using a smart contract for storing and exchanging encrypted messages. In your smart contract, you will probably use
bytes type for storing the encrypted message. We are also using
ethers.js package for interaction with the smart contract, which requires type
number when sending data to
bytes argument. If you want to do the same, you can convert the buffer to
number type as:
numberArray = buffer.toJSON().data;
4. Bob decrypts the message using his private key
Finally, when Bob receives the encrypted data, he can reconstruct the original object and request MetaMask to decrypt it for him. All that is done using the following function (
data buffer is the buffer outputted by the encryption function):
Now we were in the situation where we needed to interact with the smart contract using Python as well. We need matching encryption/decryption functions implemented in Python in such a case. You will need to install
PyNaCl, which provides both encryption and decryption functions. Then you can use the following code:
I hope people will start using this encryption mechanism more, forcing some standardization across the wallets. I tried not to complain too much throughout the tutorial, but the current state is truly a mess. Some design decisions strike me every time I have to deal with them. I guess we have to make this sacrifice for a user-friendly interaction.
Anyways if you liked the article, consider following me and FELTOKEN. There will be more articles about Web3 development.