Update (2022.03.25)
From @Ismael's comment and MetaMask github issue:
- The key from MetaMask's
eth_getEncryptionPublicKey
rpc is NOT the Public Key of my account, but a different key calculated from my private key with the nacl implementation of the X25519_XSalsa20_Poly1305 algorithm, which is different from Ethereum's default algorithm. - MetaMask holds the private key, but it doesn't provide a function to decrypt messages with the private key.
Update (2022.03.15)
Now message encrypting works. But message decrypting still doesn't work with public key from eth_getEncryptionPublicKey
+ eth_decrypt
. I've updated the Gist
Preface:
- I have a project that requires encrypting / decrypting multiple messages.
- Calling ethereum.request() for encrypting and decrypting triggers many MetaMask pop-ups and requires many user interactions.
- I found eth-crypto which seems to be able to reduce user interactions by calling it's built-in encrypt/decrypt functions.
What I found:
- With the same account (private key) in MetaMask, I got different pubkey if retrieving/recovering in different ways.
- Those pubkeys look different, in different length.
- Those keys seem not compatible to each other. When using them to encrypt message for example:
- Works: Use EthCrypto with EthCrypto recovered public key, Use Ethereum (MetaMask) with EthCrypto recovered public key, Use Ethereum with Ethereum public key.
- Doesn't work: Use EthCrypto with Ethereum (MetaMask) public key. (I want this most.)
Question:
- Are they totally different things?
- Is there any way to convert from one to another?
- What do I need to do to make the pubkey from eth_getEncryptionPublicKey works with EthCrypto.encryptWithPublicKey()?
Sample codes:
- Method 1: Get pubkey from web3/MetaMask directly
let pubkey_eth = await window.ethereum.request({
"method": "eth_getEncryptionPublicKey",
"params": [selectedAccount], // you must have access to the specified account
});
console.log(Buffer.from(pubkey_eth, "base64").toString("hex"));
bbfbb7e44ad3f14dfdf7089eab8a2a8407efddc5dbfeb3b00e15d2f996d1ab06 (string length 64)
let pubkey_eth_decompressed_prefix02 = EthCrypto.publicKey.decompress("02" + Buffer.from(pubkey_eth, "base64").toString("hex"));
let pubkey_eth_decompressed_prefix03 = EthCrypto.publicKey.decompress("03" + Buffer.from(pubkey_eth, "base64").toString("hex"));
// results are different, both are not usable by encryption function from eth-crypto
window.Ethereum: My public key decompressed (prefix 02) from Ethereum is bbfbb7e44ad3f14dfdf7089eab8a2a8407efddc5dbfeb3b00e15d2f996d1ab069936428a47bcf50da95add19994b7d747b2dc6b3e18fd4034bd551fdd938314c (string length 128)
window.Ethereum: My public key decompressed (prefix 03) from Ethereum is bbfbb7e44ad3f14dfdf7089eab8a2a8407efddc5dbfeb3b00e15d2f996d1ab0666c9bd75b8430af256a522e666b4828b84d2394c1e702bfcb42aae0126c7cae3 (string length 128)
- Method 2: Recover pubkey from MetaMask signed message, using eth-crypto
let secretMsgSignature = await web3.eth.sign(
EthCrypto.hash.keccak256("Secret message"),
selectedAccount
);
const secretMsgSignature_personal = await new Promise((resolve, reject) => {
web3.currentProvider.sendAsync({
"method": "personal_sign",
"params": [web3.utils.utf8ToHex("Secret message"), selectedAccount],
"from": selectedAccount,
}, (err, response) => {
if (err) {return reject(err);}
resolve(response.result);
});
});
Signature is 0xdb2fe9c70f4bfdc69299bf0dabe823b7e405dafdaae5ca0ae4eef8ef1b3abd1e5a7b777cb71608bfc80e4d33c3fdf1d6bdbeeac861a14d718e16621263b1b23e1c
Signature (personal) is 0x0a42e36b34adbaa8c7ac926b0fb78d1b91bc1f19ae2cf0cb06a8330958f868130ec68ed47ae25474fe1f1cae202c0fb5e85f4e6baeaf1ac7f609002d81f6fc981b
Both length 132
let pubkey_ethcrypto = EthCrypto.recoverPublicKey(
secretMsgSignature,
EthCrypto.hash.keccak256("Secret message")
);
let pubkey_ethcrypto_compressed = EthCrypto.publicKey.compress(pubkey_ethcrypto);
// -------
const pubkey_ethcrypto_personal = EthCrypto.recoverPublicKey(
secretMsgSignature_personal,
web3.eth.accounts.hashMessage("Secret message")
);
let pubkey_ethcrypto_compressed_personal = EthCrypto.publicKey.compress(pubkey_ethcrypto_personal);
// none of the two is the same as the pubkey we got directly from metamask
EthCrypto: My public key is f4ce65a8d150c0434e16cf4de4c22fa1870b7eeb963ce9a503c504b21f8fe52f74bd3a005bd3d07eb0ea39e048452178f22cb02214810f538962bef40104ff68 (string length 128)
EthCrypto: My public key compressed is 02f4ce65a8d150c0434e16cf4de4c22fa1870b7eeb963ce9a503c504b21f8fe52f (string length 66)
EthCrypto: My public key (personal) is f4ce65a8d150c0434e16cf4de4c22fa1870b7eeb963ce9a503c504b21f8fe52f74bd3a005bd3d07eb0ea39e048452178f22cb02214810f538962bef40104ff68 (string length 128)
EthCrypto: My public key (personal) compressed is 02f4ce65a8d150c0434e16cf4de4c22fa1870b7eeb963ce9a503c504b21f8fe52f (string length 66)
public key from eth_getEncryptionPublicKey
+eth_decrypt
. Gist gist.github.com/bluet/e944003d6d2069666799cb398d48bb01eth_getEncryptionPublicKey
is not the one used for transactions and messages. From their docs The public key is computed from entropy associated with the specified user account, using the nacl implementation of the X25519_XSalsa20_Poly1305 algorithm. eth-sig-util has encryption and decryption that shouldn't require metamask interaction. Looking at EthCrypto package it uses ECCrypto which uses a different algorithm for encryption.eth_decrypt
to decrypt messages encrypted with Ethereum public key. I didn't realize that until I saw the issue on MetaMask github github.com/MetaMask/metamask-extension/issues/… .