#!/usr/bin/python
#
# Wrap the gpg command to provide evolution with a bit of extra functionality
# This is certainly a hack and you should feel very bad about using it.
#
# Public Domain, Authored by Martin Owens <doctormo@gmail.com> 2016
#
GPG_TRIED_FOOTER = """
--==-- --==-- --==--
I tried to encrypted this message using GPG (GNU Privacy Guard)
Your public key isn't available, so this email can be read by
anyone who might be snooping.
"""
import os
import sys
import atexit
from collections import defaultdict
from subprocess import Popen, PIPE, call
from tempfile import mkdtemp, mktemp
from datetime import date
from shutil import rmtree
to_date = lambda d: date(*[int(p) for p in d.split('-')])
class GPG(object):
keyserver = 'hkp://pgp.mit.edu'
remote_commands = ['--search-keys', '--recv-keys']
def __init__(self, cmd='/usr/bin/gpg', local=False):
self.command = cmd
self.photos = []
self.local = local
self.homedir = mkdtemp() if local else None
atexit.register(self.at_exit)
def at_exit(self):
"""Remove any temporary files and cleanup"""
# Clean up any used local home directory (only if it's local)
if self.local and self.homedir and os.path.isdir(self.homedir):
rmtree(self.homedir)
# Clean up any downloaded photo-ids
for photo in self.photos:
if os.path.isfile(photo):
os.unlink(photo)
try:
os.rmdir(os.path.dirname(photo))
except OSError:
pass
def __call__(self, *args):
"""Call gpg command for result"""
# Add key server if required
if any([cmd in args for cmd in self.remote_commands]):
args = ('--keyserver', self.keyserver) + args
if self.homedir:
args = ('--homedir', self.homedir) + args
command = Popen([self.command, '--batch'] + list(args), stdout=PIPE)
(out, err) = command.communicate()
self.status = command.returncode
return out
def list_keys(self, *keys, **options):
"""Returns a list of keys (with photos if needed)"""
with_photos = options.get('photos', False)
args = ()
if with_photos:
args += ('--list-options', 'show-photos',
'--photo-viewer', 'echo PHOTO:%I')
out = self(*(args + ('--list-keys',) + keys))
# Processing the output with this parser
units = []
current = defaultdict(list)
for line in out.split('\n'):
if not line.strip():
# We should always output entries if they have a uid and key
if current and 'uid' in current and 'key' in current:
# But ignore revoked keys if revoked option is True
if not (current.get('revoked', False) and options.get('revoked', False)):
units.append(dict(current))
current = defaultdict(list)
elif line.startswith('PHOTO:'):
current['photo'] = line.split(':', 1)[-1]
self.photos.append(current['photo'])
elif ' of size ' in line:
continue
elif ' ' in line:
(kind, line) = line.split(' ', 1)
if kind == 'pub':
current['expires'] = False
current['revoked'] = False
if '[' in line:
(line, mod) = line.strip().split('[', 1)
(mod, _) = mod.split(']', 1)
if ': ' in mod:
(mod, edited) = mod.split(': ', 1)
current[mod] = to_date(edited)
(key, created) = line.split(' ', 1)
current['created'] = to_date(created)
(current['bits'], current['key']) = key.split('/', 1)
elif kind in ('uid', 'sub'):
current[kind].append(line.strip())
else:
current[kind] = line.strip()
return units
@property
def default_photo(self):
if not hasattr(self, '_photo'):
self._photo = mktemp('.svg')
with open(self._photo, 'w') as fhl:
fhl.write("""<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="120" height="120" viewbox="0 0 120 120">
<path style="stroke:#6c6c6c;stroke-width:.5px;fill:#ece8e6;"
d="M 0.25,0.25018138 0.25,119.66328 25.941297,116.2119 C 21.595604,99.862371 26.213982,56.833751 43.785402,47.903791 17.34816,4.9549214 103.06892,5.4226914 76.631675,48.405571 96.179208,63.458931 98.051138,99.588601 93.51442,116.28034 l 26.23558,3.46961 0,-119.41309862 z"/>
</svg>""")
self.photos.append(self._photo)
return self._photo
def recieve_keys(self, *keys, **options):
"""Present the opotunity to add the key to the user:
Returns
- True if the key was already or is now imported.
- False if keys were available but the user canceled.
- None if no keys were found within the search.
"""
keys = self.search_keys(*keys)
if not keys:
return None # User doesn't have GPG
# Always use a temporary gpg home to review keys
gpg = GPG(cmd=self.command, local=True) if not self.local else self
# B. Import each of the keys
gpg('--recv-keys', *zip(*keys)[0])
# C. List keys (with photo options)
choices = []
for key in gpg.list_keys(photos=True):
choices.append(key.get('photo', self.default_photo))
choices.append('\n'.join(key['uid']))
choices.append(key['key'])
choices.append(str(key['expires']))
if len(choices) / 4 == 1:
title = "Can I use this GPG key to encrypt for this user?"
else:
title = "Please select the GPG key to use for encryption"
# Show using gtk zenity (easier than gtk3 directly)
p = Popen(['zenity',
'--width', '900', '--height', '700', '--title', title,
'--list', '--imagelist', '--print-column', '3',
'--column', 'Photo ID',
'--column', 'ID',
'--column', 'Key',
'--column', 'Expires',
] + choices, stdout=PIPE, stderr=PIPE)
# Returncode is generated after communicate!
key = p.communicate()[0].strip()
# Select the default first key if one choice.
# (person pressed ok without looking)
if not key and len(choices) == 4:
key = choices[2]
if p.returncode != 0:
# Cancel was pressed
return False
# E. Import the selected key
self('--recv-keys', key)
return self.status == 0
def is_key_available(self, search):
"""Return False if the email is not found in the local key list"""
self('--list-keys', search)
if self.status == 2: # Keys not found
return False
# We return true, even if gpg returned some other kind of error
# Because this prevents us running more commands to a broken gpg
return True
def search_keys(self, *keys):
"""Returns a list of (key_id, info) tuples from a search"""
out = self('--search-keys', *keys)
found = []
prev = []
for line in out.split("\n"):
if line.startswith('gpg:'):
continue
if 'created:' in line:
key_id = line.split('key ')[-1].split(',')[0]
if '(revoked)' not in line:
found.append((key_id, prev))
prev = []
else:
prev.append(line)
return found
if __name__ == '__main__':
cmd = sys.argv[0] + '.orig'
if not os.path.isfile(cmd):
sys.stderr.write("Can't find pass-through command '%s'\n" % args[0])
sys.exit(-13)
args = [cmd] + sys.argv[1:]
# Check to see if call is from an application
if 'GIO_LAUNCHED_DESKTOP_FILE' in os.environ:
# We use our moved gpg command file
gpg = GPG(cmd=cmd)
# Check if we've got a missing key during an encryption, we get the
# very next argument after a -r or -R argument (which should be
# the email address)
for recipient in [args[i+1] for (i, v) in enumerate(args) if v in ('-r', '-R')]:
# Only check email addresses
if '@' in recipient:
if not gpg.is_key_available(recipient):
if gpg.recieve_keys(recipient) is None:
pass
# We can add a footer to the message here explaining GPG
# We can't do this, evolution will wrap it all up in a
# message structure.
#msg = sys.stdin.read()
#if msg:
# msg += GPG_TRIED_FOOTER
#sys.stdout.write(msg)
#sys.exit(0)
# We call and do not PIPE anything (pass-through)
try:
sys.exit(call(args))
except KeyboardInterrupt:
sys.exit(-14)
This hack wraps the gpg calls evolution makes in order to call out to get keys using a GUI.
Please log in to leave a comment!