1. #!/usr/bin/python
  2. #
  3. # Wrap the gpg command to provide evolution with a bit of extra functionality
  4. # This is certainly a hack and you should feel very bad about using it.
  5. #
  6. # Public Domain, Authored by Martin Owens <doctormo@gmail.com> 2016
  7. #
  8. GPG_TRIED_FOOTER = """
  9. --==-- --==-- --==--
  10. I tried to encrypted this message using GPG (GNU Privacy Guard)
  11. Your public key isn't available, so this email can be read by
  12. anyone who might be snooping.
  13. """
  14. import os
  15. import sys
  16. import atexit
  17. from collections import defaultdict
  18. from subprocess import Popen, PIPE, call
  19. from tempfile import mkdtemp, mktemp
  20. from datetime import date
  21. from shutil import rmtree
  22. to_date = lambda d: date(*[int(p) for p in d.split('-')])
  23. class GPG(object):
  24. keyserver = 'hkp://pgp.mit.edu'
  25. remote_commands = ['--search-keys', '--recv-keys']
  26. def __init__(self, cmd='/usr/bin/gpg', local=False):
  27. self.command = cmd
  28. self.photos = []
  29. self.local = local
  30. self.homedir = mkdtemp() if local else None
  31. atexit.register(self.at_exit)
  32. def at_exit(self):
  33. """Remove any temporary files and cleanup"""
  34. # Clean up any used local home directory (only if it's local)
  35. if self.local and self.homedir and os.path.isdir(self.homedir):
  36. rmtree(self.homedir)
  37. # Clean up any downloaded photo-ids
  38. for photo in self.photos:
  39. if os.path.isfile(photo):
  40. os.unlink(photo)
  41. try:
  42. os.rmdir(os.path.dirname(photo))
  43. except OSError:
  44. pass
  45. def __call__(self, *args):
  46. """Call gpg command for result"""
  47. # Add key server if required
  48. if any([cmd in args for cmd in self.remote_commands]):
  49. args = ('--keyserver', self.keyserver) + args
  50. if self.homedir:
  51. args = ('--homedir', self.homedir) + args
  52. command = Popen([self.command, '--batch'] + list(args), stdout=PIPE)
  53. (out, err) = command.communicate()
  54. self.status = command.returncode
  55. return out
  56. def list_keys(self, *keys, **options):
  57. """Returns a list of keys (with photos if needed)"""
  58. with_photos = options.get('photos', False)
  59. args = ()
  60. if with_photos:
  61. args += ('--list-options', 'show-photos',
  62. '--photo-viewer', 'echo PHOTO:%I')
  63. out = self(*(args + ('--list-keys',) + keys))
  64. # Processing the output with this parser
  65. units = []
  66. current = defaultdict(list)
  67. for line in out.split('\n'):
  68. if not line.strip():
  69. # We should always output entries if they have a uid and key
  70. if current and 'uid' in current and 'key' in current:
  71. # But ignore revoked keys if revoked option is True
  72. if not (current.get('revoked', False) and options.get('revoked', False)):
  73. units.append(dict(current))
  74. current = defaultdict(list)
  75. elif line.startswith('PHOTO:'):
  76. current['photo'] = line.split(':', 1)[-1]
  77. self.photos.append(current['photo'])
  78. elif ' of size ' in line:
  79. continue
  80. elif ' ' in line:
  81. (kind, line) = line.split(' ', 1)
  82. if kind == 'pub':
  83. current['expires'] = False
  84. current['revoked'] = False
  85. if '[' in line:
  86. (line, mod) = line.strip().split('[', 1)
  87. (mod, _) = mod.split(']', 1)
  88. if ': ' in mod:
  89. (mod, edited) = mod.split(': ', 1)
  90. current[mod] = to_date(edited)
  91. (key, created) = line.split(' ', 1)
  92. current['created'] = to_date(created)
  93. (current['bits'], current['key']) = key.split('/', 1)
  94. elif kind in ('uid', 'sub'):
  95. current[kind].append(line.strip())
  96. else:
  97. current[kind] = line.strip()
  98. return units
  99. @property
  100. def default_photo(self):
  101. if not hasattr(self, '_photo'):
  102. self._photo = mktemp('.svg')
  103. with open(self._photo, 'w') as fhl:
  104. fhl.write("""<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="120" height="120" viewbox="0 0 120 120">
  105. <path style="stroke:#6c6c6c;stroke-width:.5px;fill:#ece8e6;"
  106. 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"/>
  107. </svg>""")
  108. self.photos.append(self._photo)
  109. return self._photo
  110. def recieve_keys(self, *keys, **options):
  111. """Present the opotunity to add the key to the user:
  112. Returns
  113. - True if the key was already or is now imported.
  114. - False if keys were available but the user canceled.
  115. - None if no keys were found within the search.
  116. """
  117. keys = self.search_keys(*keys)
  118. if not keys:
  119. return None # User doesn't have GPG
  120. # Always use a temporary gpg home to review keys
  121. gpg = GPG(cmd=self.command, local=True) if not self.local else self
  122. # B. Import each of the keys
  123. gpg('--recv-keys', *zip(*keys)[0])
  124. # C. List keys (with photo options)
  125. choices = []
  126. for key in gpg.list_keys(photos=True):
  127. choices.append(key.get('photo', self.default_photo))
  128. choices.append('\n'.join(key['uid']))
  129. choices.append(key['key'])
  130. choices.append(str(key['expires']))
  131. if len(choices) / 4 == 1:
  132. title = "Can I use this GPG key to encrypt for this user?"
  133. else:
  134. title = "Please select the GPG key to use for encryption"
  135. # Show using gtk zenity (easier than gtk3 directly)
  136. p = Popen(['zenity',
  137. '--width', '900', '--height', '700', '--title', title,
  138. '--list', '--imagelist', '--print-column', '3',
  139. '--column', 'Photo ID',
  140. '--column', 'ID',
  141. '--column', 'Key',
  142. '--column', 'Expires',
  143. ] + choices, stdout=PIPE, stderr=PIPE)
  144. # Returncode is generated after communicate!
  145. key = p.communicate()[0].strip()
  146. # Select the default first key if one choice.
  147. # (person pressed ok without looking)
  148. if not key and len(choices) == 4:
  149. key = choices[2]
  150. if p.returncode != 0:
  151. # Cancel was pressed
  152. return False
  153. # E. Import the selected key
  154. self('--recv-keys', key)
  155. return self.status == 0
  156. def is_key_available(self, search):
  157. """Return False if the email is not found in the local key list"""
  158. self('--list-keys', search)
  159. if self.status == 2: # Keys not found
  160. return False
  161. # We return true, even if gpg returned some other kind of error
  162. # Because this prevents us running more commands to a broken gpg
  163. return True
  164. def search_keys(self, *keys):
  165. """Returns a list of (key_id, info) tuples from a search"""
  166. out = self('--search-keys', *keys)
  167. found = []
  168. prev = []
  169. for line in out.split("\n"):
  170. if line.startswith('gpg:'):
  171. continue
  172. if 'created:' in line:
  173. key_id = line.split('key ')[-1].split(',')[0]
  174. if '(revoked)' not in line:
  175. found.append((key_id, prev))
  176. prev = []
  177. else:
  178. prev.append(line)
  179. return found
  180. if __name__ == '__main__':
  181. cmd = sys.argv[0] + '.orig'
  182. if not os.path.isfile(cmd):
  183. sys.stderr.write("Can't find pass-through command '%s'\n" % args[0])
  184. sys.exit(-13)
  185. args = [cmd] + sys.argv[1:]
  186. # Check to see if call is from an application
  187. if 'GIO_LAUNCHED_DESKTOP_FILE' in os.environ:
  188. # We use our moved gpg command file
  189. gpg = GPG(cmd=cmd)
  190. # Check if we've got a missing key during an encryption, we get the
  191. # very next argument after a -r or -R argument (which should be
  192. # the email address)
  193. for recipient in [args[i+1] for (i, v) in enumerate(args) if v in ('-r', '-R')]:
  194. # Only check email addresses
  195. if '@' in recipient:
  196. if not gpg.is_key_available(recipient):
  197. if gpg.recieve_keys(recipient) is None:
  198. pass
  199. # We can add a footer to the message here explaining GPG
  200. # We can't do this, evolution will wrap it all up in a
  201. # message structure.
  202. #msg = sys.stdin.read()
  203. #if msg:
  204. # msg += GPG_TRIED_FOOTER
  205. #sys.stdout.write(msg)
  206. #sys.exit(0)
  207. # We call and do not PIPE anything (pass-through)
  208. try:
  209. sys.exit(call(args))
  210. except KeyboardInterrupt:
  211. sys.exit(-14)
 
 

1079

Evolution GPG Hack

This hack wraps the gpg calls evolution makes in order to call out to get keys using a GUI.

PasteBin

Lines
253
Words
982
Size
9.1 KB
Created
Revisions
4
Type
text/x-python
Public Domain (PD)
Please log in to leave a comment!