Compare commits

...

70 Commits

Author SHA1 Message Date
Isis Lovecruft 2beac24161
Merge branch 'release/2.0.2' 2015-03-19 00:25:25 +00:00
Isis Lovecruft 4f1b1f6a8d
Refactor _make_binary_stream() error handling and variable names. 2015-03-19 00:12:21 +00:00
Isis Lovecruft da59707945
Add docstring for _make_binary_encoding(). 2015-03-19 00:10:40 +00:00
Isis Lovecruft a6d024ff49
Separate _copy_data() binary encoding into new binary() function. 2015-03-19 00:08:29 +00:00
Isis Lovecruft a06c93d6e7
Fix switched b() functions.
* FIXES mistake from commit 16107bc8a8
2015-03-19 00:07:19 +00:00
Isis Lovecruft 76d70c68aa
Add .coveragerc file. 2015-03-18 19:44:25 +00:00
Isis Lovecruft 94f81c3226
Merge branch 'fix/82-passphrase-empty-str' into develop 2015-03-18 19:40:23 +00:00
Isis Lovecruft a749acf486
Avoid writing raw encrypted bytes to terminal debug loggers. 2015-03-18 19:39:22 +00:00
Isis Lovecruft 16107bc8a8
Workaround possible upstream GnuPG bug sign fail when passphrase=''.
If passed `--passphrase-fd 0` and piped the passphrase `''` (an empty
string), GnuPG will truncate the message being signed. The truncation
appears to only affect messages which contain newlines; for those
messages, the first line (up unto and including the newline character)
will be truncated and thus missing from the output signed message or
signature, causing verification for signatures on the original
non-truncated message to fail.

 * FIXES #82: https://github.com/isislovecruft/python-gnupg/issues/82
2015-03-18 19:33:09 +00:00
Isis Lovecruft 9be01ec6df
Add two more signature passphrase tests with bytes literals for #82. 2015-03-18 19:31:33 +00:00
Isis Lovecruft e90ae54738
Add two signature passphrase tests for reproducing Issue #82. 2015-03-18 05:47:40 +00:00
Isis Lovecruft fea39ec83e
Merge branch 'fix/4-test-deleting-secret-and-subkeys' into develop 2015-03-18 05:34:31 +00:00
Isis Lovecruft b0584854e1
Add test for subkey deletion (which should delete public and secret).
* FIXES part of #4:
   https://github.com/isislovecruft/python-gnupg/issues/4
2015-03-18 05:32:28 +00:00
Isis Lovecruft 49079f9672
Add a test for deletion of secret keys.
* FIXES part of #4:
   https://github.com/isislovecruft/python-gnupg/issues/4
2015-03-18 05:32:00 +00:00
Isis Lovecruft adc4994a0d
Cleanup test for public key deletion.
This changes the test to explicitely test that only the specified public
key is deleted, not its secret portions, nor any other key.

 * FIXES part of #4:
   https://github.com/isislovecruft/python-gnupg/issues/4
2015-03-18 05:30:13 +00:00
Isis Lovecruft 8c5730666a
Merge branch 'fix/101-pypy-gen-key' into develop 2015-03-18 05:01:21 +00:00
Isis Lovecruft 5895997554
Fix UnicodeDecodeError in gen_key() when run with PyPy.
* FIXES #101:
   https://github.com/isislovecruft/python-gnupg/issues/101
2015-03-18 05:00:00 +00:00
Isis Lovecruft 917a3dcd89
Merge branch 'fix/102-py3k-decrypt-bytes-literal' into develop 2015-03-18 04:56:26 +00:00
Isis Lovecruft b7f520244e
Add test for Issue #102 based on @doktorstick's example code. 2015-03-18 04:37:09 +00:00
Isis Lovecruft 742ccd77fb
Merge remote-tracking branch 'mvk/master' into develop 2015-03-18 03:52:50 +00:00
Isis Lovecruft d66d86ca7e
Fix bug in example/make-8192-bit-key.py resulting from parser changes. 2015-03-18 03:50:16 +00:00
Max Kovgan 0af2720754 fix broken py2 compatibity 2015-03-16 19:39:38 +02:00
Isis Lovecruft 10df44b75f
Merge branch 'master' into develop 2015-03-13 05:06:50 +00:00
Isis Lovecruft af781626a9
Merge branch 'release/2.0.1' 2015-03-13 05:06:34 +00:00
Isis Lovecruft cf4d3efd8d
Merge branch 'fix/94-encrypt-from-stream' into develop 2015-03-13 03:48:49 +00:00
Isis Lovecruft b7ff69092a
Avoid writing to /tmp and borking terminals in regression test from #94. 2015-03-13 03:47:42 +00:00
Isis Lovecruft 8e6c2a752c
Merge branch 'fix/93-encrypt-to-stream' into develop 2015-03-13 03:46:47 +00:00
Isis Lovecruft 79285c4c17
Add @doktorstick's example code for reproducing #93 as a unittest.
* ADD regression test for #93.
2015-03-13 03:45:20 +00:00
Isis Lovecruft 6d1890389c
Actually check output file contents in to test_encrypt_*() tests.
This provides more accurate testing for issues like #93.
2015-03-13 03:44:12 +00:00
Isis Lovecruft 8579331562
Merge branch 'fix/94-encrypt-from-stream' into develop 2015-03-13 03:22:31 +00:00
Isis Lovecruft 0c87da3d78
Recognise builtin Python2 and Python3 file handle types as streams.
* FIXES Issue #93 for both Python2 and Python3.
2015-03-13 03:20:32 +00:00
Isis Lovecruft 38685ae001
Add @doktorstick's example code for reproducing Issue #93 as a test. 2015-03-13 03:18:52 +00:00
Isis Lovecruft 9d8d6cce5c
Merge branch 'fix/89-python3' into develop 2015-03-13 02:47:57 +00:00
Isis Lovecruft 43164fa7db
Fix Python3 stream encoding issues in _copy_data().
These issues were introduced in f8ccdc50.  Because we no longer convert
everything to an io.BytesIO in _encrypt() with _make_binary_stream(),
all io.StringIO()s which are passed through must take encoded strings
and io.BytesIO()s must take bytes (and there is actually a difference
with Python3).

Additionally, there appears to be an issue where the `outstream` passed
to _copy_data() is sometimes a _io.BufferedWriter and other times an
encodings.utf_8.StreamWriter.  I am not sure yet where this problem was
introduced.  For now, the workaround for dealing with the Python3
bytes/str io.BytesIO/io.StringIO problem also provides a workaround for
this issue.

 * FIXES #88.
 * FIXES #89 for Python3.
 * FIXES #93.
2015-03-13 02:45:05 +00:00
Isis Lovecruft a7e772f10a
Make an open() file mode explicitly binary.
It already was binary, due to the `from codecs import open as open`,
however we should be more explicit.
2015-03-13 02:32:52 +00:00
Isis Lovecruft 4be6fb75e3
Fix potential UnicodeEncodeError in gen_key_input(). 2015-03-13 02:32:52 +00:00
Isis Lovecruft b970917701
Fix multiple encoding errors in tests. 2015-03-13 02:32:51 +00:00
Isis Lovecruft 782a81b46a
Split encryption tests for file-like objects into multiple tests.
This modifies the tests added in #89.
2015-03-13 02:32:50 +00:00
Isis Lovecruft b3dd20a7e5
Merge branch 'fix/99-recv-keys-network-test' into develop 2015-03-13 02:26:36 +00:00
Isis Lovecruft af403fe144
Move 'test_recv_keys_default' to a new test group which doesn't run.
* FIXES #99 temporarily.
2015-03-13 02:25:10 +00:00
Isis Lovecruft 657be31ae1
Change a str to a repr in a log message. 2015-03-13 02:21:18 +00:00
Isis Lovecruft 749ef6fa00
PEP8 whitespace fixes in gnupg/_meta.py. 2015-03-13 02:20:48 +00:00
Isis Lovecruft 2cf3dd1c86
Add coverage related commands to Makefile and clean up test directives. 2015-03-13 02:20:13 +00:00
Isis Lovecruft 3127c21d55
Merge branch 'fix/100-listpackets-good-passphrase' into develop 2015-03-11 03:44:31 +00:00
Isis Lovecruft 2c57c0f6d0
Handle [GOOD|BAD|MISSING]_PASSPHRASE statuses in _parsers.ListPackets.
* FIXES #100.
2015-03-11 03:25:55 +00:00
Isis Lovecruft 88bfaaffc2
Merge branch 'fix/98-pinentry-launched' into develop 2015-03-09 08:52:12 +00:00
Isis Lovecruft eb205774fb
Add support for PINENTRY_LAUNCHED status message.
* FIXES #98.
2015-03-09 08:50:52 +00:00
Isis Lovecruft 5d03c3c5eb
Merge branch 'fix/96-no-use-agent' into develop 2015-03-09 08:45:22 +00:00
Isis Lovecruft ae5cb33d63
Unset GPG.user_agent if using gpg2 binary.
* FIXES #96.
2015-03-09 08:42:50 +00:00
Thomas Tanner d3e6ae33b4 no-use-agent is obsolete for GPG2
(cherry picked from commit 19fd35c7232e42a4112c8f18686df1c0407c2d0d)
Signed-off-by: Isis Lovecruft <isis@leap.se>

 * FIXES #96.
 * CLOSES #96.
 * CLOSES #46.
2015-03-09 08:21:04 +00:00
Isis Lovecruft d3b7dd0353
Merge branch 'fix/91-missing-passphrase' into develop 2015-03-09 08:14:55 +00:00
Isis Lovecruft d31d0cf131
Handle MISSING_PASSPHRASE in _parsers.Sign.
* FIXES #91.
2015-03-09 08:12:41 +00:00
Isis Lovecruft f3c193d8b4
Merge remote-tracking branch 'charles-dyfis-net/homedir-perm-check-skippable' into develop 2015-03-09 07:52:59 +00:00
Isis Lovecruft 55b586fd18
Merge remote-tracking branch '6si/feature/allow_output_flag' into develop 2015-03-09 07:48:34 +00:00
Isis Lovecruft f858080148
Merge remote-tracking branch 'charles-dyfis-net/pass_through_good_options' into develop 2015-03-09 07:24:38 +00:00
Isis Lovecruft 572429eed9
Merge branch 'fix/89-fix-encrypting-streams' into develop 2015-03-09 07:19:36 +00:00
Isis Lovecruft ceb1c2fbbd
Add _STREAMLIKE_TYPES for determining stream-likeness in _is_stream().
* FIXES Python3 problems with various StringIO imports commit 8c261eb
   from fix for #89 in PR #92.
2015-03-09 07:01:15 +00:00
Isis Lovecruft 90c6613684
Merge branch 'master' into develop 2015-02-24 21:59:04 +00:00
Isis Lovecruft 939728694c
Merge branch 'release/2.0.0' 2015-02-24 21:58:37 +00:00
Isis Lovecruft d66b23b896
Add support for running on PyPy. 2015-02-22 22:57:59 +00:00
Garrett Robinson 6c15f25ee5 Unit test for encrypting file-like objects 2015-01-20 11:18:55 -08:00
Garrett Robinson 8c261eba30 Expand set of classes recognized by `_util._is_stream`
Adds additional commonly used stream classes from the standard library
to `_util._is_stream`. This means these classes can be used successfully
wherever `_is_stream` is used to decide whether or not to encode data
throughout the codebase, including in `_encrypt`.
2015-01-20 09:22:54 -08:00
Garrett Robinson f8ccdc5028 Fix `GPG.encrypt` for file-like objects
`GPG.encrypt_file` was refactored into `GPG.encrypt` in 295d98f, which
broke the functionality of `GPG.encrypt_file` for encrypting file-like
stream objects such as StringIO, BytesIO, etc.

The main difference between `GPG.encrypt_file` and `GPG.encrypt` is that
`GPG.encrypt` first converts its `data` argument into a binary stream
via `_make_binary_stream`. This is unnecessary when the argument is
already a stream, as was often the case in calls to `GPG.encrypt_file`.
Additionally, `_make_binary_stream` typically fails when it attempts
to encode a stream object, which means it is no longer possible to
achieve the functionality of `GPG.encrypt_file` with `GPG.encrypt` after
the refactor.

This commit only converts `data` to a binary stream if it is not already
a stream, re-using `_util._is_stream` to make that determination.
2015-01-17 16:09:39 -08:00
Viral Bajaria 8a7699236c add output as a valid option 2015-01-06 11:13:46 -08:00
Isis Lovecruft 5025df1661
Merge branch 'master' into develop 2014-11-27 04:14:11 +00:00
Isis Lovecruft b1dab1570d
Merge branch 'release/1.4.0' 2014-11-27 04:14:03 +00:00
Isis Lovecruft 8f92335476
Merge branch 'master' into develop 2014-11-27 02:59:22 +00:00
Isis Lovecruft 613e84cd56
Merge branch 'release/1.3.3' 2014-11-27 02:58:47 +00:00
Charles Duffy a1c45a6f63 Not sufficient to drop bad options; good ones need to be passed through.
This code was broken: Half of it required `options` to be a string, and the
other half required `options` to be a list (which the tests enforced, but the
constructor would silently drop for normal-path initialization).
2014-11-26 17:43:36 -06:00
Charles Duffy 77c6c3d0e5 Make homedir permissions check optional 2014-11-26 17:39:04 -06:00
9 changed files with 699 additions and 112 deletions

33
.coveragerc 100644
View File

@ -0,0 +1,33 @@
[run]
source =
gnupg
branch = True
#parallel = True
timid = True
[report]
modules = gnupg
omit =
*/test*
*/_version*
*/__init__*
*/copyleft*
*/sitecustomize*
# Regexes for lines to exclude from report generation:
exclude_lines =
pragma: no cover
# don't complain if the code doesn't hit unimplemented sections:
raise NotImplementedError
pass
# don't complain if non-runnable or debuging code isn't run:
if 0:
if False:
def __repr__
if __name__ == .__main__.:
# Ignore source code which cannot be found:
ignore_errors = True
# Exit with status code 2 if under this percentage is covered:
fail_under = 10
[html]
directory = docs/coverage-html

View File

@ -2,6 +2,8 @@ SHELL=/bin/sh
TESTDIR=./gnupg/test TESTDIR=./gnupg/test
TESTHANDLE=$(TESTDIR)/test_gnupg.py TESTHANDLE=$(TESTDIR)/test_gnupg.py
FILES=$(SHELL find ./gnupg/ -name "*.py" -printf "%p,") FILES=$(SHELL find ./gnupg/ -name "*.py" -printf "%p,")
PYTHON=$(SHELL which python)
PYTHON3=$(SHELL which python3)
PKG_NAME=python-gnupg PKG_NAME=python-gnupg
DOC_DIR=docs DOC_DIR=docs
DOC_BUILD_DIR:=$(DOC_DIR)/_build DOC_BUILD_DIR:=$(DOC_DIR)/_build
@ -50,23 +52,70 @@ test-before: cleanup-src cleanup-tests
which python && python --version which python && python --version
-which pip && pip --version && pip list -which pip && pip --version && pip list
test: test-before test-run: test-before
python $(TESTHANDLE) basic encodings parsers keyrings listkeys genkey \ python $(TESTHANDLE) \
sign crypt basic \
encodings \
parsers \
keyrings \
listkeys \
genkey \
sign \
crypt
py3k-test-run: test-before
python3 $(TESTHANDLE) \
basic \
encodings \
parsers \
keyrings \
listkeys \
genkey \
sign \
crypt
coverage-run: test-before
coverage run --rcfile=".coveragerc" $(PYTHON) $(TESTHANDLE) \
basic \
encodings \
parsers \
keyrings \
listkeys \
genkeys \
sign \
crypt
py3k-coverage-run: test-before
coverage run --rcfile=".coveragerc" $(PYTHON3) $(TESTHANDLE) \
basic \
encodings \
parsers \
keyrings \
listkeys \
genkeys \
sign \
crypt
coverage-report:
coverage report --rcfile=".coveragerc"
coverage-html:
coverage html --rcfile=".coveragerc"
clean-test:
touch gnupg/test/placeholder.log touch gnupg/test/placeholder.log
mv gnupg/test/*.log gnupg/test/logs/ mv gnupg/test/*.log gnupg/test/logs/
rm gnupg/test/logs/placeholder.log rm gnupg/test/logs/placeholder.log
touch gnupg/test/random_seed_is_sekritly_pi touch gnupg/test/random_seed_is_sekritly_pi
rm gnupg/test/random_seed* rm gnupg/test/random_seed*
py3k-test: test-before test: test-run clean-test
python3 $(TESTHANDLE) basic encodings parsers keyrings listkeys genkey \
sign crypt py3k-test: py3k-test-run clean-test
touch gnupg/test/placeholder.log
mv gnupg/test/*.log gnupg/test/logs/ coverage: coverage-run coverage-report coverage-html clean-test
rm gnupg/test/logs/placeholder.log
touch gnupg/test/random_seed_is_sekritly_pi py3k-coverage: py3k-coverage-run coverage-report coverage-html clean-test
rm gnupg/test/random_seed*
install: install:
python setup.py install --record installed-files.txt python setup.py install --record installed-files.txt

View File

@ -170,7 +170,7 @@ def displayNewKey(key):
# `result` is a `gnupg._parsers.ListKeys`, which is list-like, so iterate # `result` is a `gnupg._parsers.ListKeys`, which is list-like, so iterate
# over all the keys and display their info: # over all the keys and display their info:
for gpgkey in keylist: for gpgkey in keylist:
for k, v in gpgkey: for k, v in gpgkey.items():
log.info("%s: %s" % (k.capitalize(), v)) log.info("%s: %s" % (k.capitalize(), v))
return keylist return keylist

View File

@ -32,14 +32,22 @@ import encodings
import locale import locale
import os import os
import platform import platform
import psutil
import shlex import shlex
import subprocess import subprocess
import sys import sys
import threading import threading
## Using psutil is recommended, but since the extension doesn't run with the
## PyPy interpreter, we'll run even if it's not present.
try:
import psutil
except ImportError:
psutil = None
from . import _parsers from . import _parsers
from . import _util from . import _util
from ._util import b
from ._util import s
from ._parsers import _check_preferences from ._parsers import _check_preferences
from ._parsers import _sanitise_list from ._parsers import _sanitise_list
@ -81,10 +89,19 @@ class GPGMeta(type):
the same. (Sorry Windows users; maybe you should switch to anything the same. (Sorry Windows users; maybe you should switch to anything
else.) else.)
.. note: This function will only run if the psutil_ Python extension
is installed. Because psutil won't run with the PyPy interpreter,
use of it is optional (although highly recommended).
.. _psutil: https://pypi.python.org/pypi/psutil
:returns: True if there exists a gpg-agent process running under the :returns: True if there exists a gpg-agent process running under the
same effective user ID as that of this program. Otherwise, same effective user ID as that of this program. Otherwise,
returns False. returns False.
""" """
if not psutil:
return False
this_process = psutil.Process(os.getpid()) this_process = psutil.Process(os.getpid())
ownership_match = False ownership_match = False
@ -132,7 +149,7 @@ class GPGBase(object):
def __init__(self, binary=None, home=None, keyring=None, secring=None, def __init__(self, binary=None, home=None, keyring=None, secring=None,
use_agent=False, default_preference_list=None, use_agent=False, default_preference_list=None,
verbose=False, options=None): ignore_homedir_permissions=False, verbose=False, options=None):
"""Create a ``GPGBase``. """Create a ``GPGBase``.
This class is used to set up properties for controlling the behaviour This class is used to set up properties for controlling the behaviour
@ -155,13 +172,14 @@ class GPGBase(object):
:ivar str secring: The filename in **homedir** to use as the keyring :ivar str secring: The filename in **homedir** to use as the keyring
file for secret keys. file for secret keys.
""" """
self.ignore_homedir_permissions = ignore_homedir_permissions
self.binary = _util._find_binary(binary) self.binary = _util._find_binary(binary)
self.homedir = os.path.expanduser(home) if home else _util._conf self.homedir = os.path.expanduser(home) if home else _util._conf
pub = _parsers._fix_unsafe(keyring) if keyring else 'pubring.gpg' pub = _parsers._fix_unsafe(keyring) if keyring else 'pubring.gpg'
sec = _parsers._fix_unsafe(secring) if secring else 'secring.gpg' sec = _parsers._fix_unsafe(secring) if secring else 'secring.gpg'
self.keyring = os.path.join(self._homedir, pub) self.keyring = os.path.join(self._homedir, pub)
self.secring = os.path.join(self._homedir, sec) self.secring = os.path.join(self._homedir, sec)
self.options = _parsers._sanitise(options) if options else None self.options = list(_parsers._sanitise_list(options)) if options else None
#: The version string of our GnuPG binary #: The version string of our GnuPG binary
self.binary_version = '0.0.0' self.binary_version = '0.0.0'
@ -197,7 +215,7 @@ class GPGBase(object):
"'verbose' must be boolean, string, or 0 <= n <= 9" "'verbose' must be boolean, string, or 0 <= n <= 9"
assert isinstance(use_agent, bool), "'use_agent' must be boolean" assert isinstance(use_agent, bool), "'use_agent' must be boolean"
if self.options is not None: if self.options is not None:
assert isinstance(self.options, str), "options not string" assert isinstance(self.options, list), "options not list"
except (AssertionError, AttributeError) as ae: except (AssertionError, AttributeError) as ae:
log.error("GPGBase.__init__(): %s" % str(ae)) log.error("GPGBase.__init__(): %s" % str(ae))
raise RuntimeError(str(ae)) raise RuntimeError(str(ae))
@ -398,6 +416,9 @@ class GPGBase(object):
log.debug("GPGBase._homedir_setter(): Check existence of '%s'" % hd) log.debug("GPGBase._homedir_setter(): Check existence of '%s'" % hd)
_util._create_if_necessary(hd) _util._create_if_necessary(hd)
if self.ignore_homedir_permissions:
self._homedir = hd
else:
try: try:
log.debug("GPGBase._homedir_setter(): checking permissions") log.debug("GPGBase._homedir_setter(): checking permissions")
assert _util._has_readwrite(hd), \ assert _util._has_readwrite(hd), \
@ -518,8 +539,8 @@ class GPGBase(object):
if passphrase: cmd.append('--batch --passphrase-fd 0') if passphrase: cmd.append('--batch --passphrase-fd 0')
if self.use_agent: cmd.append('--use-agent') if self.use_agent is True: cmd.append('--use-agent')
else: cmd.append('--no-use-agent') elif self.use_agent is False: cmd.append('--no-use-agent')
# The arguments for debugging and verbosity should be placed into the # The arguments for debugging and verbosity should be placed into the
# cmd list before the options/args in order to resolve Issue #76: # cmd list before the options/args in order to resolve Issue #76:
@ -785,6 +806,19 @@ class GPGBase(object):
## We could use _handle_io here except for the fact that if the ## 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. ## passphrase is bad, gpg bails and you can't write the message.
result = self._result_map['sign'](self) result = self._result_map['sign'](self)
## If the passphrase is an empty string, the message up to and
## including its first newline will be cut off before making it to the
## GnuPG process. Therefore, if the passphrase='' or passphrase=b'',
## we set passphrase=None. See Issue #82:
## https://github.com/isislovecruft/python-gnupg/issues/82
if _util._is_string(passphrase):
passphrase = passphrase if len(passphrase) > 0 else None
elif _util._is_bytes(passphrase):
passphrase = s(passphrase) if len(passphrase) > 0 else None
else:
passphrase = None
proc = self._open_subprocess(args, passphrase is not None) proc = self._open_subprocess(args, passphrase is not None)
try: try:
if passphrase: if passphrase:
@ -975,21 +1009,21 @@ class GPGBase(object):
for recp in recipients.split(' '): for recp in recipients.split(' '):
self._add_recipient_string(args, hidden_recipients, recp) self._add_recipient_string(args, hidden_recipients, recp)
## ...and now that we've proven py3k is better... ## ...and now that we've proven py3k is better...
else: else:
log.debug("Don't know what to do with recipients: '%s'" log.debug("Don't know what to do with recipients: %r"
% recipients) % recipients)
result = self._result_map['crypt'](self) result = self._result_map['crypt'](self)
log.debug("Got data '%s' with type '%s'." log.debug("Got data '%s' with type '%s'." % (data, type(data)))
% (data, type(data))) self._handle_io(args, data, result, passphrase=passphrase, binary=True)
self._handle_io(args, data, result, # Avoid writing raw encrypted bytes to terminal loggers and breaking
passphrase=passphrase, binary=True) # them in that adorable way where they spew hieroglyphics until reset:
if armor:
log.debug("\n%s" % result.data) log.debug("\n%s" % result.data)
if output_filename: if output_filename:
log.info("Writing encrypted output to file: %s" % output_filename) log.info("Writing encrypted output to file: %s" % output_filename)
with open(output_filename, 'w+') as fh: with open(output_filename, 'wb') as fh:
fh.write(result.data) fh.write(result.data)
fh.flush() fh.flush()
log.info("Encrypted output written successfully.") log.info("Encrypted output written successfully.")

View File

@ -367,7 +367,7 @@ def _sanitise(*args):
checked += (val + " ") checked += (val + " ")
log.debug("_check_option(): No checks for %s" % val) log.debug("_check_option(): No checks for %s" % val)
return checked return checked.rstrip(' ')
is_flag = lambda x: x.startswith('--') is_flag = lambda x: x.startswith('--')
@ -516,6 +516,7 @@ def _get_options_group(group=None):
'--import', '--import',
'--verify', '--verify',
'--verify-files', '--verify-files',
'--output',
]) ])
#: These options expect a string. see :func:`_check_preferences`. #: These options expect a string. see :func:`_check_preferences`.
pref_options = frozenset(['--digest-algo', pref_options = frozenset(['--digest-algo',
@ -557,6 +558,9 @@ def _get_options_group(group=None):
'--list-public-keys', '--list-public-keys',
'--list-secret-keys', '--list-secret-keys',
'--list-sigs', '--list-sigs',
'--lock-multiple',
'--lock-never',
'--lock-once',
'--no-default-keyring', '--no-default-keyring',
'--no-default-recipient', '--no-default-recipient',
'--no-emit-version', '--no-emit-version',
@ -908,6 +912,7 @@ class Sign(object):
timestamp = None timestamp = None
#: xxx fill me in #: xxx fill me in
what = None what = None
status = None
def __init__(self, gpg): def __init__(self, gpg):
self._gpg = gpg self._gpg = gpg
@ -930,9 +935,9 @@ class Sign(object):
:raises: :exc:`~exceptions.ValueError` if the status message is unknown. :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
""" """
if key in ("USERID_HINT", "NEED_PASSPHRASE", "BAD_PASSPHRASE", if key in ("USERID_HINT", "NEED_PASSPHRASE", "BAD_PASSPHRASE",
"GOOD_PASSPHRASE", "BEGIN_SIGNING", "CARDCTRL", "GOOD_PASSPHRASE", "MISSING_PASSPHRASE", "PINENTRY_LAUNCHED",
"INV_SGNR", "SIGEXPIRED"): "BEGIN_SIGNING", "CARDCTRL", "INV_SGNR", "SIGEXPIRED"):
pass self.status = key.replace("_", " ").lower()
elif key == "SIG_CREATED": elif key == "SIG_CREATED":
(self.sig_type, self.sig_algo, self.sig_hash_algo, (self.sig_type, self.sig_algo, self.sig_hash_algo,
self.what, self.timestamp, self.fingerprint) = value.split() self.what, self.timestamp, self.fingerprint) = value.split()
@ -949,6 +954,7 @@ class Sign(object):
else: else:
raise ValueError("Unknown status message: %r" % key) raise ValueError("Unknown status message: %r" % key)
class ListKeys(list): class ListKeys(list):
"""Handle status messages for --list-keys. """Handle status messages for --list-keys.
@ -1271,7 +1277,8 @@ class Verify(object):
self.trust_level = self.TRUST_LEVELS[key] self.trust_level = self.TRUST_LEVELS[key]
elif key in ("RSA_OR_IDEA", "NODATA", "IMPORT_RES", "PLAINTEXT", elif key in ("RSA_OR_IDEA", "NODATA", "IMPORT_RES", "PLAINTEXT",
"PLAINTEXT_LENGTH", "POLICY_URL", "DECRYPTION_INFO", "PLAINTEXT_LENGTH", "POLICY_URL", "DECRYPTION_INFO",
"DECRYPTION_OKAY", "INV_SGNR", "PROGRESS"): "DECRYPTION_OKAY", "INV_SGNR", "PROGRESS",
"PINENTRY_LAUNCHED"):
pass pass
elif key == "BADSIG": elif key == "BADSIG":
self.valid = False self.valid = False
@ -1524,21 +1531,21 @@ class ListPackets(object):
:raises: :exc:`~exceptions.ValueError` if the status message is unknown. :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
""" """
if key == 'NODATA': if key in ('NO_SECKEY', 'BEGIN_DECRYPTION', 'DECRYPTION_FAILED',
'END_DECRYPTION', 'GOOD_PASSPHRASE', 'BAD_PASSPHRASE'):
pass
elif key == 'NODATA':
self.status = nodata(value) self.status = nodata(value)
elif key == 'ENC_TO': elif key == 'ENC_TO':
key, _, _ = value.split() key, _, _ = value.split()
if not self.key: if not self.key:
self.key = key self.key = key
self.encrypted_to.append(key) self.encrypted_to.append(key)
elif key == 'NEED_PASSPHRASE': elif key == ('NEED_PASSPHRASE', 'MISSING_PASSPHRASE'):
self.need_passphrase = True self.need_passphrase = True
elif key == 'NEED_PASSPHRASE_SYM': elif key == 'NEED_PASSPHRASE_SYM':
self.need_passphrase_sym = True self.need_passphrase_sym = True
elif key == 'USERID_HINT': elif key == 'USERID_HINT':
self.userid_hint = value.strip().split() self.userid_hint = value.strip().split()
elif key in ('NO_SECKEY', 'BEGIN_DECRYPTION', 'DECRYPTION_FAILED',
'END_DECRYPTION'):
pass
else: else:
raise ValueError("Unknown status message: %r" % key) raise ValueError("Unknown status message: %r" % key)

View File

@ -28,18 +28,58 @@ from time import mktime
import codecs import codecs
import encodings import encodings
import os import os
import psutil
import threading import threading
import random import random
import re import re
import string import string
import sys import sys
# These are all the classes which are stream-like; they are used in
# :func:`_is_stream`.
_STREAMLIKE_TYPES = []
# These StringIO classes are actually utilised.
try: try:
import io
from io import StringIO from io import StringIO
from io import BytesIO from io import BytesIO
except ImportError: except ImportError:
from cStringIO import StringIO from cStringIO import StringIO
else:
# The io.IOBase type covers the above example for an open file handle in
# Python3, as well as both io.BytesIO and io.StringIO.
_STREAMLIKE_TYPES.append(io.IOBase)
# The remaining StringIO classes which are imported are used to determine if a
# object is a stream-like in :func:`_is_stream`.
if 2 == sys.version_info[0]:
# Import the StringIO class from the StringIO module since it is a
# commonly used stream class. It is distinct from either of the
# StringIO's that may be loaded in the above try/except clause, so the
# name is prefixed with an underscore to distinguish it.
from StringIO import StringIO as _StringIO_StringIO
_STREAMLIKE_TYPES.append(_StringIO_StringIO)
# Import the cStringIO module to test for the cStringIO stream types,
# InputType and OutputType. See
# http://stackoverflow.com/questions/14735295/to-check-an-instance-is-stringio
import cStringIO as _cStringIO
_STREAMLIKE_TYPES.append(_cStringIO.InputType)
_STREAMLIKE_TYPES.append(_cStringIO.OutputType)
# In Python2:
#
# >>> type(open('README.md', 'rb'))
# <open file 'README.md', mode 'rb' at 0x7f9493951d20>
#
# whereas, in Python3, the `file` builtin doesn't exist and instead we get:
#
# >>> type(open('README.md', 'rb'))
# <_io.BufferedReader name='README.md'>
#
# which is covered by the above addition of io.IOBase.
_STREAMLIKE_TYPES.append(file)
from . import _logger from . import _logger
@ -125,6 +165,51 @@ def find_encodings(enc=None, system=False):
return coder return coder
if _py3k:
def b(x):
"""See http://python3porting.com/problems.html#nicer-solutions"""
coder = find_encodings()
if isinstance(x, bytes):
return coder.encode(x.decode(coder.name))[0]
else:
return coder.encode(x)[0]
def s(x):
if isinstance(x, str):
return x
elif isinstance(x, (bytes, bytearray)):
return x.decode(find_encodings().name)
else:
raise NotImplemented
else:
def b(x):
"""See http://python3porting.com/problems.html#nicer-solutions"""
return x
def s(x):
if isinstance(x, basestring):
return x
elif isinstance(x, (bytes, bytearray)):
return x.decode(find_encodings().name)
else:
raise NotImplemented
def binary(data):
coder = find_encodings()
if _py3k and isinstance(data, bytes):
encoded = coder.encode(data.decode(coder.name))[0]
elif _py3k and isinstance(data, str):
encoded = coder.encode(data)[0]
elif not _py3k and type(data) is not str:
encoded = coder.encode(data)[0]
else:
encoded = data
return encoded
def author_info(name, contact=None, public_key=None): def author_info(name, contact=None, public_key=None):
"""Easy object-oriented representation of contributor info. """Easy object-oriented representation of contributor info.
@ -144,8 +229,6 @@ def _copy_data(instream, outstream):
""" """
sent = 0 sent = 0
coder = find_encodings()
while True: while True:
if ((_py3k and isinstance(instream, str)) or if ((_py3k and isinstance(instream, str)) or
(not _py3k and isinstance(instream, basestring))): (not _py3k and isinstance(instream, basestring))):
@ -155,17 +238,15 @@ def _copy_data(instream, outstream):
data = instream.read(1024) data = instream.read(1024)
if len(data) == 0: if len(data) == 0:
break break
sent += len(data) sent += len(data)
log.debug("Sending chunk %d bytes:\n%s" encoded = binary(data)
% (sent, data)) log.debug("Sending %d bytes of data..." % sent)
log.debug("Encoded data (type %s):\n%s" % (type(encoded), encoded))
if not _py3k:
try: try:
outstream.write(data) outstream.write(encoded)
except UnicodeError:
try:
outstream.write(coder.encode(data))
except IOError:
log.exception("Error sending data: Broken pipe")
break
except IOError as ioe: except IOError as ioe:
# Can get 'broken pipe' errors even when all data was sent # Can get 'broken pipe' errors even when all data was sent
if 'Broken pipe' in str(ioe): if 'Broken pipe' in str(ioe):
@ -173,6 +254,48 @@ def _copy_data(instream, outstream):
else: else:
log.exception(ioe) log.exception(ioe)
break break
else:
log.debug("Wrote data type <type 'str'> to outstream.")
else:
try:
outstream.write(bytes(encoded))
except TypeError as te:
# XXX FIXME This appears to happen because
# _threaded_copy_data() sometimes passes the `outstream` as an
# object with type <_io.BufferredWriter> and at other times
# with type <encodings.utf_8.StreamWriter>. We hit the
# following error when the `outstream` has type
# <encodings.utf_8.StreamWriter>.
if not "convert 'bytes' object to str implicitly" in str(te):
log.error(str(te))
try:
outstream.write(encoded.decode())
except TypeError as yate:
# We hit the "'str' does not support the buffer interface"
# error in Python3 when the `outstream` is an io.BytesIO and
# we try to write a str to it. We don't care about that
# error, we'll just try again with bytes.
if not "does not support the buffer interface" in str(yate):
log.error(str(yate))
except IOError as ioe:
# Can get 'broken pipe' errors even when all data was sent
if 'Broken pipe' in str(ioe):
log.error('Error sending data: Broken pipe')
else:
log.exception(ioe)
break
else:
log.debug("Wrote data type <class 'str'> outstream.")
except IOError as ioe:
# Can get 'broken pipe' errors even when all data was sent
if 'Broken pipe' in str(ioe):
log.error('Error sending data: Broken pipe')
else:
log.exception(ioe)
break
else:
log.debug("Wrote data type <class 'bytes'> to outstream.")
try: try:
outstream.close() outstream.close()
except IOError as ioe: except IOError as ioe:
@ -350,7 +473,32 @@ def _is_stream(input):
:rtype: bool :rtype: bool
:returns: True if :param:input is a stream, False if otherwise. :returns: True if :param:input is a stream, False if otherwise.
""" """
return isinstance(input, BytesIO) or isinstance(input, StringIO) return isinstance(input, tuple(_STREAMLIKE_TYPES))
def _is_string(thing):
"""Check that **thing** is a string. The definition of the latter depends
upon the Python version.
:param thing: The thing to check if it's a string.
:rtype: bool
:returns: ``True`` if **thing** is string (or unicode in Python2).
"""
if (_py3k and isinstance(thing, str)):
return True
if (not _py3k and isinstance(thing, basestring)):
return True
return False
def _is_bytes(thing):
"""Check that **thing** is bytes.
:param thing: The thing to check if it's bytes.
:rtype: bool
:returns: ``True`` if **thing** is bytes or a bytearray.
"""
if isinstance(thing, (bytes, bytearray)):
return True
return False
def _is_list_or_tuple(instance): def _is_list_or_tuple(instance):
"""Check that ``instance`` is a list or tuple. """Check that ``instance`` is a list or tuple.
@ -383,21 +531,26 @@ def _is_gpg2(version):
return True return True
return False return False
def _make_binary_stream(s, encoding): def _make_binary_stream(thing, encoding=None, armor=True):
"""Encode **thing**, then make it stream/file-like.
:param thing: The thing to turn into a encoded stream.
:rtype: ``io.BytesIO`` or ``io.StringIO``.
:returns: The encoded **thing**, wrapped in an ``io.BytesIO`` (if
available), otherwise wrapped in a ``io.StringIO``.
""" """
xxx fill me in
"""
try:
if _py3k: if _py3k:
if isinstance(s, str): if isinstance(thing, str):
s = s.encode(encoding) thing = thing.encode(encoding)
else: else:
if type(s) is not str: if type(thing) is not str:
s = s.encode(encoding) thing = thing.encode(encoding)
from io import BytesIO
rv = BytesIO(s) try:
except ImportError: rv = BytesIO(thing)
rv = StringIO(s) except NameError:
rv = StringIO(thing)
return rv return rv
def _make_passphrase(length=None, save=False, file=None): def _make_passphrase(length=None, save=False, file=None):
@ -418,7 +571,7 @@ def _make_passphrase(length=None, save=False, file=None):
passphrase = _make_random_string(length) passphrase = _make_random_string(length)
if save: if save:
ruid, euid, suid = psutil.Process(os.getpid()).uids ruid, euid, suid = os.getresuid()
gid = os.getgid() gid = os.getgid()
now = mktime(localtime()) now = mktime(localtime())

View File

@ -60,7 +60,7 @@ class GPG(GPGBase):
def __init__(self, binary=None, homedir=None, verbose=False, def __init__(self, binary=None, homedir=None, verbose=False,
use_agent=False, keyring=None, secring=None, use_agent=False, keyring=None, secring=None,
options=None): ignore_homedir_permissions=False, options=None):
"""Initialize a GnuPG process wrapper. """Initialize a GnuPG process wrapper.
:param str binary: Name for GnuPG binary executable. If the absolute :param str binary: Name for GnuPG binary executable. If the absolute
@ -73,6 +73,10 @@ class GPG(GPGBase):
and private keyrings. Default is whatever GnuPG and private keyrings. Default is whatever GnuPG
defaults to. defaults to.
:type ignore_homedir_permissions: :obj:`bool`
:param ignore_homedir_permissions: If true, bypass check that homedir
be writable.
:type verbose: :obj:`str` or :obj:`int` or :obj:`bool` :type verbose: :obj:`str` or :obj:`int` or :obj:`bool`
:param verbose: String or numeric value to pass to GnuPG's :param verbose: String or numeric value to pass to GnuPG's
``--debug-level`` option. See the GnuPG man page for ``--debug-level`` option. See the GnuPG man page for
@ -117,13 +121,16 @@ class GPG(GPGBase):
secring=secring, secring=secring,
options=options, options=options,
verbose=verbose, verbose=verbose,
use_agent=use_agent,) use_agent=use_agent,
ignore_homedir_permissions=ignore_homedir_permissions,
)
log.info(textwrap.dedent(""" log.info(textwrap.dedent("""
Initialised settings: Initialised settings:
binary: %s binary: %s
binary version: %s binary version: %s
homedir: %s homedir: %s
ignore_homedir_permissions: %s
keyring: %s keyring: %s
secring: %s secring: %s
default_preference_list: %s default_preference_list: %s
@ -134,6 +141,7 @@ class GPG(GPGBase):
""" % (self.binary, """ % (self.binary,
self.binary_version, self.binary_version,
self.homedir, self.homedir,
self.ignore_homedir_permissions,
self.keyring, self.keyring,
self.secring, self.secring,
self.default_preference_list, self.default_preference_list,
@ -153,6 +161,12 @@ class GPG(GPGBase):
# fatal error (at least it does with GnuPG>=2.0.0): # fatal error (at least it does with GnuPG>=2.0.0):
self.create_trustdb() self.create_trustdb()
# The --no-use-agent and --use-agent options were deprecated in GnuPG
# 2.x, so we should set use_agent to None here to avoid having
# GPGBase._make_args() add either one.
if self.is_gpg2():
self.use_agent = None
@functools.wraps(_trust._create_trustdb) @functools.wraps(_trust._create_trustdb)
def create_trustdb(self): def create_trustdb(self):
if self.is_gpg2(): if self.is_gpg2():
@ -787,7 +801,7 @@ class GPG(GPGBase):
key = key.replace('_','-').title() key = key.replace('_','-').title()
## to set 'cert', 'Key-Usage' must be blank string ## to set 'cert', 'Key-Usage' must be blank string
if not key in ('Key-Usage', 'Subkey-Usage'): if not key in ('Key-Usage', 'Subkey-Usage'):
if str(val).strip(): if type('')(val).strip():
parms[key] = val parms[key] = val
## if Key-Type is 'default', make Subkey-Type also be 'default' ## if Key-Type is 'default', make Subkey-Type also be 'default'
@ -952,6 +966,9 @@ generate keys. Please see
.. seealso:: :meth:`._encrypt` .. seealso:: :meth:`._encrypt`
""" """
if _is_stream(data):
stream = data
else:
stream = _make_binary_stream(data, self._encoding) stream = _make_binary_stream(data, self._encoding)
result = self._encrypt(stream, recipients, **kwargs) result = self._encrypt(stream, recipients, **kwargs)
stream.close() stream.close()

View File

@ -26,6 +26,7 @@ A test harness and unittests for gnupg.py.
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import print_function from __future__ import print_function
from __future__ import with_statement from __future__ import with_statement
from argparse import ArgumentParser from argparse import ArgumentParser
from codecs import open as open from codecs import open as open
from functools import wraps from functools import wraps
@ -288,8 +289,8 @@ class GPGTestCase(unittest.TestCase):
self.assertTrue(os.path.isabs(self.gpg.binary)) self.assertTrue(os.path.isabs(self.gpg.binary))
def test_make_args_drop_protected_options(self): def test_make_args_drop_protected_options(self):
"""Test that unsupported gpg options are dropped.""" """Test that unsupported gpg options are dropped, and supported ones remain."""
self.gpg.options = ['--tyrannosaurus-rex', '--stegosaurus'] self.gpg.options = ['--tyrannosaurus-rex', '--stegosaurus', '--lock-never']
gpg_binary_path = _util._find_binary('gpg') gpg_binary_path = _util._find_binary('gpg')
cmd = self.gpg._make_args(None, False) cmd = self.gpg._make_args(None, False)
expected = [gpg_binary_path, expected = [gpg_binary_path,
@ -297,7 +298,8 @@ class GPGTestCase(unittest.TestCase):
'--homedir "%s"' % self.homedir, '--homedir "%s"' % self.homedir,
'--no-default-keyring --keyring %s' % self.keyring, '--no-default-keyring --keyring %s' % self.keyring,
'--secret-keyring %s' % self.secring, '--secret-keyring %s' % self.secring,
'--no-use-agent'] '--no-use-agent',
'--lock-never']
self.assertListEqual(cmd, expected) self.assertListEqual(cmd, expected)
def test_make_args(self): def test_make_args(self):
@ -388,7 +390,10 @@ class GPGTestCase(unittest.TestCase):
def test_gen_key_input(self): def test_gen_key_input(self):
"""Test that GnuPG batch file creation is successful.""" """Test that GnuPG batch file creation is successful."""
key_input = self.generate_key_input("Francisco Ferrer", "an.ok") key_input = self.generate_key_input("Francisco Ferrer", "an.ok")
if _util._py3k:
self.assertIsInstance(key_input, str) self.assertIsInstance(key_input, str)
else:
self.assertIsInstance(key_input, basestring)
self.assertGreater(key_input.find('Francisco Ferrer'), 0) self.assertGreater(key_input.find('Francisco Ferrer'), 0)
def test_rsa_key_generation(self): def test_rsa_key_generation(self):
@ -621,6 +626,66 @@ class GPGTestCase(unittest.TestCase):
passphrase='wrong horse battery staple') passphrase='wrong horse battery staple')
self.assertFalse(sig, "Bad passphrase should fail") self.assertFalse(sig, "Bad passphrase should fail")
def test_signature_string_passphrase_empty_string(self):
"""Test that a signing attempt with passphrase='' creates a valid
signature.
See Issue #82: https://github.com/isislovecruft/python-gnupg/issues/82
"""
with open(os.path.join(_files, 'test_key_1.sec')) as fh1:
res1 = self.gpg.import_keys(fh1.read())
key1 = res1.fingerprints[0]
message = 'abc\ndef\n'
sig = self.gpg.sign(message, default_key=key1, passphrase='')
self.assertTrue(sig)
self.assertTrue(message in str(sig))
def test_signature_string_passphrase_empty_bytes_literal(self):
"""Test that a signing attempt with passphrase=b'' creates a valid
signature.
See Issue #82: https://github.com/isislovecruft/python-gnupg/issues/82
"""
with open(os.path.join(_files, 'test_key_1.sec')) as fh1:
res1 = self.gpg.import_keys(fh1.read())
key1 = res1.fingerprints[0]
message = 'abc\ndef\n'
sig = self.gpg.sign(message, default_key=key1, passphrase=b'')
self.assertTrue(sig)
print("%r" % str(sig))
self.assertTrue(message in str(sig))
def test_signature_string_passphrase_bytes_literal(self):
"""Test that a signing attempt with passphrase=b'overalls' creates a
valid signature.
"""
with open(os.path.join(_files, 'kat.sec')) as fh1:
res1 = self.gpg.import_keys(fh1.read())
key1 = res1.fingerprints[0]
message = 'abc\ndef\n'
sig = self.gpg.sign(message, default_key=key1, passphrase=b'overalls')
self.assertTrue(sig)
print("%r" % str(sig))
self.assertTrue(message in str(sig))
def test_signature_string_passphrase_None(self):
"""Test that a signing attempt with passphrase=None fails creates a
valid signature.
See Issue #82: https://github.com/isislovecruft/python-gnupg/issues/82
"""
with open(os.path.join(_files, 'test_key_1.sec')) as fh1:
res1 = self.gpg.import_keys(fh1.read())
key1 = res1.fingerprints[0]
message = 'abc\ndef\n'
sig = self.gpg.sign(message, default_key=key1, passphrase=None)
self.assertTrue(sig)
self.assertTrue(message in str(sig))
def test_signature_file(self): def test_signature_file(self):
"""Test that signing a message file works.""" """Test that signing a message file works."""
key = self.generate_key("Leonard Adleman", "rsa.com") key = self.generate_key("Leonard Adleman", "rsa.com")
@ -743,17 +808,84 @@ class GPGTestCase(unittest.TestCase):
if os.path.isfile(sigfn): if os.path.isfile(sigfn):
os.unlink(sigfn) os.unlink(sigfn)
def test_deletion(self): def test_deletion_public_key(self):
"""Test that key deletion works.""" """Test that key deletion for public keys works, and that it leaves the
self.gpg.import_keys(KEYS_TO_IMPORT) corresponding secret key intact.
"""
key1 = None
key2 = None
with open(os.path.join(_files, 'test_key_1.sec')) as fh1:
res1 = self.gpg.import_keys(fh1.read())
key1 = res1.fingerprints[0]
with open(os.path.join(_files, 'test_key_2.sec')) as fh2:
res2 = self.gpg.import_keys(fh2.read())
key2 = res2.fingerprints[0]
public_keys = self.gpg.list_keys() public_keys = self.gpg.list_keys()
self.assertTrue(is_list_with_len(public_keys, 2), self.assertTrue(len(public_keys), 2)
"2-element list expected, got %d" % len(public_keys))
self.gpg.delete_keys(public_keys[0]['fingerprint']) self.gpg.delete_keys(key1)
public_keys = self.gpg.list_keys() public_keys = self.gpg.list_keys()
self.assertTrue(is_list_with_len(public_keys, 1), secret_keys = self.gpg.list_keys(secret=True)
"1-element list expected, got %d" % len(public_keys)) self.assertTrue(len(public_keys), 1)
log.debug("test_deletion ends") self.assertTrue(len(secret_keys), 2)
def test_deletion_secret_key(self):
"""Test that key deletion for secret keys works, and that it leaves the
corresponding public key intact.
"""
key1 = None
key2 = None
with open(os.path.join(_files, 'test_key_1.sec')) as fh1:
res1 = self.gpg.import_keys(fh1.read())
key1 = res1.fingerprints[0]
with open(os.path.join(_files, 'test_key_2.sec')) as fh2:
res2 = self.gpg.import_keys(fh2.read())
key2 = res2.fingerprints[0]
public_keys = self.gpg.list_keys()
secret_keys = self.gpg.list_keys(secret=True)
self.assertEqual(len(public_keys), 2)
self.assertEqual(len(secret_keys), 2)
self.gpg.delete_keys(key1, secret=True)
public_keys = self.gpg.list_keys()
secret_keys = self.gpg.list_keys(secret=True)
self.assertEqual(len(public_keys), 2)
self.assertEqual(len(secret_keys), 1)
def test_deletion_subkeys(self):
"""Test that key deletion for subkeys deletes both the public and
secret portions of the key.
"""
key1 = None
key2 = None
with open(os.path.join(_files, 'test_key_1.sec')) as fh1:
res1 = self.gpg.import_keys(fh1.read())
key1 = res1.fingerprints[0]
with open(os.path.join(_files, 'test_key_2.sec')) as fh2:
res2 = self.gpg.import_keys(fh2.read())
key2 = res2.fingerprints[0]
public_keys = self.gpg.list_keys()
secret_keys = self.gpg.list_keys(secret=True)
self.assertEqual(len(public_keys), 2)
self.assertEqual(len(secret_keys), 2)
self.gpg.delete_keys(key1, subkeys=True)
public_keys = self.gpg.list_keys()
secret_keys = self.gpg.list_keys(secret=True)
self.assertEqual(len(public_keys), 1)
self.assertEqual(len(secret_keys), 1)
def test_encryption(self): def test_encryption(self):
"""Test encryption of a message string""" """Test encryption of a message string"""
@ -776,6 +908,75 @@ authentication."""
log.debug("Encrypted: %s" % encrypted) log.debug("Encrypted: %s" % encrypted)
self.assertNotEquals(message, encrypted) self.assertNotEquals(message, encrypted)
def _encryption_test_setup(self):
passphrase = "craiggentry"
key = self.generate_key("Craig Gentry", "xorr.ox", passphrase=passphrase)
fpr = str(key.fingerprint)
gentry = self.gpg.export_keys(key.fingerprint)
self.gpg.import_keys(gentry)
message = """
In 2010 Riggio and Sicari presented a practical application of homomorphic
encryption to a hybrid wireless sensor/mesh network. The system enables
transparent multi-hop wireless backhauls that are able to perform statistical
analysis of different kinds of data (temperature, humidity, etc.) coming from
a WSN while ensuring both end-to-end encryption and hop-by-hop
authentication."""
return (message, fpr, passphrase)
def _encryption_test(self, stream_type, message, fingerprint, passphrase):
stream = stream_type(message)
encrypted = self.gpg.encrypt(stream, fingerprint).data
decrypted = self.gpg.decrypt(encrypted, passphrase=passphrase).data
if isinstance(decrypted, bytes):
decrypted = decrypted.decode()
if isinstance(message, bytes):
message = message.decode()
self.assertEqual(message, decrypted)
def test_encryption_of_file_like_objects_io_StringIO(self):
"""Test encryption of file-like object io.StringIO."""
message, fpr, passphrase = self._encryption_test_setup()
try:
from io import StringIO
if _util._py3k:
self._encryption_test(StringIO, message, fpr, passphrase)
else:
self._encryption_test(StringIO, unicode(message), fpr, passphrase)
except ImportError:
pass
def test_encryption_of_file_like_objects_io_BytesIO(self):
"""Test encryption of file-like object io.BytesIO."""
message, fpr, passphrase = self._encryption_test_setup()
try:
from io import BytesIO
if _util._py3k:
self._encryption_test(BytesIO, bytes(message, 'utf-8'), fpr, passphrase)
else:
self._encryption_test(BytesIO, message, fpr, passphrase)
except ImportError:
pass
def test_encryption_of_file_like_objects_StringIO_StringIO(self):
"""Test encryption of file-like object StringIO.StringIO (Python2 only)."""
message, fpr, passphrase = self._encryption_test_setup()
if not _util._py3k:
from StringIO import StringIO
self._encryption_test(StringIO, message, fpr, passphrase)
def test_encryption_of_file_like_objects_cStringIO_StringIO(self):
"""Test encryption of file-like object cStringIO.StringIO (Python2 only)."""
message, fpr, passphrase = self._encryption_test_setup()
if not _util._py3k:
from cStringIO import StringIO
self._encryption_test(StringIO, message, fpr, passphrase)
def test_encryption_alt_encoding(self): def test_encryption_alt_encoding(self):
"""Test encryption with latin-1 encoding""" """Test encryption with latin-1 encoding"""
key = self.generate_key("Craig Gentry", "xorr.ox", key = self.generate_key("Craig Gentry", "xorr.ox",
@ -784,11 +985,7 @@ authentication."""
key = self.generate_key("Marten van Dijk", "xorr.ox") key = self.generate_key("Marten van Dijk", "xorr.ox")
dijk = str(key.fingerprint) dijk = str(key.fingerprint)
self.gpg._encoding = 'latin-1' self.gpg._encoding = 'latin-1'
if _util._py3k: data = u'Hello, André!'.encode(self.gpg._encoding)
data = 'Hello, André!'
else:
data = unicode('Hello, André', self.gpg._encoding)
data = data.encode(self.gpg._encoding)
encrypted = self.gpg.encrypt(data, gentry) encrypted = self.gpg.encrypt(data, gentry)
edata = str(encrypted.data) edata = str(encrypted.data)
self.assertNotEqual(data, edata) self.assertNotEqual(data, edata)
@ -885,6 +1082,29 @@ authentication."""
self.assertEqual(message, decrypted) self.assertEqual(message, decrypted)
def test_decryption_with_bytes_literal(self):
"""Test that ``decrypt(encrypt(b'foo'), ...)`` is successful."""
with open(os.path.join(_files, 'kat.sec')) as katsec:
self.gpg.import_keys(katsec.read())
kat = self.gpg.list_keys('kat')[0]['fingerprint']
message_filename = os.path.join(_files, 'cypherpunk_manifesto')
with open(message_filename, 'rb') as f:
output = os.path.join(self.gpg.homedir, 'test-decryption-with-bytes-literal.gpg')
kwargs = dict(compress_algo='Uncompressed')
message = b'Dance like a psycho'
encrypted = self.gpg.encrypt(message, kat, **kwargs)
self.assertTrue(encrypted.ok)
self.assertGreater(len(str(encrypted)), 0)
decrypted = self.gpg.decrypt(encrypted.data, passphrase='overalls')
self.assertTrue(decrypted.ok)
self.assertGreater(len(str(decrypted)), 0)
# Decode the message so that we can easily compare it with the
# decrypted version in both Python2 and Python3:
decoded = message.decode(self.gpg._encoding, self.gpg._decode_errors)
self.assertEqual(str(decrypted), decoded)
def test_encryption_one_hidden_recipient_one_not(self): def test_encryption_one_hidden_recipient_one_not(self):
"""Test to ensure hidden recipient isn't detailed in packet info""" """Test to ensure hidden recipient isn't detailed in packet info"""
@ -957,7 +1177,8 @@ boolean circuit causes a considerable overhead."""
## We expect Alice's key to be hidden (returned as zero's) and Bob's ## We expect Alice's key to be hidden (returned as zero's) and Bob's
## key to be there. ## key to be there.
expected_values = ["0000000000000000", "0000000000000000"] expected_values = ["0000000000000000", "0000000000000000"]
self.assertEquals(expected_values, self.gpg.list_packets(encrypted).encrypted_to) packets = self.gpg.list_packets(encrypted)
self.assertEquals(expected_values, packets.encrypted_to)
def test_encryption_decryption_multi_recipient(self): def test_encryption_decryption_multi_recipient(self):
"""Test decryption of an encrypted string for multiple users""" """Test decryption of an encrypted string for multiple users"""
@ -1081,9 +1302,9 @@ know, maybe you shouldn't be doing it in the first place.
self.assertTrue(os.path.isfile(output)) self.assertTrue(os.path.isfile(output))
# Check the contents: # Check the contents:
with open(output) as fh: with open(output, 'rb') as fh:
encrypted_message = fh.read() encrypted_message = fh.read()
log.debug("Encrypted file contains:\n\n%s\n" % encrypted_message) self.assertTrue(b"-----BEGIN PGP MESSAGE-----" in encrypted_message)
def test_encryption_to_filehandle(self): def test_encryption_to_filehandle(self):
"""Test that ``encrypt(..., output=filelikething)`` is successful.""" """Test that ``encrypt(..., output=filelikething)`` is successful."""
@ -1103,9 +1324,45 @@ know, maybe you shouldn't be doing it in the first place.
self.assertTrue(os.path.isfile(output)) self.assertTrue(os.path.isfile(output))
# Check the contents: # Check the contents:
with open(output) as fh: with open(output, 'rb') as fh:
encrypted_message = fh.read() encrypted_message = fh.read()
log.debug("Encrypted file contains:\n\n%s\n" % encrypted_message) self.assertTrue(b"-----BEGIN PGP MESSAGE-----" in encrypted_message)
def test_encryption_from_filehandle(self):
"""Test that ``encrypt(open('foo'), ...)`` is successful."""
message_filename = os.path.join(_files, 'cypherpunk_manifesto')
with open(message_filename, 'rb') as f:
output = os.path.join(self.gpg.homedir, 'test-encryption-from-filehandle.gpg')
kwargs = dict(passphrase='speedtest',
symmetric=True,
cipher_algo='AES256',
encrypt=False,
output=output)
encrypted = self.gpg.encrypt(f, None, **kwargs)
self.assertTrue(encrypted.ok)
self.assertGreater(len(encrypted.data), 0)
def test_encryption_with_output(self):
"""Test that ``encrypt('foo', ..., output='/foo/bar/baz')`` is successful."""
message_filename = os.path.join(_files, 'cypherpunk_manifesto')
with open (message_filename, 'rb') as f:
data = f.read()
output = os.path.join(self.gpg.homedir, 'test-encryption-with-output.gpg')
kwargs = dict(passphrase='speedtest',
symmetric=True,
cipher_algo='AES256',
encrypt=False,
output=output)
encrypted = self.gpg.encrypt(data, None, **kwargs)
self.assertTrue(encrypted.ok)
self.assertGreater(len(encrypted.data), 0)
self.assertTrue(os.path.isfile(output))
# Check the contents:
with open(output, 'rb') as fh:
encrypted_message = fh.read()
self.assertTrue(b"-----BEGIN PGP MESSAGE-----" in encrypted_message)
suites = { 'parsers': set(['test_parsers_fix_unsafe', suites = { 'parsers': set(['test_parsers_fix_unsafe',
@ -1142,27 +1399,41 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe',
'test_signature_verification_detached', 'test_signature_verification_detached',
'test_signature_verification_detached_binary', 'test_signature_verification_detached_binary',
'test_signature_file', 'test_signature_file',
'test_signature_string_passphrase_empty_string',
'test_signature_string_passphrase_empty_bytes_literal',
'test_signature_string_passphrase_bytes_literal',
'test_signature_string_passphrase_None',
'test_signature_string_bad_passphrase', 'test_signature_string_bad_passphrase',
'test_signature_string_verification', 'test_signature_string_verification',
'test_signature_string_algorithm_encoding']), 'test_signature_string_algorithm_encoding']),
'crypt': set(['test_encryption', 'crypt': set(['test_encryption',
'test_encryption_of_file_like_objects_io_StringIO',
'test_encryption_of_file_like_objects_io_BytesIO',
'test_encryption_of_file_like_objects_StringIO_StringIO',
'test_encryption_of_file_like_objects_cStringIO_StringIO',
'test_encryption_alt_encoding', 'test_encryption_alt_encoding',
'test_encryption_multi_recipient', 'test_encryption_multi_recipient',
'test_encryption_decryption_multi_recipient', 'test_encryption_decryption_multi_recipient',
'test_encryption_one_hidden_recipient_one_not', 'test_encryption_one_hidden_recipient_one_not',
'test_encryption_throw_keyids', 'test_encryption_throw_keyids',
'test_decryption', 'test_decryption',
'test_decryption_with_bytes_literal',
'test_symmetric_encryption_and_decryption', 'test_symmetric_encryption_and_decryption',
'test_file_encryption_and_decryption', 'test_file_encryption_and_decryption',
'test_encryption_to_filename', 'test_encryption_to_filename',
'test_encryption_to_filehandle',]), 'test_encryption_to_filehandle',
'test_encryption_from_filehandle',
'test_encryption_with_output',]),
'listkeys': set(['test_list_keys_after_generation']), 'listkeys': set(['test_list_keys_after_generation']),
'keyrings': set(['test_public_keyring', 'keyrings': set(['test_public_keyring',
'test_secret_keyring', 'test_secret_keyring',
'test_import_and_export', 'test_import_and_export',
'test_deletion', 'test_deletion_public_key',
'test_import_only', 'test_deletion_secret_key',
'test_recv_keys_default',]), } 'test_deletion_subkeys',
'test_import_only']),
'recvkeys': set(['test_recv_keys_default']),
}
def main(args): def main(args):
if not args.quiet: if not args.quiet:

View File

@ -22,11 +22,19 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import print_function from __future__ import print_function
import platform
import setuptools import setuptools
import sys import sys
import os import os
import versioneer import versioneer
try:
import __pypy__
except ImportError:
_isPyPy = False
else:
_isPyPy = True
versioneer.versionfile_source = 'gnupg/_version.py' versioneer.versionfile_source = 'gnupg/_version.py'
versioneer.versionfile_build = 'gnupg/_version.py' versioneer.versionfile_build = 'gnupg/_version.py'
@ -75,6 +83,13 @@ def get_requirements():
# Required to make `collections.OrderedDict` available on Python<=2.6 # Required to make `collections.OrderedDict` available on Python<=2.6
requirements.append('ordereddict==1.1#a0ed854ee442051b249bfad0f638bbec') requirements.append('ordereddict==1.1#a0ed854ee442051b249bfad0f638bbec')
# Don't try to install psutil on PyPy:
if _isPyPy:
for line in requirements[:]:
if line.startswith('psutil'):
print("Not installing %s on PyPy..." % line)
requirements.remove(line)
return requirements, links return requirements, links
@ -89,8 +104,8 @@ This module allows easy access to GnuPG's key management, encryption and \
signature functionality from Python programs, by interacting with GnuPG \ signature functionality from Python programs, by interacting with GnuPG \
through file descriptors. Input arguments are strictly checked and sanitised, \ through file descriptors. Input arguments are strictly checked and sanitised, \
and therefore this module should be safe to use in networked applications \ and therefore this module should be safe to use in networked applications \
requiring direct user input. It is intended for use with Python 2.6 or \ requiring direct user input. It is intended for use on Windows, MacOS X, BSD, \
greater. or Linux, with Python 2.6, Python 2.7, Python 3.3, Python 3.4, or PyPy.
""", """,
license="GPLv3+", license="GPLv3+",
@ -119,7 +134,13 @@ greater.
classifiers=[ classifiers=[
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: Android",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: BSD",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 2", "Programming Language :: Python :: 2",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
@ -127,6 +148,8 @@ greater.
"Programming Language :: Python :: 2.7", "Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.4",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Security :: Cryptography", "Topic :: Security :: Cryptography",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Utilities",] "Topic :: Utilities",]