From c865dcc03c7181f6a9ab7fab011cb617dbbf0bdf Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sun, 10 Jun 2018 12:18:27 +0100 Subject: [PATCH] Script to store passwords in a file with GPG or using OSX's secure keychain Submitted-by: https://github.com/lorenzog Signed-off-by: Nicolas Sebrecht --- contrib/store-pw-with-gpg/README.md | 37 +++++++ contrib/store-pw-with-gpg/gpg-pw.py | 99 +++++++++++++++++++ .../store-pw-with-gpg/offlineimaprc.sample | 63 ++++++++++++ contrib/store-pw-with-gpg/passwords-gmail.txt | 2 + 4 files changed, 201 insertions(+) create mode 100644 contrib/store-pw-with-gpg/README.md create mode 100644 contrib/store-pw-with-gpg/gpg-pw.py create mode 100644 contrib/store-pw-with-gpg/offlineimaprc.sample create mode 100644 contrib/store-pw-with-gpg/passwords-gmail.txt diff --git a/contrib/store-pw-with-gpg/README.md b/contrib/store-pw-with-gpg/README.md new file mode 100644 index 0000000..d31f030 --- /dev/null +++ b/contrib/store-pw-with-gpg/README.md @@ -0,0 +1,37 @@ +# gpg-offlineimap + +Python bindings for offlineimap to use gpg instead of storing cleartext passwords + +Author: Lorenzo G. +[GitHub](https://github.com/lorenzog/gpg-offlineimap) + +## Quickstart + +Requirements: a working GPG set-up. Ideally with gpg-agent. Should work +out of the box on most modern Linux desktop environments. + + 1. Enable IMAP in gmail (if you have two factor authentication, you + need to create an app-specific password) + + 2. Create a directory `~/Mail` + + 3. In `~/Mail`, create a password file `passwords-gmail.txt`. Format: + `account@gmail.com password`. Look at the example file in this + directory. + + 4. **ENCRYPT** the file: `gpg -e passwords-gmail.txt`. It should create + a file `passwords-gmail.txt.gpg`. Check you can decrypt it: `gpg -d + passwords-gmail.txt.gpg`: it will ask you for your GPG password and + show it to you. + + 5. Use the file `offlineimaprc.sample` as a sample for your own + `.offlineimaprc`; edit it by following the comments. Minimal items + to configure: the `remoteuser` field and the `pythonfile` parameter + pointing at the `offlineimap.py` file in this directory. + + 6. Run it: `offlineimap`. It should ask you for your GPG passphrase to + decrypt the password file. + + 7. If all works well, delete the cleartext password file. + + diff --git a/contrib/store-pw-with-gpg/gpg-pw.py b/contrib/store-pw-with-gpg/gpg-pw.py new file mode 100644 index 0000000..945334f --- /dev/null +++ b/contrib/store-pw-with-gpg/gpg-pw.py @@ -0,0 +1,99 @@ +#!/usr/bin/python +# Originally taken from: http://stevelosh.com/blog/2012/10/the-homely-mutt/ +# by Steve Losh +# Modified by Lorenzo Grespan on Jan, 2014 + +import re +import subprocess +from sys import argv +import logging +from os.path import expanduser +import unittest +import os +import sys + +logging.basicConfig(level=logging.INFO) + + +DEFAULT_PASSWORDS_FILE = os.path.join( + os.path.expanduser('~/Mail'), + 'passwords.gpg') + + +def get_keychain_pass(account=None, server=None): + '''Mac OSX keychain password extraction''' + params = { + 'security': '/usr/bin/security', + 'command': 'find-internet-password', + 'account': account, + 'server': server, + 'keychain': expanduser('~') + '/Library/Keychains/login.keychain', + } + command = ("%(security)s -v %(command)s" + " -g -a %(account)s -s %(server)s %(keychain)s" % params) + output = subprocess.check_output( + command, shell=True, stderr=subprocess.STDOUT) + outtext = [l for l in output.splitlines() + if l.startswith('password: ')][0] + return find_password(outtext) + + +def find_password(text): + '''Helper method for osx password extraction''' + # a non-capturing group + r = re.match(r'password: (?:0x[A-F0-9]+ )?"(.*)"', text) + if r: + return r.group(1) + else: + logging.warn("Not found") + return None + + +def get_gpg_pass(account, storage): + '''GPG method''' + command = ("gpg", "-d", storage) + # get attention + print '\a' # BEL + output = subprocess.check_output(command) + # p = subprocess.Popen(command, stdout=subprocess.PIPE) + # output, err = p.communicate() + for line in output.split('\n'): + r = re.match(r'{} ([a-zA-Z0-9]+)'.format(account), line) + if r: + return r.group(1) + return None + + +def get_pass(account=None, server=None, passwd_file=None): + '''Main method''' + if not passwd_file: + storage = DEFAULT_PASSWORDS_FILE + else: + storage = os.path.join( + os.path.expanduser('~/Mail'), + passwd_file) + if os.path.exists('/usr/bin/security'): + return get_keychain_pass(account, server) + if os.path.exists(storage): + logging.info("Using {}".format(storage)) + return get_gpg_pass(account, storage) + else: + logging.warn("No password file found") + sys.exit(1) + return None + + +# test with: python -m unittest +# really basic tests.. nothing to see. move along +class Tester(unittest.TestCase): + def testMatchSimple(self): + text = 'password: "exampleonetimepass "' + self.assertTrue(find_password(text)) + + def testMatchComplex(self): + text = r'password: 0x74676D62646D736B646970766C66696B0A "anotherexamplepass\012"' + self.assertTrue(find_password(text)) + + +if __name__ == "__main__": + print get_pass(argv[1], argv[2], argv[3]) diff --git a/contrib/store-pw-with-gpg/offlineimaprc.sample b/contrib/store-pw-with-gpg/offlineimaprc.sample new file mode 100644 index 0000000..c04e221 --- /dev/null +++ b/contrib/store-pw-with-gpg/offlineimaprc.sample @@ -0,0 +1,63 @@ +[general] +# GPG quirks, leave unconfigured +ui = ttyui +# you can use any name as long as it matches the 'account1, 'account2' in the rest +# of the file +accounts = account1, account2 +# this is where the `gpg-pw.py` file is on disk +pythonfile=~/where/is/the/file/gpg-pw.py +fsync = False + +# you can call this any way you like +[Account account1] +localrepository = account1-local +remoterepository = account1-remote +# no need to touch this +status_backend = sqlite + +[Account account2] +localrepository = account2-local +remoterepository = account2-remote +status_backend = sqlite + +# thi sis a gmail account +[Repository account1-local] +type = Maildir +# create with maildirmake or by hand by creating cur, new, tmp +localfolders = ~/Mail/Mailboxes/account1 +# standard Gmail stuff +nametrans = lambda folder: { 'drafts': '[Gmail]/Drafts', + 'sent': '[Gmail]/Sent mail', + 'flagged': '[Gmail]/Starred', + 'trash': '[Gmail]/Trash', + 'archive': '[Gmail]/All Mail' + }.get(folder, folder) + +[Repository account1-remote] +maxconnections = 1 +type = Gmail +ssl=yes +# for osx, you might need to download the certs by hand +#sslcacertfile=~/Mail/certs.pem +#sslcacertfile=~/Mail/imap.gmail.com.pem +# sslcacertfile=/etc/ssl/cert.pem + +# or use Linux's standard certs +sslcacertfile=/etc/ssl/certs/ca-certificates.crt +# your account +remoteuser = account1@gmail.com +remotepasseval = get_pass(account="account1@gmail.com", server="imap.gmail.com", passwd_file="passwords-gmail.txt.gpg") +realdelete = no +createfolders = no +nametrans = lambda folder: {'[Gmail]/Drafts': 'drafts', + '[Gmail]/Sent Mail': 'sent', + '[Gmail]/Starred': 'star', + '[Gmail]/Trash': 'trash', + '[Gmail]/All Mail': 'archive', + }.get(folder, folder) +folderfilter = lambda folder: folder not in ['[Gmail]/Trash', + '[Gmail]/Spam', + ] + +[Repository account2-remote] +# copy the stanza above, change the 'account' parameter of get_pass, etc. diff --git a/contrib/store-pw-with-gpg/passwords-gmail.txt b/contrib/store-pw-with-gpg/passwords-gmail.txt new file mode 100644 index 0000000..16abe38 --- /dev/null +++ b/contrib/store-pw-with-gpg/passwords-gmail.txt @@ -0,0 +1,2 @@ +account1@gmail.com password1 +account2@gmail.com password2