Merge branch 'master' of github.com:isislovecruft/python-gnupg

fix/44-verbose-arg
Les Aker 2014-06-14 19:39:07 -04:00
commit ff96904233
23 changed files with 860 additions and 1144 deletions

6
.gitignore vendored
View File

@ -83,3 +83,9 @@ gpg
# setuptools/distribute files: # setuptools/distribute files:
PKG-INFO PKG-INFO
MANIFEST MANIFEST
# sphinx default build
docs/_build
# ignore keys which have been generated example scripts:
8192-bit-key/*

View File

@ -2,6 +2,11 @@ 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,")
PKG_NAME=python-gnupg
DOC_DIR=docs
DOC_BUILD_DIR:=$(DOC_DIR)/_build
DOC_HTML_DIR:=$(DOC_BUILD_DIR)/html
DOC_BUILD_ZIP:=$(PKG_NAME)-docs.zip
.PHONY=all .PHONY=all
all: uninstall install test all: uninstall install test
@ -73,11 +78,15 @@ py3k-uninstall: uninstall
reinstall: uninstall install reinstall: uninstall install
py3k-reinstall: py3k-uninstall py3k-install py3k-reinstall: py3k-uninstall py3k-install
cleandocs: docs-clean:
sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" \ -rm -rf $(DOC_BUILD_DIR)
-o docs gnupg/ tests/
docs: docs-completely-new:
cd docs && \ sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" -o $(DOC_DIR) gnupg/ tests/
make clean && \
make html docs-html:
cd $(DOC_DIR) && make clean && make html
docs-zipfile: docs-html
cd $(DOC_HTML_DIR) && { find . -name '*' | zip -@ -v ../$(DOC_BUILD_ZIP) ;};
@echo "Built documentation in $(DOC_BUILD_DIR)/$(DOC_BUILD_ZIP)"

View File

@ -2,7 +2,7 @@
# #
# You can set these variables from the command line. # You can set these variables from the command line.
SPHINXOPTS = SPHINXOPTS = -E -n
SPHINXBUILD = sphinx-build SPHINXBUILD = sphinx-build
PAPER = PAPER =
BUILDDIR = _build BUILDDIR = _build

View File

@ -25,6 +25,16 @@ div.header-wrapper {
border-bottom: 3px solid #2e3436; border-bottom: 3px solid #2e3436;
} }
div.headertitle a {
font-family: "Georgia", "Times New Roman", serif;
font-size: 2em;
color: rgb(252, 175, 62);
font-weight: normal;
}
h1 {
color: #204a87;
}
/* Default body styles */ /* Default body styles */
a { a {

View File

@ -1,232 +0,0 @@
/// XXX: make it cross browser
/**
* make the code below compatible with browsers without
* an installed firebug like debugger
*/
if (!window.console || !console.firebug) {
var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml",
"group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"];
window.console = {};
for (var i = 0; i < names.length; ++i)
window.console[names[i]] = function() {}
}
/**
* small helper function to urldecode strings
*/
jQuery.urldecode = function(x) {
return decodeURIComponent(x).replace(/\+/g, ' ');
}
/**
* small helper function to urlencode strings
*/
jQuery.urlencode = encodeURIComponent;
/**
* This function returns the parsed url parameters of the
* current request. Multiple values per key are supported,
* it will always return arrays of strings for the value parts.
*/
jQuery.getQueryParameters = function(s) {
if (typeof s == 'undefined')
s = document.location.search;
var parts = s.substr(s.indexOf('?') + 1).split('&');
var result = {};
for (var i = 0; i < parts.length; i++) {
var tmp = parts[i].split('=', 2);
var key = jQuery.urldecode(tmp[0]);
var value = jQuery.urldecode(tmp[1]);
if (key in result)
result[key].push(value);
else
result[key] = [value];
}
return result;
}
/**
* small function to check if an array contains
* a given item.
*/
jQuery.contains = function(arr, item) {
for (var i = 0; i < arr.length; i++) {
if (arr[i] == item)
return true;
}
return false;
}
/**
* highlight a given string on a jquery object by wrapping it in
* span elements with the given class name.
*/
jQuery.fn.highlightText = function(text, className) {
function highlight(node) {
if (node.nodeType == 3) {
var val = node.nodeValue;
var pos = val.toLowerCase().indexOf(text);
if (pos >= 0 && !jQuery.className.has(node.parentNode, className)) {
var span = document.createElement("span");
span.className = className;
span.appendChild(document.createTextNode(val.substr(pos, text.length)));
node.parentNode.insertBefore(span, node.parentNode.insertBefore(
document.createTextNode(val.substr(pos + text.length)),
node.nextSibling));
node.nodeValue = val.substr(0, pos);
}
}
else if (!jQuery(node).is("button, select, textarea")) {
jQuery.each(node.childNodes, function() {
highlight(this)
});
}
}
return this.each(function() {
highlight(this);
});
}
/**
* Small JavaScript module for the documentation.
*/
var Documentation = {
init : function() {
this.fixFirefoxAnchorBug();
this.highlightSearchWords();
this.initModIndex();
},
/**
* i18n support
*/
TRANSLATIONS : {},
PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; },
LOCALE : 'unknown',
// gettext and ngettext don't access this so that the functions
// can savely bound to a different name (_ = Documentation.gettext)
gettext : function(string) {
var translated = Documentation.TRANSLATIONS[string];
if (typeof translated == 'undefined')
return string;
return (typeof translated == 'string') ? translated : translated[0];
},
ngettext : function(singular, plural, n) {
var translated = Documentation.TRANSLATIONS[singular];
if (typeof translated == 'undefined')
return (n == 1) ? singular : plural;
return translated[Documentation.PLURALEXPR(n)];
},
addTranslations : function(catalog) {
for (var key in catalog.messages)
this.TRANSLATIONS[key] = catalog.messages[key];
this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
this.LOCALE = catalog.locale;
},
/**
* add context elements like header anchor links
*/
addContextElements : function() {
$('div[id] > :header:first').each(function() {
$('<a class="headerlink">\u00B6</a>').
attr('href', '#' + this.id).
attr('title', _('Permalink to this headline')).
appendTo(this);
});
$('dt[id]').each(function() {
$('<a class="headerlink">\u00B6</a>').
attr('href', '#' + this.id).
attr('title', _('Permalink to this definition')).
appendTo(this);
});
},
/**
* workaround a firefox stupidity
*/
fixFirefoxAnchorBug : function() {
if (document.location.hash && $.browser.mozilla)
window.setTimeout(function() {
document.location.href += '';
}, 10);
},
/**
* highlight the search words provided in the url in the text
*/
highlightSearchWords : function() {
var params = $.getQueryParameters();
var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
if (terms.length) {
var body = $('div.body');
window.setTimeout(function() {
$.each(terms, function() {
body.highlightText(this.toLowerCase(), 'highlight');
});
}, 10);
$('<li class="highlight-link"><a href="javascript:Documentation.' +
'hideSearchWords()">' + _('Hide Search Matches') + '</a></li>')
.appendTo($('.sidebar .this-page-menu'));
}
},
/**
* init the modindex toggle buttons
*/
initModIndex : function() {
var togglers = $('img.toggler').click(function() {
var src = $(this).attr('src');
var idnum = $(this).attr('id').substr(7);
console.log($('tr.cg-' + idnum).toggle());
if (src.substr(-9) == 'minus.png')
$(this).attr('src', src.substr(0, src.length-9) + 'plus.png');
else
$(this).attr('src', src.substr(0, src.length-8) + 'minus.png');
}).css('display', '');
if (DOCUMENTATION_OPTIONS.COLLAPSE_MODINDEX) {
togglers.click();
}
},
/**
* helper function to hide the search marks again
*/
hideSearchWords : function() {
$('.sidebar .this-page-menu li.highlight-link').fadeOut(300);
$('span.highlight').removeClass('highlight');
},
/**
* make the url absolute
*/
makeURL : function(relativeURL) {
return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;
},
/**
* get the current relative url
*/
getCurrentURL : function() {
var path = document.location.pathname;
var parts = path.split(/\//);
$.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() {
if (this == '..')
parts.pop();
});
var url = parts.join('/');
return path.substring(url.lastIndexOf('/') + 1, path.length - 1);
}
};
// quick alias for translations
_ = Documentation.gettext;
$(document).ready(function() {
Documentation.init();
});

386
docs/_static/haiku.css vendored
View File

@ -1,386 +0,0 @@
/* custom stuff I put in FIXME where is it "supposed" to go? */
div.admonition-todo
{
border: 1px solid red;
background-color: #Fdd;
}
div.admonition-todo p.admonition-title
{
margin: 0;
color: red;
text-transform: lowercase;
}
p.admonition-title
{
font-size: 120%;
font-weight: bold;
}
dl.class>dt, dl.interface>dt, dl.function>dt, dl.staticmethod>dt
{
font-size: 150%;
background-color:#ddd;
}
dl.method>dt
{
background-color: #eee;
border-bottom: 2px solid #ddd;
}
dl.method:hover
{
background-color:#ffd;
}
/** end custom */
html {
margin: 0px;
padding: 0px;
background: #FFF url(bg-page.png) top left repeat-x;
}
body {
line-height: 1.5;
margin: auto;
padding: 0px;
font-family: "DejaVu Sans", Arial, Helvetica, sans-serif;
min-width: 59em;
max-width: 70em;
color: #333333;
}
div.footer {
padding: 8px;
font-size: 11px;
text-align: center;
letter-spacing: 0.5px;
}
/* link colors and text decoration */
a:link {
font-weight: bold;
text-decoration: none;
color: #dc3c01;
}
a:visited {
font-weight: bold;
text-decoration: none;
color: #892601;
}
a:hover, a:active {
text-decoration: underline;
color: #ff4500;
}
/* Some headers act as anchors, don't give them a hover effect */
h1 a:hover, a:active {
text-decoration: none;
color: #0c3762;
}
h2 a:hover, a:active {
text-decoration: none;
color: #0c3762;
}
h3 a:hover, a:active {
text-decoration: none;
color: #0c3762;
}
h4 a:hover, a:active {
text-decoration: none;
color: #0c3762;
}
a.headerlink {
color: #a7ce38;
padding-left: 5px;
}
a.headerlink:hover {
color: #a7ce38;
}
/* basic text elements */
div.content {
margin-top: 20px;
margin-left: 40px;
margin-right: 40px;
margin-bottom: 50px;
font-size: 0.9em;
}
/* heading and navigation */
div.header {
position: relative;
left: 0px;
top: 0px;
height: 85px;
/* background: #eeeeee; */
padding: 0 40px;
}
div.header h1 {
font-size: 1.6em;
font-weight: normal;
letter-spacing: 1px;
color: #0c3762;
border: 0;
margin: 0;
padding-top: 15px;
}
div.header h1 a {
font-weight: normal;
color: #0c3762;
}
div.header h2 {
font-size: 1.3em;
font-weight: normal;
letter-spacing: 1px;
text-transform: uppercase;
color: #aaa;
border: 0;
margin-top: -3px;
padding: 0;
}
div.header img.rightlogo {
float: right;
}
div.title {
font-size: 1.3em;
font-weight: bold;
color: #0c3762;
border-bottom: dotted thin #e0e0e0;
margin-bottom: 25px;
}
div.topnav {
/* background: #e0e0e0; */
}
div.topnav p {
margin-top: 0;
margin-left: 40px;
margin-right: 40px;
margin-bottom: 0px;
text-align: right;
font-size: 0.8em;
}
div.bottomnav {
background: #eeeeee;
}
div.bottomnav p {
margin-right: 40px;
text-align: right;
font-size: 0.8em;
}
a.uplink {
font-weight: normal;
}
/* contents box */
table.index {
margin: 0px 0px 30px 30px;
padding: 1px;
border-width: 1px;
border-style: dotted;
border-color: #e0e0e0;
}
table.index tr.heading {
background-color: #e0e0e0;
text-align: center;
font-weight: bold;
font-size: 1.1em;
}
table.index tr.index {
background-color: #eeeeee;
}
table.index td {
padding: 5px 20px;
}
table.index a:link, table.index a:visited {
font-weight: normal;
text-decoration: none;
color: #dc3c01;
}
table.index a:hover, table.index a:active {
text-decoration: underline;
color: #ff4500;
}
/* Haiku User Guide styles and layout */
/* Rounded corner boxes */
/* Common declarations */
div.admonition {
-webkit-border-radius: 10px;
-khtml-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
border-style: dotted;
border-width: thin;
border-color: #dcdcdc;
padding: 10px 15px 10px 15px;
margin-bottom: 15px;
margin-top: 15px;
}
div.note {
padding: 10px 15px 10px 80px;
background: #e4ffde url(alert_info_32.png) 15px 15px no-repeat;
min-height: 42px;
}
div.warning {
padding: 10px 15px 10px 80px;
background: #fffbc6 url(alert_warning_32.png) 15px 15px no-repeat;
min-height: 42px;
}
div.seealso {
background: #e4ffde;
}
/* More layout and styles */
h1 {
font-size: 1.3em;
font-weight: bold;
color: #0c3762;
border-bottom: dotted thin #e0e0e0;
margin-top: 30px;
}
h2 {
font-size: 1.2em;
font-weight: normal;
color: #0c3762;
border-bottom: dotted thin #e0e0e0;
margin-top: 30px;
}
h3 {
font-size: 1.1em;
font-weight: normal;
color: #0c3762;
margin-top: 30px;
}
h4 {
font-size: 1.0em;
font-weight: normal;
color: #0c3762;
margin-top: 30px;
}
p {
text-align: justify;
}
p.last {
margin-bottom: 0;
}
ol {
padding-left: 20px;
}
ul {
padding-left: 5px;
margin-top: 3px;
}
li {
line-height: 1.3;
}
div.content ul > li {
-moz-background-clip:border;
-moz-background-inline-policy:continuous;
-moz-background-origin:padding;
background: transparent url(bullet_orange.png) no-repeat scroll left 0.45em;
list-style-image: none;
list-style-type: none;
padding: 0 0 0 1.666em;
margin-bottom: 3px;
}
td {
vertical-align: top;
}
tt {
background-color: #e2e2e2;
font-size: 1.0em;
font-family: monospace;
}
pre {
border-color: #0c3762;
border-style: dotted;
border-width: thin;
margin: 0 0 12px 0;
padding: 0.8em;
background-color: #f0f0f0;
}
hr {
border-top: 1px solid #ccc;
border-bottom: 0;
border-right: 0;
border-left: 0;
margin-bottom: 10px;
margin-top: 20px;
}
/* printer only pretty stuff */
@media print {
.noprint {
display: none;
}
/* for acronyms we want their definitions inlined at print time */
acronym[title]:after {
font-size: small;
content: " (" attr(title) ")";
font-style: italic;
}
/* and not have mozilla dotted underline */
acronym {
border: none;
}
div.topnav, div.bottomnav, div.header, table.index {
display: none;
}
div.content {
margin: 0px;
padding: 0px;
}
html {
background: #FFF;
}
}
.viewcode-back {
font-family: "DejaVu Sans", Arial, Helvetica, sans-serif;
}
div.viewcode-block:target {
background-color: #f4debf;
border-top: 1px solid #ac9;
border-bottom: 1px solid #ac9;
margin: -1px -12px;
padding: 0 12px;
}

File diff suppressed because one or more lines are too long

View File

@ -12,6 +12,7 @@
# serve to show the default. # serve to show the default.
import sys, os import sys, os
import psutil
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
@ -28,11 +29,16 @@ autoclass_content = 'both'
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = '1.0' needs_sphinx = '1.1'
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] extensions = ['sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.intersphinx',
'sphinx.ext.doctest',
'sphinxcontrib.fulltoc',
]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_static'] templates_path = ['_static']
@ -41,7 +47,7 @@ templates_path = ['_static']
source_suffix = '.rst' source_suffix = '.rst'
# The encoding of source files. # The encoding of source files.
#source_encoding = 'utf-8-sig' source_encoding = 'utf-8-sig'
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = 'index'
@ -82,7 +88,7 @@ add_function_parentheses = True
# If true, the current module name will be prepended to all description # If true, the current module name will be prepended to all description
# unit titles (such as .. function::). # unit titles (such as .. function::).
#add_module_names = True add_module_names = False
# If true, sectionauthor and moduleauthor directives will be shown in the # If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default. # output. They are ignored by default.
@ -94,6 +100,8 @@ pygments_style = 'monokai'
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
#modindex_common_prefix = [] #modindex_common_prefix = []
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------------

View File

@ -17,6 +17,7 @@ do:
:private-members: :private-members:
:show-inheritance: :show-inheritance:
.. _meta:
meta module meta module
----------- -----------
@ -28,10 +29,13 @@ doing some serious hacking.
.. automodule:: gnupg._meta .. automodule:: gnupg._meta
:members: :members:
:undoc-members:
:private-members: :private-members:
:special-members:
:exclude-members: _agent_proc, __module__, __dict__, _decode_errors, init,
__weakref__, _result_map, __metaclass__
:show-inheritance: :show-inheritance:
.. _parsers:
parsers module parsers module
-------------- --------------
@ -39,10 +43,11 @@ parsers module
These are classes for parsing both user inputs and status file descriptor These are classes for parsing both user inputs and status file descriptor
flags from GnuPG's output. The latter are used in order to determine what our flags from GnuPG's output. The latter are used in order to determine what our
GnuPG process is doing and retrieve information about its operations, which GnuPG process is doing and retrieve information about its operations, which
are stored in corresponding classes in :attr:`gnupg.GPG._result_dict`. Some are stored in corresponding classes in
status flags aren't handled yet -- infomation on *all* of the flags (well, at :attr:`~gnupg._meta.GPGBase._result_map`. Some status flags aren't handled yet
least the documented ones…) can be found in the docs/DETAILS file in GnuPG's -- information on *all* of the flags (well, at least the documented ones…) can
source_, which has been included here_ as well. be found in the :file:`docs/DETAILS` file in GnuPG's source_, which has been
included here_ as well.
.. automodule:: gnupg._parsers .. automodule:: gnupg._parsers
@ -52,6 +57,8 @@ source_, which has been included here_ as well.
:show-inheritance: :show-inheritance:
.. _util:
util module util module
----------- -----------
@ -75,17 +82,16 @@ by Steve Traugott, which in turn is a modification of the pycrypto GnuPG
interface written by A.M. Kuchling. interface written by A.M. Kuchling.
This version is patched to sanitize untrusted inputs, due to the necessity of This version is patched to sanitize untrusted inputs, due to the necessity of
executing :class:`subprocess.Popen([...], shell=True)` in order to communicate executing ``subprocess.Popen([...], shell=True)`` in order to communicate with
with GnuPG. Several speed improvements were also made based on code profiling, GnuPG. Several speed improvements were also made based on code profiling, and
and the API has been cleaned up to support an easier, more Pythonic, the API has been cleaned up to support an easier, more Pythonic, interaction.
interaction.
Previous Authors' Documentation Previous Authors' Documentation
------------------------------- -------------------------------
Steve Traugott's documentation: Steve Traugott's documentation:
|
| Portions of this module are derived from A.M. Kuchling's well-designed | 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 | GPG.py, using Richard Jones' updated version 1.3, which can be found in
| the pycrypto CVS repository on Sourceforge: | the pycrypto CVS repository on Sourceforge:
@ -107,7 +113,7 @@ Steve Traugott's documentation:
Vinay Sajip's documentation: Vinay Sajip's documentation:
|
| This version of the module has been modified from Steve Traugott's version | 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 | (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 | Vinay Sajip to make use of the subprocess module (Steve's version uses
@ -122,4 +128,4 @@ Vinay Sajip's documentation:
.. _GnuPG: http://gnupg.org .. _GnuPG: http://gnupg.org
.. _python-gnupg: https://code.google.com/p/python-gnupg/ .. _python-gnupg: https://code.google.com/p/python-gnupg/
.. _source: http://http://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=shortlog;h=refs/heads/master .. _source: http://http://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=shortlog;h=refs/heads/master
.. _here: ./DETAILS.html .. _here: ./_static/DETAILS.html

View File

@ -21,7 +21,7 @@ Contents:
Source, license, & bug reports Source, license, & bug reports
============================== ==============================
The source code which was used to generate this documentation is accessible by The source code which was used to generate this documentation is accessible by
clicking the little [source]_ links next to the docs. Current source code can clicking the little `source` links next to the docs. Current source code can
be found in this github repository_. The **master** branch always reflects the be found in this github repository_. The **master** branch always reflects the
latest release, all releases are tagged with signed, annotated git tags, and latest release, all releases are tagged with signed, annotated git tags, and
the **develop** branch represents the state of the next release. the **develop** branch represents the state of the next release.

View File

@ -0,0 +1,213 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Create a new 8192-bit GnuPG keypair.
:authors: Isis <isis@patternsinthevoid.net> 0xa3adb67a2cdb8b35
:license: MIT license
:copyright: (c) 2013 Isis Agora Lovecruft
"""
from __future__ import print_function
from __future__ import absolute_import
from __future__ import unicode_literals
import logging
import gnupg
import sys
from gnupg import _logger
# Set up logging:
log = _logger.create_logger(9)
log.setLevel(9)
#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Settings
#
# You probably want to edit the following variables. Ones which are currently
# set to strings are necessary; the ones which are set to `None` are optional.
# The directory to use as the homedir for GnuPG (it will contain the
# secring.gpg and pubring.gpg, etc.)
NEWKEY_DIR = './8192-bit-key'
# The name you go by, as it should appear in the primary keyid, i.e. "Evey
# Hammond":
NAME = 'Someone'
# The comment which goes in parantheses after the name and before the email
# address on the key's primary uid. Leave as None to not have one.
NAME_COMMENT = None
# The email address for the primary UID (You *should* actually be able to put
# whatever you want here, like a domain or something, because the GnuPG
# `--allow-freeform-uid` option will be used. I've not actually tested this
# though.)
NAME_EMAIL = 'someone@example.com'
# Expiration date for the new key. To use the default expiration of one year,
# set to None.
#EXPIRE_DATE = '1999-09-19'
EXPIRE_DATE = None
# GnuPG-1.4.x allows the automated creation of passphraseless keys. If using
# GnuPG-1.4.x, and you don't specify the passphrase, you can of course set it
# later with `$ gpg --edit-key` and then at the prompt typing `password`. If
# using a GnuPG from the 2.x series, you *must* specify a password here
# (though you can still change it afterward).
PASSPHRASE = None
# Type of key, i.e. 'RSA' or 'DSA' or something else. I've only tested
# 8192-bit keys with RSA.
KEY_TYPE = 'RSA'
# Uses for the key. Can be things like 'cert,sign' or 'cert' or 'cert,auth'.
KEY_USAGE = 'cert'
# Key bitlength. You likely want 8192, if you're using this script.
#
# It *is* possible to create 16834-bit keys, though it requires modifying and
# recompiling GnuPG. Doing this is a bit janky due to internal GnuPG buffers
# in several parts of the codebase being limited to 8192-bits, the key cannot
# be handled by *most* keyservers (there appears to be only one public
# keyserver which supports 16384-bit keys being uploaded to it), and the
# 16834-bit key will likely require the modified GnuPG to work with it (even
# then some operations, such as removal of the primary secret key, but not the
# primary public key, from the keychain will be badly broken).
KEY_LENGTH = 8192
# Type of subkey. None to skip subkey generation. You can add keys later
# through `$ gpg --edit-key`. For compatibility with people who aren't doing
# crazy things with their keys, you maybe probably want to use `--edit-key` to
# create some nice, normal, "overly-paranoid" 4096-bit keys.
SUBKEY_TYPE = 'RSA'
# Same as KEY_USAGE.
#SUBKEY_USAGE = None
SUBKEY_USAGE = 'sign'
# Same as KEY_LENGTH.
#SUBKEY_LENGTH = None
SUBKEY_LENGTH = 4096
# The default keyserver for the key, which is embedded into the key, telling
# other people's GnuPGs to fetch (and send updates) to this URL:
KEYSERVER = None
# Set the cipher, hash, and compression preference values for this key. This
# expects the same type of string as the sub-command setpref in the
# --edit-key menu. The default preferences are given in
# ``gnupg.GPG.default_preference_list``.
PREFERENCES = None
#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
gpg = gnupg.GPG(homedir=NEWKEY_DIR)
allparams = {'name_real': NAME,
'name_comment': NAME_COMMENT,
'name_email': NAME_EMAIL,
'expire_date': EXPIRE_DATE,
'passphrase': PASSPHRASE,
'key_type': KEY_TYPE,
'key_usage': KEY_USAGE,
'key_length': KEY_LENGTH,
'subkey_type': SUBKEY_TYPE,
'subkey_usage': SUBKEY_USAGE,
'subkey_length': SUBKEY_LENGTH,
'keyserver': KEYSERVER,
'preferences': PREFERENCES}
def createBatchfile(keyparams=allparams):
"""Create the batchfile for our new key.
:params dict keyparams: A dictionary of arguments for creating the key. It
should probably be ``allparams``.
:rtype: str
:returns: A string containing the entire GnuPG batchfile.
"""
useparams = {}
for key, value in keyparams.items():
if value:
useparams.update({key: value})
batchfile = gpg.gen_key_input(separate_keyring=True,
save_batchfile=True,
**useparams)
log.info("Generated GnuPG batch file:\n%s" % batchfile)
return batchfile
def createKey(batchfile):
"""Create a new keypair from a **batchfile**.
Writes the new keys into keyrings named after ``NAME_EMAIL`` inside the
``NEWKEY_DIR``.
:params str batchfile: A GnuPG batchfile. See :func:`createBatchfile`.
"""
key = gpg.gen_key(batchfile)
fingerprint = key.fingerprint
if not fingerprint:
log.error("Key creation seems to have failed: %s" % key.status)
return None, None
return key, fingerprint
def displayNewKey(key):
"""Use ``gnupg.GPG.list_keys()`` to display details of the new key."""
if key.keyring:
gpg.keyring = key.keyring
if key.secring:
gpg.secring = key.secring
# Using '--fingerprint' twice will display subkey fingerprints too:
gpg.options = ['--fingerprint', '--fingerprint']
keylist = gpg.list_keys(secret=True)
# `result` is a `gnupg._parsers.ListKeys`, which is list-like, so iterate
# over all the keys and display their info:
for gpgkey in keylist:
for k, v in gpgkey:
log.info("%s: %s" % (k.capitalize(), v))
return keylist
def exportNewKey(fingerprint):
"""Export the new keys into .asc files.
:param str fingerprint: A full key fingerprint.
"""
log.info("Exporting key: %s" % fingerprint)
keyfn = os.path.join(gpg.homedir,
fingerprint + '-8192-bit-key') + os.path.extsep
pubkey = gpg.export_keys(fingerprint)
seckey = gpg.export_keys(fingerprint, secret=True)
subkey = gpg.export_keys(fingerprint, secret=True, subkeys=True)
with open(keyfn + 'pub' + os.path.extsep + 'asc', 'w') as fh:
fh.write(pubkey)
with open(keyfn + 'sec' + os.path.extsep + 'asc', 'w') as fh:
fh.write(seckey)
with open(keyfn + 'sub' + os.path.extsep + 'asc', 'w') as fh:
fh.write(subkey)
if __name__ == '__main__':
if (NAME == 'Someone') or (NAME_EMAIL == 'someone@example.com'):
log.info("Please edit the settings variables within this script.")
log.info("Exiting...")
exit(1)
else:
try:
batchfile = createBatchfile()
key, fingerprint = createKey(batchfile)
log.info("New key with fingerprint %r created" % fingerprint)
displayNewKey(key)
exportNewKey(fingerprint)
except Exception as error:
log.error(error)

View File

@ -17,10 +17,7 @@
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details. # FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
'''log.py '''Logging module for python-gnupg.'''
----------
Logging module for python-gnupg.
'''
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import print_function from __future__ import print_function
@ -51,10 +48,15 @@ def status(self, message, *args, **kwargs):
def create_logger(level=logging.NOTSET): def create_logger(level=logging.NOTSET):
"""Create a logger for python-gnupg at a specific message level. """Create a logger for python-gnupg at a specific message level.
:type level: int or str :type level: :obj:`int` or :obj:`str`
:param level: A string or an integer for the lowest level to log. :param level: A string or an integer for the lowest level to include in
Available levels: logs.
**Available levels:**
==== ======== ========================================
int str description int str description
==== ======== ========================================
0 NOTSET Disable all logging. 0 NOTSET Disable all logging.
9 GNUPG Log GnuPG's internal status messages. 9 GNUPG Log GnuPG's internal status messages.
10 DEBUG Log module level debuging messages. 10 DEBUG Log module level debuging messages.
@ -62,6 +64,7 @@ def create_logger(level=logging.NOTSET):
30 WARN Warning messages. 30 WARN Warning messages.
40 ERROR Error messages and tracebacks. 40 ERROR Error messages and tracebacks.
50 CRITICAL Unhandled exceptions and tracebacks. 50 CRITICAL Unhandled exceptions and tracebacks.
==== ======== ========================================
""" """
_test = os.path.join(os.path.join(os.getcwd(), 'gnupg'), 'test') _test = os.path.join(os.path.join(os.getcwd(), 'gnupg'), 'test')
_now = datetime.now().strftime("%Y-%m-%d_%H%M%S") _now = datetime.now().strftime("%Y-%m-%d_%H%M%S")
@ -75,21 +78,19 @@ def create_logger(level=logging.NOTSET):
if level > logging.NOTSET: if level > logging.NOTSET:
logging.basicConfig(level=level, filename=_fn, logging.basicConfig(level=level, filename=_fn,
filemode="a", format=_fmt) filemode="a", format=_fmt)
logging.captureWarnings(True)
logging.logThreads = True logging.logThreads = True
if hasattr(logging,'captureWarnings'):
logging.captureWarnings(True)
colouriser = _ansistrm.ColorizingStreamHandler colouriser = _ansistrm.ColorizingStreamHandler
colouriser.level_map[9] = (None, 'blue', False) colouriser.level_map[9] = (None, 'blue', False)
colouriser.level_map[10] = (None, 'cyan', False) colouriser.level_map[10] = (None, 'cyan', False)
handler = colouriser(stream=sys.stderr) handler = colouriser(sys.stderr)
handler.setLevel(level) handler.setLevel(level)
formatr = logging.Formatter(_fmt) formatr = logging.Formatter(_fmt)
handler.setFormatter(formatr) handler.setFormatter(formatr)
print("Starting the logger...")
else: else:
handler = NullHandler() handler = NullHandler()
print("GnuPG logging disabled...")
log = logging.getLogger('gnupg') log = logging.getLogger('gnupg')
log.addHandler(handler) log.addHandler(handler)

View File

@ -17,10 +17,8 @@
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details. # FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
'''meta.py '''Meta and base classes for hiding internal functions, and controlling
---------- attribute creation and handling.
Meta and base classes for hiding internal functions, and controlling attribute
creation and handling.
''' '''
from __future__ import absolute_import from __future__ import absolute_import
@ -52,6 +50,10 @@ class GPGMeta(type):
Detects running gpg-agent processes and the presence of a pinentry Detects running gpg-agent processes and the presence of a pinentry
program, and disables pinentry so that python-gnupg can write the program, and disables pinentry so that python-gnupg can write the
passphrase to the controlled GnuPG process without killing the agent. passphrase to the controlled GnuPG process without killing the agent.
:attr _agent_proc: If a :program:`gpg-agent` process is currently running
for the effective userid, then **_agent_proc** will be
set to a ``psutil.Process`` for that process.
""" """
def __new__(cls, name, bases, attrs): def __new__(cls, name, bases, attrs):
@ -69,13 +71,13 @@ class GPGMeta(type):
If there is a matching gpg-agent process, set a :class:`psutil.Process` If there is a matching gpg-agent process, set a :class:`psutil.Process`
instance containing the gpg-agent process' information to instance containing the gpg-agent process' information to
:attr:`GPG._agent_proc`. ``cls._agent_proc``.
: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 None. returns None.
""" """
identity = os.getresuid() identity = psutil.Process(os.getpid()).uids
for proc in psutil.process_iter(): for proc in psutil.process_iter():
if (proc.name == "gpg-agent") and proc.is_running: if (proc.name == "gpg-agent") and proc.is_running:
log.debug("Found gpg-agent process with pid %d" % proc.pid) log.debug("Found gpg-agent process with pid %d" % proc.pid)
@ -87,8 +89,13 @@ class GPGMeta(type):
class GPGBase(object): class GPGBase(object):
"""Base class for property storage and to control process initialisation.""" """Base class for storing properties and controlling process initialisation.
:const _result_map: A *dict* containing classes from
:mod:`~gnupg._parsers`, used for parsing results
obtained from GnuPG commands.
:const _decode_errors: How to handle encoding errors.
"""
__metaclass__ = GPGMeta __metaclass__ = GPGMeta
_decode_errors = 'strict' _decode_errors = 'strict'
_result_map = { 'crypt': _parsers.Crypt, _result_map = { 'crypt': _parsers.Crypt,
@ -103,7 +110,28 @@ 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): verbose=False, options=None):
"""Create a ``GPGBase``.
This class is used to set up properties for controlling the behaviour
of configuring various options for GnuPG, such as setting GnuPG's
**homedir** , and the paths to its **binary** and **keyring** .
:const binary: (:obj:`str`) The full path to the GnuPG binary.
:ivar homedir: (:class:`~gnupg._util.InheritableProperty`) The full
path to the current setting for the GnuPG
``--homedir``.
:ivar _generated_keys: (:class:`~gnupg._util.InheritableProperty`)
Controls setting the directory for storing any
keys which are generated with
:meth:`~gnupg.GPG.gen_key`.
:ivar str keyring: The filename in **homedir** to use as the keyring
file for public keys.
:ivar str secring: The filename in **homedir** to use as the keyring
file for secret keys.
"""
self.binary = _util._find_binary(binary) self.binary = _util._find_binary(binary)
self.homedir = home if home else _util._conf self.homedir = 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'
@ -125,7 +153,7 @@ class GPGBase(object):
self._filesystemencoding = encodings.normalize_encoding( self._filesystemencoding = encodings.normalize_encoding(
sys.getfilesystemencoding().lower()) sys.getfilesystemencoding().lower())
self._keyserver = 'hkp://subkeys.pgp.net' self._keyserver = 'hkp://wwwkeys.pgp.net'
self.__generated_keys = os.path.join(self.homedir, 'generated-keys') self.__generated_keys = os.path.join(self.homedir, 'generated-keys')
try: try:
@ -136,8 +164,8 @@ class GPGBase(object):
if self.options is not None: if self.options is not None:
assert isinstance(self.options, str), "options not string" assert isinstance(self.options, str), "options not string"
except (AssertionError, AttributeError) as ae: except (AssertionError, AttributeError) as ae:
log.error("GPGBase.__init__(): %s" % ae.message) log.error("GPGBase.__init__(): %s" % str(ae))
raise RuntimeError(ae.message) raise RuntimeError(str(ae))
else: else:
if verbose is True: if verbose is True:
# The caller wants logging, but we need a valid --debug-level # The caller wants logging, but we need a valid --debug-level
@ -154,15 +182,14 @@ class GPGBase(object):
self.__remove_path__('pinentry') self.__remove_path__('pinentry')
def __remove_path__(self, prog=None, at_exit=True): def __remove_path__(self, prog=None, at_exit=True):
"""Remove a the directories containing a program from the system's """Remove the directories containing a program from the system's
``$PATH``. If :attr:`GPG.binary` is in a directory being removed, it ``$PATH``. If ``GPGBase.binary`` is in a directory being removed, it
is symlinked to './gpg' is linked to :file:'./gpg' in the current directory.
:param str prog: The program to remove from ``$PATH``. :param str prog: The program to remove from ``$PATH``.
:param bool at_exit: Add the program back into the ``$PATH`` when the :param bool at_exit: Add the program back into the ``$PATH`` when the
Python interpreter exits, and delete any symlinks Python interpreter exits, and delete any symlinks
to :attr:`GPG.binary` which were created. to ``GPGBase.binary`` which were created.
""" """
#: A list of ``$PATH`` entries which were removed to disable pinentry. #: A list of ``$PATH`` entries which were removed to disable pinentry.
self._removed_path_entries = [] self._removed_path_entries = []
@ -173,7 +200,7 @@ class GPGBase(object):
try: try:
program = _util._which(prog)[0] program = _util._which(prog)[0]
except (OSError, IOError, IndexError) as err: except (OSError, IOError, IndexError) as err:
log.err(err.message) log.err(str(err))
log.err("Cannot find program '%s', not changing PATH." % prog) log.err("Cannot find program '%s', not changing PATH." % prog)
return return
@ -222,7 +249,7 @@ class GPGBase(object):
@staticmethod @staticmethod
def update_path(environment, path): def update_path(environment, path):
"""Add paths to the string at os.environ['PATH']. """Add paths to the string at ``os.environ['PATH']``.
:param str environment: The environment mapping to update. :param str environment: The environment mapping to update.
:param list path: A list of strings to update the PATH with. :param list path: A list of strings to update the PATH with.
@ -270,7 +297,7 @@ class GPGBase(object):
Note that "original state" does not mean the default preference Note that "original state" does not mean the default preference
list for whichever version of GnuPG is being used. It means the list for whichever version of GnuPG is being used. It means the
default preference list defined by :attr:`GPGBase._preferences`. default preference list defined by :attr:`GPGBase._prefs`.
Using BZIP2 is avoided due to not interacting well with some versions Using BZIP2 is avoided due to not interacting well with some versions
of GnuPG>=2.0.0. of GnuPG>=2.0.0.
@ -293,14 +320,14 @@ class GPGBase(object):
should contain the desired keyserver protocol should contain the desired keyserver protocol
which is supported by the keyserver, for example, which is supported by the keyserver, for example,
``'hkps://keys.mayfirst.org'``. The default ``'hkps://keys.mayfirst.org'``. The default
keyserver is ``'hkp://subkeys.pgp.net'``. keyserver is ``'hkp://wwwkeys.pgp.net'``.
""" """
self._keyserver = location self._keyserver = location
@keyserver.deleter @keyserver.deleter
def keyserver(self): def keyserver(self):
"""Reset the keyserver to the default setting.""" """Reset the keyserver to the default setting."""
self._keyserver = 'hkp://subkeys.pgp.net' self._keyserver = 'hkp://wwwkeys.pgp.net'
def _homedir_getter(self): def _homedir_getter(self):
"""Get the directory currently being used as GnuPG's homedir. """Get the directory currently being used as GnuPG's homedir.
@ -321,11 +348,11 @@ class GPGBase(object):
created. Lastly, the ``direcory`` will be checked that the EUID has created. Lastly, the ``direcory`` will be checked that the EUID has
read and write permissions for it. read and write permissions for it.
:param str homedir: A relative or absolute path to the directory to use :param str directory: A relative or absolute path to the directory to
for storing/accessing GnuPG's files, including use for storing/accessing GnuPG's files, including
keyrings and the trustdb. keyrings and the trustdb.
:raises: :exc:`RuntimeError` if unable to find a suitable directory to :raises: :exc:`~exceptions.RuntimeError` if unable to find a suitable
use. directory to use.
""" """
if not directory: if not directory:
log.debug("GPGBase._homedir_setter(): Using default homedir: '%s'" log.debug("GPGBase._homedir_setter(): Using default homedir: '%s'"
@ -346,8 +373,8 @@ class GPGBase(object):
except AssertionError as ae: except AssertionError as ae:
msg = ("Unable to set '%s' as GnuPG homedir" % directory) msg = ("Unable to set '%s' as GnuPG homedir" % directory)
log.debug("GPGBase.homedir.setter(): %s" % msg) log.debug("GPGBase.homedir.setter(): %s" % msg)
log.debug(ae.message) log.debug(str(ae))
raise RuntimeError(ae.message) raise RuntimeError(str(ae))
else: else:
log.info("Setting homedir to '%s'" % hd) log.info("Setting homedir to '%s'" % hd)
self._homedir = hd self._homedir = hd
@ -365,17 +392,18 @@ class GPGBase(object):
def _generated_keys_setter(self, directory): def _generated_keys_setter(self, directory):
"""Set the directory for storing generated keys. """Set the directory for storing generated keys.
If unspecified, use $GNUPGHOME/generated-keys. If specified, ensure If unspecified, use
that the ``directory`` does not contain various shell escape :meth:`~gnupg._meta.GPGBase.homedir`/generated-keys. If specified,
characters. If ``directory`` is not found, it will be automatically ensure that the ``directory`` does not contain various shell escape
created. Lastly, the ``direcory`` will be checked that the EUID has characters. If ``directory`` isn't found, it will be automatically
read and write permissions for it. created. Lastly, the ``directory`` will be checked to ensure that the
current EUID has read and write permissions for it.
:param str directory: A relative or absolute path to the directory to :param str directory: A relative or absolute path to the directory to
use for storing/accessing GnuPG's files, including keyrings and use for storing/accessing GnuPG's files, including keyrings and
the trustdb. the trustdb.
:raises: :exc:`RuntimeError` if unable to find a suitable directory to :raises: :exc:`~exceptions.RuntimeError` if unable to find a suitable
use. directory to use.
""" """
if not directory: if not directory:
directory = os.path.join(self.homedir, 'generated-keys') directory = os.path.join(self.homedir, 'generated-keys')
@ -397,8 +425,8 @@ class GPGBase(object):
except AssertionError as ae: except AssertionError as ae:
msg = ("Unable to set '%s' as generated keys dir" % directory) msg = ("Unable to set '%s' as generated keys dir" % directory)
log.debug("GPGBase._generated_keys_setter(): %s" % msg) log.debug("GPGBase._generated_keys_setter(): %s" % msg)
log.debug(ae.message) log.debug(str(ae))
raise RuntimeError(ae.message) raise RuntimeError(str(ae))
else: else:
log.info("Setting homedir to '%s'" % hd) log.info("Setting homedir to '%s'" % hd)
self.__generated_keys = hd self.__generated_keys = hd
@ -407,10 +435,11 @@ class GPGBase(object):
_generated_keys_setter) _generated_keys_setter)
def _make_args(self, args, passphrase=False): def _make_args(self, args, passphrase=False):
"""Make a list of command line elements for GPG. The value of ``args`` """Make a list of command line elements for GPG.
will be appended only if it passes the checks in
:func:`parsers._sanitise`. The ``passphrase`` argument needs to be True The value of ``args`` will be appended only if it passes the checks in
if a passphrase will be sent to GPG, else False. :func:`gnupg._parsers._sanitise`. The ``passphrase`` argument needs to
be True if a passphrase will be sent to GnuPG, else False.
:param list args: A list of strings of options and flags to pass to :param list args: A list of strings of options and flags to pass to
``GPG.binary``. This is input safe, meaning that ``GPG.binary``. This is input safe, meaning that
@ -489,12 +518,13 @@ class GPGBase(object):
Calls methods on the response object for each valid token found, with Calls methods on the response object for each valid token found, with
the arg being the remainder of the status line. the arg being the remainder of the status line.
:param stream: A byte-stream, file handle, or :class:`subprocess.PIPE` :param stream: A byte-stream, file handle, or a
to parse the for status codes from the GnuPG process. :data:`subprocess.PIPE` for parsing the status codes
from the GnuPG process.
:param result: The result parser class from :mod:`_parsers` with which :param result: The result parser class from :mod:`~gnupg._parsers`
to call ``handle_status`` and parse the output of the ``handle_status()`` method of that class will be
``stream``. called in order to parse the output of ``stream``.
""" """
lines = [] lines = []
while True: while True:
@ -535,8 +565,8 @@ class GPGBase(object):
and stored as ``result.data``. and stored as ``result.data``.
:param stream: An open file-like object to read() from. :param stream: An open file-like object to read() from.
:param result: An instance of one of the result parsing classes from :param result: An instance of one of the :ref:`result parsing classes
:attr:`GPGBase._result_mapping`. <parsers>` from :const:`~gnupg._meta.GPGBase._result_map`.
""" """
chunks = [] chunks = []
log.debug("Reading data from stream %r..." % stream.__repr__()) log.debug("Reading data from stream %r..." % stream.__repr__())
@ -604,13 +634,13 @@ class GPGBase(object):
:param str keyids: A space-delimited string containing the keyids to :param str keyids: A space-delimited string containing the keyids to
request. request.
:param str keyserver: The keyserver to request the ``keyids`` from; :param str keyserver: The keyserver to request the ``keyids`` from;
defaults to :property:`gnupg.GPG.keyserver`. defaults to `gnupg.GPG.keyserver`.
""" """
if not keyserver: if not keyserver:
keyserver = self.keyserver keyserver = self.keyserver
args = ['--keyserver {}'.format(keyserver), args = ['--keyserver {0}'.format(keyserver),
'--recv-keys {}'.format(keyids)] '--recv-keys {0}'.format(keyids)]
log.info('Requesting keys from %s: %s' % (keyserver, keyids)) log.info('Requesting keys from %s: %s' % (keyserver, keyids))
result = self._result_map['import'](self) result = self._result_map['import'](self)
@ -632,8 +662,9 @@ class GPGBase(object):
:param bool binary: If True, do not ascii armour the output. :param bool binary: If True, do not ascii armour the output.
:param str digest_algo: The hash digest to use. Again, to see which :param str digest_algo: The hash digest to use. Again, to see which
hashes your GnuPG is capable of using, do: hashes your GnuPG is capable of using, do:
``$ gpg --with-colons --list-config digestname``. ``$ gpg --with-colons --list-config
The default, if unspecified, is ``'SHA512'``. digestname``. The default, if unspecified, is
``'SHA512'``.
""" """
log.debug("_sign_file():") log.debug("_sign_file():")
if binary: if binary:
@ -665,7 +696,7 @@ class GPGBase(object):
_util._write_passphrase(proc.stdin, passphrase, self._encoding) _util._write_passphrase(proc.stdin, passphrase, self._encoding)
writer = _util._threaded_copy_data(file, proc.stdin) writer = _util._threaded_copy_data(file, proc.stdin)
except IOError as ioe: except IOError as ioe:
log.exception("Error writing message: %s" % ioe.message) log.exception("Error writing message: %s" % str(ioe))
writer = None writer = None
self._collect_output(proc, result, writer, proc.stdin) self._collect_output(proc, result, writer, proc.stdin)
return result return result
@ -681,46 +712,55 @@ class GPGBase(object):
cipher_algo='AES256', cipher_algo='AES256',
digest_algo='SHA512', digest_algo='SHA512',
compress_algo='ZLIB'): compress_algo='ZLIB'):
"""Encrypt the message read from the file-like object ``data``. """Encrypt the message read from the file-like object **data**.
:param str data: The file or bytestream to encrypt. :param str data: The file or bytestream to encrypt.
:param str recipients: The recipients to encrypt to. Recipients must :param str recipients: The recipients to encrypt to. Recipients must
be specified keyID/fingerprint. Care should be taken in Python2.x be specified keyID/fingerprint.
to make sure that the given fingerprint is in fact a string and
not a unicode object. .. warning:: Care should be taken in Python2 to make sure that the
given fingerprints for **recipients** are in fact strings
and not unicode objects.
:param str default_key: The keyID/fingerprint of the key to use for :param str default_key: The keyID/fingerprint of the key to use for
signing. If given, ``data`` will be encrypted and signed. signing. If given, **data** will be encrypted
*and* signed.
:param str passphrase: If given, and ``default_key`` is also given, :param str passphrase: If given, and **default_key** is also given,
use this passphrase to unlock the secret portion of the use this passphrase to unlock the secret
``default_key`` to sign the encrypted ``data``. Otherwise, if portion of the **default_key** to sign the
``default_key`` is not given, but ``symmetric=True``, then use encrypted **data**. Otherwise, if
this passphrase as the passphrase for symmetric **default_key** is not given, but **symmetric**
encryption. Signing and symmetric encryption should *not* be is ``True``, then use this passphrase as the
combined when sending the ``data`` to other recipients, else the passphrase for symmetric encryption. Signing
passphrase to the secret key would be shared with them. and symmetric encryption should *not* be
combined when sending the **data** to other
recipients, else the passphrase to the secret
key would be shared with them.
:param bool armor: If True, ascii armor the output; otherwise, the :param bool armor: If True, ascii armor the output; otherwise, the
output will be in binary format. (Default: True) output will be in binary format. (Default: True)
:param bool encrypt: If True, encrypt the ``data`` using the :param bool encrypt: If True, encrypt the **data** using the
``recipients`` public keys. (Default: True) **recipients** public keys. (Default: True)
:param bool symmetric: If True, encrypt the ``data`` to ``recipients`` :param bool symmetric: If True, encrypt the **data** to **recipients**
using a symmetric key. See the ``passphrase`` parameter. Symmetric using a symmetric key. See the **passphrase**
encryption and public key encryption can be used simultaneously, parameter. Symmetric encryption and public key
and will result in a ciphertext which is decryptable with either encryption can be used simultaneously, and will
the symmetric ``passphrase`` or one of the corresponding private result in a ciphertext which is decryptable
keys. with either the symmetric **passphrase** or one
of the corresponding private keys.
:param bool always_trust: If True, ignore trust warnings on recipient :param bool always_trust: If True, ignore trust warnings on
keys. If False, display trust warnings. (default: True) **recipients** keys. If False, display trust
warnings. (default: True)
:param str output: The output file to write to. If not specified, the :param str output: The output file to write to. If not specified, the
encrypted output is returned, and thus should be stored as an encrypted output is returned, and thus should be
object in Python. For example: stored as an object in Python. For example:
>>> import shutil >>> import shutil
>>> import gnupg >>> import gnupg
@ -743,16 +783,19 @@ class GPGBase(object):
:param str cipher_algo: The cipher algorithm to use. To see available :param str cipher_algo: The cipher algorithm to use. To see available
algorithms with your version of GnuPG, do: algorithms with your version of GnuPG, do:
``$ gpg --with-colons --list-config ciphername``. :command:`$ gpg --with-colons --list-config
The default ``cipher_algo``, if unspecified, is ``'AES256'``. ciphername`. The default **cipher_algo**, if
unspecified, is ``'AES256'``.
:param str digest_algo: The hash digest to use. Again, to see which :param str digest_algo: The hash digest to use. Again, to see which
hashes your GnuPG is capable of using, do: hashes your GnuPG is capable of using, do:
``$ gpg --with-colons --list-config digestname``. :command:`$ gpg --with-colons --list-config
The default, if unspecified, is ``'SHA512'``. digestname`. The default, if unspecified, is
``'SHA512'``.
:param str compress_algo: The compression algorithm to use. Can be one :param str compress_algo: The compression algorithm to use. Can be one
of ``'ZLIB'``, ``'BZIP2'``, ``'ZIP'``, or ``'Uncompressed'``. of ``'ZLIB'``, ``'BZIP2'``, ``'ZIP'``, or
``'Uncompressed'``.
""" """
args = [] args = []

View File

@ -17,15 +17,18 @@
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details. # FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
'''parsers.py '''Classes for parsing GnuPG status messages and sanitising commandline
------------- options.
Classes for parsing GnuPG status messages and sanitising commandline options.
''' '''
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import print_function from __future__ import print_function
import collections try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
import re import re
from . import _util from . import _util
@ -50,8 +53,8 @@ def _check_keyserver(location):
:param str location: A string containing the default keyserver. This :param str location: A string containing the default keyserver. This
should contain the desired keyserver protocol which should contain the desired keyserver protocol which
is supported by the keyserver, for example, the is supported by the keyserver, for example, the
default is ``'hkp://subkeys.pgp.net'``. default is ``'hkp://wwwkeys .pgp.net'``.
:rtype: str or None :rtype: :obj:`str` or :obj:`None`
:returns: A string specifying the protocol and keyserver hostname, if the :returns: A string specifying the protocol and keyserver hostname, if the
checks passed. If not, returns None. checks passed. If not, returns None.
""" """
@ -74,10 +77,11 @@ def _check_keyserver(location):
def _check_preferences(prefs, pref_type=None): def _check_preferences(prefs, pref_type=None):
"""Check cipher, digest, and compression preference settings. """Check cipher, digest, and compression preference settings.
MD5 is not allowed. This is not 1994.[0] SHA1 is allowed grudgingly.[1] MD5 is not allowed. This is `not 1994`__. SHA1 is allowed_ grudgingly_.
[0]: http://www.cs.colorado.edu/~jrblack/papers/md5e-full.pdf __ http://www.cs.colorado.edu/~jrblack/papers/md5e-full.pdf
[1]: http://eprint.iacr.org/2008/469.pdf .. _allowed: http://eprint.iacr.org/2008/469.pdf
.. _grudgingly: https://www.schneier.com/blog/archives/2012/10/when_will_we_se.html
""" """
if prefs is None: return if prefs is None: return
@ -115,8 +119,8 @@ def _check_preferences(prefs, pref_type=None):
return allowed return allowed
def _fix_unsafe(shell_input): def _fix_unsafe(shell_input):
"""Find characters used to escape from a string into a shell, and wrap them """Find characters used to escape from a string into a shell, and wrap them in
in quotes if they exist. Regex pilfered from python-3.x shlex module. quotes if they exist. Regex pilfered from Python3 :mod:`shlex` module.
:param str shell_input: The input intended for the GnuPG process. :param str shell_input: The input intended for the GnuPG process.
""" """
@ -161,11 +165,15 @@ def _is_allowed(input):
class and its name will need to be added to this class and its name will need to be added to this
set. set.
:rtype: Exception or str :raises: :exc:`UsageError` if **input** is not a subset of the hard-coded
:raise: :exc:UsageError if ``_allowed`` is not a subset of ``_possible``. set of all GnuPG options in :func:`_get_all_gnupg_options`.
ProtectedOption if ``input`` is not in the set ``_allowed``.
:return: The original parameter ``input``, unmodified and unsanitized, :exc:`ProtectedOption` if **input** is not in the set of allowed
if no errors occur. options.
:rtype: str
:return: The original **input** parameter, unmodified and unsanitized, if
no errors occur.
""" """
gnupg_options = _get_all_gnupg_options() gnupg_options = _get_all_gnupg_options()
allowed = _get_options_group("allowed") allowed = _get_options_group("allowed")
@ -220,12 +228,12 @@ def _is_hex(string):
def _is_string(thing): def _is_string(thing):
"""Python character arrays are a mess. """Python character arrays are a mess.
If Python2, check if ``thing`` is a ``unicode()`` or ``str()``. If Python2, check if **thing** is an :obj:`unicode` or a :obj:`str`.
If Python3, check if ``thing`` is a ``str()``. If Python3, check if **thing** is a :obj:`str`.
:param thing: The thing to check. :param thing: The thing to check.
:returns: ``True`` if ``thing`` is a "string" according to whichever :returns: ``True`` if **thing** is a string according to whichever version
version of Python we're running in. of Python we're running in.
""" """
if _util._py3k: return isinstance(thing, str) if _util._py3k: return isinstance(thing, str)
else: return isinstance(thing, basestring) else: return isinstance(thing, basestring)
@ -238,18 +246,20 @@ def _sanitise(*args):
sanitised, allowed options. sanitised, allowed options.
Each new option that we support that is not a boolean, but instead has Each new option that we support that is not a boolean, but instead has
some extra inputs, i.e. "--encrypt-file foo.txt", will need some basic some additional inputs following it, i.e. "--encrypt-file foo.txt", will
safety checks added here. need some basic safety checks added here.
GnuPG has three-hundred and eighteen commandline flags. Also, not all GnuPG has three-hundred and eighteen commandline flags. Also, not all
implementations of OpenPGP parse PGP packets and headers in the same way, implementations of OpenPGP parse PGP packets and headers in the same way,
so there is added potential there for messing with calls to GPG. so there is added potential there for messing with calls to GPG.
For information on the PGP message format specification, see: For information on the PGP message format specification, see
https://www.ietf.org/rfc/rfc1991.txt :rfc:`1991`.
If you're asking, "Is this *really* necessary?": No, not really -- we could If you're asking, "Is this *really* necessary?": No, not really -- we could
just do a check as described here: https://xkcd.com/1181/ just follow the security precautions recommended by `this xkcd`__.
__ https://xkcd.com/1181/
:param str args: (optional) The boolean arguments which will be passed to :param str args: (optional) The boolean arguments which will be passed to
the GnuPG process. the GnuPG process.
@ -260,22 +270,23 @@ def _sanitise(*args):
## see TODO file, tag :cleanup:sanitise: ## see TODO file, tag :cleanup:sanitise:
def _check_option(arg, value): def _check_option(arg, value):
""" """Check that a single ``arg`` is an allowed option.
Check that a single :param:arg is an allowed option. If it is allowed,
quote out any escape characters in :param:values, and add the pair to If it is allowed, quote out any escape characters in ``value``, and
:ivar:sanitised. add the pair to :ivar:`sanitised`. Otherwise, drop them.
:param str arg: The arguments which will be passed to the GnuPG :param str arg: The arguments which will be passed to the GnuPG
process, and, optionally their corresponding values. process, and, optionally their corresponding values.
The values are any additional arguments following the The values are any additional arguments following the
GnuPG option or flag. For example, if we wanted to pass GnuPG option or flag. For example, if we wanted to
"--encrypt --recipient isis@leap.se" to gpg, then pass ``"--encrypt --recipient isis@leap.se"`` to
"--encrypt" would be an arg without a value, and GnuPG, then ``"--encrypt"`` would be an arg without a
"--recipient" would also be an arg, with a value of value, and ``"--recipient"`` would also be an arg,
"isis@leap.se". with a value of ``"isis@leap.se"``.
:ivar list checked: The sanitised, allowed options and values. :ivar list checked: The sanitised, allowed options and values.
:rtype: str :rtype: str
:returns: A string of the items in ``checked`` delimited by spaces. :returns: A string of the items in ``checked``, delimited by spaces.
""" """
checked = str() checked = str()
none_options = _get_options_group("none_options") none_options = _get_options_group("none_options")
@ -290,7 +301,7 @@ def _sanitise(*args):
flag = _is_allowed(arg) flag = _is_allowed(arg)
assert flag is not None, "_check_option(): got None for flag" assert flag is not None, "_check_option(): got None for flag"
except (AssertionError, ProtectedOption) as error: except (AssertionError, ProtectedOption) as error:
log.warn("_check_option(): %s" % error.message) log.warn("_check_option(): %s" % str(error))
else: else:
checked += (flag + ' ') checked += (flag + ' ')
@ -298,7 +309,7 @@ def _sanitise(*args):
values = value.split(' ') values = value.split(' ')
for v in values: for v in values:
## these can be handled separately, without _fix_unsafe(), ## these can be handled separately, without _fix_unsafe(),
## because they are only allowed if the pass the regex ## because they are only allowed if they pass the regex
if (flag in none_options) and (v is None): if (flag in none_options) and (v is None):
continue continue
@ -332,8 +343,12 @@ def _sanitise(*args):
if flag in ['--encrypt', '--encrypt-files', '--decrypt', if flag in ['--encrypt', '--encrypt-files', '--decrypt',
'--decrypt-files', '--import', '--verify']: '--decrypt-files', '--import', '--verify']:
if _util._is_file(val): checked += (val + " ") if ( (_util._is_file(val))
else: log.debug("%s not file: %s" % (flag, val)) or
((flag == '--verify') and (val == '-')) ):
checked += (val + " ")
else:
log.debug("%s not file: %s" % (flag, val))
elif flag in ['--cipher-algo', '--personal-cipher-prefs', elif flag in ['--cipher-algo', '--personal-cipher-prefs',
'--personal-cipher-preferences']: '--personal-cipher-preferences']:
@ -372,7 +387,8 @@ def _sanitise(*args):
groups[last] = str(filo.pop()) groups[last] = str(filo.pop())
## accept the read-from-stdin arg: ## accept the read-from-stdin arg:
if len(filo) >= 1 and filo[len(filo)-1] == '-': if len(filo) >= 1 and filo[len(filo)-1] == '-':
groups[last] += str(' - \'\'') ## gross hack groups[last] += str(' - ') ## gross hack
filo.pop()
else: else:
groups[last] = str() groups[last] = str()
while len(filo) > 1 and not is_flag(filo[len(filo)-1]): while len(filo) > 1 and not is_flag(filo[len(filo)-1]):
@ -570,21 +586,22 @@ def _get_all_gnupg_options():
This is hardcoded within a local scope to reduce the chance of a tampered This is hardcoded within a local scope to reduce the chance of a tampered
GnuPG binary reporting falsified option sets, i.e. because certain options GnuPG binary reporting falsified option sets, i.e. because certain options
(namedly the '--no-options' option, which prevents the usage of gpg.conf (namedly the ``--no-options`` option, which prevents the usage of gpg.conf
files) are necessary and statically specified in files) are necessary and statically specified in
:meth:`gnupg.GPG._makeargs`, if the inputs into Python are already :meth:`gnupg._meta.GPGBase._make_args`, if the inputs into Python are
controlled, and we were to summon the GnuPG binary to ask it for its already controlled, and we were to summon the GnuPG binary to ask it for
options, it would be possible to receive a falsified options set missing its options, it would be possible to receive a falsified options set
the '--no-options' option in response. This seems unlikely, and the method missing the ``--no-options`` option in response. This seems unlikely, and
is stupid and ugly, but at least we'll never have to debug whether or not the method is stupid and ugly, but at least we'll never have to debug
an option *actually* disappeared in a different GnuPG version, or some whether or not an option *actually* disappeared in a different GnuPG
funny business is happening. version, or some funny business is happening.
These are the options as of GnuPG 1.4.12; the current stable branch of the These are the options as of GnuPG 1.4.12; the current stable branch of the
2.1.x tree contains a few more -- if you need them you'll have to add them 2.1.x tree contains a few more -- if you need them you'll have to add them
in here. in here.
:ivar frozenset gnupg_options: All known GPG options and flags. :type gnupg_options: frozenset
:ivar gnupg_options: All known GPG options and flags.
:rtype: frozenset :rtype: frozenset
:returns: ``gnupg_options`` :returns: ``gnupg_options``
""" """
@ -787,8 +804,8 @@ def progress(status_code):
class GenKey(object): class GenKey(object):
"""Handle status messages for key generation. """Handle status messages for key generation.
Calling the GenKey.__str__() method of this class will return the Calling the ``__str__()`` method of this class will return the generated
generated key's fingerprint, or a status string explaining the results. key's fingerprint, or a status string explaining the results.
""" """
def __init__(self, gpg): def __init__(self, gpg):
self._gpg = gpg self._gpg = gpg
@ -799,11 +816,13 @@ class GenKey(object):
self.status = None self.status = None
self.subkey_created = False self.subkey_created = False
self.primary_created = False self.primary_created = False
#: This will store the filename of the key's public keyring if #: This will store the key's public keyring filename, if
#: :meth:`GPG.gen_key_input` was called with ``separate_keyring=True`` #: :meth:`~gnupg.GPG.gen_key_input` was called with
#: ``separate_keyring=True``.
self.keyring = None self.keyring = None
#: This will store the filename of the key's secret keyring if #: This will store the key's secret keyring filename, if :
#: :meth:`GPG.gen_key_input` was called with ``separate_keyring=True`` #: :meth:`~gnupg.GPG.gen_key_input` was called with
#: ``separate_keyring=True``.
self.secring = None self.secring = None
def __nonzero__(self): def __nonzero__(self):
@ -823,7 +842,7 @@ class GenKey(object):
def _handle_status(self, key, value): def _handle_status(self, key, value):
"""Parse a status code from the attached GnuPG process. """Parse a status code from the attached GnuPG process.
:raises: :exc:`ValueError` if the status message is unknown. :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
""" """
if key in ("GOOD_PASSPHRASE"): if key in ("GOOD_PASSPHRASE"):
pass pass
@ -860,7 +879,7 @@ class DeleteResult(object):
def _handle_status(self, key, value): def _handle_status(self, key, value):
"""Parse a status code from the attached GnuPG process. """Parse a status code from the attached GnuPG process.
:raises: :exc:`ValueError` if the status message is unknown. :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
""" """
if key == "DELETE_PROBLEM": if key == "DELETE_PROBLEM":
self.status = self.problem_reason.get(value, "Unknown error: %r" self.status = self.problem_reason.get(value, "Unknown error: %r"
@ -905,7 +924,7 @@ class Sign(object):
def _handle_status(self, key, value): def _handle_status(self, key, value):
"""Parse a status code from the attached GnuPG process. """Parse a status code from the attached GnuPG process.
:raises: :exc:`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", "BEGIN_SIGNING", "CARDCTRL",
@ -1008,7 +1027,7 @@ class ImportResult(object):
_fields = '''count no_user_id imported imported_rsa unchanged _fields = '''count no_user_id imported imported_rsa unchanged
n_uids n_subk n_sigs n_revoc sec_read sec_imported sec_dups n_uids n_subk n_sigs n_revoc sec_read sec_imported sec_dups
not_imported'''.split() not_imported'''.split()
_counts = collections.OrderedDict( _counts = OrderedDict(
zip(_fields, [int(0) for x in range(len(_fields))]) ) zip(_fields, [int(0) for x in range(len(_fields))]) )
#: A list of strings containing the fingerprints of the GnuPG keyIDs #: A list of strings containing the fingerprints of the GnuPG keyIDs
@ -1037,7 +1056,7 @@ class ImportResult(object):
def _handle_status(self, key, value): def _handle_status(self, key, value):
"""Parse a status code from the attached GnuPG process. """Parse a status code from the attached GnuPG process.
:raises: :exc:`ValueError` if the status message is unknown. :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
""" """
if key == "IMPORTED": if key == "IMPORTED":
# this duplicates info we already see in import_ok & import_problem # this duplicates info we already see in import_ok & import_problem
@ -1183,7 +1202,7 @@ class Verify(object):
def _handle_status(self, key, value): def _handle_status(self, key, value):
"""Parse a status code from the attached GnuPG process. """Parse a status code from the attached GnuPG process.
:raises: :exc:`ValueError` if the status message is unknown. :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
""" """
if key in self.TRUST_LEVELS: if key in self.TRUST_LEVELS:
self.trust_text = key self.trust_text = key
@ -1278,7 +1297,7 @@ class Crypt(Verify):
def _handle_status(self, key, value): def _handle_status(self, key, value):
"""Parse a status code from the attached GnuPG process. """Parse a status code from the attached GnuPG process.
:raises: :exc:`ValueError` if the status message is unknown. :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
""" """
if key in ("ENC_TO", "USERID_HINT", "GOODMDC", "END_DECRYPTION", if key in ("ENC_TO", "USERID_HINT", "GOODMDC", "END_DECRYPTION",
"BEGIN_SIGNING", "NO_SECKEY", "ERROR", "NODATA", "BEGIN_SIGNING", "NO_SECKEY", "ERROR", "NODATA",
@ -1345,7 +1364,7 @@ class ListPackets(object):
def _handle_status(self, key, value): def _handle_status(self, key, value):
"""Parse a status code from the attached GnuPG process. """Parse a status code from the attached GnuPG process.
:raises: :exc:`ValueError` if the status message is unknown. :raises: :exc:`~exceptions.ValueError` if the status message is unknown.
""" """
if key == 'NODATA': if key == 'NODATA':
self.status = nodata(value) self.status = nodata(value)

View File

@ -17,9 +17,7 @@
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details. # FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
'''trust.py '''Functions for handling trustdb and trust calculations.
-----------
Functions for handling trustdb and trust calculations.
The functions within this module take an instance of :class:`gnupg.GPGBase` or The functions within this module take an instance of :class:`gnupg.GPGBase` or
a suitable subclass as their first argument. a suitable subclass as their first argument.
@ -44,11 +42,12 @@ def _create_trustdb(cls):
def export_ownertrust(cls, trustdb=None): def export_ownertrust(cls, trustdb=None):
"""Export ownertrust to a trustdb file. """Export ownertrust to a trustdb file.
If there is already a file named 'trustdb.gpg' in the current GnuPG If there is already a file named :file:`trustdb.gpg` in the current GnuPG
homedir, it will be renamed to 'trustdb.gpg.bak'. homedir, it will be renamed to :file:`trustdb.gpg.bak`.
:param string trustdb: The path to the trustdb.gpg file. If not given, :param string trustdb: The path to the trustdb.gpg file. If not given,
defaults to 'trustdb.gpg' in the current GnuPG homedir. defaults to ``'trustdb.gpg'`` in the current GnuPG
homedir.
""" """
if trustdb is None: if trustdb is None:
trustdb = os.path.join(cls.homedir, 'trustdb.gpg') trustdb = os.path.join(cls.homedir, 'trustdb.gpg')
@ -56,7 +55,7 @@ def export_ownertrust(cls, trustdb=None):
try: try:
os.rename(trustdb, trustdb + '.bak') os.rename(trustdb, trustdb + '.bak')
except (OSError, IOError) as err: except (OSError, IOError) as err:
log.debug(err.message) log.debug(str(err))
export_proc = cls._open_subprocess('--export-ownertrust') export_proc = cls._open_subprocess('--export-ownertrust')
tdb = open(trustdb, 'wb') tdb = open(trustdb, 'wb')
@ -65,8 +64,9 @@ def export_ownertrust(cls, trustdb=None):
def import_ownertrust(self, trustdb=None): def import_ownertrust(self, trustdb=None):
"""Import ownertrust from a trustdb file. """Import ownertrust from a trustdb file.
:param string trustdb: The path to the trustdb.gpg file. If not given, :param str trustdb: The path to the trustdb.gpg file. If not given,
defaults to 'trustdb.gpg' in the current GnuPG homedir. defaults to :file:`trustdb.gpg` in the current GnuPG
homedir.
""" """
if trustdb is None: if trustdb is None:
trustdb = os.path.join(cls.homedir, 'trustdb.gpg') trustdb = os.path.join(cls.homedir, 'trustdb.gpg')
@ -78,11 +78,11 @@ def import_ownertrust(self, trustdb=None):
def fix_trustdb(cls, trustdb=None): def fix_trustdb(cls, trustdb=None):
"""Attempt to repair a broken trustdb.gpg file. """Attempt to repair a broken trustdb.gpg file.
GnuPG>=2.0.x has this magical-seeming flag: '--fix-trustdb'. You'd think GnuPG>=2.0.x has this magical-seeming flag: `--fix-trustdb`. You'd think
it would fix the the trustdb. Hah! It doesn't. Here's what it does it would fix the the trustdb. Hah! It doesn't. Here's what it does
instead: instead::
(python-gnupg)!isiswintermute:(testing/digest-algo *$=)~/code/python-gnupg gpg2 --fix-trustdb (gpg)~/code/python-gnupg $ gpg2 --fix-trustdb
gpg: You may try to re-create the trustdb using the commands: gpg: You may try to re-create the trustdb using the commands:
gpg: cd ~/.gnupg gpg: cd ~/.gnupg
gpg: gpg2 --export-ownertrust > otrust.tmp gpg: gpg2 --export-ownertrust > otrust.tmp
@ -92,8 +92,9 @@ def fix_trustdb(cls, trustdb=None):
Brilliant piece of software engineering right there. Brilliant piece of software engineering right there.
:param string trustdb: The path to the trustdb.gpg file. If not given, :param str trustdb: The path to the trustdb.gpg file. If not given,
defaults to 'trustdb.gpg' in the current GnuPG homedir. defaults to :file:`trustdb.gpg` in the current GnuPG
homedir.
""" """
if trustdb is None: if trustdb is None:
trustdb = os.path.join(cls.homedir, 'trustdb.gpg') trustdb = os.path.join(cls.homedir, 'trustdb.gpg')

View File

@ -17,10 +17,7 @@
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details. # FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details.
'''util.py '''Extra utilities for python-gnupg.'''
----------
Extra utilities for python-gnupg.
'''
from __future__ import absolute_import from __future__ import absolute_import
from datetime import datetime from datetime import datetime
@ -31,6 +28,7 @@ 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
@ -150,7 +148,7 @@ def _copy_data(instream, outstream):
break 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 ioe.message: if 'Broken pipe' in str(ioe):
log.error('Error sending data: Broken pipe') log.error('Error sending data: Broken pipe')
else: else:
log.exception(ioe) log.exception(ioe)
@ -254,7 +252,8 @@ def _find_binary(binary=None):
our process real uid has exec permissions. our process real uid has exec permissions.
:param str binary: The path to the GnuPG binary. :param str binary: The path to the GnuPG binary.
:raises: :exc:RuntimeError if it appears that GnuPG is not installed. :raises: :exc:`~exceptions.RuntimeError` if it appears that GnuPG is not
installed.
:rtype: str :rtype: str
:returns: The absolute path to the GnuPG binary to use, if no exceptions :returns: The absolute path to the GnuPG binary to use, if no exceptions
occur. occur.
@ -272,6 +271,8 @@ def _find_binary(binary=None):
except IndexError as ie: except IndexError as ie:
log.info("Could not determine absolute path of binary: '%s'" log.info("Could not determine absolute path of binary: '%s'"
% binary) % binary)
elif os.access(binary, os.X_OK):
found = binary
if found is None: if found is None:
try: found = _which('gpg')[0] try: found = _which('gpg')[0]
except IndexError as ie: except IndexError as ie:
@ -287,7 +288,7 @@ def _find_binary(binary=None):
assert not os.path.islink(found), "Path to gpg binary is symlink" assert not os.path.islink(found), "Path to gpg binary is symlink"
assert os.access(found, os.X_OK), "Lacking +x perms for gpg binary" assert os.access(found, os.X_OK), "Lacking +x perms for gpg binary"
except (AssertionError, AttributeError) as ae: except (AssertionError, AttributeError) as ae:
log.error(ae.message) log.error(str(ae))
else: else:
return found return found
@ -303,22 +304,31 @@ def _has_readwrite(path):
""" """
return os.access(path, os.R_OK ^ os.W_OK) return os.access(path, os.R_OK ^ os.W_OK)
def _is_file(input): def _is_file(filename):
"""Check that the size of the thing which is supposed to be a filename has """Check that the size of the thing which is supposed to be a filename has
size greater than zero, without following symbolic links or using size greater than zero, without following symbolic links or using
:func:os.path.isfile. :func:os.path.isfile.
:param input: An object to check. :param filename: An object to check.
:rtype: bool :rtype: bool
:returns: True if :param:input is file-like, False otherwise. :returns: True if **filename** is file-like, False otherwise.
""" """
try: try:
assert os.lstat(input).st_size > 0, "not a file: %s" % input statinfo = os.lstat(filename)
except (AssertionError, TypeError, IOError, OSError) as err: log.debug("lstat(%r) with type=%s gave us %r"
log.error(err.message, exc_info=1) % (repr(filename), type(filename), repr(statinfo)))
return False if not (statinfo.st_size > 0):
raise ValueError("'%s' appears to be an empty file!" % filename)
except OSError as oserr:
log.error(oserr)
if filename == '-':
log.debug("Got '-' for filename, assuming sys.stdin...")
return True
except (ValueError, TypeError, IOError) as err:
log.error(err)
else: else:
return True return True
return False
def _is_stream(input): def _is_stream(input):
"""Check that the input is a byte stream. """Check that the input is a byte stream.
@ -395,7 +405,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 = os.getresuid() ruid, euid, suid = psutil.Process(os.getpid()).uids
gid = os.getgid() gid = os.getgid()
now = mktime(localtime()) now = mktime(localtime())
@ -518,7 +528,7 @@ def _which(executable, flags=os.X_OK):
def _write_passphrase(stream, passphrase, encoding): def _write_passphrase(stream, passphrase, encoding):
"""Write the passphrase from memory to the GnuPG process' stdin. """Write the passphrase from memory to the GnuPG process' stdin.
:type stream: file, :class:BytesIO, or :class:StringIO :type stream: file, :class:`~io.BytesIO`, or :class:`~io.StringIO`
:param stream: The input file descriptor to write the password to. :param stream: The input file descriptor to write the password to.
:param str passphrase: The passphrase for the secret key material. :param str passphrase: The passphrase for the secret key material.
:param str encoding: The data encoding expected by GnuPG. Usually, this :param str encoding: The data encoding expected by GnuPG. Usually, this
@ -565,6 +575,7 @@ class InheritableProperty(object):
else: else:
getattr(obj, self.fdel.__name__)() getattr(obj, self.fdel.__name__)()
class Storage(dict): class Storage(dict):
"""A dictionary where keys are stored as class attributes. """A dictionary where keys are stored as class attributes.
@ -595,7 +606,7 @@ class Storage(dict):
try: try:
del self[key] del self[key]
except KeyError as k: except KeyError as k:
raise AttributeError(k.message) raise AttributeError(k.args[0])
def __repr__(self): def __repr__(self):
return '<Storage ' + dict.__repr__(self) + '>' return '<Storage ' + dict.__repr__(self) + '>'

View File

@ -21,10 +21,10 @@
=========== ===========
A Python interface to GnuPG. A Python interface to GnuPG.
.. moduleauthor:: Isis Agora Lovecruft <isis@patternsinthevoid.net> .. moduleauthor:: Isis Lovecruft <isis@patternsinthevoid.net>
see also :attr:`gnupg.__authors__` see also :attr:`gnupg.__authors__`
:license: see :attr:`gnupg.__license__` .. license:: see :attr:`gnupg.__license__`
:info: see <https://www.github.com/isislovecruft/python-gnupg> .. info:: https://github.com/isislovecruft/python-gnupg
""" """
from __future__ import absolute_import from __future__ import absolute_import
@ -41,7 +41,7 @@ try:
except ImportError: except ImportError:
from cStringIO import StringIO from cStringIO import StringIO
## see PEP-328 http://docs.python.org/2.5/whatsnew/pep-328.html #: see :pep:`328` http://docs.python.org/2.5/whatsnew/pep-328.html
from . import _parsers from . import _parsers
from . import _util from . import _util
from . import _trust from . import _trust
@ -70,35 +70,36 @@ class GPG(GPGBase):
"""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
path is not given, the evironment variable $PATH is path is not given, the environment variable
searched for the executable and checked that the ``$PATH`` is searched for the executable and
real uid/gid of the user has sufficient permissions. checked that the real uid/gid of the user has
sufficient permissions.
:param str homedir: Full pathname to directory containing the public :param str homedir: Full pathname to directory containing the public
and private keyrings. Default is whatever GnuPG and private keyrings. Default is whatever GnuPG
defaults to. defaults to.
:param str,int,bool verbose: String or numeric value to pass to gpg's :type verbose: :obj:`str` or :obj:`int` or :obj:`bool`
``--debug-level`` option. See the gpg man :param verbose: String or numeric value to pass to GnuPG's
page for the list of valid options. If ``--debug-level`` option. See the GnuPG man page for
False, debug output is not generated by the list of valid options. If False, debug output is
the gpg binary. If True, defaults to not generated by the GnuPG binary. If True, defaults
``--debug-level basic.`` to ``--debug-level basic.``
:param str keyring: Name of keyring file containing public key data, if :param str keyring: Name of keyring file containing public key data.
unspecified, defaults to 'pubring.gpg' in the If unspecified, defaults to :file:`pubring.gpg` in
``homedir`` directory. the **homedir** directory.
:param str secring: Name of alternative secret keyring file to use. If :param str secring: Name of alternative secret keyring file to use. If
left unspecified, this will default to using left unspecified, this will default to using
'secring.gpg' in the :param:homedir directory, and :file:`secring.gpg` in the **homedir** directory,
create that file if it does not exist. and create that file if it does not exist.
:param list options: A list of additional options to pass to the GPG :param list options: A list of additional options to pass to the GnuPG
binary. binary.
:raises: :exc:`RuntimeError` with explanation message if there is a :raises: A :exc:`~exceptions.RuntimeError` with explanation message
problem invoking gpg. if there is a problem invoking GnuPG.
Example: Example:
@ -227,7 +228,7 @@ class GPG(GPGBase):
In simpler terms: this function isn't for signing your friends' keys, In simpler terms: this function isn't for signing your friends' keys,
it's for something like signing an email. it's for something like signing an email.
:type data: str or file :type data: :obj:`str` or :obj:`file`
:param data: A string or file stream to sign. :param data: A string or file stream to sign.
:param str default_key: The key to sign with. :param str default_key: The key to sign with.
:param str passphrase: The passphrase to pipe to stdin. :param str passphrase: The passphrase to pipe to stdin.
@ -236,10 +237,10 @@ class GPG(GPGBase):
:param bool binary: If True, do not ascii armour the output. :param bool binary: If True, do not ascii armour the output.
:param str digest_algo: The hash digest to use. Again, to see which :param str digest_algo: The hash digest to use. Again, to see which
hashes your GnuPG is capable of using, do: hashes your GnuPG is capable of using, do:
``$ gpg --with-colons --list-config digestname``. :command:`$ gpg --with-colons --list-config digestname`.
The default, if unspecified, is ``'SHA512'``. The default, if unspecified, is ``'SHA512'``.
""" """
if 'default_key' in kwargs.items(): if 'default_key' in kwargs:
log.info("Signing message '%r' with keyid: %s" log.info("Signing message '%r' with keyid: %s"
% (data, kwargs['default_key'])) % (data, kwargs['default_key']))
else: else:
@ -306,15 +307,19 @@ class GPG(GPGBase):
return result return result
log.debug('verify_file(): Handling detached verification') log.debug('verify_file(): Handling detached verification')
sig_fh = None sig_fh = None
data_fh = None
try: try:
sig_fh = open(sig_file) sig_fh = open(sig_file, 'rb')
args = ["--verify %s - " % sig_fh.name] data_fh = open(file, 'rb')
args = ["--verify %s -" % sig_fh.name]
proc = self._open_subprocess(args) proc = self._open_subprocess(args)
writer = _util._threaded_copy_data(file, proc.stdin) writer = _util._threaded_copy_data(data_fh, proc.stdin)
self._collect_output(proc, result, stdin=proc.stdin) self._collect_output(proc, result, writer, stdin=proc.stdin)
finally: finally:
if sig_fh and not sig_fh.closed: if sig_fh and not sig_fh.closed:
sig_fh.close() sig_fh.close()
if data_fh and not data_fh.closed:
data_fh.close()
return result return result
def import_keys(self, key_data): def import_keys(self, key_data):
@ -373,7 +378,7 @@ class GPG(GPGBase):
:param str keyids: Each ``keyids`` argument should be a string :param str keyids: Each ``keyids`` argument should be a string
containing a keyid to request. containing a keyid to request.
:param str keyserver: The keyserver to request the ``keyids`` from; :param str keyserver: The keyserver to request the ``keyids`` from;
defaults to :property:`gnupg.GPG.keyserver`. defaults to `gnupg.GPG.keyserver`.
""" """
if keyids: if keyids:
keys = ' '.join([key for key in keyids]) keys = ' '.join([key for key in keyids])
@ -384,33 +389,33 @@ class GPG(GPGBase):
def delete_keys(self, fingerprints, secret=False, subkeys=False): def delete_keys(self, fingerprints, secret=False, subkeys=False):
"""Delete a key, or list of keys, from the current keyring. """Delete a key, or list of keys, from the current keyring.
The keys must be refered to by their full fingerprint for GnuPG to The keys must be referred to by their full fingerprints for GnuPG to
delete them. If ``secret=True``, the corresponding secret keyring will delete them. If ``secret=True``, the corresponding secret keyring will
be deleted from :attr:`GPG.secring`. be deleted from :obj:`.secring`.
:type fingerprints: str or list or tuple
:type fingerprints: :obj:`str` or :obj:`list` or :obj:`tuple`
:param fingerprints: A string, or a list/tuple of strings, :param fingerprints: A string, or a list/tuple of strings,
representing the fingerprint(s) for the key(s) to delete. representing the fingerprint(s) for the key(s)
to delete.
:param bool secret: If True, delete the corresponding secret key(s) :param bool secret: If True, delete the corresponding secret key(s)
also. (default: False) also. (default: False)
:param bool subkeys: If True, delete the secret subkey first, then the :param bool subkeys: If True, delete the secret subkey first, then the
public key. (default: False) Same as: public key. (default: False) Same as:
``$ gpg --delete-secret-and-public-key 0x12345678`` :command:`$gpg --delete-secret-and-public-key 0x12345678`.
""" """
which='keys' which = 'keys'
if secret: if secret:
which='secret-keys' which = 'secret-keys'
if subkeys: if subkeys:
which='secret-and-public-keys' which = 'secret-and-public-keys'
if _is_list_or_tuple(fingerprints): if _is_list_or_tuple(fingerprints):
fingerprints = ' '.join(fingerprints) fingerprints = ' '.join(fingerprints)
args = ['--batch'] args = ['--batch']
args.append("--delete-{} {}".format(which, fingerprints)) args.append("--delete-{0} {1}".format(which, fingerprints))
result = self._result_map['delete'](self) result = self._result_map['delete'](self)
p = self._open_subprocess(args) p = self._open_subprocess(args)
@ -425,17 +430,17 @@ class GPG(GPGBase):
:param bool secret: If True, export only the secret key. :param bool secret: If True, export only the secret key.
:param bool subkeys: If True, export the secret subkeys. :param bool subkeys: If True, export the secret subkeys.
""" """
which='' which = ''
if subkeys: if subkeys:
which='-secret-subkeys' which = '-secret-subkeys'
elif secret: elif secret:
which='-secret-keys' which = '-secret-keys'
if _is_list_or_tuple(keyids): if _is_list_or_tuple(keyids):
keyids = ' '.join(['%s' % k for k in keyids]) keyids = ' '.join(['%s' % k for k in keyids])
args = ["--armor"] args = ["--armor"]
args.append("--export{} {}".format(which, keyids)) args.append("--export{0} {1}".format(which, keyids))
p = self._open_subprocess(args) p = self._open_subprocess(args)
## gpg --export produces no status-fd output; stdout will be empty in ## gpg --export produces no status-fd output; stdout will be empty in
@ -467,9 +472,9 @@ class GPG(GPGBase):
>>> assert print2 in pubkeys.fingerprints >>> assert print2 in pubkeys.fingerprints
""" """
which='public-keys' which = 'public-keys'
if secret: if secret:
which='secret-keys' which = 'secret-keys'
args = "--list-%s --fixed-list-mode --fingerprint " % (which,) args = "--list-%s --fixed-list-mode --fingerprint " % (which,)
args += "--with-colons --list-options no-show-photos" args += "--with-colons --list-options no-show-photos"
args = [args] args = [args]
@ -547,7 +552,7 @@ class GPG(GPGBase):
:param dict input: A dictionary of parameters and values for the new :param dict input: A dictionary of parameters and values for the new
key. key.
:returns: The result mapping with details of the new key, which is a :returns: The result mapping with details of the new key, which is a
:class:`parsers.GenKey <GenKey>` object. :class:`GenKey <gnupg._parsers.GenKey>` object.
""" """
args = ["--gen-key --batch"] args = ["--gen-key --batch"]
key = self._result_map['generate'](self) key = self._result_map['generate'](self)
@ -557,20 +562,22 @@ class GPG(GPGBase):
fpr = str(key.fingerprint) fpr = str(key.fingerprint)
if len(fpr) == 20: if len(fpr) == 20:
if self.temp_keyring or self.temp_secring: for d in map(lambda x: os.path.dirname(x),
if not os.path.exists(self._keys_dir): [self.temp_keyring, self.temp_secring]):
os.makedirs(self._keys_dir) if not os.path.exists(d):
prefix = os.path.join(self._keys_dir, fpr) os.makedirs(d)
if self.temp_keyring: if self.temp_keyring:
if os.path.isfile(self.temp_keyring): if os.path.isfile(self.temp_keyring):
prefix = os.path.join(self.temp_keyring, fpr)
try: os.rename(self.temp_keyring, prefix+".pubring") try: os.rename(self.temp_keyring, prefix+".pubring")
except OSError as ose: log.error(ose.message) except OSError as ose: log.error(str(ose))
if self.temp_secring: if self.temp_secring:
if os.path.isfile(self.temp_secring): if os.path.isfile(self.temp_secring):
prefix = os.path.join(self.temp_secring, fpr)
try: os.rename(self.temp_secring, prefix+".secring") try: os.rename(self.temp_secring, prefix+".secring")
except OSError as ose: log.error(ose.message) except OSError as ose: log.error(str(ose))
log.info("Key created. Fingerprint: %s" % fpr) log.info("Key created. Fingerprint: %s" % fpr)
key.keyring = self.temp_keyring key.keyring = self.temp_keyring
@ -582,11 +589,11 @@ class GPG(GPGBase):
def gen_key_input(self, separate_keyring=False, save_batchfile=False, def gen_key_input(self, separate_keyring=False, save_batchfile=False,
testing=False, **kwargs): testing=False, **kwargs):
"""Generate a batch file for input to :meth:`GPG.gen_key()`. """Generate a batch file for input to :meth:`~gnupg.GPG.gen_key`.
The GnuPG batch file key generation feature allows unattended key The GnuPG batch file key generation feature allows unattended key
generation by creating a file with special syntax and then providing it generation by creating a file with special syntax and then providing it
to: ``gpg --gen-key --batch``. Batch files look like this: to: :command:`gpg --gen-key --batch`. Batch files look like this:
| Name-Real: Alice | Name-Real: Alice
| Name-Email: alice@inter.net | Name-Email: alice@inter.net
@ -645,9 +652,10 @@ class GPG(GPGBase):
:param bool separate_keyring: Specify for the new key to be written to :param bool separate_keyring: Specify for the new key to be written to
a separate pubring.gpg and secring.gpg. If True, a separate pubring.gpg and secring.gpg. If True,
:meth:`GPG.gen_key` will automatically rename the separate keyring :meth:`~gnupg.GPG.gen_key` will automatically rename the separate
and secring to whatever the fingerprint of the generated key ends keyring and secring to whatever the fingerprint of the generated
up being, suffixed with '.pubring' and '.secring' respectively. key ends up being, suffixed with '.pubring' and '.secring'
respectively.
:param bool save_batchfile: Save a copy of the generated batch file to :param bool save_batchfile: Save a copy of the generated batch file to
disk in a file named <name_real>.batch, where <name_real> is the disk in a file named <name_real>.batch, where <name_real> is the
@ -661,11 +669,12 @@ class GPG(GPGBase):
:param str name_real: The name field of the UID in the generated key. :param str name_real: The name field of the UID in the generated key.
:param str name_comment: The comment in the UID of the generated key. :param str name_comment: The comment in the UID of the generated key.
:param str name_email: The email in the UID of the generated key. :param str name_email: The email in the UID of the generated key.
(default: $USER@$(hostname) ) Remember to use UTF-8 encoding for (default: ``$USER`` @ :command:`hostname` ) Remember to use UTF-8
the entirety of the UID. At least one of ``name_real``, encoding for the entirety of the UID. At least one of
``name_comment``, or ``name_email`` must be provided, or else no ``name_real``, ``name_comment``, or ``name_email`` must be
user ID is created. provided, or else no user ID is created.
:param str key_type: One of 'RSA', 'DSA', 'ELG-E', or 'default'. :param str key_type: One of 'RSA', 'DSA', 'ELG-E', or 'default'.
(default: 'RSA', if using GnuPG v1.x, otherwise 'default') Starts (default: 'RSA', if using GnuPG v1.x, otherwise 'default') Starts
@ -704,7 +713,7 @@ class GPG(GPGBase):
:param str subkey_usage: Key usage for a subkey; similar to :param str subkey_usage: Key usage for a subkey; similar to
``key_usage``. ``key_usage``.
:type expire_date: int or str :type expire_date: :obj:`int` or :obj:`str`
:param expire_date: Can be specified as an iso-date or as :param expire_date: Can be specified as an iso-date or as
<int>[d|w|m|y] Set the expiration date for the key (and the <int>[d|w|m|y] Set the expiration date for the key (and the
subkey). It may either be entered in ISO date format (2000-08-15) subkey). It may either be entered in ISO date format (2000-08-15)
@ -726,17 +735,17 @@ class GPG(GPGBase):
:param str passphrase: The passphrase for the new key. The default is :param str passphrase: The passphrase for the new key. The default is
to not use any passphrase. Note that GnuPG>=2.1.x will not allow to not use any passphrase. Note that GnuPG>=2.1.x will not allow
you to specify a passphrase for batch key generation -- GnuPG will you to specify a passphrase for batch key generation -- GnuPG will
ignore the ``passphrase`` parameter, stop, and ask the user for ignore the **passphrase** parameter, stop, and ask the user for
the new passphrase. However, we can put the command the new passphrase. However, we can put the command
'%no-protection' into the batch key generation file to allow a ``%no-protection`` into the batch key generation file to allow a
passwordless key to be created, which can then have its passphrase passwordless key to be created, which can then have its passphrase
set later with '--edit-key'. set later with ``--edit-key``.
:param str preferences: Set the cipher, hash, and compression :param str preferences: Set the cipher, hash, and compression
preference values for this key. This expects the same type of preference values for this key. This expects the same type of
string as the sub-command setpref in the --edit-key menu. string as the sub-command setpref in the --edit-key menu.
:param str revoker: Should be given as 'algo:fpr' [case sensitive]. :param str revoker: Should be given as 'algo:fpr' (case sensitive).
Add a designated revoker to the generated key. Algo is the public Add a designated revoker to the generated key. Algo is the public
key algorithm of the designated revoker (i.e. RSA=1, DSA=17, etc.) key algorithm of the designated revoker (i.e. RSA=1, DSA=17, etc.)
fpr is the fingerprint of the designated revoker. The optional fpr is the fingerprint of the designated revoker. The optional
@ -747,18 +756,18 @@ class GPG(GPGBase):
preferred keyserver URL for the key. preferred keyserver URL for the key.
:param str handle: This is an optional parameter only used with the :param str handle: This is an optional parameter only used with the
status lines KEY_CREATED and KEY_NOT_CREATED. string may be up to status lines ``KEY_CREATED`` and ``KEY_NOT_CREATED``. string may
100 characters and should not contain spaces. It is useful for be up to 100 characters and should not contain spaces. It is
batch key generation to associate a key parameter block with a useful for batch key generation to associate a key parameter block
status line. with a status line.
:rtype: str :rtype: str
:returns: A suitable input string for the :meth:`GPG.gen_key` method, :returns: A suitable input string for the :meth:`GPG.gen_key` method,
the latter of which will create the new keypair. the latter of which will create the new keypair.
see See `this GnuPG Manual section`__ for more details.
http://www.gnupg.org/documentation/manuals/gnupg-devel/Unattended-GPG-key-generation.html
for more details. __ http://www.gnupg.org/documentation/manuals/gnupg-devel/Unattended-GPG-key-generation.html
""" """
#: A boolean for determining whether to set subkey_type to 'default' #: A boolean for determining whether to set subkey_type to 'default'
default_type = False default_type = False
@ -931,20 +940,21 @@ generate keys. Please see
>>> decrypted >>> decrypted
'The crow flies at midnight.' 'The crow flies at midnight.'
:param str cipher_algo: The cipher algorithm to use. To see available :param str cipher_algo: The cipher algorithm to use. To see available
algorithms with your version of GnuPG, do: algorithms with your version of GnuPG, do:
``$ gpg --with-colons --list-config ciphername``. :command:`$ gpg --with-colons --list-config ciphername`.
The default ``cipher_algo``, if unspecified, is ``'AES256'``. The default ``cipher_algo``, if unspecified, is ``'AES256'``.
:param str digest_algo: The hash digest to use. Again, to see which :param str digest_algo: The hash digest to use. Again, to see which
hashes your GnuPG is capable of using, do: hashes your GnuPG is capable of using, do:
``$ gpg --with-colons --list-config digestname``. :command:`$ gpg --with-colons --list-config digestname`.
The default, if unspecified, is ``'SHA512'``. The default, if unspecified, is ``'SHA512'``.
:param str compress_algo: The compression algorithm to use. Can be one :param str compress_algo: The compression algorithm to use. Can be one
of ``'ZLIB'``, ``'BZIP2'``, ``'ZIP'``, or ``'Uncompressed'``. of ``'ZLIB'``, ``'BZIP2'``, ``'ZIP'``, or ``'Uncompressed'``.
See also: :meth:`GPGBase._encrypt` .. seealso:: :meth:`._encrypt`
""" """
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)

110
gnupg/test/test_gnupg.py 100644 → 100755
View File

@ -47,8 +47,8 @@ import tempfile
## these dependencies require Python>=2.6 in order to have proper SSL support. ## these dependencies require Python>=2.6 in order to have proper SSL support.
## ##
## Use unittest2 if we're on Python2.6 or less: ## Use unittest2 if we're on Python2.6 or less:
if sys.version_info.major == 2 and sys.version_info.minor <= 6: if sys.version_info[0] == 2 and sys.version_info[1] <= 6:
unittest = __import__(unittest2) import unittest2 as unittest
else: else:
import unittest import unittest
@ -62,7 +62,7 @@ try:
import gnupg._parsers as _parsers import gnupg._parsers as _parsers
import gnupg._logger as _logger import gnupg._logger as _logger
except (ImportError, ValueError) as ierr: except (ImportError, ValueError) as ierr:
raise SystemExit(ierr.message) raise SystemExit(str(ierr))
log = _util.log log = _util.log
@ -173,7 +173,6 @@ class GPGTestCase(unittest.TestCase):
self.keyring = self.gpg.keyring self.keyring = self.gpg.keyring
self.secring = self.gpg.secring self.secring = self.gpg.secring
self.insecure_prng = False self.insecure_prng = False
self.gpg._keys_dir = os.path.join(_files, 'generated-keys')
def tearDown(self): def tearDown(self):
"""This is called once per self.test_* method after the test run.""" """This is called once per self.test_* method after the test run."""
@ -326,15 +325,19 @@ class GPGTestCase(unittest.TestCase):
def test_copy_data_bytesio(self): def test_copy_data_bytesio(self):
"""Test that _copy_data() is able to duplicate byte streams.""" """Test that _copy_data() is able to duplicate byte streams."""
message = "This is a BytesIO string." message = b"This is a BytesIO string."
instream = io.BytesIO(message) instream = io.BytesIO(message)
self.assertEqual(unicode(message), instream.getvalue()) self.assertEqual(message, instream.getvalue())
out_filename = 'test-copy-data-bytesio' out_filename = 'test-copy-data-bytesio'
# Create the test file: # Create the test file:
outfile = os.path.join(os.getcwdu(), out_filename) try:
outstream = open(outfile, 'w+') cwd = os.getcwdu()
except AttributeError:
cwd = os.getcwd() # not present in Python 3
outfile = os.path.join(cwd, out_filename)
outstream = open(outfile, 'wb+')
# _copy_data() will close both file descriptors # _copy_data() will close both file descriptors
_util._copy_data(instream, outstream) _util._copy_data(instream, outstream)
@ -523,7 +526,7 @@ class GPGTestCase(unittest.TestCase):
self.assertIsNotNone(key) self.assertIsNotNone(key)
self.assertNotEquals(key, "") self.assertNotEquals(key, "")
self.assertGreater(len(str(key)), 0) self.assertGreater(len(str(key)), 0)
keyfile = os.path.join(self.gpg._keys_dir, 'test_key_3.pub') keyfile = os.path.join(_files, 'test_key_3.pub')
log.debug("Storing downloaded key as %s" % keyfile) log.debug("Storing downloaded key as %s" % keyfile)
with open(keyfile, 'w') as fh: with open(keyfile, 'w') as fh:
fh.write(str(key)) fh.write(str(key))
@ -673,44 +676,72 @@ class GPGTestCase(unittest.TestCase):
def test_signature_verification_detached(self): def test_signature_verification_detached(self):
"""Test that verification of a detached signature of a file works.""" """Test that verification of a detached signature of a file works."""
key = self.generate_key("Paulo S.L.M. Barreto", "anub.is")
with open(os.path.join(_files, 'cypherpunk_manifesto'), 'rb') as cm:
sig = self.gpg.sign(cm, default_key=key.fingerprint,
passphrase='paulos.l.m.barreto',
detach=True, clearsign=False)
self.assertTrue(sig.data, "File signing should succeed")
sigfilename = os.path.join(_files, 'cypherpunk_manifesto.sig')
with open(sigfilename,'w') as sigfile:
sigfile.write(sig.data)
sigfile.seek(0)
verified = self.gpg.verify_file(cm, sigfilename) key = self.generate_key("Paulo S.L.M. Barreto", "anub.is")
datafn = os.path.join(_files, 'cypherpunk_manifesto')
sigfn = os.path.extsep.join([datafn, 'sig'])
datafd = open(datafn, 'rb')
sig = self.gpg.sign(datafd, default_key=key.fingerprint,
passphrase='paulos.l.m.barreto',
detach=True,
clearsign=False)
self.assertTrue(sig.data, "File signing should succeed")
sigfd = open(sigfn, 'wb')
sigfd.write(sig.data)
sigfd.flush()
datafd.seek(0)
sigfd.seek(0)
verified = self.gpg.verify_file(datafn, sigfn)
if key.fingerprint != verified.fingerprint: if key.fingerprint != verified.fingerprint:
log.warn("key fingerprint: %r", key.fingerprint) log.warn("key fingerprint: %r", key.fingerprint)
log.warn("verified fingerprint: %r", verified.fingerprint) log.warn("verified fingerprint: %r", verified.fingerprint)
self.assertEqual(key.fingerprint, verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint)
if os.path.isfile(sigfilename): if os.path.isfile(sigfn):
os.unlink(sigfilename) os.unlink(sigfn)
def test_signature_verification_detached_binary(self): def test_signature_verification_detached_binary(self):
"""Test that detached signature verification in binary mode fails.""" """Test that detached signature verification in binary mode fails."""
key = self.generate_key("Adi Shamir", "rsa.com") key = self.generate_key("Adi Shamir", "rsa.com")
datafile = os.path.join(_files, 'cypherpunk_manifesto') datafn = os.path.join(_files, 'cypherpunk_manifesto')
with open(datafile, 'rb') as cm: sigfn = os.path.extsep.join([datafn, 'sig'])
sig = self.gpg.sign(cm, default_key=key.fingerprint,
datafd = open(datafn, 'rb')
data = datafd.read()
datafd.close()
sig = self.gpg.sign(data, default_key=key.fingerprint,
passphrase='adishamir', passphrase='adishamir',
detach=True, binary=True, clearsign=False) detach=True,
binary=True,
clearsign=False)
self.assertTrue(sig.data, "File signing should succeed") self.assertTrue(sig.data, "File signing should succeed")
with open(datafile+'.sig', 'w') as bs:
bs.write(sig.data) sigfd = open(sigfn, 'wb')
bs.flush() sigfd.write(sig.data)
sigfd.flush()
sigfd.close()
self.assertTrue(sigfd.closed, "Sigfile '%s' should be closed" % sigfn)
with self.assertRaises(UnicodeDecodeError): with self.assertRaises(UnicodeDecodeError):
print("SIG=%s" % sig) print("SIG=%s" % sig)
with open(datafile+'.sig', 'rb') as fsig:
with open(datafile, 'rb') as fdata: verifysig = open(sigfn, 'rb')
self.gpg.verify_file(fdata, fsig) verification = self.gpg.verify_file(data, verifysig)
self.assertTrue(isinstance(verification, gnupg._parsers.Verify))
self.assertFalse(verification.valid)
if os.path.isfile(sigfn):
os.unlink(sigfn)
def test_deletion(self): def test_deletion(self):
"""Test that key deletion works.""" """Test that key deletion works."""
@ -793,14 +824,14 @@ authentication."""
riggio_input = self.gpg.gen_key_input(separate_keyring=True, **riggio) riggio_input = self.gpg.gen_key_input(separate_keyring=True, **riggio)
log.info("Key stored in separate keyring: %s" % self.gpg.temp_keyring) log.info("Key stored in separate keyring: %s" % self.gpg.temp_keyring)
riggio = self.gpg.gen_key(riggio_input) riggio = self.gpg.gen_key(riggio_input)
self.gpg.options = ['--keyring {}'.format(riggio.keyring)] self.gpg.options = ['--keyring {0}'.format(riggio.keyring)]
riggio_key = self.gpg.export_keys(riggio.fingerprint) riggio_key = self.gpg.export_keys(riggio.fingerprint)
self.gpg.import_keys(riggio_key) self.gpg.import_keys(riggio_key)
sicari_input = self.gpg.gen_key_input(separate_keyring=True, **sicari) sicari_input = self.gpg.gen_key_input(separate_keyring=True, **sicari)
log.info("Key stored in separate keyring: %s" % self.gpg.temp_keyring) log.info("Key stored in separate keyring: %s" % self.gpg.temp_keyring)
sicari = self.gpg.gen_key(sicari_input) sicari = self.gpg.gen_key(sicari_input)
self.gpg.options.append('--keyring {}'.format(sicari.keyring)) self.gpg.options.append('--keyring {0}'.format(sicari.keyring))
sicari_key = self.gpg.export_keys(sicari.fingerprint) sicari_key = self.gpg.export_keys(sicari.fingerprint)
self.gpg.import_keys(sicari_key) self.gpg.import_keys(sicari_key)
@ -898,15 +929,15 @@ analysis of different kinds of data (temperature, humidity, etc.) coming from
a WSN while ensuring both end-to-end encryption and hop-by-hop a WSN while ensuring both end-to-end encryption and hop-by-hop
authentication.""" authentication."""
enc = self.gpg.encrypt(message, alice_pfpr, bob_pfpr) enc = self.gpg.encrypt(message, alice_pfpr, bob_pfpr)
encrypted = str(enc.data) encrypted = str(enc)
log.debug("encryption_decryption_multi_recipient() Ciphertext = %s" log.debug("encryption_decryption_multi_recipient() Ciphertext = %s"
% encrypted) % encrypted)
self.assertNotEquals(message, encrypted) self.assertNotEquals(message, encrypted)
dec_alice = self.gpg.decrypt(encrypted, passphrase="test") dec_alice = self.gpg.decrypt(encrypted, passphrase="test")
self.assertEquals(message, str(dec_alice.data)) self.assertEquals(message, str(dec_alice))
dec_bob = self.gpg.decrypt(encrypted, passphrase="test") dec_bob = self.gpg.decrypt(encrypted, passphrase="test")
self.assertEquals(message, str(dec_bob.data)) self.assertEquals(message, str(dec_bob))
def test_symmetric_encryption_and_decryption(self): def test_symmetric_encryption_and_decryption(self):
"""Test symmetric encryption and decryption""" """Test symmetric encryption and decryption"""
@ -916,7 +947,7 @@ know, maybe you shouldn't be doing it in the first place.
encrypted = str(self.gpg.encrypt(msg, passphrase='quiscustodiet', encrypted = str(self.gpg.encrypt(msg, passphrase='quiscustodiet',
symmetric=True, encrypt=False)) symmetric=True, encrypt=False))
decrypt = self.gpg.decrypt(encrypted, passphrase='quiscustodiet') decrypt = self.gpg.decrypt(encrypted, passphrase='quiscustodiet')
decrypted = str(decrypt.data) decrypted = str(decrypt)
log.info("Symmetrically encrypted data:\n%s" % encrypted) log.info("Symmetrically encrypted data:\n%s" % encrypted)
log.info("Symmetrically decrypted data:\n%s" % decrypted) log.info("Symmetrically decrypted data:\n%s" % decrypted)
@ -948,9 +979,8 @@ know, maybe you shouldn't be doing it in the first place.
with open(enc_outf) as enc2: with open(enc_outf) as enc2:
fdata = enc2.read() fdata = enc2.read()
ddata = str(self.gpg.decrypt(fdata, passphrase="overalls")) ddata = self.gpg.decrypt(fdata, passphrase="overalls").data
data = data.encode(self.gpg._encoding)
if ddata != data: if ddata != data:
log.debug("data was: %r" % data) log.debug("data was: %r" % data)
log.debug("new (from filehandle): %r" % fdata) log.debug("new (from filehandle): %r" % fdata)

View File

@ -1,58 +1,3 @@
# # sha256: UI5KRMglOjhqD4bZyb1KG0y7L5TojUmhnBUTZTymbEU
# python-gnupg/requirements.txt psutil==1.2.1
# -----------------------------
# Pip requirements.txt file. This file is also parsed for distribute to use in
# setup.py.
#_____________________________________________________________________________
# This file is part of python-gnupg, a Python interface to GnuPG.
# Copyright © 2013 Isis Lovecruft, <isis@leap.se> 0xA3ADB67A2CDB8B35
# © 2013 Andrej B.
# © 2013 LEAP Encryption Access Project
# © 2008-2012 Vinay Sajip
# © 2005 Steve Traugott
# © 2004 A.M. Kuchling
#
# This program 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, either version 3 of the License, or (at your option)
# any later version.
#
# This program 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 included LICENSE file for details.
#______________________________________________________________________________
#
# Force pip upgrade due to security vulnerabilities.
#
# This has actually has little to do with installing python-gnupg, since
# older versions of pip would install everything just fine. except that, in
# my opinion, using GnuPG for privacy is silly when the installation of
# python-gnupg with an older version of pip is trivially exploitable through
# a MITM attack. see https://github.com/pypa/pip/pull/791
#
# Also, note that SSL package delivery is *not* entirely fixed yet. See
# https://github.com/TheTorProject/ooni-backend/pull/1#discussion_r4084881
#
#pip>=1.3.1
#
# NOTE: setuptools is currently (as of 27 May 2013) being merged back into its
# parent project, distribute. By using the included distribute_setup.py
# script, we make sure that we have a recent version of setuptools/distribute,
# which is the *only* Python packaging framework compatible at this point with
# both Python>=2.4 and Python3.x.
#
# A new version of distribute is necessary due to the merging of setuptools
# back into its parent project, distribute. Also, the only way to package for
# both Python 2 and 3 is to use distribute.
#
#distribute>=0.6.45
#
# Sphinx is only necessary for building documentation, so it is added in
# setup.py under extras_require['docs'].
#
# If you want to build the documentation, uncomment this line:
#Sphinx>=1.1
#
# And, this one is actually used in the gnupg module code:
#
psutil>=0.5.1

View File

@ -1,7 +1,12 @@
[upload_docs] [upload_docs]
upload-dir = docs/_build/html upload-dir = docs/_build/html
show-response = true
verbose = true
[upload] [upload]
sign = True sign = True
identity = 0xa3adb67a2cdb8b35 identity = 0xa3adb67a2cdb8b35
[aliases]
upload_all = sdist bdist_egg bdist_wheel upload

View File

@ -23,7 +23,11 @@ from __future__ import absolute_import
from __future__ import print_function from __future__ import print_function
import setuptools import setuptools
import sys
import os
import versioneer import versioneer
versioneer.versionfile_source = 'gnupg/_version.py' versioneer.versionfile_source = 'gnupg/_version.py'
versioneer.versionfile_build = 'gnupg/_version.py' versioneer.versionfile_build = 'gnupg/_version.py'
versioneer.tag_prefix = '' versioneer.tag_prefix = ''
@ -34,6 +38,49 @@ __contact__ = 'isis@patternsinthevoid.net'
__url__ = 'https://github.com/isislovecruft/python-gnupg' __url__ = 'https://github.com/isislovecruft/python-gnupg'
def python26():
"""Returns True if we're running on Python2.6."""
if sys.version[:3] == "2.6":
return True
return False
def get_requirements():
"""Extract the list of requirements from our requirements.txt.
:rtype: 2-tuple
:returns: Two lists, the first is a list of requirements in the form of
pkgname==version. The second is a list of URIs or VCS checkout strings
which specify the dependency links for obtaining a copy of the
requirement.
"""
requirements_file = os.path.join(os.getcwd(), 'requirements.txt')
requirements = []
links=[]
try:
with open(requirements_file) as reqfile:
for line in reqfile.readlines():
line = line.strip()
if line.startswith('#'):
continue
elif line.startswith(
('https://', 'git://', 'hg://', 'svn://')):
links.append(line)
else:
requirements.append(line)
except (IOError, OSError) as error:
print(error)
if python26():
# Required to make `collections.OrderedDict` available on Python<=2.6
requirements.append('ordereddict==1.1#a0ed854ee442051b249bfad0f638bbec')
return requirements, links
requires, deplinks = get_requirements()
setuptools.setup( setuptools.setup(
name = "gnupg", name = "gnupg",
description="A Python wrapper for GnuPG", description="A Python wrapper for GnuPG",
@ -62,8 +109,10 @@ greater.
scripts=['versioneer.py'], scripts=['versioneer.py'],
test_suite='gnupg.test.test_gnupg', test_suite='gnupg.test.test_gnupg',
install_requires=['psutil>=0.5.1'], install_requires=requires,
extras_require={'docs': ["Sphinx>=1.1", "repoze.sphinx"]}, dependency_links=deplinks,
extras_require={'docs': ["Sphinx>=1.1",
"sphinxcontrib-fulltoc==1.0"]},
platforms="Linux, BSD, OSX, Windows", platforms="Linux, BSD, OSX, Windows",
download_url="https://github.com/isislovecruft/python-gnupg/archive/master.zip", download_url="https://github.com/isislovecruft/python-gnupg/archive/master.zip",