Since metamask never exposes the private key of a wallet they added another way of encrypting and decrypting data:
https://docs.metamask.io/guide/rpc-api.html#eth-decrypt
https://docs.metamask.io/guide/rpc-api.html#eth-getencryptionpublickey
https://docs.metamask.io/guide/rpc-api.html#encrypting
Since metamask is "special" in this way it is very hard to support encrypting in multiple wallets. So this is a request to add metamasks methods to ethers.js.
Here is how MetaMask can decrypt messages:
// string to buffer to UInt8Array
const recieverPrivateKeyUint8Array = nacl_decodeHex(receiverPrivateKey);
const recieverEncryptionPrivateKey = nacl.box.keyPair.fromSecretKey(
recieverPrivateKeyUint8Array
).secretKey;
// assemble decryption parameters
const nonce = naclUtil.decodeBase64(encryptedData.nonce);
const ciphertext = naclUtil.decodeBase64(encryptedData.ciphertext);
const ephemPublicKey = naclUtil.decodeBase64(
encryptedData.ephemPublicKey
);
// decrypt
const decryptedMessage = nacl.box.open(
ciphertext,
nonce,
ephemPublicKey,
recieverEncryptionPrivateKey
);
// return decrypted msg data
let output;
try {
output = naclUtil.encodeUTF8(decryptedMessage);
} catch (err) {
throw new Error('Decryption failed.');
}
if (output) {
return output;
}
throw new Error('Decryption failed.');
Source:
https://github.com/MetaMask/eth-sig-util/blob/3d96bb1e1e5d1a1c548095b18e17fdb2802abe5b/src/index.ts#L503-L537
https://github.com/MetaMask/eth-simple-keyring/blob/b23bbaf5cb2ac21a33ebaafc567af6cb1cb57cae/index.js
https://github.com/MetaMask/eth-simple-keyring/blob/b23bbaf5cb2ac21a33ebaafc567af6cb1cb57cae/README.md
https://github.com/MetaMask/eth-simple-keyring/search?q=decryptMessage&type=code
https://github.com/MetaMask/KeyringController/blob/master/package.json#L48
https://github.com/MetaMask/KeyringController/blob/1a0dbbaf45e54e7b80200d4a9c6b21ce74b61b2f/index.js#L424
https://github.com/MetaMask/metamask-extension/blob/38fe75b7d9255b0fb7515ef2f78250a5b9791aa3/app/scripts/lib/encryption-public-key-manager.js#L82
💰 I commit a bounty of USD 250 to anybody that can solve this issue. Terms:
eth-decrypt, eth-getencryptionpublickey, encrypting above.Please don’t jump on opening a PR or working on this until I have a chance to evaluate whether this is something that makes sense to add to ethers.
I’ve considered adding similar features to ethers before, but the consequences and caveats to making encryption “easy” require people to think carefully what they are doing. The example I usually give of this is that when data is encrypted, the length does not change, so you could imagine encrypting a vote for “yes” vs “no”; even when encrypted, if the encrypted text is 3 bytes long, the input was “yes”. So appropriate padding (or random size) is essential to prevent side-channel attacks.
I’ll research this more. Ideally there is an EIP in progress for something like this, and then the ability can be added more genetically to Signer. :)
(Or if you would like, an ancillary package doesn’t require my blessings on any level, if you want to jump the gun ;))
@ricmoo This is more request to copy the functionality of metamask as is. Since we would like compatibility to encrypt with ethers and decrypt via metamask and the other way around.
Normally you could use the wallets public key to encrypt and its private key to decrypt but metamask does not allow access to the private key via interface and has completely different way of generating an encryption public key and a metamask only eth_decrypt method to decrypt this. This makes it a pain in the ass for compatibility and since metamask will not change their ways the other solution is to support this here 🤷♂️
Right. My concern is adding methods to Provider that only work on MetaMask. It is important to keep things abstract, and come up with standards so things continue to work in the future and for users not using metamask.
I just want a moment to research this and reach out to MM before I get flooded with a bunch of PRs because of a bounty offered on this. I am quite particular with making changes, especially changing/adding nee methods to fundamental classes.
If the standard is sound, or on its way to broader adoption (EIP or other wallets following suite), then I can also add it to the Wallet object and other Signers.
Just to add to this, you can still use provider.send for any specific functionality like the mentioned methods of metamask, until this is looked upon.
await window.ethereum.enable()
const provider = new ethers.providers.Web3Provider(window.ethereum);
const accounts = await provider.listAccounts();
const pubkey = await provider.send('eth_getEncryptionPublicKey', [accounts[0]]);
console.log(pubkey); // zpKOsHVU1YdbTKwZJ4u/YBSsu+q6VxJvTfnU8LLCmCg=
The docs are available here.
@ricmoo Ok, I get it :) Will wait for you decision.
@zemse Yeah the problem here is that users don't have metamask installed. Encrypt and decrypt should be supported without a provider needed since there are not actually blockchain operations if you know what I mean...
Now I’m confused again. How would encrypt and decrypt work if they don’t have MetaMask installed?
Oh, so this feature request is about encryption and decryption methods like metamask to be implemented for general wallets/signers. If there is a signer e.g. Wallet, that keeps the private key with it, then it would work. But if there is a Signer other than metamask (like a ledger or trezor) which similarly doesn't expose private key, then how can those methods be implemented for them? Pls correct me if I am misunderstanding.
@zemse Exactly. No it would not work for ledger or trezor. This are not actually blockchain functionalities its just metamasks way to reveal them as eth_decrypt and eth_getEncryptionPublicKey RPC calls. And the encryption itself is already a standalone lib as you can see here: https://docs.metamask.io/guide/rpc-api.html#encrypting
So if you have a wallet with privateKey it should be possible to generate the same encryption key as in eth_getEncryptionPublicKey call and be able to decrypt as in eth_decrypt.
Ok, so it sounds like these methods can only be implemented for Wallet class since it is supposed to be initialized with a private key, while no changes in general Signer.
You can still extend the Wallet class and add those methods to it, you can even publish the package.
class WalletExtended extends ethers.Wallet {
// implement these methods
}
Do you want to add any idea as to why this should be included in the ethers.js code base (since the code snippet appears to contain some dependencies), this might help @ricmoo to take a positive decision.
Yeah my point of view is that with NFTs and other kinds of blockchain applications encrypting / decrypting or to better say restricting access to only a specific person / owner will become more and more important. Wallets are literally private/public keys and are very useful for this kinds of applications. Like you can encrypt a file with someones public key only because he has a specific token in his wallet. And he and only he will be able to decrypt it with said wallet. The problem arises with compatibility. There is no coherent way to encrypt / decrypt across different wallets. I believe metamasks approach is ok since this can then also get supported by wallets like ledger and trezor. And having this supported in ethers will further approve the options on the market and allow more secure / better applications. So I only see reason too support rather then not.
This is one reason I want to open a dialog with MetaMask too. There are actually EIP-191 compatible ways to do this, which would mean Ledger and any weird signer (that manages a key) could perform these operations.
In a nutshell, you would add a new version to EIP-191 with some qualified indentifying field (probably the hash of the origin?), sign that and then hash that to get the private key. Then any encrypted data should have an ephemeral session key randomly chosen and the public key pretended tot be encrypted data, used for the ECDH shared secret. I’m always a fan of the CTR mode of operation, but CBC might make more sense, since we want the size to be fuzzy anyways.
By choosing an unused version we can be relatively assured that data has not been signed before. Also this require RFC-6979 be used, but most popular signing libraries do that already.
This way won’t be backwards compatible with existing keys, but the sooner we move to the generalized solutions, the better. :)
And we can provide some alternate way methods for legacy encryption...
(this is something I have thought a lot about, but want it down properly, not just stapled in ;))
@ricmoo I am all for opening a dialog and standardising a way :) So if there are any discussions open I would love to contribute. I would also like to ship apps with best UX asap 😂 But yeah I understand.
Just my 5 cents: adding x25519-xsalsa20-poly1305 would increase pkg size. That's three abstractions: curve25519, XSalsa20, Poly1305 - neither of which are browser built-ins.
I don't know why everything has to be complicated.
Can we open a discussion with metamask regarding this so we can find a solution that works on both ends?
@fulldecent Can you help with this?
I understand that there is interest in a new standard for encrypting/decrypting files. That effort is identified as requiring:
In my experience, it is infeasible that this can be accomplished in 2021 with the slowest part being MetaMask and firmware upgrades. For reference I can say that it has taken several years for MetaMask to adopt ERC-721 and their support was minimal at that. I am a biased source here but some people consider 721 to be a super-important use case.
Please use a new issue to discuss any new possible standards and achieve consensus with other projects.
For this issue I hope we can continue to discuss making Ethers.JS compatible with the existing and well-documented (i.e. open source software) process that MetaMask already supports.
I can help by bringing resources to this effort. I quadruple my original bounty (now USD 1,000) and extend this commitment to 2021-07-31 (previously 2021-07-02).
Most helpful comment
I don't know why everything has to be complicated.