switch from GnuPGInterface to python-gnupg, so gpg is available under windows. Fixes #5096, #3615, #1890, #996
This commit is contained in:
parent
1136ae5b16
commit
6cd8e07fae
7 changed files with 976 additions and 844 deletions
1
debian/rules
vendored
1
debian/rules
vendored
|
@ -12,6 +12,5 @@ DEB_MAKE_BUILD_TARGET := all
|
||||||
DEB_MAKE_INSTALL_TARGET = install DESTDIR=$(DEB_DESTDIR)
|
DEB_MAKE_INSTALL_TARGET = install DESTDIR=$(DEB_DESTDIR)
|
||||||
|
|
||||||
binary-install/gajim::
|
binary-install/gajim::
|
||||||
rm $(DEB_DESTDIR)/usr/share/gajim/src/common/GnuPGInterface.py*
|
|
||||||
dh_pysupport -pgajim
|
dh_pysupport -pgajim
|
||||||
convert $(DEB_DESTDIR)/usr/share/icons/hicolor/64x64/apps/gajim.png -resize 32x32 $(DEB_DESTDIR)/usr/share/pixmaps/gajim.xpm
|
convert $(DEB_DESTDIR)/usr/share/icons/hicolor/64x64/apps/gajim.png -resize 32x32 $(DEB_DESTDIR)/usr/share/pixmaps/gajim.xpm
|
||||||
|
|
|
@ -23,17 +23,17 @@
|
||||||
##
|
##
|
||||||
|
|
||||||
import gajim
|
import gajim
|
||||||
|
import os
|
||||||
from os import tmpfile
|
from os import tmpfile
|
||||||
from common import helpers
|
|
||||||
|
|
||||||
if gajim.HAVE_GPG:
|
if gajim.HAVE_GPG:
|
||||||
import GnuPGInterface
|
import gnupg
|
||||||
|
|
||||||
class GnuPG(GnuPGInterface.GnuPG):
|
class GnuPG(gnupg.GPG):
|
||||||
def __init__(self, use_agent=False):
|
def __init__(self, use_agent=False):
|
||||||
GnuPGInterface.GnuPG.__init__(self)
|
gnupg.GPG.__init__(self)
|
||||||
|
self.passphrase = None
|
||||||
self.use_agent = use_agent
|
self.use_agent = use_agent
|
||||||
self._setup_my_options()
|
|
||||||
self.always_trust = False
|
self.always_trust = False
|
||||||
|
|
||||||
def _setup_my_options(self):
|
def _setup_my_options(self):
|
||||||
|
@ -46,166 +46,56 @@ if gajim.HAVE_GPG:
|
||||||
if self.use_agent:
|
if self.use_agent:
|
||||||
self.options.extra_args.append('--use-agent')
|
self.options.extra_args.append('--use-agent')
|
||||||
|
|
||||||
def _read_response(self, child_stdout):
|
|
||||||
# Internal method: reads all the output from GPG, taking notice
|
|
||||||
# only of lines that begin with the magic [GNUPG:] prefix.
|
|
||||||
# (See doc/DETAILS in the GPG distribution for info on GPG's
|
|
||||||
# output when --status-fd is specified.)
|
|
||||||
#
|
|
||||||
# Returns a dictionary, mapping GPG's keywords to the arguments
|
|
||||||
# for that keyword.
|
|
||||||
|
|
||||||
resp = {}
|
|
||||||
while True:
|
|
||||||
line = helpers.temp_failure_retry(child_stdout.readline)
|
|
||||||
if line == "": break
|
|
||||||
line = line.rstrip()
|
|
||||||
if line[0:9] == '[GNUPG:] ':
|
|
||||||
# Chop off the prefix
|
|
||||||
line = line[9:]
|
|
||||||
L = line.split(None, 1)
|
|
||||||
keyword = L[0]
|
|
||||||
if len(L) > 1:
|
|
||||||
resp[ keyword ] = L[1]
|
|
||||||
else:
|
|
||||||
resp[ keyword ] = ""
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def encrypt(self, str_, recipients, always_trust=False):
|
def encrypt(self, str_, recipients, always_trust=False):
|
||||||
self.options.recipients = recipients # a list!
|
result = super(GnuPG, self).encrypt(str_, recipients,
|
||||||
|
always_trust=always_trust, passphrase=self.passphrase)
|
||||||
|
|
||||||
opt = ['--encrypt']
|
if result.status == 'invalid recipient':
|
||||||
if always_trust or self.always_trust:
|
|
||||||
opt.append('--always-trust')
|
|
||||||
proc = self.run(opt, create_fhs=['stdin', 'stdout', 'status',
|
|
||||||
'stderr'])
|
|
||||||
proc.handles['stdin'].write(str_)
|
|
||||||
try:
|
|
||||||
proc.handles['stdin'].close()
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
output = proc.handles['stdout'].read()
|
|
||||||
try:
|
|
||||||
proc.handles['stdout'].close()
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
stat = proc.handles['status']
|
|
||||||
resp = self._read_response(stat)
|
|
||||||
try:
|
|
||||||
proc.handles['status'].close()
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
error = proc.handles['stderr'].read()
|
|
||||||
proc.handles['stderr'].close()
|
|
||||||
|
|
||||||
try: proc.wait()
|
|
||||||
except IOError: pass
|
|
||||||
if 'INV_RECP' in resp and resp['INV_RECP'].split()[0] == '10':
|
|
||||||
# unusable recipient "Key not trusted"
|
|
||||||
return '', 'NOT_TRUSTED'
|
return '', 'NOT_TRUSTED'
|
||||||
if 'BEGIN_ENCRYPTION' in resp and 'END_ENCRYPTION' in resp:
|
|
||||||
# Encryption succeeded, even if there is output on stderr. Maybe
|
if result.ok:
|
||||||
# verbose is on
|
|
||||||
error = ''
|
error = ''
|
||||||
return self._stripHeaderFooter(output), helpers.decode_string(error)
|
else:
|
||||||
|
error = result.status
|
||||||
|
|
||||||
|
return self._stripHeaderFooter(str(result)), error
|
||||||
|
|
||||||
def decrypt(self, str_, keyID):
|
def decrypt(self, str_, keyID):
|
||||||
proc = self.run(['--decrypt', '-q', '-u %s'%keyID], create_fhs=['stdin', 'stdout'])
|
data = self._addHeaderFooter(str_, 'MESSAGE')
|
||||||
enc = self._addHeaderFooter(str_, 'MESSAGE')
|
result = super(GnuPG, self).decrypt(data,
|
||||||
proc.handles['stdin'].write(enc)
|
passphrase=self.passphrase)
|
||||||
proc.handles['stdin'].close()
|
|
||||||
|
|
||||||
output = proc.handles['stdout'].read()
|
return str(result)
|
||||||
proc.handles['stdout'].close()
|
|
||||||
|
|
||||||
try: proc.wait()
|
|
||||||
except IOError: pass
|
|
||||||
return output
|
|
||||||
|
|
||||||
def sign(self, str_, keyID):
|
def sign(self, str_, keyID):
|
||||||
proc = self.run(['-b', '-u %s'%keyID], create_fhs=['stdin', 'stdout', 'status', 'stderr'])
|
result = super(GnuPG, self).sign(str_, keyid=keyID, detach=True,
|
||||||
proc.handles['stdin'].write(str_)
|
passphrase=self.passphrase)
|
||||||
try:
|
|
||||||
proc.handles['stdin'].close()
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
output = proc.handles['stdout'].read()
|
if result.fingerprint:
|
||||||
try:
|
return self._stripHeaderFooter(str(result))
|
||||||
proc.handles['stdout'].close()
|
# if 'KEYEXPIRED' in resp:
|
||||||
proc.handles['stderr'].close()
|
# return 'KEYEXPIRED'
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
stat = proc.handles['status']
|
|
||||||
resp = self._read_response(stat)
|
|
||||||
try:
|
|
||||||
proc.handles['status'].close()
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try: proc.wait()
|
|
||||||
except IOError: pass
|
|
||||||
if 'GOOD_PASSPHRASE' in resp or 'SIG_CREATED' in resp:
|
|
||||||
return self._stripHeaderFooter(output)
|
|
||||||
if 'KEYEXPIRED' in resp:
|
|
||||||
return 'KEYEXPIRED'
|
|
||||||
return 'BAD_PASSPHRASE'
|
return 'BAD_PASSPHRASE'
|
||||||
|
|
||||||
def verify(self, str_, sign):
|
def verify(self, str_, sign):
|
||||||
if str_ is None:
|
if str_ is None:
|
||||||
return ''
|
return ''
|
||||||
f = tmpfile()
|
data = '-----BEGIN PGP SIGNED MESSAGE-----' + os.linesep
|
||||||
fd = f.fileno()
|
data = data + 'Hash: SHA1' + os.linesep + os.linesep
|
||||||
f.write(str_)
|
data = data + str_ + os.linesep
|
||||||
f.seek(0)
|
data = data + self._addHeaderFooter(sign, 'SIGNATURE')
|
||||||
|
result = super(GnuPG, self).verify(data)
|
||||||
|
|
||||||
proc = self.run(['--verify', '--enable-special-filenames', '-', '-&%s'%fd], create_fhs=['stdin', 'status', 'stderr'])
|
if result.valid:
|
||||||
|
return result.key_id
|
||||||
f.close()
|
return ''
|
||||||
sign = self._addHeaderFooter(sign, 'SIGNATURE')
|
|
||||||
proc.handles['stdin'].write(sign)
|
|
||||||
proc.handles['stdin'].close()
|
|
||||||
proc.handles['stderr'].close()
|
|
||||||
|
|
||||||
stat = proc.handles['status']
|
|
||||||
resp = self._read_response(stat)
|
|
||||||
proc.handles['status'].close()
|
|
||||||
|
|
||||||
try: proc.wait()
|
|
||||||
except IOError: pass
|
|
||||||
|
|
||||||
keyid = ''
|
|
||||||
if 'GOODSIG' in resp:
|
|
||||||
keyid = resp['GOODSIG'].split()[0]
|
|
||||||
return keyid
|
|
||||||
|
|
||||||
def get_keys(self, secret=False):
|
def get_keys(self, secret=False):
|
||||||
if secret:
|
|
||||||
opt = '--list-secret-keys'
|
|
||||||
else:
|
|
||||||
opt = '--list-keys'
|
|
||||||
proc = self.run(['--with-colons', opt],
|
|
||||||
create_fhs=['stdout'])
|
|
||||||
output = proc.handles['stdout'].read()
|
|
||||||
proc.handles['stdout'].close()
|
|
||||||
|
|
||||||
try: proc.wait()
|
|
||||||
except IOError: pass
|
|
||||||
|
|
||||||
keys = {}
|
keys = {}
|
||||||
lines = output.split('\n')
|
result = super(GnuPG, self).list_keys(secret=secret)
|
||||||
for line in lines:
|
for key in result:
|
||||||
sline = line.split(':')
|
# Take first not empty uid
|
||||||
if (sline[0] == 'sec' and secret) or \
|
keys[key['keyid'][8:]] = [uid for uid in key['uids'] if uid][0]
|
||||||
(sline[0] == 'pub' and not secret):
|
|
||||||
# decode escaped chars
|
|
||||||
name = eval('"' + sline[9].replace('"', '\\"') + '"')
|
|
||||||
# make it unicode instance
|
|
||||||
keys[sline[4][8:]] = helpers.decode_string(name)
|
|
||||||
return keys
|
return keys
|
||||||
|
|
||||||
def get_secret_keys(self):
|
def get_secret_keys(self):
|
||||||
|
@ -216,7 +106,7 @@ if gajim.HAVE_GPG:
|
||||||
Remove header and footer from data
|
Remove header and footer from data
|
||||||
"""
|
"""
|
||||||
if not data: return ''
|
if not data: return ''
|
||||||
lines = data.split('\n')
|
lines = data.splitlines()
|
||||||
while lines[0] != '':
|
while lines[0] != '':
|
||||||
lines.remove(lines[0])
|
lines.remove(lines[0])
|
||||||
while lines[0] == '':
|
while lines[0] == '':
|
||||||
|
@ -233,9 +123,9 @@ if gajim.HAVE_GPG:
|
||||||
"""
|
"""
|
||||||
Add header and footer from data
|
Add header and footer from data
|
||||||
"""
|
"""
|
||||||
out = "-----BEGIN PGP %s-----\n" % type_
|
out = "-----BEGIN PGP %s-----" % type_ + os.linesep
|
||||||
out = out + "Version: PGP\n"
|
out = out + "Version: PGP" + os.linesep
|
||||||
out = out + "\n"
|
out = out + os.linesep
|
||||||
out = out + data + "\n"
|
out = out + data + os.linesep
|
||||||
out = out + "-----END PGP %s-----\n" % type_
|
out = out + "-----END PGP %s-----" % type_ + os.linesep
|
||||||
return out
|
return out
|
||||||
|
|
|
@ -1,673 +0,0 @@
|
||||||
# -*- coding:utf-8 -*-
|
|
||||||
## src/common/GnuPGInterface.py
|
|
||||||
##
|
|
||||||
## Copyright (C) 2001 Frank J. Tobin <ftobin AT neverending.org>
|
|
||||||
## Copyright (C) 2005 Nikos Kouremenos <kourem AT gmail.com>
|
|
||||||
## Copyright (C) 2006-2010 Yann Leboulanger <asterix AT lagaule.org>
|
|
||||||
## Copyright (C) 2008 Jean-Marie Traissard <jim AT lapin.org>
|
|
||||||
##
|
|
||||||
## This file is part of Gajim.
|
|
||||||
##
|
|
||||||
## Gajim is free software; you can redistribute it and/or modify
|
|
||||||
## it under the terms of the GNU General Public License as published
|
|
||||||
## by the Free Software Foundation; version 3 only.
|
|
||||||
##
|
|
||||||
## Gajim is distributed in the hope that it will be useful,
|
|
||||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
## GNU General Public License for more details.
|
|
||||||
##
|
|
||||||
## You should have received a copy of the GNU General Public License
|
|
||||||
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
##
|
|
||||||
|
|
||||||
"""
|
|
||||||
Interface to GNU Privacy Guard (GnuPG)
|
|
||||||
|
|
||||||
GnuPGInterface is a Python module to interface with GnuPG.
|
|
||||||
It concentrates on interacting with GnuPG via filehandles,
|
|
||||||
providing access to control GnuPG via versatile and extensible means.
|
|
||||||
|
|
||||||
This module is based on GnuPG::Interface, a Perl module by the same author.
|
|
||||||
|
|
||||||
Normally, using this module will involve creating a
|
|
||||||
GnuPG object, setting some options in it's 'options' data member
|
|
||||||
(which is of type Options), creating some pipes
|
|
||||||
to talk with GnuPG, and then calling the run() method, which will
|
|
||||||
connect those pipes to the GnuPG process. run() returns a
|
|
||||||
Process object, which contains the filehandles to talk to GnuPG with.
|
|
||||||
|
|
||||||
Example code:
|
|
||||||
|
|
||||||
>>> import GnuPGInterface
|
|
||||||
>>>
|
|
||||||
>>> plaintext = "Three blind mice"
|
|
||||||
>>> passphrase = "This is the passphrase"
|
|
||||||
>>>
|
|
||||||
>>> gnupg = GnuPGInterface.GnuPG()
|
|
||||||
>>> gnupg.options.armor = 1
|
|
||||||
>>> gnupg.options.meta_interactive = 0
|
|
||||||
>>> gnupg.options.extra_args.append('--no-secmem-warning')
|
|
||||||
>>>
|
|
||||||
>>> # Normally we might specify something in
|
|
||||||
>>> # gnupg.options.recipients, like
|
|
||||||
>>> # gnupg.options.recipients = [ '0xABCD1234', 'bob@foo.bar' ]
|
|
||||||
>>> # but since we're doing symmetric-only encryption, it's not needed.
|
|
||||||
>>> # If you are doing standard, public-key encryption, using
|
|
||||||
>>> # --encrypt, you will need to specify recipients before
|
|
||||||
>>> # calling gnupg.run()
|
|
||||||
>>>
|
|
||||||
>>> # First we'll encrypt the test_text input symmetrically
|
|
||||||
>>> p1 = gnupg.run(['--symmetric'],
|
|
||||||
... create_fhs=['stdin', 'stdout', 'passphrase'])
|
|
||||||
>>>
|
|
||||||
>>> p1.handles['passphrase'].write(passphrase)
|
|
||||||
>>> p1.handles['passphrase'].close()
|
|
||||||
>>>
|
|
||||||
>>> p1.handles['stdin'].write(plaintext)
|
|
||||||
>>> p1.handles['stdin'].close()
|
|
||||||
>>>
|
|
||||||
>>> ciphertext = p1.handles['stdout'].read()
|
|
||||||
>>> p1.handles['stdout'].close()
|
|
||||||
>>>
|
|
||||||
>>> # process cleanup
|
|
||||||
>>> p1.wait()
|
|
||||||
>>>
|
|
||||||
>>> # Now we'll decrypt what we just encrypted it,
|
|
||||||
>>> # using the convience method to get the
|
|
||||||
>>> # passphrase to GnuPG
|
|
||||||
>>> gnupg.passphrase = passphrase
|
|
||||||
>>>
|
|
||||||
>>> p2 = gnupg.run(['--decrypt'], create_fhs=['stdin', 'stdout'])
|
|
||||||
>>>
|
|
||||||
>>> p2.handles['stdin'].write(ciphertext)
|
|
||||||
>>> p2.handles['stdin'].close()
|
|
||||||
>>>
|
|
||||||
>>> decrypted_plaintext = p2.handles['stdout'].read()
|
|
||||||
>>> p2.handles['stdout'].close()
|
|
||||||
>>>
|
|
||||||
>>> # process cleanup
|
|
||||||
>>> p2.wait()
|
|
||||||
>>>
|
|
||||||
>>> # Our decrypted plaintext:
|
|
||||||
>>> decrypted_plaintext
|
|
||||||
'Three blind mice'
|
|
||||||
>>>
|
|
||||||
>>> # ...and see it's the same as what we orignally encrypted
|
|
||||||
>>> assert decrypted_plaintext == plaintext, \
|
|
||||||
"GnuPG decrypted output does not match original input"
|
|
||||||
>>>
|
|
||||||
>>>
|
|
||||||
>>> ##################################################
|
|
||||||
>>> # Now let's trying using run()'s attach_fhs paramter
|
|
||||||
>>>
|
|
||||||
>>> # we're assuming we're running on a unix...
|
|
||||||
>>> input = open('/etc/motd')
|
|
||||||
>>>
|
|
||||||
>>> p1 = gnupg.run(['--symmetric'], create_fhs=['stdout'],
|
|
||||||
... attach_fhs={'stdin': input})
|
|
||||||
>>>
|
|
||||||
>>> # GnuPG will read the stdin from /etc/motd
|
|
||||||
>>> ciphertext = p1.handles['stdout'].read()
|
|
||||||
>>>
|
|
||||||
>>> # process cleanup
|
|
||||||
>>> p1.wait()
|
|
||||||
>>>
|
|
||||||
>>> # Now let's run the output through GnuPG
|
|
||||||
>>> # We'll write the output to a temporary file,
|
|
||||||
>>> import tempfile
|
|
||||||
>>> temp = tempfile.TemporaryFile()
|
|
||||||
>>>
|
|
||||||
>>> p2 = gnupg.run(['--decrypt'], create_fhs=['stdin'],
|
|
||||||
... attach_fhs={'stdout': temp})
|
|
||||||
>>>
|
|
||||||
>>> # give GnuPG our encrypted stuff from the first run
|
|
||||||
>>> p2.handles['stdin'].write(ciphertext)
|
|
||||||
>>> p2.handles['stdin'].close()
|
|
||||||
>>>
|
|
||||||
>>> # process cleanup
|
|
||||||
>>> p2.wait()
|
|
||||||
>>>
|
|
||||||
>>> # rewind the tempfile and see what GnuPG gave us
|
|
||||||
>>> temp.seek(0)
|
|
||||||
>>> decrypted_plaintext = temp.read()
|
|
||||||
>>>
|
|
||||||
>>> # compare what GnuPG decrypted with our original input
|
|
||||||
>>> input.seek(0)
|
|
||||||
>>> input_data = input.read()
|
|
||||||
>>>
|
|
||||||
>>> assert decrypted_plaintext == input_data, \
|
|
||||||
"GnuPG decrypted output does not match original input"
|
|
||||||
|
|
||||||
To do things like public-key encryption, simply pass do something
|
|
||||||
like:
|
|
||||||
|
|
||||||
gnupg.passphrase = 'My passphrase'
|
|
||||||
gnupg.options.recipients = [ 'bob@foobar.com' ]
|
|
||||||
gnupg.run( ['--sign', '--encrypt'], create_fhs=..., attach_fhs=...)
|
|
||||||
|
|
||||||
Here is an example of subclassing GnuPGInterface.GnuPG,
|
|
||||||
so that it has an encrypt_string() method that returns
|
|
||||||
ciphertext.
|
|
||||||
|
|
||||||
>>> import GnuPGInterface
|
|
||||||
>>>
|
|
||||||
>>> class MyGnuPG(GnuPGInterface.GnuPG):
|
|
||||||
...
|
|
||||||
... def __init__(self):
|
|
||||||
... GnuPGInterface.GnuPG.__init__(self)
|
|
||||||
... self.setup_my_options()
|
|
||||||
...
|
|
||||||
... def setup_my_options(self):
|
|
||||||
... self.options.armor = 1
|
|
||||||
... self.options.meta_interactive = 0
|
|
||||||
... self.options.extra_args.append('--no-secmem-warning')
|
|
||||||
...
|
|
||||||
... def encrypt_string(self, string, recipients):
|
|
||||||
... gnupg.options.recipients = recipients # a list!
|
|
||||||
...
|
|
||||||
... proc = gnupg.run(['--encrypt'], create_fhs=['stdin', 'stdout'])
|
|
||||||
...
|
|
||||||
... proc.handles['stdin'].write(string)
|
|
||||||
... proc.handles['stdin'].close()
|
|
||||||
...
|
|
||||||
... output = proc.handles['stdout'].read()
|
|
||||||
... proc.handles['stdout'].close()
|
|
||||||
...
|
|
||||||
... proc.wait()
|
|
||||||
... return output
|
|
||||||
...
|
|
||||||
>>> gnupg = MyGnuPG()
|
|
||||||
>>> ciphertext = gnupg.encrypt_string("The secret", ['0x260C4FA3'])
|
|
||||||
>>>
|
|
||||||
>>> # just a small sanity test here for doctest
|
|
||||||
>>> import types
|
|
||||||
>>> assert isinstance(ciphertext, types.StringType), \
|
|
||||||
"What GnuPG gave back is not a string!"
|
|
||||||
|
|
||||||
Here is an example of generating a key:
|
|
||||||
>>> import GnuPGInterface
|
|
||||||
>>> gnupg = GnuPGInterface.GnuPG()
|
|
||||||
>>> gnupg.options.meta_interactive = 0
|
|
||||||
>>>
|
|
||||||
>>> # We will be creative and use the logger filehandle to capture
|
|
||||||
>>> # what GnuPG says this time, instead stderr; no stdout to listen to,
|
|
||||||
>>> # but we capture logger to surpress the dry-run command.
|
|
||||||
>>> # We also have to capture stdout since otherwise doctest complains;
|
|
||||||
>>> # Normally you can let stdout through when generating a key.
|
|
||||||
>>>
|
|
||||||
>>> proc = gnupg.run(['--gen-key'], create_fhs=['stdin', 'stdout',
|
|
||||||
... 'logger'])
|
|
||||||
>>>
|
|
||||||
>>> proc.handles['stdin'].write('''Key-Type: DSA
|
|
||||||
... Key-Length: 1024
|
|
||||||
... # We are only testing syntax this time, so dry-run
|
|
||||||
... %dry-run
|
|
||||||
... Subkey-Type: ELG-E
|
|
||||||
... Subkey-Length: 1024
|
|
||||||
... Name-Real: Joe Tester
|
|
||||||
... Name-Comment: with stupid passphrase
|
|
||||||
... Name-Email: joe@foo.bar
|
|
||||||
... Expire-Date: 2y
|
|
||||||
... Passphrase: abc
|
|
||||||
... %pubring foo.pub
|
|
||||||
... %secring foo.sec
|
|
||||||
... ''')
|
|
||||||
>>>
|
|
||||||
>>> proc.handles['stdin'].close()
|
|
||||||
>>>
|
|
||||||
>>> report = proc.handles['logger'].read()
|
|
||||||
>>> proc.handles['logger'].close()
|
|
||||||
>>>
|
|
||||||
>>> proc.wait()
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import fcntl
|
|
||||||
|
|
||||||
__author__ = "Frank J. Tobin, ftobin@neverending.org"
|
|
||||||
__version__ = "0.3.2"
|
|
||||||
__revision__ = "$Id: GnuPGInterface.py,v 1.22 2002/01/11 20:22:04 ftobin Exp $"
|
|
||||||
|
|
||||||
# "standard" filehandles attached to processes
|
|
||||||
_stds = [ 'stdin', 'stdout', 'stderr' ]
|
|
||||||
|
|
||||||
# the permissions each type of fh needs to be opened with
|
|
||||||
_fd_modes = { 'stdin': 'w',
|
|
||||||
'stdout': 'r',
|
|
||||||
'stderr': 'r',
|
|
||||||
'passphrase': 'w',
|
|
||||||
'command': 'w',
|
|
||||||
'logger': 'r',
|
|
||||||
'status': 'r'
|
|
||||||
}
|
|
||||||
|
|
||||||
# correlation between handle names and the arguments we'll pass
|
|
||||||
_fd_options = { 'passphrase': '--passphrase-fd',
|
|
||||||
'logger': '--logger-fd',
|
|
||||||
'status': '--status-fd',
|
|
||||||
'command': '--command-fd' }
|
|
||||||
|
|
||||||
class GnuPG:
|
|
||||||
"""
|
|
||||||
Class instances represent GnuPG
|
|
||||||
|
|
||||||
Instance attributes of a GnuPG object are:
|
|
||||||
|
|
||||||
* call -- string to call GnuPG with. Defaults to "gpg"
|
|
||||||
|
|
||||||
* passphrase -- Since it is a common operation
|
|
||||||
to pass in a passphrase to GnuPG,
|
|
||||||
and working with the passphrase filehandle mechanism directly
|
|
||||||
can be mundane, if set, the passphrase attribute
|
|
||||||
works in a special manner. If the passphrase attribute is set,
|
|
||||||
and no passphrase file object is sent in to run(),
|
|
||||||
then GnuPG instnace will take care of sending the passphrase to
|
|
||||||
GnuPG, the executable instead of having the user sent it in manually.
|
|
||||||
|
|
||||||
* options -- Object of type GnuPGInterface.Options.
|
|
||||||
Attribute-setting in options determines
|
|
||||||
the command-line options used when calling GnuPG.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.call = 'gpg'
|
|
||||||
self.passphrase = None
|
|
||||||
self.options = Options()
|
|
||||||
|
|
||||||
def run(self, gnupg_commands, args=None, create_fhs=None, attach_fhs=None):
|
|
||||||
"""
|
|
||||||
Calls GnuPG with the list of string commands gnupg_commands, complete
|
|
||||||
with prefixing dashes
|
|
||||||
|
|
||||||
For example, gnupg_commands could be
|
|
||||||
'["--sign", "--encrypt"]'
|
|
||||||
Returns a GnuPGInterface.Process object.
|
|
||||||
|
|
||||||
args is an optional list of GnuPG command arguments (not options),
|
|
||||||
such as keyID's to export, filenames to process, etc.
|
|
||||||
|
|
||||||
create_fhs is an optional list of GnuPG filehandle
|
|
||||||
names that will be set as keys of the returned Process object's
|
|
||||||
'handles' attribute. The generated filehandles can be used
|
|
||||||
to communicate with GnuPG via standard input, standard output,
|
|
||||||
the status-fd, passphrase-fd, etc.
|
|
||||||
|
|
||||||
Valid GnuPG filehandle names are:
|
|
||||||
* stdin
|
|
||||||
* stdout
|
|
||||||
* stderr
|
|
||||||
* status
|
|
||||||
* passphase
|
|
||||||
* command
|
|
||||||
* logger
|
|
||||||
|
|
||||||
The purpose of each filehandle is described in the GnuPG
|
|
||||||
documentation.
|
|
||||||
|
|
||||||
attach_fhs is an optional dictionary with GnuPG filehandle
|
|
||||||
names mapping to opened files. GnuPG will read or write
|
|
||||||
to the file accordingly. For example, if 'my_file' is an
|
|
||||||
opened file and 'attach_fhs[stdin] is my_file', then GnuPG
|
|
||||||
will read its standard input from my_file. This is useful
|
|
||||||
if you want GnuPG to read/write to/from an existing file.
|
|
||||||
For instance:
|
|
||||||
|
|
||||||
f = open("encrypted.gpg")
|
|
||||||
gnupg.run(["--decrypt"], attach_fhs={'stdin': f})
|
|
||||||
|
|
||||||
Using attach_fhs also helps avoid system buffering
|
|
||||||
issues that can arise when using create_fhs, which
|
|
||||||
can cause the process to deadlock.
|
|
||||||
|
|
||||||
If not mentioned in create_fhs or attach_fhs,
|
|
||||||
GnuPG filehandles which are a std* (stdin, stdout, stderr)
|
|
||||||
are defaulted to the running process' version of handle.
|
|
||||||
Otherwise, that type of handle is simply not used when calling GnuPG.
|
|
||||||
For example, if you do not care about getting data from GnuPG's
|
|
||||||
status filehandle, simply do not specify it.
|
|
||||||
|
|
||||||
run() returns a Process() object which has a 'handles'
|
|
||||||
which is a dictionary mapping from the handle name
|
|
||||||
(such as 'stdin' or 'stdout') to the respective
|
|
||||||
newly-created FileObject connected to the running GnuPG process.
|
|
||||||
For instance, if the call was
|
|
||||||
|
|
||||||
process = gnupg.run(["--decrypt"], stdin=1)
|
|
||||||
|
|
||||||
after run returns 'process.handles["stdin"]'
|
|
||||||
is a FileObject connected to GnuPG's standard input,
|
|
||||||
and can be written to.
|
|
||||||
"""
|
|
||||||
if args is None: args = []
|
|
||||||
if create_fhs is None: create_fhs = []
|
|
||||||
if attach_fhs is None: attach_fhs = {}
|
|
||||||
|
|
||||||
for std in _stds:
|
|
||||||
if std not in attach_fhs \
|
|
||||||
and std not in create_fhs:
|
|
||||||
attach_fhs.setdefault(std, getattr(sys, std))
|
|
||||||
|
|
||||||
handle_passphrase = 0
|
|
||||||
|
|
||||||
if self.passphrase is not None \
|
|
||||||
and 'passphrase' not in attach_fhs \
|
|
||||||
and 'passphrase' not in create_fhs:
|
|
||||||
handle_passphrase = 1
|
|
||||||
create_fhs.append('passphrase')
|
|
||||||
|
|
||||||
process = self._attach_fork_exec(gnupg_commands, args,
|
|
||||||
create_fhs, attach_fhs)
|
|
||||||
|
|
||||||
if handle_passphrase:
|
|
||||||
passphrase_fh = process.handles['passphrase']
|
|
||||||
passphrase_fh.write( self.passphrase )
|
|
||||||
passphrase_fh.close()
|
|
||||||
del process.handles['passphrase']
|
|
||||||
|
|
||||||
return process
|
|
||||||
|
|
||||||
|
|
||||||
def _attach_fork_exec(self, gnupg_commands, args, create_fhs, attach_fhs):
|
|
||||||
"""
|
|
||||||
This is like run(), but without the passphrase-helping (note that run()
|
|
||||||
calls this)
|
|
||||||
"""
|
|
||||||
process = Process()
|
|
||||||
|
|
||||||
for fh_name in create_fhs + attach_fhs.keys():
|
|
||||||
if fh_name not in _fd_modes:
|
|
||||||
raise KeyError, \
|
|
||||||
"unrecognized filehandle name '%s'; must be one of %s" \
|
|
||||||
% (fh_name, _fd_modes.keys())
|
|
||||||
|
|
||||||
for fh_name in create_fhs:
|
|
||||||
# make sure the user doesn't specify a filehandle
|
|
||||||
# to be created *and* attached
|
|
||||||
if fh_name in attach_fhs:
|
|
||||||
raise ValueError, \
|
|
||||||
"cannot have filehandle '%s' in both create_fhs and attach_fhs" \
|
|
||||||
% fh_name
|
|
||||||
|
|
||||||
pipe = os.pipe()
|
|
||||||
# fix by drt@un.bewaff.net noting
|
|
||||||
# that since pipes are unidirectional on some systems,
|
|
||||||
# so we have to 'turn the pipe around'
|
|
||||||
# if we are writing
|
|
||||||
if _fd_modes[fh_name] == 'w': pipe = (pipe[1], pipe[0])
|
|
||||||
process._pipes[fh_name] = Pipe(pipe[0], pipe[1], 0)
|
|
||||||
|
|
||||||
for fh_name, fh in attach_fhs.items():
|
|
||||||
process._pipes[fh_name] = Pipe(fh.fileno(), fh.fileno(), 1)
|
|
||||||
|
|
||||||
process.pid = os.fork()
|
|
||||||
|
|
||||||
if process.pid == 0: self._as_child(process, gnupg_commands, args)
|
|
||||||
return self._as_parent(process)
|
|
||||||
|
|
||||||
|
|
||||||
def _as_parent(self, process):
|
|
||||||
"""
|
|
||||||
Stuff run after forking in parent
|
|
||||||
"""
|
|
||||||
for k, p in process._pipes.items():
|
|
||||||
if not p.direct:
|
|
||||||
os.close(p.child)
|
|
||||||
process.handles[k] = os.fdopen(p.parent, _fd_modes[k])
|
|
||||||
|
|
||||||
# user doesn't need these
|
|
||||||
del process._pipes
|
|
||||||
|
|
||||||
return process
|
|
||||||
|
|
||||||
|
|
||||||
def _as_child(self, process, gnupg_commands, args):
|
|
||||||
"""
|
|
||||||
Stuff run after forking in child
|
|
||||||
"""
|
|
||||||
# child
|
|
||||||
for std in _stds:
|
|
||||||
p = process._pipes[std]
|
|
||||||
os.dup2( p.child, getattr(sys, "__%s__" % std).fileno() )
|
|
||||||
|
|
||||||
for k, p in process._pipes.items():
|
|
||||||
if p.direct and k not in _stds:
|
|
||||||
# we want the fh to stay open after execing
|
|
||||||
fcntl.fcntl( p.child, fcntl.F_SETFD, 0 )
|
|
||||||
|
|
||||||
fd_args = []
|
|
||||||
|
|
||||||
for k, p in process._pipes.items():
|
|
||||||
# set command-line options for non-standard fds
|
|
||||||
if k not in _stds:
|
|
||||||
fd_args.extend([ _fd_options[k], "%d" % p.child ])
|
|
||||||
|
|
||||||
if not p.direct: os.close(p.parent)
|
|
||||||
|
|
||||||
command = [ self.call ] + fd_args + self.options.get_args() \
|
|
||||||
+ gnupg_commands + args
|
|
||||||
|
|
||||||
os.execvp( command[0], command )
|
|
||||||
|
|
||||||
|
|
||||||
class Pipe:
|
|
||||||
"""
|
|
||||||
Simple struct holding stuff about pipes we use
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parent, child, direct):
|
|
||||||
self.parent = parent
|
|
||||||
self.child = child
|
|
||||||
self.direct = direct
|
|
||||||
|
|
||||||
|
|
||||||
class Options:
|
|
||||||
"""
|
|
||||||
Objects of this class encompass options passed to GnuPG.
|
|
||||||
This class is responsible for determining command-line arguments
|
|
||||||
which are based on options. It can be said that a GnuPG
|
|
||||||
object has-a Options object in its options attribute.
|
|
||||||
|
|
||||||
Attributes which correlate directly to GnuPG options:
|
|
||||||
|
|
||||||
Each option here defaults to false or None, and is described in
|
|
||||||
GnuPG documentation.
|
|
||||||
|
|
||||||
Booleans (set these attributes to booleans)
|
|
||||||
|
|
||||||
* armor
|
|
||||||
* no_greeting
|
|
||||||
* no_verbose
|
|
||||||
* quiet
|
|
||||||
* batch
|
|
||||||
* always_trust
|
|
||||||
* rfc1991
|
|
||||||
* openpgp
|
|
||||||
* force_v3_sigs
|
|
||||||
* no_options
|
|
||||||
* textmode
|
|
||||||
|
|
||||||
Strings (set these attributes to strings)
|
|
||||||
|
|
||||||
* homedir
|
|
||||||
* default_key
|
|
||||||
* comment
|
|
||||||
* compress_algo
|
|
||||||
* options
|
|
||||||
|
|
||||||
Lists (set these attributes to lists)
|
|
||||||
|
|
||||||
* recipients (***NOTE*** plural of 'recipient')
|
|
||||||
* encrypt_to
|
|
||||||
|
|
||||||
Meta options
|
|
||||||
|
|
||||||
Meta options are options provided by this module that do
|
|
||||||
not correlate directly to any GnuPG option by name,
|
|
||||||
but are rather bundle of options used to accomplish
|
|
||||||
a specific goal, such as obtaining compatibility with PGP 5.
|
|
||||||
The actual arguments each of these reflects may change with time. Each
|
|
||||||
defaults to false unless otherwise specified.
|
|
||||||
|
|
||||||
meta_pgp_5_compatible -- If true, arguments are generated to try
|
|
||||||
to be compatible with PGP 5.x.
|
|
||||||
|
|
||||||
meta_pgp_2_compatible -- If true, arguments are generated to try
|
|
||||||
to be compatible with PGP 2.x.
|
|
||||||
|
|
||||||
meta_interactive -- If false, arguments are generated to try to
|
|
||||||
help the using program use GnuPG in a non-interactive
|
|
||||||
environment, such as CGI scripts. Default is true.
|
|
||||||
|
|
||||||
extra_args -- Extra option arguments may be passed in
|
|
||||||
via the attribute extra_args, a list.
|
|
||||||
|
|
||||||
>>> import GnuPGInterface
|
|
||||||
>>>
|
|
||||||
>>> gnupg = GnuPGInterface.GnuPG()
|
|
||||||
>>> gnupg.options.armor = 1
|
|
||||||
>>> gnupg.options.recipients = ['Alice', 'Bob']
|
|
||||||
>>> gnupg.options.extra_args = ['--no-secmem-warning']
|
|
||||||
>>>
|
|
||||||
>>> # no need for users to call this normally; just for show here
|
|
||||||
>>> gnupg.options.get_args()
|
|
||||||
['--armor', '--recipient', 'Alice', '--recipient', 'Bob', '--no-secmem-warning']
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
# booleans
|
|
||||||
self.armor = 0
|
|
||||||
self.no_greeting = 0
|
|
||||||
self.verbose = 0
|
|
||||||
self.no_verbose = 0
|
|
||||||
self.quiet = 0
|
|
||||||
self.batch = 0
|
|
||||||
self.always_trust = 0
|
|
||||||
self.rfc1991 = 0
|
|
||||||
self.openpgp = 0
|
|
||||||
self.force_v3_sigs = 0
|
|
||||||
self.no_options = 0
|
|
||||||
self.textmode = 0
|
|
||||||
|
|
||||||
# meta-option booleans
|
|
||||||
self.meta_pgp_5_compatible = 0
|
|
||||||
self.meta_pgp_2_compatible = 0
|
|
||||||
self.meta_interactive = 1
|
|
||||||
|
|
||||||
# strings
|
|
||||||
self.homedir = None
|
|
||||||
self.default_key = None
|
|
||||||
self.comment = None
|
|
||||||
self.compress_algo = None
|
|
||||||
self.options = None
|
|
||||||
|
|
||||||
# lists
|
|
||||||
self.encrypt_to = []
|
|
||||||
self.recipients = []
|
|
||||||
|
|
||||||
# miscellaneous arguments
|
|
||||||
self.extra_args = []
|
|
||||||
|
|
||||||
def get_args( self ):
|
|
||||||
"""
|
|
||||||
Generate a list of GnuPG arguments based upon attributes
|
|
||||||
"""
|
|
||||||
return self.get_meta_args() + self.get_standard_args() + self.extra_args
|
|
||||||
|
|
||||||
def get_standard_args( self ):
|
|
||||||
"""
|
|
||||||
Generate a list of standard, non-meta or extra arguments
|
|
||||||
"""
|
|
||||||
args = []
|
|
||||||
if self.homedir is not None:
|
|
||||||
args.extend( [ '--homedir', self.homedir ] )
|
|
||||||
if self.options is not None:
|
|
||||||
args.extend( [ '--options', self.options ] )
|
|
||||||
if self.comment is not None:
|
|
||||||
args.extend( [ '--comment', self.comment ] )
|
|
||||||
if self.compress_algo is not None:
|
|
||||||
args.extend( [ '--compress-algo', self.compress_algo ] )
|
|
||||||
if self.default_key is not None:
|
|
||||||
args.extend( [ '--default-key', self.default_key ] )
|
|
||||||
|
|
||||||
if self.no_options: args.append( '--no-options' )
|
|
||||||
if self.armor: args.append( '--armor' )
|
|
||||||
if self.textmode: args.append( '--textmode' )
|
|
||||||
if self.no_greeting: args.append( '--no-greeting' )
|
|
||||||
if self.verbose: args.append( '--verbose' )
|
|
||||||
if self.no_verbose: args.append( '--no-verbose' )
|
|
||||||
if self.quiet: args.append( '--quiet' )
|
|
||||||
if self.batch: args.append( '--batch' )
|
|
||||||
if self.always_trust: args.append( '--always-trust' )
|
|
||||||
if self.force_v3_sigs: args.append( '--force-v3-sigs' )
|
|
||||||
if self.rfc1991: args.append( '--rfc1991' )
|
|
||||||
if self.openpgp: args.append( '--openpgp' )
|
|
||||||
|
|
||||||
for r in self.recipients: args.extend( [ '--recipient', r ] )
|
|
||||||
for r in self.encrypt_to: args.extend( [ '--encrypt-to', r ] )
|
|
||||||
|
|
||||||
return args
|
|
||||||
|
|
||||||
def get_meta_args( self ):
|
|
||||||
"""
|
|
||||||
Get a list of generated meta-arguments
|
|
||||||
"""
|
|
||||||
args = []
|
|
||||||
|
|
||||||
if self.meta_pgp_5_compatible: args.extend( [ '--compress-algo', '1',
|
|
||||||
'--force-v3-sigs'
|
|
||||||
] )
|
|
||||||
if self.meta_pgp_2_compatible: args.append( '--rfc1991' )
|
|
||||||
if not self.meta_interactive: args.extend( [ '--batch', '--no-tty' ] )
|
|
||||||
|
|
||||||
return args
|
|
||||||
|
|
||||||
|
|
||||||
class Process:
|
|
||||||
"""
|
|
||||||
Objects of this class encompass properties of a GnuPG process spawned by
|
|
||||||
GnuPG.run()
|
|
||||||
|
|
||||||
# gnupg is a GnuPG object
|
|
||||||
process = gnupg.run( [ '--decrypt' ], stdout = 1 )
|
|
||||||
out = process.handles['stdout'].read()
|
|
||||||
...
|
|
||||||
os.waitpid( process.pid, 0 )
|
|
||||||
|
|
||||||
Data Attributes
|
|
||||||
|
|
||||||
handles -- This is a map of filehandle-names to
|
|
||||||
the file handles, if any, that were requested via run() and hence
|
|
||||||
are connected to the running GnuPG process. Valid names
|
|
||||||
of this map are only those handles that were requested.
|
|
||||||
|
|
||||||
pid -- The PID of the spawned GnuPG process.
|
|
||||||
Useful to know, since once should call
|
|
||||||
os.waitpid() to clean up the process, especially
|
|
||||||
if multiple calls are made to run().
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._pipes = {}
|
|
||||||
self.handles = {}
|
|
||||||
self.pid = None
|
|
||||||
self._waited = None
|
|
||||||
|
|
||||||
def wait(self):
|
|
||||||
"""
|
|
||||||
Wait on the process to exit, allowing for child cleanup. Will raise an
|
|
||||||
IOError if the process exits non-zero
|
|
||||||
"""
|
|
||||||
e = os.waitpid(self.pid, 0)[1]
|
|
||||||
if e != 0:
|
|
||||||
raise IOError, "GnuPG exited non-zero, with code %d" % (e << 8)
|
|
||||||
|
|
||||||
def _run_doctests():
|
|
||||||
import doctest, GnuPGInterface
|
|
||||||
return doctest.testmod(GnuPGInterface)
|
|
||||||
|
|
||||||
# deprecated
|
|
||||||
GnuPGInterface = GnuPG
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
_run_doctests()
|
|
|
@ -218,7 +218,6 @@ class CommonConnection:
|
||||||
if self.gpg.passphrase is None and not use_gpg_agent:
|
if self.gpg.passphrase is None and not use_gpg_agent:
|
||||||
# We didn't set a passphrase
|
# We didn't set a passphrase
|
||||||
return None
|
return None
|
||||||
if self.gpg.passphrase is not None or use_gpg_agent:
|
|
||||||
signed = self.gpg.sign(msg, keyID)
|
signed = self.gpg.sign(msg, keyID)
|
||||||
if signed == 'BAD_PASSPHRASE':
|
if signed == 'BAD_PASSPHRASE':
|
||||||
self.USE_GPG = False
|
self.USE_GPG = False
|
||||||
|
@ -596,14 +595,12 @@ class CommonConnection:
|
||||||
|
|
||||||
def ask_gpg_keys(self):
|
def ask_gpg_keys(self):
|
||||||
if self.gpg:
|
if self.gpg:
|
||||||
keys = self.gpg.get_keys()
|
return self.gpg.get_keys()
|
||||||
return keys
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def ask_gpg_secrete_keys(self):
|
def ask_gpg_secrete_keys(self):
|
||||||
if self.gpg:
|
if self.gpg:
|
||||||
keys = self.gpg.get_secret_keys()
|
return self.gpg.get_secret_keys()
|
||||||
return keys
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def load_roster_from_db(self):
|
def load_roster_from_db(self):
|
||||||
|
|
|
@ -150,7 +150,7 @@ except ImportError:
|
||||||
|
|
||||||
HAVE_GPG = True
|
HAVE_GPG = True
|
||||||
try:
|
try:
|
||||||
import GnuPGInterface
|
import gnupg
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAVE_GPG = False
|
HAVE_GPG = False
|
||||||
else:
|
else:
|
||||||
|
|
921
src/common/gnupg.py
Normal file
921
src/common/gnupg.py
Normal file
|
@ -0,0 +1,921 @@
|
||||||
|
""" A wrapper for the 'gpg' command::
|
||||||
|
|
||||||
|
Portions of this module are derived from A.M. Kuchling's well-designed
|
||||||
|
GPG.py, using Richard Jones' updated version 1.3, which can be found
|
||||||
|
in the pycrypto CVS repository on Sourceforge:
|
||||||
|
|
||||||
|
http://pycrypto.cvs.sourceforge.net/viewvc/pycrypto/gpg/GPG.py
|
||||||
|
|
||||||
|
This module is *not* forward-compatible with amk's; some of the
|
||||||
|
old interface has changed. For instance, since I've added decrypt
|
||||||
|
functionality, I elected to initialize with a 'gnupghome' argument
|
||||||
|
instead of 'keyring', so that gpg can find both the public and secret
|
||||||
|
keyrings. I've also altered some of the returned objects in order for
|
||||||
|
the caller to not have to know as much about the internals of the
|
||||||
|
result classes.
|
||||||
|
|
||||||
|
While the rest of ISconf is released under the GPL, I am releasing
|
||||||
|
this single file under the same terms that A.M. Kuchling used for
|
||||||
|
pycrypto.
|
||||||
|
|
||||||
|
Steve Traugott, stevegt@terraluna.org
|
||||||
|
Thu Jun 23 21:27:20 PDT 2005
|
||||||
|
|
||||||
|
This version of the module has been modified from Steve Traugott's version
|
||||||
|
(see http://trac.t7a.org/isconf/browser/trunk/lib/python/isconf/GPG.py) by
|
||||||
|
Vinay Sajip to make use of the subprocess module (Steve's version uses os.fork()
|
||||||
|
and so does not work on Windows). Renamed to gnupg.py to avoid confusion with
|
||||||
|
the previous versions.
|
||||||
|
|
||||||
|
Modifications Copyright (C) 2008-2010 Vinay Sajip. All rights reserved.
|
||||||
|
|
||||||
|
A unittest harness (test_gnupg.py) has also been added.
|
||||||
|
"""
|
||||||
|
import locale
|
||||||
|
|
||||||
|
__author__ = "Vinay Sajip"
|
||||||
|
__date__ = "$08-Oct-2010 23:01:07$"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from io import StringIO
|
||||||
|
from io import TextIOWrapper
|
||||||
|
from io import BufferedReader
|
||||||
|
from io import BufferedWriter
|
||||||
|
except ImportError:
|
||||||
|
from cStringIO import StringIO
|
||||||
|
class BufferedReader: pass
|
||||||
|
class BufferedWriter: pass
|
||||||
|
|
||||||
|
import locale
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
from subprocess import Popen
|
||||||
|
from subprocess import PIPE
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
try:
|
||||||
|
import logging.NullHandler as NullHandler
|
||||||
|
except ImportError:
|
||||||
|
class NullHandler(logging.Handler):
|
||||||
|
def handle(self, record):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
unicode
|
||||||
|
_py3k = False
|
||||||
|
except NameError:
|
||||||
|
_py3k = True
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
if not logger.handlers:
|
||||||
|
logger.addHandler(NullHandler())
|
||||||
|
|
||||||
|
def _copy_data(instream, outstream):
|
||||||
|
# Copy one stream to another
|
||||||
|
sent = 0
|
||||||
|
if hasattr(sys.stdin, 'encoding'):
|
||||||
|
enc = sys.stdin.encoding
|
||||||
|
else:
|
||||||
|
enc = 'ascii'
|
||||||
|
while True:
|
||||||
|
data = instream.read(1024)
|
||||||
|
if len(data) == 0:
|
||||||
|
break
|
||||||
|
sent += len(data)
|
||||||
|
logger.debug("sending chunk (%d): %r", sent, data[:256])
|
||||||
|
try:
|
||||||
|
outstream.write(data)
|
||||||
|
except UnicodeError:
|
||||||
|
outstream.write(data.encode(enc))
|
||||||
|
except:
|
||||||
|
# Can sometimes get 'broken pipe' errors even when the data has all
|
||||||
|
# been sent
|
||||||
|
logger.exception('Error sending data')
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
outstream.close()
|
||||||
|
except IOError:
|
||||||
|
logger.warning('Exception occurred while closing: ignored', exc_info=1)
|
||||||
|
logger.debug("closed output, %d bytes sent", sent)
|
||||||
|
|
||||||
|
def _threaded_copy_data(instream, outstream):
|
||||||
|
wr = threading.Thread(target=_copy_data, args=(instream, outstream))
|
||||||
|
wr.setDaemon(True)
|
||||||
|
logger.debug('data copier: %r, %r, %r', wr, instream, outstream)
|
||||||
|
wr.start()
|
||||||
|
return wr
|
||||||
|
|
||||||
|
def _write_passphrase(stream, passphrase, encoding):
|
||||||
|
passphrase = '%s\n' % passphrase
|
||||||
|
passphrase = passphrase.encode(encoding)
|
||||||
|
stream.write(passphrase)
|
||||||
|
logger.debug("Wrote passphrase: %r", passphrase)
|
||||||
|
|
||||||
|
def _is_sequence(instance):
|
||||||
|
return isinstance(instance,list) or isinstance(instance,tuple)
|
||||||
|
|
||||||
|
def _wrap_input(inp):
|
||||||
|
if isinstance(inp, BufferedWriter):
|
||||||
|
oldinp = inp
|
||||||
|
inp = TextIOWrapper(inp)
|
||||||
|
logger.debug('wrapped input: %r -> %r', oldinp, inp)
|
||||||
|
return inp
|
||||||
|
|
||||||
|
def _wrap_output(outp):
|
||||||
|
if isinstance(outp, BufferedReader):
|
||||||
|
oldoutp = outp
|
||||||
|
outp = TextIOWrapper(outp)
|
||||||
|
logger.debug('wrapped output: %r -> %r', oldoutp, outp)
|
||||||
|
return outp
|
||||||
|
|
||||||
|
#The following is needed for Python2.7 :-(
|
||||||
|
def _make_file(s):
|
||||||
|
try:
|
||||||
|
rv = StringIO(s)
|
||||||
|
except (TypeError, UnicodeError):
|
||||||
|
from io import BytesIO
|
||||||
|
rv = BytesIO(s)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def _make_binary_stream(s, encoding):
|
||||||
|
try:
|
||||||
|
if _py3k:
|
||||||
|
if isinstance(s, str):
|
||||||
|
s = s.encode(encoding)
|
||||||
|
else:
|
||||||
|
if type(s) is not str:
|
||||||
|
s = s.encode(encoding)
|
||||||
|
from io import BytesIO
|
||||||
|
rv = BytesIO(s)
|
||||||
|
except ImportError:
|
||||||
|
rv = StringIO(s)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
class GPG(object):
|
||||||
|
"Encapsulate access to the gpg executable"
|
||||||
|
def __init__(self, gpgbinary='gpg', gnupghome=None, verbose=False):
|
||||||
|
"""Initialize a GPG process wrapper. Options are:
|
||||||
|
|
||||||
|
gpgbinary -- full pathname for GPG binary.
|
||||||
|
|
||||||
|
gnupghome -- full pathname to where we can find the public and
|
||||||
|
private keyrings. Default is whatever gpg defaults to.
|
||||||
|
"""
|
||||||
|
self.gpgbinary = gpgbinary
|
||||||
|
self.gnupghome = gnupghome
|
||||||
|
self.verbose = verbose
|
||||||
|
self.encoding = locale.getpreferredencoding()
|
||||||
|
if self.encoding is None: # This happens on Jython!
|
||||||
|
self.encoding = sys.stdin.encoding
|
||||||
|
if gnupghome and not os.path.isdir(self.gnupghome):
|
||||||
|
os.makedirs(self.gnupghome,0x1C0)
|
||||||
|
p = self._open_subprocess(["--version"])
|
||||||
|
result = Verify() # any result will do for this
|
||||||
|
self._collect_output(p, result)
|
||||||
|
if p.returncode != 0:
|
||||||
|
raise ValueError("Error invoking gpg: %s: %s" % (p.returncode,
|
||||||
|
result.stderr))
|
||||||
|
|
||||||
|
def _open_subprocess(self, args, passphrase=False):
|
||||||
|
# Internal method: open a pipe to a GPG subprocess and return
|
||||||
|
# the file objects for communicating with it.
|
||||||
|
cmd = [self.gpgbinary, '--status-fd 2 --no-tty']
|
||||||
|
if self.gnupghome:
|
||||||
|
cmd.append('--homedir "%s" ' % self.gnupghome)
|
||||||
|
if passphrase:
|
||||||
|
cmd.append('--batch --passphrase-fd 0')
|
||||||
|
|
||||||
|
cmd.extend(args)
|
||||||
|
cmd = ' '.join(cmd)
|
||||||
|
if self.verbose:
|
||||||
|
print(cmd)
|
||||||
|
logger.debug("%s", cmd)
|
||||||
|
return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||||
|
|
||||||
|
def _read_response(self, stream, result):
|
||||||
|
# Internal method: reads all the output from GPG, taking notice
|
||||||
|
# only of lines that begin with the magic [GNUPG:] prefix.
|
||||||
|
#
|
||||||
|
# Calls methods on the response object for each valid token found,
|
||||||
|
# with the arg being the remainder of the status line.
|
||||||
|
lines = []
|
||||||
|
while True:
|
||||||
|
line = stream.readline()
|
||||||
|
lines.append(line)
|
||||||
|
if self.verbose:
|
||||||
|
print(line)
|
||||||
|
logger.debug("%s", line.rstrip())
|
||||||
|
if line == "": break
|
||||||
|
line = line.rstrip()
|
||||||
|
if line[0:9] == '[GNUPG:] ':
|
||||||
|
# Chop off the prefix
|
||||||
|
line = line[9:]
|
||||||
|
L = line.split(None, 1)
|
||||||
|
keyword = L[0]
|
||||||
|
if len(L) > 1:
|
||||||
|
value = L[1]
|
||||||
|
else:
|
||||||
|
value = ""
|
||||||
|
result.handle_status(keyword, value)
|
||||||
|
result.stderr = ''.join(lines)
|
||||||
|
|
||||||
|
def _read_data(self, stream, result):
|
||||||
|
# Read the contents of the file from GPG's stdout
|
||||||
|
chunks = []
|
||||||
|
while True:
|
||||||
|
data = stream.read(1024)
|
||||||
|
if len(data) == 0:
|
||||||
|
break
|
||||||
|
logger.debug("chunk: %r" % data[:256])
|
||||||
|
chunks.append(data)
|
||||||
|
if _py3k:
|
||||||
|
# Join using b'' or '', as appropriate
|
||||||
|
result.data = type(data)().join(chunks)
|
||||||
|
else:
|
||||||
|
result.data = ''.join(chunks)
|
||||||
|
|
||||||
|
def _collect_output(self, process, result, writer=None):
|
||||||
|
"""
|
||||||
|
Drain the subprocesses output streams, writing the collected output
|
||||||
|
to the result. If a writer thread (writing to the subprocess) is given,
|
||||||
|
make sure it's joined before returning.
|
||||||
|
"""
|
||||||
|
stderr = _wrap_output(process.stderr)
|
||||||
|
rr = threading.Thread(target=self._read_response, args=(stderr, result))
|
||||||
|
rr.setDaemon(True)
|
||||||
|
logger.debug('stderr reader: %r', rr)
|
||||||
|
rr.start()
|
||||||
|
|
||||||
|
stdout = process.stdout # _wrap_output(process.stdout)
|
||||||
|
dr = threading.Thread(target=self._read_data, args=(stdout, result))
|
||||||
|
dr.setDaemon(True)
|
||||||
|
logger.debug('stdout reader: %r', dr)
|
||||||
|
dr.start()
|
||||||
|
|
||||||
|
dr.join()
|
||||||
|
rr.join()
|
||||||
|
if writer is not None:
|
||||||
|
writer.join()
|
||||||
|
process.wait()
|
||||||
|
|
||||||
|
def _handle_io(self, args, file, result, passphrase=None, binary=False):
|
||||||
|
"Handle a call to GPG - pass input data, collect output data"
|
||||||
|
# Handle a basic data call - pass data to GPG, handle the output
|
||||||
|
# including status information. Garbage In, Garbage Out :)
|
||||||
|
p = self._open_subprocess(args, passphrase is not None)
|
||||||
|
if not binary and not isinstance(file, BufferedReader):
|
||||||
|
stdin = _wrap_input(p.stdin)
|
||||||
|
else:
|
||||||
|
stdin = p.stdin
|
||||||
|
if passphrase:
|
||||||
|
_write_passphrase(stdin, passphrase, self.encoding)
|
||||||
|
writer = _threaded_copy_data(file, stdin)
|
||||||
|
self._collect_output(p, result, writer)
|
||||||
|
return result
|
||||||
|
|
||||||
|
#
|
||||||
|
# SIGNATURE METHODS
|
||||||
|
#
|
||||||
|
def sign(self, message, **kwargs):
|
||||||
|
"""sign message"""
|
||||||
|
file = _make_binary_stream(message, self.encoding)
|
||||||
|
return self.sign_file(file, **kwargs)
|
||||||
|
|
||||||
|
def sign_file(self, file, keyid=None, passphrase=None, clearsign=True,
|
||||||
|
detach=False):
|
||||||
|
"""sign file"""
|
||||||
|
logger.debug("sign_file: %s", file)
|
||||||
|
args = ["-sa"]
|
||||||
|
# You can't specify detach-sign and clearsign together: gpg ignores
|
||||||
|
# the detach-sign in that case.
|
||||||
|
if detach:
|
||||||
|
args.append("--detach-sign")
|
||||||
|
elif clearsign:
|
||||||
|
args.append("--clearsign")
|
||||||
|
if keyid:
|
||||||
|
args.append("--default-key %s" % keyid)
|
||||||
|
result = Sign(self.encoding)
|
||||||
|
#We could use _handle_io here except for the fact that if the
|
||||||
|
#passphrase is bad, gpg bails and you can't write the message.
|
||||||
|
#self._handle_io(args, _make_file(message), result, passphrase=passphrase)
|
||||||
|
p = self._open_subprocess(args, passphrase is not None)
|
||||||
|
try:
|
||||||
|
stdin = p.stdin
|
||||||
|
if passphrase:
|
||||||
|
_write_passphrase(stdin, passphrase, self.encoding)
|
||||||
|
writer = _threaded_copy_data(file, stdin)
|
||||||
|
except IOError:
|
||||||
|
logging.exception("error writing message")
|
||||||
|
writer = None
|
||||||
|
self._collect_output(p, result, writer)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def verify(self, data):
|
||||||
|
"""Verify the signature on the contents of the string 'data'
|
||||||
|
|
||||||
|
>>> gpg = GPG(gnupghome="keys")
|
||||||
|
>>> input = gpg.gen_key_input(Passphrase='foo')
|
||||||
|
>>> key = gpg.gen_key(input)
|
||||||
|
>>> assert key
|
||||||
|
>>> sig = gpg.sign('hello',keyid=key.fingerprint,passphrase='bar')
|
||||||
|
>>> assert not sig
|
||||||
|
>>> sig = gpg.sign('hello',keyid=key.fingerprint,passphrase='foo')
|
||||||
|
>>> assert sig
|
||||||
|
>>> verify = gpg.verify(sig.data)
|
||||||
|
>>> assert verify
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.verify_file(_make_binary_stream(data, self.encoding))
|
||||||
|
|
||||||
|
def verify_file(self, file, data_filename=None):
|
||||||
|
"Verify the signature on the contents of the file-like object 'file'"
|
||||||
|
logger.debug('verify_file: %r, %r', file, data_filename)
|
||||||
|
result = Verify()
|
||||||
|
args = ['--verify']
|
||||||
|
if data_filename is None:
|
||||||
|
self._handle_io(args, file, result, binary=True)
|
||||||
|
else:
|
||||||
|
logger.debug('Handling detached verification')
|
||||||
|
import tempfile
|
||||||
|
fd, fn = tempfile.mkstemp(prefix='pygpg')
|
||||||
|
s = file.read()
|
||||||
|
logger.debug('Wrote to temp file: %r', s)
|
||||||
|
os.write(fd, s)
|
||||||
|
os.close(fd)
|
||||||
|
args.append(fn)
|
||||||
|
args.append(data_filename)
|
||||||
|
try:
|
||||||
|
p = self._open_subprocess(args)
|
||||||
|
self._collect_output(p, result)
|
||||||
|
finally:
|
||||||
|
os.unlink(fn)
|
||||||
|
return result
|
||||||
|
|
||||||
|
#
|
||||||
|
# KEY MANAGEMENT
|
||||||
|
#
|
||||||
|
|
||||||
|
def import_keys(self, key_data):
|
||||||
|
""" import the key_data into our keyring
|
||||||
|
|
||||||
|
>>> import shutil
|
||||||
|
>>> shutil.rmtree("keys")
|
||||||
|
>>> gpg = GPG(gnupghome="keys")
|
||||||
|
>>> input = gpg.gen_key_input()
|
||||||
|
>>> result = gpg.gen_key(input)
|
||||||
|
>>> print1 = result.fingerprint
|
||||||
|
>>> result = gpg.gen_key(input)
|
||||||
|
>>> print2 = result.fingerprint
|
||||||
|
>>> pubkey1 = gpg.export_keys(print1)
|
||||||
|
>>> seckey1 = gpg.export_keys(print1,secret=True)
|
||||||
|
>>> seckeys = gpg.list_keys(secret=True)
|
||||||
|
>>> pubkeys = gpg.list_keys()
|
||||||
|
>>> assert print1 in seckeys.fingerprints
|
||||||
|
>>> assert print1 in pubkeys.fingerprints
|
||||||
|
>>> str(gpg.delete_keys(print1))
|
||||||
|
'Must delete secret key first'
|
||||||
|
>>> str(gpg.delete_keys(print1,secret=True))
|
||||||
|
'ok'
|
||||||
|
>>> str(gpg.delete_keys(print1))
|
||||||
|
'ok'
|
||||||
|
>>> str(gpg.delete_keys("nosuchkey"))
|
||||||
|
'No such key'
|
||||||
|
>>> seckeys = gpg.list_keys(secret=True)
|
||||||
|
>>> pubkeys = gpg.list_keys()
|
||||||
|
>>> assert not print1 in seckeys.fingerprints
|
||||||
|
>>> assert not print1 in pubkeys.fingerprints
|
||||||
|
>>> result = gpg.import_keys('foo')
|
||||||
|
>>> assert not result
|
||||||
|
>>> result = gpg.import_keys(pubkey1)
|
||||||
|
>>> pubkeys = gpg.list_keys()
|
||||||
|
>>> seckeys = gpg.list_keys(secret=True)
|
||||||
|
>>> assert not print1 in seckeys.fingerprints
|
||||||
|
>>> assert print1 in pubkeys.fingerprints
|
||||||
|
>>> result = gpg.import_keys(seckey1)
|
||||||
|
>>> assert result
|
||||||
|
>>> seckeys = gpg.list_keys(secret=True)
|
||||||
|
>>> pubkeys = gpg.list_keys()
|
||||||
|
>>> assert print1 in seckeys.fingerprints
|
||||||
|
>>> assert print1 in pubkeys.fingerprints
|
||||||
|
>>> assert print2 in pubkeys.fingerprints
|
||||||
|
|
||||||
|
"""
|
||||||
|
result = ImportResult()
|
||||||
|
logger.debug('import_keys: %r', key_data[:256])
|
||||||
|
data = _make_binary_stream(key_data, self.encoding)
|
||||||
|
self._handle_io(['--import'], data, result, binary=True)
|
||||||
|
logger.debug('import_keys result: %r', result.__dict__)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def delete_keys(self, fingerprints, secret=False):
|
||||||
|
which='key'
|
||||||
|
if secret:
|
||||||
|
which='secret-key'
|
||||||
|
if _is_sequence(fingerprints):
|
||||||
|
fingerprints = ' '.join(fingerprints)
|
||||||
|
args = ["--batch --delete-%s %s" % (which, fingerprints)]
|
||||||
|
result = DeleteResult()
|
||||||
|
p = self._open_subprocess(args)
|
||||||
|
self._collect_output(p, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def export_keys(self, keyids, secret=False):
|
||||||
|
"export the indicated keys. 'keyid' is anything gpg accepts"
|
||||||
|
which=''
|
||||||
|
if secret:
|
||||||
|
which='-secret-key'
|
||||||
|
if _is_sequence(keyids):
|
||||||
|
keyids = ' '.join(keyids)
|
||||||
|
args = ["--armor --export%s %s" % (which, keyids)]
|
||||||
|
p = self._open_subprocess(args)
|
||||||
|
# gpg --export produces no status-fd output; stdout will be
|
||||||
|
# empty in case of failure
|
||||||
|
#stdout, stderr = p.communicate()
|
||||||
|
result = DeleteResult() # any result will do
|
||||||
|
self._collect_output(p, result)
|
||||||
|
logger.debug('export_keys result: %r', result.data)
|
||||||
|
return result.data.decode(self.encoding)
|
||||||
|
|
||||||
|
def list_keys(self, secret=False):
|
||||||
|
""" list the keys currently in the keyring
|
||||||
|
|
||||||
|
>>> import shutil
|
||||||
|
>>> shutil.rmtree("keys")
|
||||||
|
>>> gpg = GPG(gnupghome="keys")
|
||||||
|
>>> input = gpg.gen_key_input()
|
||||||
|
>>> result = gpg.gen_key(input)
|
||||||
|
>>> print1 = result.fingerprint
|
||||||
|
>>> result = gpg.gen_key(input)
|
||||||
|
>>> print2 = result.fingerprint
|
||||||
|
>>> pubkeys = gpg.list_keys()
|
||||||
|
>>> assert print1 in pubkeys.fingerprints
|
||||||
|
>>> assert print2 in pubkeys.fingerprints
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
which='keys'
|
||||||
|
if secret:
|
||||||
|
which='secret-keys'
|
||||||
|
args = "--list-%s --fixed-list-mode --fingerprint --with-colons" % (which)
|
||||||
|
args = [args]
|
||||||
|
p = self._open_subprocess(args)
|
||||||
|
|
||||||
|
# there might be some status thingumy here I should handle... (amk)
|
||||||
|
# ...nope, unless you care about expired sigs or keys (stevegt)
|
||||||
|
|
||||||
|
# Get the response information
|
||||||
|
result = ListKeys()
|
||||||
|
self._collect_output(p, result)
|
||||||
|
lines = result.data.decode(self.encoding).splitlines()
|
||||||
|
valid_keywords = 'pub uid sec fpr'.split()
|
||||||
|
for line in lines:
|
||||||
|
if self.verbose:
|
||||||
|
print(line)
|
||||||
|
logger.debug("line: %r", line.rstrip())
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
L = line.strip().split(':')
|
||||||
|
if not L:
|
||||||
|
continue
|
||||||
|
keyword = L[0]
|
||||||
|
if keyword in valid_keywords:
|
||||||
|
getattr(result, keyword)(L)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def gen_key(self, input):
|
||||||
|
"""Generate a key; you might use gen_key_input() to create the
|
||||||
|
control input.
|
||||||
|
|
||||||
|
>>> gpg = GPG(gnupghome="keys")
|
||||||
|
>>> input = gpg.gen_key_input()
|
||||||
|
>>> result = gpg.gen_key(input)
|
||||||
|
>>> assert result
|
||||||
|
>>> result = gpg.gen_key('foo')
|
||||||
|
>>> assert not result
|
||||||
|
|
||||||
|
"""
|
||||||
|
args = ["--gen-key --batch"]
|
||||||
|
result = GenKey()
|
||||||
|
file = _make_file(input)
|
||||||
|
self._handle_io(args, file, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def gen_key_input(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Generate --gen-key input per gpg doc/DETAILS
|
||||||
|
"""
|
||||||
|
parms = {}
|
||||||
|
for key, val in list(kwargs.items()):
|
||||||
|
key = key.replace('_','-').title()
|
||||||
|
parms[key] = val
|
||||||
|
parms.setdefault('Key-Type','RSA')
|
||||||
|
parms.setdefault('Key-Length',1024)
|
||||||
|
parms.setdefault('Name-Real', "Autogenerated Key")
|
||||||
|
parms.setdefault('Name-Comment', "Generated by gnupg.py")
|
||||||
|
try:
|
||||||
|
logname = os.environ['LOGNAME']
|
||||||
|
except KeyError:
|
||||||
|
logname = os.environ['USERNAME']
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
parms.setdefault('Name-Email', "%s@%s" % (logname.replace(' ', '_'),
|
||||||
|
hostname))
|
||||||
|
out = "Key-Type: %s\n" % parms.pop('Key-Type')
|
||||||
|
for key, val in list(parms.items()):
|
||||||
|
out += "%s: %s\n" % (key, val)
|
||||||
|
out += "%commit\n"
|
||||||
|
return out
|
||||||
|
|
||||||
|
# Key-Type: RSA
|
||||||
|
# Key-Length: 1024
|
||||||
|
# Name-Real: ISdlink Server on %s
|
||||||
|
# Name-Comment: Created by %s
|
||||||
|
# Name-Email: isdlink@%s
|
||||||
|
# Expire-Date: 0
|
||||||
|
# %commit
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Key-Type: DSA
|
||||||
|
# Key-Length: 1024
|
||||||
|
# Subkey-Type: ELG-E
|
||||||
|
# Subkey-Length: 1024
|
||||||
|
# Name-Real: Joe Tester
|
||||||
|
# Name-Comment: with stupid passphrase
|
||||||
|
# Name-Email: joe@foo.bar
|
||||||
|
# Expire-Date: 0
|
||||||
|
# Passphrase: abc
|
||||||
|
# %pubring foo.pub
|
||||||
|
# %secring foo.sec
|
||||||
|
# %commit
|
||||||
|
|
||||||
|
#
|
||||||
|
# ENCRYPTION
|
||||||
|
#
|
||||||
|
def encrypt_file(self, file, recipients, sign=None,
|
||||||
|
always_trust=False, passphrase=None,
|
||||||
|
armor=True, output=None):
|
||||||
|
"Encrypt the message read from the file-like object 'file'"
|
||||||
|
args = ['--encrypt']
|
||||||
|
if armor: # create ascii-armored output - set to False for binary output
|
||||||
|
args.append('--armor')
|
||||||
|
if output: # write the output to a file with the specified name
|
||||||
|
if os.path.exists(output):
|
||||||
|
os.remove(output) # to avoid overwrite confirmation message
|
||||||
|
args.append('--output %s' % output)
|
||||||
|
if not _is_sequence(recipients):
|
||||||
|
recipients = (recipients,)
|
||||||
|
for recipient in recipients:
|
||||||
|
args.append('--recipient %s' % recipient)
|
||||||
|
if sign:
|
||||||
|
args.append("--sign --default-key %s" % sign)
|
||||||
|
if always_trust:
|
||||||
|
args.append("--always-trust")
|
||||||
|
result = Crypt(self.encoding)
|
||||||
|
self._handle_io(args, file, result, passphrase=passphrase, binary=True)
|
||||||
|
logger.debug('encrypt result: %r', result.data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def encrypt(self, data, recipients, **kwargs):
|
||||||
|
"""Encrypt the message contained in the string 'data'
|
||||||
|
|
||||||
|
>>> import shutil
|
||||||
|
>>> if os.path.exists("keys"):
|
||||||
|
... shutil.rmtree("keys")
|
||||||
|
>>> gpg = GPG(gnupghome="keys")
|
||||||
|
>>> input = gpg.gen_key_input(passphrase='foo')
|
||||||
|
>>> result = gpg.gen_key(input)
|
||||||
|
>>> print1 = result.fingerprint
|
||||||
|
>>> input = gpg.gen_key_input()
|
||||||
|
>>> result = gpg.gen_key(input)
|
||||||
|
>>> print2 = result.fingerprint
|
||||||
|
>>> result = gpg.encrypt("hello",print2)
|
||||||
|
>>> message = str(result)
|
||||||
|
>>> assert message != 'hello'
|
||||||
|
>>> result = gpg.decrypt(message)
|
||||||
|
>>> assert result
|
||||||
|
>>> str(result)
|
||||||
|
'hello'
|
||||||
|
>>> result = gpg.encrypt("hello again",print1)
|
||||||
|
>>> message = str(result)
|
||||||
|
>>> result = gpg.decrypt(message)
|
||||||
|
>>> result.status
|
||||||
|
'need passphrase'
|
||||||
|
>>> result = gpg.decrypt(message,passphrase='bar')
|
||||||
|
>>> result.status
|
||||||
|
'decryption failed'
|
||||||
|
>>> assert not result
|
||||||
|
>>> result = gpg.decrypt(message,passphrase='foo')
|
||||||
|
>>> result.status
|
||||||
|
'decryption ok'
|
||||||
|
>>> str(result)
|
||||||
|
'hello again'
|
||||||
|
>>> result = gpg.encrypt("signed hello",print2,sign=print1)
|
||||||
|
>>> result.status
|
||||||
|
'need passphrase'
|
||||||
|
>>> result = gpg.encrypt("signed hello",print2,sign=print1,passphrase='foo')
|
||||||
|
>>> result.status
|
||||||
|
'encryption ok'
|
||||||
|
>>> message = str(result)
|
||||||
|
>>> result = gpg.decrypt(message)
|
||||||
|
>>> result.status
|
||||||
|
'decryption ok'
|
||||||
|
>>> assert result.fingerprint == print1
|
||||||
|
|
||||||
|
"""
|
||||||
|
data = _make_binary_stream(data, self.encoding)
|
||||||
|
return self.encrypt_file(data, recipients, **kwargs)
|
||||||
|
|
||||||
|
def decrypt(self, message, **kwargs):
|
||||||
|
data = _make_binary_stream(message, self.encoding)
|
||||||
|
return self.decrypt_file(data, **kwargs)
|
||||||
|
|
||||||
|
def decrypt_file(self, file, always_trust=False, passphrase=None,
|
||||||
|
output=None):
|
||||||
|
args = ["--decrypt"]
|
||||||
|
if output: # write the output to a file with the specified name
|
||||||
|
if os.path.exists(output):
|
||||||
|
os.remove(output) # to avoid overwrite confirmation message
|
||||||
|
args.append('--output %s' % output)
|
||||||
|
if always_trust:
|
||||||
|
args.append("--always-trust")
|
||||||
|
result = Crypt(self.encoding)
|
||||||
|
self._handle_io(args, file, result, passphrase, binary=True)
|
||||||
|
logger.debug('decrypt result: %r', result.data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
class Verify(object):
|
||||||
|
"Handle status messages for --verify"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.valid = False
|
||||||
|
self.fingerprint = self.creation_date = self.timestamp = None
|
||||||
|
self.signature_id = self.key_id = None
|
||||||
|
self.username = None
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
return self.valid
|
||||||
|
|
||||||
|
__bool__ = __nonzero__
|
||||||
|
|
||||||
|
def handle_status(self, key, value):
|
||||||
|
if key in ("TRUST_UNDEFINED", "TRUST_NEVER", "TRUST_MARGINAL",
|
||||||
|
"TRUST_FULLY", "TRUST_ULTIMATE", "RSA_OR_IDEA"):
|
||||||
|
pass
|
||||||
|
elif key in ("PLAINTEXT", "PLAINTEXT_LENGTH"):
|
||||||
|
pass
|
||||||
|
elif key == "BADSIG":
|
||||||
|
self.valid = False
|
||||||
|
self.key_id, self.username = value.split(None, 1)
|
||||||
|
elif key == "GOODSIG":
|
||||||
|
self.valid = True
|
||||||
|
self.key_id, self.username = value.split(None, 1)
|
||||||
|
elif key == "VALIDSIG":
|
||||||
|
(self.fingerprint,
|
||||||
|
self.creation_date,
|
||||||
|
self.sig_timestamp,
|
||||||
|
self.expire_timestamp) = value.split()[:4]
|
||||||
|
elif key == "SIG_ID":
|
||||||
|
(self.signature_id,
|
||||||
|
self.creation_date, self.timestamp) = value.split()
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown status message: %r" % key)
|
||||||
|
|
||||||
|
class ImportResult(object):
|
||||||
|
"Handle status messages for --import"
|
||||||
|
|
||||||
|
counts = '''count no_user_id imported imported_rsa unchanged
|
||||||
|
n_uids n_subk n_sigs n_revoc sec_read sec_imported
|
||||||
|
sec_dups not_imported'''.split()
|
||||||
|
def __init__(self):
|
||||||
|
self.imported = []
|
||||||
|
self.results = []
|
||||||
|
self.fingerprints = []
|
||||||
|
for result in self.counts:
|
||||||
|
setattr(self, result, None)
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
if self.not_imported: return False
|
||||||
|
if not self.fingerprints: return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
__bool__ = __nonzero__
|
||||||
|
|
||||||
|
ok_reason = {
|
||||||
|
'0': 'Not actually changed',
|
||||||
|
'1': 'Entirely new key',
|
||||||
|
'2': 'New user IDs',
|
||||||
|
'4': 'New signatures',
|
||||||
|
'8': 'New subkeys',
|
||||||
|
'16': 'Contains private key',
|
||||||
|
}
|
||||||
|
|
||||||
|
problem_reason = {
|
||||||
|
'0': 'No specific reason given',
|
||||||
|
'1': 'Invalid Certificate',
|
||||||
|
'2': 'Issuer Certificate missing',
|
||||||
|
'3': 'Certificate Chain too long',
|
||||||
|
'4': 'Error storing certificate',
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_status(self, key, value):
|
||||||
|
if key == "IMPORTED":
|
||||||
|
# this duplicates info we already see in import_ok & import_problem
|
||||||
|
pass
|
||||||
|
elif key == "NODATA":
|
||||||
|
self.results.append({'fingerprint': None,
|
||||||
|
'problem': '0', 'text': 'No valid data found'})
|
||||||
|
elif key == "IMPORT_OK":
|
||||||
|
reason, fingerprint = value.split()
|
||||||
|
reasons = []
|
||||||
|
for code, text in list(self.ok_reason.items()):
|
||||||
|
if int(reason) | int(code) == int(reason):
|
||||||
|
reasons.append(text)
|
||||||
|
reasontext = '\n'.join(reasons) + "\n"
|
||||||
|
self.results.append({'fingerprint': fingerprint,
|
||||||
|
'ok': reason, 'text': reasontext})
|
||||||
|
self.fingerprints.append(fingerprint)
|
||||||
|
elif key == "IMPORT_PROBLEM":
|
||||||
|
try:
|
||||||
|
reason, fingerprint = value.split()
|
||||||
|
except:
|
||||||
|
reason = value
|
||||||
|
fingerprint = '<unknown>'
|
||||||
|
self.results.append({'fingerprint': fingerprint,
|
||||||
|
'problem': reason, 'text': self.problem_reason[reason]})
|
||||||
|
elif key == "IMPORT_RES":
|
||||||
|
import_res = value.split()
|
||||||
|
for i in range(len(self.counts)):
|
||||||
|
setattr(self, self.counts[i], int(import_res[i]))
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown status message: %r" % key)
|
||||||
|
|
||||||
|
def summary(self):
|
||||||
|
l = []
|
||||||
|
l.append('%d imported'%self.imported)
|
||||||
|
if self.not_imported:
|
||||||
|
l.append('%d not imported'%self.not_imported)
|
||||||
|
return ', '.join(l)
|
||||||
|
|
||||||
|
class ListKeys(list):
|
||||||
|
''' Handle status messages for --list-keys.
|
||||||
|
|
||||||
|
Handle pub and uid (relating the latter to the former).
|
||||||
|
|
||||||
|
Don't care about (info from src/DETAILS):
|
||||||
|
|
||||||
|
crt = X.509 certificate
|
||||||
|
crs = X.509 certificate and private key available
|
||||||
|
sub = subkey (secondary key)
|
||||||
|
ssb = secret subkey (secondary key)
|
||||||
|
uat = user attribute (same as user id except for field 10).
|
||||||
|
sig = signature
|
||||||
|
rev = revocation signature
|
||||||
|
pkd = public key data (special field format, see below)
|
||||||
|
grp = reserved for gpgsm
|
||||||
|
rvk = revocation key
|
||||||
|
'''
|
||||||
|
def __init__(self):
|
||||||
|
self.curkey = None
|
||||||
|
self.fingerprints = []
|
||||||
|
|
||||||
|
def key(self, args):
|
||||||
|
vars = ("""
|
||||||
|
type trust length algo keyid date expires dummy ownertrust uid
|
||||||
|
""").split()
|
||||||
|
self.curkey = {}
|
||||||
|
for i in range(len(vars)):
|
||||||
|
self.curkey[vars[i]] = args[i]
|
||||||
|
self.curkey['uids'] = [self.curkey['uid']]
|
||||||
|
del self.curkey['uid']
|
||||||
|
self.append(self.curkey)
|
||||||
|
|
||||||
|
pub = sec = key
|
||||||
|
|
||||||
|
def fpr(self, args):
|
||||||
|
self.curkey['fingerprint'] = args[9]
|
||||||
|
self.fingerprints.append(args[9])
|
||||||
|
|
||||||
|
def uid(self, args):
|
||||||
|
self.curkey['uids'].append(args[9])
|
||||||
|
|
||||||
|
def handle_status(self, key, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Crypt(Verify):
|
||||||
|
"Handle status messages for --encrypt and --decrypt"
|
||||||
|
def __init__(self, encoding):
|
||||||
|
Verify.__init__(self)
|
||||||
|
self.data = ''
|
||||||
|
self.ok = False
|
||||||
|
self.status = ''
|
||||||
|
self.encoding = encoding
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
if self.ok: return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
__bool__ = __nonzero__
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.data.decode(self.encoding)
|
||||||
|
|
||||||
|
def handle_status(self, key, value):
|
||||||
|
if key in ("ENC_TO", "USERID_HINT", "GOODMDC", "END_DECRYPTION",
|
||||||
|
"BEGIN_SIGNING", "NO_SECKEY"):
|
||||||
|
pass
|
||||||
|
elif key in ("NEED_PASSPHRASE", "BAD_PASSPHRASE", "GOOD_PASSPHRASE",
|
||||||
|
"DECRYPTION_FAILED"):
|
||||||
|
self.status = key.replace("_", " ").lower()
|
||||||
|
elif key == "NEED_PASSPHRASE_SYM":
|
||||||
|
self.status = 'need symmetric passphrase'
|
||||||
|
elif key == "BEGIN_DECRYPTION":
|
||||||
|
self.status = 'decryption incomplete'
|
||||||
|
elif key == "BEGIN_ENCRYPTION":
|
||||||
|
self.status = 'encryption incomplete'
|
||||||
|
elif key == "DECRYPTION_OKAY":
|
||||||
|
self.status = 'decryption ok'
|
||||||
|
self.ok = True
|
||||||
|
elif key == "END_ENCRYPTION":
|
||||||
|
self.status = 'encryption ok'
|
||||||
|
self.ok = True
|
||||||
|
elif key == "INV_RECP":
|
||||||
|
self.status = 'invalid recipient'
|
||||||
|
elif key == "KEYEXPIRED":
|
||||||
|
self.status = 'key expired'
|
||||||
|
elif key == "SIG_CREATED":
|
||||||
|
self.status = 'sig created'
|
||||||
|
elif key == "SIGEXPIRED":
|
||||||
|
self.status = 'sig expired'
|
||||||
|
else:
|
||||||
|
Verify.handle_status(self, key, value)
|
||||||
|
|
||||||
|
class GenKey(object):
|
||||||
|
"Handle status messages for --gen-key"
|
||||||
|
def __init__(self):
|
||||||
|
self.type = None
|
||||||
|
self.fingerprint = None
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
if self.fingerprint: return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
__bool__ = __nonzero__
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.fingerprint or ''
|
||||||
|
|
||||||
|
def handle_status(self, key, value):
|
||||||
|
if key in ("PROGRESS", "GOOD_PASSPHRASE", "NODATA"):
|
||||||
|
pass
|
||||||
|
elif key == "KEY_CREATED":
|
||||||
|
(self.type,self.fingerprint) = value.split()
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown status message: %r" % key)
|
||||||
|
|
||||||
|
class DeleteResult(object):
|
||||||
|
"Handle status messages for --delete-key and --delete-secret-key"
|
||||||
|
def __init__(self):
|
||||||
|
self.status = 'ok'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.status
|
||||||
|
|
||||||
|
problem_reason = {
|
||||||
|
'1': 'No such key',
|
||||||
|
'2': 'Must delete secret key first',
|
||||||
|
'3': 'Ambigious specification',
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_status(self, key, value):
|
||||||
|
if key == "DELETE_PROBLEM":
|
||||||
|
self.status = self.problem_reason.get(value,
|
||||||
|
"Unknown error: %r" % value)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown status message: %r" % key)
|
||||||
|
|
||||||
|
class Sign(object):
|
||||||
|
"Handle status messages for --sign"
|
||||||
|
def __init__(self, encoding):
|
||||||
|
self.type = None
|
||||||
|
self.fingerprint = None
|
||||||
|
self.encoding = encoding
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
return self.fingerprint is not None
|
||||||
|
|
||||||
|
__bool__ = __nonzero__
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.data.decode(self.encoding)
|
||||||
|
|
||||||
|
def handle_status(self, key, value):
|
||||||
|
if key in ("USERID_HINT", "NEED_PASSPHRASE", "BAD_PASSPHRASE",
|
||||||
|
"GOOD_PASSPHRASE", "BEGIN_SIGNING"):
|
||||||
|
pass
|
||||||
|
elif key == "SIG_CREATED":
|
||||||
|
(self.type,
|
||||||
|
algo, hashalgo, cls,
|
||||||
|
self.timestamp, self.fingerprint
|
||||||
|
) = value.split()
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown status message: %r" % key)
|
|
@ -60,8 +60,8 @@ class FeaturesWindow:
|
||||||
_('Feature not available under Windows.')),
|
_('Feature not available under Windows.')),
|
||||||
_('OpenGPG message encryption'): (self.gpg_available,
|
_('OpenGPG message encryption'): (self.gpg_available,
|
||||||
_('Encrypting chat messages with gpg keys.'),
|
_('Encrypting chat messages with gpg keys.'),
|
||||||
_('Requires gpg and python-GnuPGInterface.'),
|
_('Requires gpg and python-gnupg (http://code.google.com/p/python-gnupg/).'),
|
||||||
_('Feature not available under Windows.')),
|
_('Requires gpg.exe in PATH.')),
|
||||||
_('Network-manager'): (self.network_manager_available,
|
_('Network-manager'): (self.network_manager_available,
|
||||||
_('Autodetection of network status.'),
|
_('Autodetection of network status.'),
|
||||||
_('Requires gnome-network-manager and python-dbus.'),
|
_('Requires gnome-network-manager and python-dbus.'),
|
||||||
|
@ -181,8 +181,6 @@ class FeaturesWindow:
|
||||||
return dbus_support.supported
|
return dbus_support.supported
|
||||||
|
|
||||||
def gpg_available(self):
|
def gpg_available(self):
|
||||||
if os.name == 'nt':
|
|
||||||
return False
|
|
||||||
return gajim.HAVE_GPG
|
return gajim.HAVE_GPG
|
||||||
|
|
||||||
def network_manager_available(self):
|
def network_manager_available(self):
|
||||||
|
|
Loading…
Add table
Reference in a new issue