Do not put your private keys in .env files in clear text. Use CryptoEnv!

Francesco Sullo
4 min readJul 4, 2022

--

Generated by MidJourney

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.

Generated by MidJourney

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

  1. the data to be encrypted
  2. 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

Generated by MidJourney

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.

--

--

Francesco Sullo
Francesco Sullo

Written by Francesco Sullo

Polymath. CTO at Superpower Labs & @MOBLANDHQ. Before founded @Passpack, and was at @Turo, @Yahoo, @Tronfoundationand others. More at https://sullo.co

No responses yet