Do not put your private keys in .env files in clear text. Use CryptoEnv!
I work a lot with blockchain, and my favorite framework to manage smart contracts in Solidity is Hardhat. To deploy a smart contract to an EVM blockchain, people put their private key in a .env
file. The file is git-ignored, of course; still, mistakes are behind the corner and the approach is very risky.
A few weekends ago, on a quiet Sunday, I got scared because I risked to share on GitHub a private key by mistake, because of a typo in .gitignore
and, then, I decided to solve the problem once and for all.
That Sunday, I built two tools…
Initially, I thought of a wrapper around Hardhat. A plugin would have been much better, but I hate Typescript and that’s it :-(
I called it Hardhood and it encrypts keys in a hidden folder inside the user’s home. However, Hardhat launches many processes and the captured shell output loses part of what is happening. I am sure I can fix it, but I wanted something that day. So, I took a break of a few hours, and when I went back to my laptop, I decided to embrace the way people does it — putting keys in .env
— improving that process, encrypting those keys.
CryptoEnv is the result of that effort. Let’s see how to use it.
In the shell
To set up your encrypted variables, you must install CryptoEnv globally
npm i -g cryptoenv
Then, to create a new encrypted env variable for OWNER_KEY
move in the folder where your app is, and run
cryptoenv -n OWNER_KEY
CryptoEnv will ask for
- the data to be encrypted
- the password to encrypt it
Then, it will save the encrypted private key in .env
, creating the file if it does not exist.
In the case above, in your .env
file you will have something like
cryptoEnv_OWNER_KEY=vnJSFJ5E4ZHT1hd8tmMduc1HbQqmkXE/dReUmjHFvud5DsquU6VrOZ+1K3wFj2wYIc8KaClbZWlAtG5HuE2QfE1hx3snHBpz0sqkhfM2v8gTTR77RnLZ23GcKYTGa2G5frcuECngSpE=
In your node app
Install it as usual
npm i -D cryptoenv
Let’s do the case of Hardhat.
You have a conf file called hardhat.config.js
. At the beginning of that file you can read the environmental variables with, for example, Dotenv and require CryptoEnv, like
require("dotenv").config();
require("cryptoenv").parse();
Later in the file, when you configure Hardhat to use your private keys, you can have something like
...
ropsten: {
url: `https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`,
accounts: [
process.env.OWNER_KEY
],
chainId: 3
},
...
Then, when you run, for example, a script to deploy to Ropsten, Hardhat will show the prompt:
CryptoEnv > Type your password to decrypt the env, or press enter to skip it
Note: To avoid that Hardhat gives you an error when you skip the decryption, you can set up a variable OWNER_KEY in the .env
file, with a fake key. When you input the password, CryptoEnv will overwrite the variable.
Notice that after saving the first encrypted key, for all the others you must use the same password.
What if the app tries to decrypt more than one time?
Some apps launch child processes. If they run more than one, the environment does not look decrypted and CryptoEnv makes a new request.
For example, when you run a script with Hardhat, it often runs a first process to compile the smart contracts, then runs a second process to execute the script. In that case, you can press enter at the first request, and input the password only at the second.
Filtering for multiple apps
Sometimes you have in a repo multiple apps and it is possible that you do not want to share data with them. You can filter your variables using RegExp like here:
require("cryptoenv").parse(/^hardhat/);
and take only the variables that start with “hardhat”. You can also pass a function that returns a boolean
const words = ["home", "office", "street"];
require("cryptoenv").parse((e) => words.includes(e));
You can also use a function for more general cases.
For example, if you want to skip the decryption when testing the contracts with Hardhat, you could require it as:
require("cryptoenv").parse(() => {
return NODE_ENV !== "test";
});
(notice that Hardhat does not set the NODE_ENV variable during tests)
Security
CryptoEnv uses the package @secrez/crypto from Secrez https://github.com/secrez/secrez
What’s next?
There are many ways to improve CryptoEnv.
Here a few ideas:
- Support Hardhood’s approach with a global conf file instead of a local
.env
for better security and to avoid involuntary shares. - Encrypt keys with different passwords (useful, for example, if the
.env
file is shared with a team). The easiest way to do it is to ignore wrong decryptions (instead of throwing an error). - Make plugins for Dotenv, Hardhat, Truffle, etc. to simplify the usage. But I leave this to the community :-)
- Support an explicit destination file instead of forcing an
.env.
For example, the target could beenv.json
. In this case, CryptoEnv should save the data respecting the JSON format.
Probably, I will also add an env command in Secrez to export encrypted keys directly from it (which would add more security).
CryptoEnv is open source. Feel free to fork it.
If you like this post, please 👏 and share it!
UPDATE April 2023
The package has been moved from cryptoenv
to @secrez/cryptoenv
and many features have been added since this post has been written. Look at the History section in the README for more details.