Passwords are particularly problematic for programmers. You're not supposed to store them without encrypting them, and you're not supposed to reveal what's been typed when your user enters one. This became particularly important to me when I decided I wanted to boost security on my laptop. I encrypt my home directory—but once I log in, any password stored as plain text in a configuration file is potentially exposed to prying eyes.
Specifically, I use an application called Mutt as my email client. It lets me read and compose emails in my Linux terminal, but normally it expects a password in its configuration file. I restricted permissions on my Mutt config file so that only I can see it, but I'm the only user of my laptop, so I'm not really concerned about authenticated users inadvertently looking at my configs. Instead, I wanted to protect myself from absent-mindedly posting my config online, either for bragging rights or version control, with my password exposed. In addition, although I have no expectations of unwelcome guests on my system, I did want to ensure that an intruder couldn't obtain my password just by running cat
on my config.
Python GnuPG
The Python module python-gnupg
is a Python wrapper for the gpg
application. The module's name is python-gnupg
, which you must not confuse with a module called gnupg
.
GnuPG (GPG) is the default encryption system for Linux, and I've been using it since 2009 or so. I feel comfortable with it and have a high level of trust in its security.
I decided that the best way to get my password into Mutt was to store my password inside an encrypted GPG file, create a prompt for my GPG password to unlock the encrypted file, and hand the password over to Mutt (actually to the offlineimap
command, which I use to synchronize my laptop with the email server).
Getting user input with Python is pretty easy. You make a call to input
, and whatever the user types is stored as a variable:
print("Enter password: ")
myinput = input()
print("You entered: ", myinput)
My problem was when I typed a password into the terminal in response to my password prompt, everything I typed was visible to anyone looking over my shoulder or scrolling through my terminal history:
$ ./test.py
Enter password: my-Complex-Passphrase
Invisible password entry with getpass
As is often the case, there's a Python module that's already solved my problem. The module is getpass4
, and from the user's perspective, it behaves exactly like input
except without displaying what the user is typing.
You can install both modules with pip:
$ python -m pip install --user \
python-gnupg getpass4
Here's my Python script to create a password prompt:
#!/usr/bin/env python
# by Seth Kenlon
# GPLv3
# install deps:
# python3 -m pip install --user python-gnupg getpass4
import gnupg
import getpass
from pathlib import Path
def get_api_pass():
homedir = str(Path.home())
gpg = gnupg.GPG(gnupghome=os.path.join(homedir,".gnupg"), use_agent=True)
passwd = getpass.getpass(prompt="Enter your GnuPG password: ", stream=None)
with open(os.path.join(homedir,'.mutt','pass.gpg'), 'rb') as f:
apipass = (gpg.decrypt_file(f, passphrase=passwd))
f.close()
return str(apipass)
if __name__ == "__main__":
apipass = get_api_pass()
print(apipass)
Save the file as password_prompt.py
if you want to try it out. If you're using offlineimap
and want to use this solution for your own password entry, then save it to some location you can point offlineimap
to in your .offlineimaprc
file (I use ~/.mutt/password_prompt.py
).
Testing the password prompt
To see the script in action, you first must create an encrypted file (I'll assume that you already have GPG set up):
$ echo "hello world" > pass
$ gpg --encrypt pass
$ mv pass.gpg ~/.mutt/pass.gpg
$ rm pass
Now run the Python script:
$ python ~/.mutt/password_prompt.py
Enter your GPG password:
hello world
Nothing displays as you type, but as long as you enter your GPG passphrase correctly, you will see the test message.
Integrating the password prompt with offlineimap
I needed to integrate my new prompt with the offlineimap
command. I chose Python for this script because I knew that offlineimap
can make calls to Python applications. If you're an offlineimap
user, you'll appreciate that the only "integration" required is changing two lines in your .offlineimaprc
file.
First, add a line referencing the Python file:
pythonfile = ~/.mutt/password_prompt.py
And then replace the remotepasseval
line in .offlineimaprc
with a call to the get_api_pass()
function in password_prompt.py
:
remotepasseval = get_api_pass()
No more passwords in your config file!
Security matters
It sometimes feels almost paranoid to think about security minutiae on your personal computer. Does your SSH config really need to be restricted to 600? Does it really matter that your email password is in an inconsequential config file buried within a hidden folder called, of all things, .mutt
? Probably not.
And yet knowing that I don't have sensitive data quietly hidden away in my config files makes it a lot easier for me to commit files to public Git repositories, to copy and paste snippets into support forums, and to share my knowledge in the form of actual, known-good configuration files. For that alone, improved security has made my life easier. And with so many great Python modules available to help, it's easy to implement.
Comments are closed.